Repository: NREL-SIIP/PowerSimulations.jl Branch: main Commit: f801e64fbc98 Files: 259 Total size: 2.7 MB Directory structure: gitextract_1kk9e1p9/ ├── .claude/ │ ├── Sienna.md │ └── claude.md ├── .devcontainer/ │ └── devcontainer.json ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.md │ │ └── feature_request.md │ └── workflows/ │ ├── TagBot.yml │ ├── cross-package-test.yml │ ├── doc-preview-cleanup.yml │ ├── docs.yml │ ├── format-check.yml │ ├── main-tests.yml │ ├── performance_comparison.yml │ └── pr_testing.yml ├── .gitignore ├── .pre-commit-config.yaml ├── CONTRIBUTING.md ├── LICENSE ├── Project.toml ├── README.md ├── codecov.yml ├── docs/ │ ├── Makefile │ ├── Project.toml │ ├── make.jl │ ├── make_tutorials.jl │ └── src/ │ ├── api/ │ │ ├── PowerSimulations.md │ │ ├── developer.md │ │ ├── glossary.md │ │ └── internal.md │ ├── code_base_developer_guide/ │ │ └── extending_powersimulations.md │ ├── explanation/ │ │ ├── chronologies.md │ │ ├── feedforward.md │ │ ├── psi_structure.md │ │ └── sequencing.md │ ├── formulation_library/ │ │ ├── Branch.md │ │ ├── DCModels.md │ │ ├── Feedforward.md │ │ ├── General.md │ │ ├── Introduction.md │ │ ├── Load.md │ │ ├── Network.md │ │ ├── Piecewise.md │ │ ├── README.md │ │ ├── RenewableGen.md │ │ ├── Service.md │ │ ├── Source.md │ │ └── ThermalGen.md │ ├── get_test_data.jl │ ├── how_to/ │ │ ├── adding_new_problem_model.md │ │ ├── debugging_infeasible_models.md │ │ ├── logging.md │ │ ├── parallel_simulations.md │ │ ├── problem_templates.md │ │ ├── read_results.md │ │ ├── register_variable.md │ │ └── simulation_recorder.md │ ├── index.md │ └── tutorials/ │ ├── decision_problem.jl │ └── pcm_simulation.jl ├── scripts/ │ └── formatter/ │ ├── Project.toml │ └── formatter_code.jl ├── src/ │ ├── PowerSimulations.jl │ ├── contingency_model/ │ │ ├── contingency.jl │ │ ├── contingency_arguments.jl │ │ └── contingency_constraints.jl │ ├── core/ │ │ ├── abstract_feedforward.jl │ │ ├── abstract_simulation_store.jl │ │ ├── auxiliary_variables.jl │ │ ├── cache_utils.jl │ │ ├── constraints.jl │ │ ├── dataset.jl │ │ ├── dataset_container.jl │ │ ├── definitions.jl │ │ ├── device_model.jl │ │ ├── dual_processing.jl │ │ ├── event_keys.jl │ │ ├── event_model.jl │ │ ├── expressions.jl │ │ ├── formulations.jl │ │ ├── initial_conditions.jl │ │ ├── model_store_params.jl │ │ ├── network_formulations.jl │ │ ├── network_model.jl │ │ ├── network_reductions.jl │ │ ├── operation_model_abstract_types.jl │ │ ├── optimization_container.jl │ │ ├── parameters.jl │ │ ├── power_flow_data_wrapper.jl │ │ ├── results_by_time.jl │ │ ├── service_model.jl │ │ ├── settings.jl │ │ ├── store_common.jl │ │ └── variables.jl │ ├── devices_models/ │ │ ├── device_constructors/ │ │ │ ├── branch_constructor.jl │ │ │ ├── hvdcsystems_constructor.jl │ │ │ ├── load_constructor.jl │ │ │ ├── reactivepowerdevice_constructor.jl │ │ │ ├── regulationdevice_constructor.jl │ │ │ ├── renewablegeneration_constructor.jl │ │ │ ├── source_constructor.jl │ │ │ └── thermalgeneration_constructor.jl │ │ └── devices/ │ │ ├── AC_branches.jl │ │ ├── HVDCsystems.jl │ │ ├── TwoTerminalDC_branches.jl │ │ ├── area_interchange.jl │ │ ├── common/ │ │ │ ├── add_auxiliary_variable.jl │ │ │ ├── add_constraint_dual.jl │ │ │ ├── add_pwl_methods.jl │ │ │ ├── add_to_expression.jl │ │ │ ├── add_variable.jl │ │ │ ├── duration_constraints.jl │ │ │ ├── get_time_series.jl │ │ │ ├── objective_function/ │ │ │ │ ├── common.jl │ │ │ │ ├── import_export.jl │ │ │ │ ├── linear_curve.jl │ │ │ │ ├── market_bid.jl │ │ │ │ ├── piecewise_linear.jl │ │ │ │ └── quadratic_curve.jl │ │ │ ├── range_constraint.jl │ │ │ ├── rateofchange_constraints.jl │ │ │ └── set_expression.jl │ │ ├── default_interface_methods.jl │ │ ├── electric_loads.jl │ │ ├── reactivepower_device.jl │ │ ├── regulation_device.jl │ │ ├── renewable_generation.jl │ │ ├── source.jl │ │ └── thermal_generation.jl │ ├── feedforward/ │ │ ├── feedforward_arguments.jl │ │ ├── feedforward_constraints.jl │ │ └── feedforwards.jl │ ├── initial_conditions/ │ │ ├── add_initial_condition.jl │ │ ├── calculate_initial_condition.jl │ │ ├── initial_condition_chronologies.jl │ │ ├── initialization.jl │ │ └── update_initial_conditions.jl │ ├── network_models/ │ │ ├── area_balance_model.jl │ │ ├── copperplate_model.jl │ │ ├── hvdc_network_constructor.jl │ │ ├── hvdc_networks.jl │ │ ├── network_constructor.jl │ │ ├── network_slack_variables.jl │ │ ├── pm_translator.jl │ │ ├── power_flow_evaluation.jl │ │ └── powermodels_interface.jl │ ├── operation/ │ │ ├── decision_model.jl │ │ ├── decision_model_store.jl │ │ ├── emulation_model.jl │ │ ├── emulation_model_store.jl │ │ ├── initial_conditions_update_in_memory_store.jl │ │ ├── model_numerical_analysis_utils.jl │ │ ├── operation_model_interface.jl │ │ ├── operation_model_simulation_interface.jl │ │ ├── operation_model_types.jl │ │ ├── operation_problem_templates.jl │ │ ├── optimization_debugging.jl │ │ ├── problem_results.jl │ │ ├── problem_template.jl │ │ ├── template_validation.jl │ │ └── time_series_interface.jl │ ├── parameters/ │ │ ├── add_parameters.jl │ │ ├── update_container_parameter_values.jl │ │ ├── update_cost_parameters.jl │ │ └── update_parameters.jl │ ├── services_models/ │ │ ├── agc.jl │ │ ├── reserve_group.jl │ │ ├── reserves.jl │ │ ├── service_slacks.jl │ │ ├── services_constructor.jl │ │ └── transmission_interface.jl │ ├── simulation/ │ │ ├── decision_model_simulation_results.jl │ │ ├── emulation_model_simulation_results.jl │ │ ├── get_components_interface.jl │ │ ├── hdf_simulation_store.jl │ │ ├── in_memory_simulation_store.jl │ │ ├── initial_condition_update_simulation.jl │ │ ├── optimization_output_cache.jl │ │ ├── optimization_output_caches.jl │ │ ├── realized_meta.jl │ │ ├── simulation.jl │ │ ├── simulation_events.jl │ │ ├── simulation_info.jl │ │ ├── simulation_internal.jl │ │ ├── simulation_models.jl │ │ ├── simulation_partition_results.jl │ │ ├── simulation_partitions.jl │ │ ├── simulation_problem_results.jl │ │ ├── simulation_results.jl │ │ ├── simulation_results_export.jl │ │ ├── simulation_sequence.jl │ │ ├── simulation_state.jl │ │ ├── simulation_store_params.jl │ │ └── simulation_store_requirements.jl │ └── utils/ │ ├── dataframes_utils.jl │ ├── datetime_utils.jl │ ├── file_utils.jl │ ├── generate_valid_formulations.jl │ ├── indexing.jl │ ├── jump_utils.jl │ ├── logging.jl │ ├── powersystems_utils.jl │ ├── print_pt_v2.jl │ ├── print_pt_v3.jl │ ├── recorder_events.jl │ └── time_series_utils.jl └── test/ ├── Project.toml ├── includes.jl ├── performance/ │ └── performance_test.jl ├── run_partitioned_simulation.jl ├── runtests.jl ├── test_basic_model_structs.jl ├── test_data/ │ └── results_export.json ├── test_device_branch_constructors.jl ├── test_device_hvdc.jl ├── test_device_lcc.jl ├── test_device_load_constructors.jl ├── test_device_renewable_generation_constructors.jl ├── test_device_source_constructors.jl ├── test_device_synchronous_condenser_constructors.jl ├── test_device_thermal_generation_constructors.jl ├── test_events.jl ├── test_formulation_combinations.jl ├── test_import_export_cost.jl ├── test_initialization_problem.jl ├── test_jump_utils.jl ├── test_market_bid_cost.jl ├── test_mbc_sanity_check.jl ├── test_model_decision.jl ├── test_model_emulation.jl ├── test_multi_interval.jl ├── test_network_constructors.jl ├── test_network_constructors_with_dlr.jl ├── test_parallel_branch_parameter_multipliers.jl ├── test_power_flow_in_the_loop.jl ├── test_print.jl ├── test_problem_template.jl ├── test_recorder_events.jl ├── test_services_constructor.jl ├── test_simulation_build.jl ├── test_simulation_execute.jl ├── test_simulation_models.jl ├── test_simulation_partitions.jl ├── test_simulation_results.jl ├── test_simulation_results_export.jl ├── test_simulation_sequence.jl ├── test_simulation_store.jl ├── test_utils/ │ ├── add_components_to_system.jl │ ├── add_dlr_ts.jl │ ├── add_market_bid_cost.jl │ ├── common_operation_model.jl │ ├── events_simulation_utils.jl │ ├── iec_simulation_utils.jl │ ├── mbc_simulation_utils.jl │ ├── mbc_system_utils.jl │ ├── mock_operation_models.jl │ ├── model_checks.jl │ ├── operations_problem_templates.jl │ ├── run_simulation.jl │ └── solver_definitions.jl └── test_utils.jl ================================================ FILE CONTENTS ================================================ ================================================ FILE: .claude/Sienna.md ================================================ # Sienna Programming Practices This document describes general programming practices and conventions that apply across all Sienna packages (PowerSystems.jl, PowerSimulations.jl, PowerFlows.jl, PowerNetworkMatrices.jl, InfrastructureSystems.jl, etc.). ## Performance Requirements **Priority:** Critical. See the [Julia Performance Tips](https://docs.julialang.org/en/v1/manual/performance-tips/). ### Anti-Patterns to Avoid #### Type instability Functions must return consistent concrete types. Check with `@code_warntype`. - Bad: `f(x) = x > 0 ? 1 : 1.0` - Good: `f(x) = x > 0 ? 1.0 : 1.0` #### Abstract field types Struct fields must have concrete types or be parameterized. - Bad: `struct Foo; data::AbstractVector; end` - Good: `struct Foo{T<:AbstractVector}; data::T; end` #### Untyped containers - Bad: `Vector{Any}()`, `Vector{Real}()` - Good: `Vector{Float64}()`, `Vector{Int}()` #### Non-const globals - Bad: `THRESHOLD = 0.5` - Good: `const THRESHOLD = 0.5` #### Unnecessary allocations - Use views instead of copies (`@view`, `@views`) - Pre-allocate arrays instead of `push!` in loops - Use in-place operations (functions ending with `!`) #### Captured variables Avoid closures that capture mutable or reassigned variables, as these cause boxing. Captured variables that do not change type (especially if never reassigned) do not incur boxing — see [Julia docs on captured variables](https://docs.julialang.org/en/v1/manual/performance-tips/#man-performance-captured). #### Splatting penalty Avoid splatting (`...`) in performance-critical code. Splatting containers with known length at compile time (e.g., `Tuple`) is acceptable. #### Abstract return types In performance-critical code, avoid returning abstract types. Returning `Union{T, Nothing}` is acceptable for functions that may fail or return nothing. Factory functions that return one of several concrete subtypes are also fine. #### Using `isa` in function logic **ABSOLUTELY FORBIDDEN unless the user explicitly asks for it.** Never write code that uses `isa` checks for type-based branching. This is always wrong — use multiple dispatch instead. - Bad: `if x isa Float64 ... elseif x isa Int ... end` - Good: Use multiple dispatch with specific type signatures - Bad: `function f(x); if x isa AbstractVector return sum(x) else return x end; end` - Good: `f(x::AbstractVector) = sum(x); f(x::Number) = x` **Why this matters:** `isa` checks force the compiler to handle multiple code paths at runtime, losing type information and preventing specialization. Multiple dispatch allows the compiler to generate optimized code for each type. Using `isa` is an anti-pattern that defeats Julia's core design. ### Best Practices - Use `@inbounds` when bounds are verified - Use broadcasting (dot syntax) for element-wise operations - Avoid `try-catch` in hot paths - Use function barriers to isolate type instability > Apply these guidelines with judgment. Not every function is performance-critical. Focus optimization efforts on hot paths and frequently called code. ## Code Conventions Style guide: [https://sienna-platform.github.io/InfrastructureSystems.jl/stable/style/](https://sienna-platform.github.io/InfrastructureSystems.jl/stable/style/) Formatter (JuliaFormatter): Use the formatter script provided in each package. Key rules: - Constructors: use `function Foo()` not `Foo() = ...` - Asserts: prefer `InfrastructureSystems.@assert_op` over `@assert` - Globals: `UPPER_CASE` for constants - Exports: all exports in main module file - Comments: complete sentences, describe why not how ## Documentation Practices and Requirements Framework: [Diataxis](https://diataxis.fr/) Sienna guide: [https://sienna-platform.github.io/InfrastructureSystems.jl/stable/docs_best_practices/explanation/](https://sienna-platform.github.io/InfrastructureSystems.jl/stable/docs_best_practices/explanation/) Docstring requirements: - Scope: all elements of public interface (IS is selective about exports) - Include: function signatures and arguments list - Automation: `DocStringExtensions.TYPEDSIGNATURES` (`TYPEDFIELDS` used sparingly in IS) - See also: add links for functions with same name (multiple dispatch) API docs: - Public: typically in `docs/src/api/public.md` using `@autodocs` with `Public=true, Private=false` - Internals: typically in `docs/src/api/internals.md` ## Design Principles - Elegance and concision in both interface and implementation - Fail fast with actionable error messages rather than hiding problems - Validate invariants explicitly in subtle cases - Avoid over-adherence to backwards compatibility for internal helpers ## Contribution Workflow Branch naming: `feature/description` or `fix/description` 1. Create feature branch 2. Follow style guide and run formatter 3. Ensure tests pass 4. Submit pull request ## AI Agent Guidance **Key priorities:** Read existing patterns first, maintain consistency, use concrete types in hot paths, run formatter, add docstrings to public API, ensure tests pass. **Critical rules:** - Always use `julia --project=` (never bare `julia`) - **NEVER use `isa` in function logic** — use multiple dispatch instead. This is absolutely forbidden unless the user explicitly asks for it. - Never edit auto-generated files directly - Verify type stability with `@code_warntype` for performance-critical code - Consider downstream package impact ## Julia Environment Best Practices **CRITICAL:** Always use `julia --project=` when running Julia code in Sienna repositories. **NEVER** use bare `julia` or `julia --project` without specifying the environment. Each package typically defines dependencies in `test/Project.toml` for testing. Common patterns: ```sh # Run tests (using test environment) julia --project=test test/runtests.jl # Run specific test julia --project=test test/runtests.jl test_file_name # Run expression julia --project=test -e 'using PackageName; ...' # Instantiate environment julia --project=test -e 'using Pkg; Pkg.instantiate()' # Build docs (using docs environment) julia --project=docs docs/make.jl ``` **Why this matters:** Running without `--project=` will fail because required packages won't be available in the default environment. The test/docs environments contain all necessary dependencies for their respective tasks. ## Troubleshooting **Type instability** - Symptom: Poor performance, many allocations - Diagnosis: `@code_warntype` on suspect function - Solution: See performance anti-patterns above **Formatter fails** - Symptom: Formatter command returns error - Solution: Run the formatter script provided in the package (e.g., `julia -e 'include("scripts/formatter/formatter_code.jl")'`) **Test failures** - Symptom: Tests fail unexpectedly - Solution: `julia --project=test -e 'using Pkg; Pkg.instantiate()'` ================================================ FILE: .claude/claude.md ================================================ # PowerSimulations.jl Power system optimization and simulation framework. Builds and solves large-scale optimization problems for operations modeling across multiple time scales (planning, day-ahead, real-time). Julia compat: `^1.10`. > **General Sienna Programming Practices:** For performance requirements, code conventions, documentation practices, and contribution workflows that apply across all Sienna packages, see [Sienna.md](Sienna.md). Always load [Sienna.md](Sienna.md) before any change or code execution. ## Core Architecture ### Operation Models The central abstraction is `OperationModel`, with two concrete types: - **`DecisionModel{M <: DecisionProblem}`** — Solves optimization problems over a specified horizon (e.g., 24h unit commitment, 1h economic dispatch). Contains a `ProblemTemplate`, an `OptimizationContainer` (JuMP model wrapper), and a `System` from PowerSystems.jl. - **`EmulationModel{M <: EmulationProblem}`** — Simulates real-time operation with a single time-step horizon. Used for AGC, reserve deployment, and similar fast-timescale problems. Built-in problem types: `GenericOpProblem`, `UnitCommitmentProblem`, `EconomicDispatchProblem`, `AGCReserveDeployment`. ### ProblemTemplate Defines what a model contains — its network representation and which device/service formulations to use: ```julia template = ProblemTemplate(NetworkModel(CopperPlatePowerModel)) set_device_model!(template, ThermalStandard, ThermalBasicUnitCommitment) set_device_model!(template, RenewableDispatch, RenewableFullDispatch) set_service_model!(template, VariableReserve{ReserveUp}, RangeReserve) ``` ### Device, Service, and Network Models These types bind a PowerSystems component type to a formulation: - **`DeviceModel{D <: PSY.Device, B <: AbstractDeviceFormulation}`** — Specifies how a device type is modeled. The formulation determines which variables, constraints, and parameters are added. Also carries feedforward specifications, time series mappings, and attributes. - **`ServiceModel{D <: PSY.Service, B <: AbstractServiceFormulation}`** — Same pattern for ancillary services (reserves, AGC). - **`NetworkModel{T <: PM.AbstractPowerModel}`** — Specifies the power flow formulation. Options include `CopperPlatePowerModel` (single node), `PTDFPowerModel` (linearized with PTDF matrix), `AreaBalancePowerModel` (zonal), and full AC/DC from PowerModels.jl. ### Formulation Hierarchy Formulations are organized by device category. The formulation type controls what gets built: - **Thermal**: `ThermalBasicUnitCommitment`, `ThermalStandardUnitCommitment`, `ThermalBasicDispatch`, `ThermalCompactUnitCommitment`, etc. UC formulations add binary on/off variables and min up/down time constraints; dispatch formulations use continuous variables only. - **Renewable**: `RenewableFullDispatch`, `RenewableConstantPowerFactor` - **Load**: `StaticPowerLoad`, `PowerLoadInterruption`, `PowerLoadDispatch` - **Storage**: `BookKeeping`, `BatteryAncillaryServices` - **Branches**: `StaticBranch`, `StaticBranchBounds`, `StaticBranchUnbounded`, `HVDCTwoTerminalDispatch` ### OptimizationContainer Wraps the JuMP model and holds all optimization artifacts in typed containers: - **Variables** — decision variables indexed by device and time - **Constraints** — constraint references - **Parameters** — time-varying data (time series, feedforward values) stored as parameter containers - **Expressions** — reusable expressions (e.g., nodal balance) that multiple devices contribute to - **Objective function** — cost components ## Simulation Architecture ### Simulation Orchestrates multi-model runs across time. A `Simulation` contains: - **`SimulationModels`** — Container holding a vector of `DecisionModel`s and an optional `EmulationModel` - **`SimulationSequence`** — Defines execution order, feedforward connections between models, and initial condition chronologies - **`SimulationState`** — Tracks evolving state across the simulation timeline ### SimulationState Maintains state that flows between models and across time steps: ``` SimulationState ├── current_time::Ref{DateTime} # Current simulation clock ├── last_decision_model::Ref{Symbol} # Which model ran last ├── decision_states::DatasetContainer # Outputs from decision models └── system_states::DatasetContainer # Actual system state evolution ``` After each model solves, its results update the relevant datasets in `SimulationState`. The next model in sequence reads from these datasets via feedforwards and initial conditions. ### Feedforward Mechanism Feedforwards transfer values between models in a simulation sequence. They parameterize a downstream model using results from an upstream model: - **`UpperBoundFeedforward`** — Constrains variables with upper bounds from source - **`LowerBoundFeedforward`** — Constrains variables with lower bounds from source - **`SemiContinuousFeedforward`** — Passes binary on/off status - **`FixValueFeedforward`** — Fixes variable values from source results Each feedforward specifies a source model, source variable, and affected component/variable in the target model. ### Initial Conditions State carried between time steps within or across models: - `DevicePower` — Previous generation level - `DeviceStatus` — On/off status - `InitialTimeDurationOn/Off` — Time in current state - `InitialEnergyLevel` — Storage state-of-charge - `AreaControlError` — AGC error state Chronologies control how initial conditions are sourced: `InterProblemChronology` (from a different model's results) or `IntraProblemChronology` (from the same model's previous solve). ### Simulation Execution Loop 1. Read current state from `SimulationState` 2. Update feedforward parameters in the current model from upstream results 3. Update initial conditions from state 4. Solve the model (`JuMP.optimize!`) 5. Write results to `SimulationState` and results store (HDF5 or in-memory) 6. Advance to next model in sequence; repeat ## Directory Structure ``` src/ ├── core/ # Core types: OptimizationContainer, DeviceModel, │ # NetworkModel, ServiceModel, formulations, │ # variable/constraint/parameter type definitions ├── operation/ # DecisionModel, EmulationModel, ProblemTemplate, │ # built-in problem templates, model build/solve logic ├── simulation/ # Simulation, SimulationModels, SimulationSequence, │ # SimulationState, results storage (HDF5, in-memory) ├── devices_models/ │ ├── devices/ # Per-device-type implementations (thermal, renewable, │ │ # loads, branches, HVDC, storage) │ └── device_constructors/ # Build functions that add variables, constraints, │ # parameters to OptimizationContainer per formulation ├── network_models/ # CopperPlate, PTDF, AreaBalance, PowerModels interface ├── services_models/ # Reserve and transmission interface implementations ├── feedforward/ # Feedforward types, argument setup, constraint builders ├── initial_conditions/ # IC types, chronologies, update logic └── parameters/ # Parameter update mechanisms for time series and state ``` ## Build Flow When `build!(model, system)` is called: 1. Template specifies device models, service models, and network model 2. For each `DeviceModel`, the formulation type dispatches to device-specific constructors that add variables, constraints, parameters, and expressions to the `OptimizationContainer` 3. Network model adds power balance and flow constraints; devices contribute to shared nodal balance expressions 4. Service models add reserve variables and participation constraints 5. Feedforwards (if in simulation context) add linking constraints/parameters 6. Objective function assembled from cost components 7. Result: a complete JuMP optimization model ready to solve ================================================ FILE: .devcontainer/devcontainer.json ================================================ { "extensions": [ "julialang.language-julia" ], "image": "ghcr.io/julia-vscode/julia-devcontainer" } ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug report about: Create a report to help us improve title: '' labels: code bug assignees: '' --- **If this is a question, something isn't working or an idea please start a Q&A discussion in the [Discussion tab](https://github.com/Sienna-Platform/PowerSimulations.jl/discussions)** Open a bug report only if you can provide the details below **Describe the bug** A clear and concise description of what the bug is. **To Reproduce** Steps to reproduce the behavior: Paste the code we can run to reproduce the error you are seeing **Expected behavior** A clear and concise description of what you expected to happen. **Screenshots** If applicable, add screenshots to help explain your problem. **Additional context** Add any other context about the problem here. ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.md ================================================ --- name: Feature request about: Suggest an idea for this project title: '' labels: feature request assignees: '' --- **If this is a question or an idea please start a Q&A discussion in the [Discussion tab](https://github.com/Sienna-Platform/PowerSimulations.jl/discussions)** **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I want to represent problem X [...] please be as specific as possible, including the mathematical formulations where appropriate. **Describe the solution you'd like** A clear and concise description of what you want to happen. **Describe alternatives you've considered** A clear and concise description of any alternative solutions or features you've considered. **Additional context** Add any other context or screenshots about the feature request here. ================================================ FILE: .github/workflows/TagBot.yml ================================================ name: TagBot on: issue_comment: types: - created workflow_dispatch: jobs: TagBot: if: github.event_name == 'workflow_dispatch' || github.actor == 'JuliaTagBot' runs-on: ubuntu-latest steps: - uses: JuliaRegistries/TagBot@v1 with: token: ${{ secrets.GITHUB_TOKEN }} ssh: ${{ secrets.DOCUMENTER_KEY }} ================================================ FILE: .github/workflows/cross-package-test.yml ================================================ name: CrossPackageTest on: push: branches: [main] tags: [v*] pull_request: jobs: test: name: Julia v${{ matrix.julia-version }} - ${{ matrix.package_name }} runs-on: ${{ matrix.os }} strategy: matrix: julia-version: [1] os: [ubuntu-latest] package_name: [HydroPowerSimulations, StorageSystemsSimulations, PowerAnalytics] continue-on-error: true steps: - uses: actions/checkout@v2 - uses: julia-actions/setup-julia@v1 with: version: ${{ matrix.julia-version }} arch: x64 - uses: julia-actions/julia-buildpkg@latest - name: Clone ${{matrix.package_name}} uses: actions/checkout@v2 with: repository: Sienna-Platform/${{matrix.package_name}}.jl path: downstream - name: Run the tests shell: julia --project=downstream {0} run: | using Pkg try # Force it to use this PR's version of the package Pkg.develop(PackageSpec(path=".")) # resolver may fail with main deps Pkg.update() Pkg.test() # resolver may fail with test time deps catch err err isa Pkg.Resolve.ResolverError || rethrow() # If we can't resolve that means this is incompatible by SemVer, and this is fine. # It means we marked this as a breaking change, so we don't need to worry about # mistakenly introducing a breaking change as we have intentionally made one. @info "Not compatible with this release. No problem." exception=err exit(0) # Exit immediately, as a success end ================================================ FILE: .github/workflows/doc-preview-cleanup.yml ================================================ name: Doc Preview Cleanup on: pull_request: types: [closed] # Ensure that only one "Doc Preview Cleanup" workflow is force pushing at a time concurrency: group: doc-preview-cleanup cancel-in-progress: false jobs: doc-preview-cleanup: runs-on: ubuntu-latest if: github.event.pull_request.head.repo.fork == false # This workflow pushes to gh-pages; permissions are per-job and independent of docs.yml permissions: contents: write steps: - name: Checkout gh-pages branch uses: actions/checkout@v4 with: ref: gh-pages - name: Delete preview and history + push changes run: | if [ -d "${preview_dir}" ]; then git config user.name "Documenter.jl" git config user.email "documenter@juliadocs.github.io" git rm -rf "${preview_dir}" git commit -m "delete preview" git branch gh-pages-new "$(echo "delete history" | git commit-tree "HEAD^{tree}")" git push --force origin gh-pages-new:gh-pages fi env: preview_dir: previews/PR${{ github.event.number }} ================================================ FILE: .github/workflows/docs.yml ================================================ name: Documentation on: push: branches: - main - 'release-' tags: '*' pull_request: jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - uses: julia-actions/setup-julia@v1 with: version: '1' - name: Install dependencies run: julia --project=docs/ -e 'using Pkg; Pkg.develop(PackageSpec(path=pwd())); Pkg.instantiate()' - name: Set DOCUMENTER_CURRENT_VERSION for tutorial download links run: | if [[ "${{ github.event_name }}" == "pull_request" ]]; then echo "DOCUMENTER_CURRENT_VERSION=previews/PR${{ github.event.pull_request.number }}" >> "$GITHUB_ENV" elif [[ "${{ github.ref }}" == refs/tags/* ]]; then echo "DOCUMENTER_CURRENT_VERSION=${GITHUB_REF_NAME}" >> "$GITHUB_ENV" elif [[ "${{ github.ref }}" == "refs/heads/main" ]] || [[ "${{ github.ref }}" =~ ^refs/heads/release- ]]; then echo "DOCUMENTER_CURRENT_VERSION=dev" >> "$GITHUB_ENV" fi - name: Build and deploy env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} DOCUMENTER_KEY: ${{ secrets.DOCUMENTER_KEY }} run: julia --project=docs --color=yes docs/make.jl ================================================ FILE: .github/workflows/format-check.yml ================================================ name: Format Check on: push: branches: - 'main' - 'release-' tags: '*' pull_request: jobs: build: runs-on: ${{ matrix.os }} strategy: matrix: julia-version: [1] julia-arch: [x86] os: [ubuntu-latest] steps: - uses: julia-actions/setup-julia@latest with: version: ${{ matrix.julia-version }} - uses: actions/checkout@v2 - name: Install JuliaFormatter and format run: | julia -e 'include("scripts/formatter/formatter_code.jl")' - uses: reviewdog/action-suggester@v1 if: github.event_name == 'pull_request' with: tool_name: JuliaFormatter fail_on_error: true - name: Format check run: | julia -e ' out = Cmd(`git diff --name-only`) |> read |> String if out == "" exit(0) else @error "Some files have not been formatted !!!" write(stdout, out) exit(1) end' ================================================ FILE: .github/workflows/main-tests.yml ================================================ name: Main - CI on: push: branches: - main schedule: - cron: 0 * * * * jobs: test: name: Julia ${{ matrix.julia-version }} - ${{ matrix.os }} runs-on: ${{ matrix.os }} strategy: matrix: julia-version: ['1', 'nightly'] julia-arch: [x64] os: [ubuntu-latest, windows-latest, macOS-latest] steps: - uses: actions/checkout@v2 - uses: julia-actions/setup-julia@latest continue-on-error: true with: version: ${{ matrix.julia-version }} arch: ${{ matrix.julia-arch }} - uses: julia-actions/julia-buildpkg@latest env: PYTHON: "" - uses: julia-actions/julia-runtest@latest continue-on-error: ${{ matrix.julia-version == 'nightly' }} env: PYTHON: "" - uses: julia-actions/julia-processcoverage@v1 - uses: codecov/codecov-action@v5 with: file: ./lcov.info flags: unittests name: codecov-umbrella fail_ci_if_error: false token: ${{ secrets.CODECOV_TOKEN }} ================================================ FILE: .github/workflows/performance_comparison.yml ================================================ name: 'Performance Comparison' on: pull_request: jobs: comparison: runs-on: ubuntu-latest steps: - uses: julia-actions/setup-julia@latest - uses: actions/checkout@v5 - name: Run Perfomance Test Main run: | julia --project=test -e 'using Pkg; Pkg.add(PackageSpec(name="PowerSimulations", rev="main")); Pkg.instantiate()' julia -t 4 --project=test test/performance/performance_test.jl "Main" - name: Run Perfomance Test Branch run: | julia --project=test -e 'using Pkg; Pkg.develop(PackageSpec(path=pwd())); Pkg.instantiate()' julia -t 4 --project=test test/performance/performance_test.jl "This Branch" - name: Read precompile results id: precompile_results run: | body="$(cat precompile_time.txt)" body="${body//'%'/'%25'}" body="${body//$'\n'/'%0A'}" body="${body//$'\r'/'%0D'}" echo "::set-output name=body::$body" - name: Read build results id: build_results run: | body="$(cat build_time.txt)" body="${body//'%'/'%25'}" 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@v4 id: fc with: issue-number: ${{ github.event.pull_request.number }} comment-author: 'github-actions[bot]' body-includes: Performance Results - name: Create comment if: steps.fc.outputs.comment-id == '' uses: peter-evans/create-or-update-comment@v5 with: issue-number: ${{ github.event.pull_request.number }} body: | Performance Results | Version | Precompile Time | | :--- | :----: | ${{ steps.precompile_results.outputs.body }} | 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@v5 with: comment-id: ${{ steps.fc.outputs.comment-id }} body: | Performance Results | Version | Precompile Time | | :--- | :----: | ${{ steps.precompile_results.outputs.body }} | Version | Build Time | | :--- | :----: | ${{ steps.build_results.outputs.body }} | Version | Build Time | | :--- | :----: | ${{ steps.solve_results.outputs.body }} edit-mode: replace ================================================ FILE: .github/workflows/pr_testing.yml ================================================ name: Test-CI on: pull_request: types: [opened, synchronize, reopened] jobs: test: name: Julia ${{ matrix.julia-version }} - ${{ matrix.os }} runs-on: ${{ matrix.os }} strategy: matrix: julia-version: ['1'] julia-arch: [x64] os: [ubuntu-latest, windows-latest, macOS-latest] steps: - uses: actions/checkout@v5 - uses: julia-actions/setup-julia@latest with: version: ${{ matrix.julia-version }} arch: ${{ matrix.julia-arch }} - uses: julia-actions/julia-buildpkg@latest env: PYTHON: "" - uses: julia-actions/julia-runtest@latest env: PYTHON: "" - uses: julia-actions/julia-processcoverage@v1 - uses: codecov/codecov-action@v5 with: file: ./lcov.info flags: unittests name: codecov-umbrella fail_ci_if_error: false token: ${{ secrets.CODECOV_TOKEN }} ================================================ FILE: .gitignore ================================================ test_simulation_results/* # Claude settings.json #Files generated by invoking Julia with --code-coverage *.jl.cov *.jl.*.cov *.log _*.jl # Files generated by invoking Julia with --track-allocation *.jl.mem # System-specific files and directories generated by the BinaryProvider and BinDeps packages # They contain absolute paths specific to the host computer, and so should not be committed deps/deps.jl deps/build.log deps/downloads/ deps/usr/ deps/src/ # Build artifacts for creating documentation generated by the Documenter package docs/build/ docs/site/ ## Autogenerated code during the documentation process generated*.md #Jupyter Ignores .ipynb_checkpoints/ .ipynb_checkpoints #Mac temp ignores .DS_Store #Figures *.pdf *.ipynb Manifest.toml .vscode *.h5 data # profiling results **/build_time.txt **/precompile_time.txt **/solve_time.txt ################################################################################ # Operating systems # ################################################################################ ######################################## # Linux # ######################################## *~ # temporary files which can be created if a process still has a handle open of # a deleted file .fuse_hidden* # KDE directory preferences .directory # Linux trash folder which might appear on any partition or disk .Trash-* # .nfs files are created when an open file is removed but is still being accessed .nfs* ######################################## # macOS # ######################################## # General .DS_Store .AppleDouble .LSOverride # Icon must end with two \r Icon # Thumbnails ._* # Files that might appear in the root of a volume .DocumentRevisions-V100 .fseventsd .Spotlight-V100 .TemporaryItems .Trashes .VolumeIcon.icns .com.apple.timemachine.donotpresent # Directories potentially created on remote AFP share .AppleDB .AppleDesktop Network Trash Folder Temporary Items .apdisk ######################################## # Windows # ######################################## # Windows thumbnail cache files Thumbs.db ehthumbs.db ehthumbs_vista.db # Dump file *.stackdump # Folder config file [Dd]esktop.ini # Recycle Bin used on file shares $RECYCLE.BIN/ # Windows Installer files *.cab *.msi *.msix *.msm *.msp # Windows shortcuts *.lnk ## Acknowledgements # Many thanks to `https://gitignore.io/`, written and maintained by Joe Blau, which contributed much material to this gitignore file. # Claude hooks settings.*.json settings.json ================================================ FILE: .pre-commit-config.yaml ================================================ repos: - repo: local hooks: - id: julia-formatter name: Run Julia formatter entry: julia scripts/formatter/formatter_code.jl language: system types: [file] pass_filenames: false ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing Community driven development of this package is encouraged. To maintain code quality standards, please adhere to the following guidelines when contributing: - To get started, sign the Contributor License Agreement. - Please do your best to adhere to the lengthy [Julia style guide](https://docs.julialang.org/en/latest/manual/style-guide/). - To submit code contributions, [fork](https://help.github.com/articles/fork-a-repo/) the repository, commit your changes, and [submit a pull request](https://help.github.com/articles/creating-a-pull-request-from-a-fork/). ================================================ FILE: LICENSE ================================================ BSD 3-Clause License Copyright (c) 2018, 2023 Alliance for Sustainable Energy, LLC and The Regents of the University of California All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================ FILE: Project.toml ================================================ name = "PowerSimulations" uuid = "e690365d-45e2-57bb-ac84-44ba829e73c4" authors = ["Jose Daniel Lara", "Clayton Barrows", "Daniel Thom", "Dheepak Krishnamurthy", "Sourabh Dalvi"] version = "0.34.2" [deps] CSV = "336ed68f-0bac-5ca0-87d4-7b16caf5d00b" DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0" DataFramesMeta = "1313f7d8-7da2-5740-9ea0-a2ca25f37964" DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8" Dates = "ade2ca70-3891-5945-98fb-dc099432e06a" Distributed = "8ba89e20-285c-5b6f-9357-94700520ee1b" Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f" DocStringExtensions = "ffbed154-4ef7-542d-bbb7-c09d3a79fcae" HDF5 = "f67ccb44-e63f-5c2f-98bd-6dc0ccc4ba2f" InfrastructureSystems = "2cd47ed4-ca9b-11e9-27f2-ab636a7671f1" InteractiveUtils = "b77e0a4c-d291-57a0-90e8-8db25a27a240" JSON3 = "0f8b85d8-7281-11e9-16c2-39a750bddbf1" JuMP = "4076af6c-e467-56ae-b986-b466b2749572" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" Logging = "56ddb016-857b-54e1-b83d-db4d58db5568" MathOptInterface = "b8f27783-ece8-5eb3-8dc8-9495eed66fee" PowerFlows = "94fada2c-fd9a-4e89-8d82-81405f5cb4f6" PowerModels = "c36e90e8-916a-50a6-bd94-075b64ef4655" PowerNetworkMatrices = "bed98974-b02a-5e2f-9fe0-a103f5c450dd" PowerSystems = "bcd98974-b02a-5e2f-9ee0-a103f5c450dd" PrettyTables = "08abe8d2-0d0c-5749-adfa-8a2ac140af0d" ProgressMeter = "92933f4c-e287-5a05-a399-4b506db050ca" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" Serialization = "9e88b42a-f829-5b0c-bbe9-9e923198166b" SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" TimeSeries = "9e3dc215-6440-5c97-bce1-76c03772f85e" TimerOutputs = "a759f4b9-e2f1-59dc-863e-4aeb61b1ea8f" [compat] CSV = "~0.10" DataFrames = "1" DataFramesMeta = "~0.15" DataStructures = "~0.18, ~0.19" Dates = "1" Distributed = "1" Distributions = "^0.25" DocStringExtensions = "~v0.9" HDF5 = "~0.17" InfrastructureSystems = "^3.5" InteractiveUtils = "1" JSON3 = "1" JuMP = "^1.28" LinearAlgebra = "1" Logging = "1" MathOptInterface = "1" PowerFlows = "^0.16" PowerModels = "^0.21.5" PowerNetworkMatrices = "^0.20" PowerSystems = "^5.8" PrettyTables = "2.4, 3.1" ProgressMeter = "^1.5" Random = "^1.10" Serialization = "1" SparseArrays = "1" TimeSeries = "~0.25" TimerOutputs = "~0.5" julia = "^1.10" ================================================ FILE: README.md ================================================ # PowerSimulations.jl [![Main - CI](https://github.com/Sienna-Platform/PowerSimulations.jl/actions/workflows/main-tests.yml/badge.svg)](https://github.com/Sienna-Platform/PowerSimulations.jl/actions/workflows/main-tests.yml) [![codecov](https://codecov.io/gh/Sienna-Platform/PowerSimulations.jl/branch/main/graph/badge.svg)](https://codecov.io/gh/Sienna-Platform/PowerSimulations.jl) [![Documentation](https://github.com/Sienna-Platform/PowerSimulations.jl/workflows/Documentation/badge.svg)](https://sienna-platform.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.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: - Provide a flexible modeling framework that can accommodate problems of different complexity and at different time-scales. - Streamline the construction of large scale optimization problems to avoid repetition of work when adding/modifying model details. - Exploit Julia's capabilities to improve computational performance of large scale power system quasi-static simulations. The flexible modeling framework is enabled through a modular set of capabilities that enable scalable power system analysis and exploration of new analysis methods. The modularity of PowerSimulations results from the structure of the simulations enabled by the package: - _Simulations_ define a set of problems that can be solved using numerical techniques. For example, an annual production cost modeling simulation can be created by formulating a unit commitment model against system data to assemble a set of 365 daily time-coupled scheduling problems. ## Simulations enabled by PowerSimulations - Integrated Resource Planning - Production Cost Modeling - Market Simulations ## Installation ```julia julia> ] (v1.9) pkg> add PowerSystems (v1.9) pkg> add PowerSimulations ``` ## Usage `PowerSimulations.jl` uses [PowerSystems.jl](https://github.com/Sienna-Platform/PowerSystems.jl) to handle the data used in the simulations. ```julia using PowerSimulations using PowerSystems ``` For information on using the package, see the [stable documentation](https://sienna-platform.github.io/PowerSimulations.jl/stable/). Use the [in-development documentation](https://sienna-platform.github.io/PowerSimulations.jl/dev/) for the version of the documentation which contains the unreleased features. ## Development Contributions to the development and enhancement of PowerSimulations is welcome. Please see [CONTRIBUTING.md](https://github.com/Sienna-Platform/PowerSimulations.jl/blob/main/CONTRIBUTING.md) for code contribution guidelines. ## License PowerSimulations is released under a BSD [license](https://github.com/Sienna-Platform/PowerSimulations.jl/blob/main/LICENSE). PowerSimulations has been developed as part of the Scalable Integrated Infrastructure Planning (SIIP) initiative at the U.S. Department of Energy's National Renewable Energy Laboratory ([NREL](https://www.nrel.gov/)) Software Record SWR-23-104. ================================================ FILE: codecov.yml ================================================ codecov: require_ci_to_pass: yes coverage: precision: 2 round: down range: "70...100" status: project: # measuring the overall project coverage default: # context, you can create multiple ones with custom titles enabled: yes # must be yes|true to enable this status target: auto # specify the target coverage for each commit status # option: "auto" (must increase from parent commit or pull request base) # option: "X%" a static target percentage to hit threshold: 5 # allowed to drop X% and still result in a "success" commit status if_not_found: success # if parent is not found report status as success, error, or failure if_ci_failed: error # if ci fails report status as success, error, or failure patch: default: target: 70 parsers: gcov: branch_detection: conditional: yes loop: yes method: no macro: no comment: layout: "reach,diff,flags,tree" behavior: default require_changes: no ================================================ FILE: docs/Makefile ================================================ BRANCH := $(shell git rev-parse --abbrev-ref HEAD) html: julia make.jl github: html -git branch -D gh-pages -git push origin --delete gh-pages ghp-import -n -b gh-pages -m "Update documentation" ./build git checkout gh-pages git push --set-upstream origin gh-pages git checkout ${BRANCH} all: github ================================================ FILE: docs/Project.toml ================================================ [deps] CSV = "336ed68f-0bac-5ca0-87d4-7b16caf5d00b" DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0" DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8" Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" DocumenterInterLinks = "d12716ef-a0f6-4df4-a9f1-a5a34e75c656" DocumenterTools = "35a29f4d-8980-5a13-9543-d66fff28ecb8" HiGHS = "87dc4568-4c63-4d18-b0c0-bb2238e4078b" HydroPowerSimulations = "fc1677e0-6ad7-4515-bf3a-bd6bf20a0b1b" InfrastructureSystems = "2cd47ed4-ca9b-11e9-27f2-ab636a7671f1" JuMP = "4076af6c-e467-56ae-b986-b466b2749572" Latexify = "23fbe1c1-3f47-55db-b15f-69d7ec21a316" Literate = "98b081ad-f1c9-55d3-8b20-4c87d4299306" PowerNetworkMatrices = "bed98974-b02a-5e2f-9fe0-a103f5c450dd" PowerSimulations = "e690365d-45e2-57bb-ac84-44ba829e73c4" PowerSystemCaseBuilder = "f00506e0-b84f-492a-93c2-c0a9afc4364e" PowerSystems = "bcd98974-b02a-5e2f-9ee0-a103f5c450dd" PrettyTables = "08abe8d2-0d0c-5749-adfa-8a2ac140af0d" TimeSeries = "9e3dc215-6440-5c97-bce1-76c03772f85e" [compat] Documenter = "^1.7" InfrastructureSystems = "3" julia = "^1.6" ================================================ FILE: docs/make.jl ================================================ using Documenter using PowerSystems using PowerSimulations using DataStructures using DocumenterInterLinks using Literate links = InterLinks( "Julia" => "https://docs.julialang.org/en/v1/", "InfrastructureSystems" => "https://sienna-platform.github.io/InfrastructureSystems.jl/stable/", "PowerSystems" => "https://sienna-platform.github.io/PowerSystems.jl/stable/", "PowerSimulations" => "https://sienna-platform.github.io/PowerSimulations.jl/stable/", "StorageSystemsSimulations" => "https://sienna-platform.github.io/StorageSystemsSimulations.jl/stable/", "HydroPowerSimulations" => "https://sienna-platform.github.io/HydroPowerSimulations.jl/dev/", ) include(joinpath(@__DIR__, "make_tutorials.jl")) make_tutorials() pages = OrderedDict( "Welcome Page" => "index.md", "Tutorials" => Any[ "Single-step Problem" => "tutorials/generated_decision_problem.md", "Multi-stage Production Cost Simulation" => "tutorials/generated_pcm_simulation.md", ], "How to..." => Any[ "...register a variable in a custom operation model" => "how_to/register_variable.md", "...create a problem template" => "how_to/problem_templates.md", "...read the simulation results" => "how_to/read_results.md", "...debug an infeasible model" => "how_to/debugging_infeasible_models.md", "...configure logging" => "how_to/logging.md", "...inspect simulation events using the recorder" => "how_to/simulation_recorder.md", "...run a parallel simulation" => "how_to/parallel_simulations.md", ], "Explanation" => Any[ "explanation/psi_structure.md", "explanation/feedforward.md", "explanation/chronologies.md", "explanation/sequencing.md", ], "Reference" => Any[ "Glossary and Acronyms" => "api/glossary.md", "Public API" => "api/PowerSimulations.md", "Developers" => ["Developer Guidelines" => "api/developer.md", "Internals" => "api/internal.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", "Branch" => "formulation_library/Branch.md", "Source" => "formulation_library/Source.md", "Services" => "formulation_library/Service.md", "Feedforwards" => "formulation_library/Feedforward.md", "Piecewise Linear Cost" => "formulation_library/Piecewise.md", ], ) makedocs(; modules = [PowerSimulations], format = Documenter.HTML(; prettyurls = haskey(ENV, "GITHUB_ACTIONS"), size_threshold = nothing), sitename = "PowerSimulations.jl", authors = "Jose Daniel Lara, Daniel Thom, Kate Doubleday, Rodrigo Henriquez-Auba, and Clayton Barrows", pages = Any[p for p in pages], plugins = [links], ) deploydocs(; repo = "github.com/Sienna-Platform/PowerSimulations.jl.git", target = "build", branch = "gh-pages", devbranch = "main", devurl = "dev", push_preview = true, versions = ["stable" => "v^", "v#.#"], ) ================================================ FILE: docs/make_tutorials.jl ================================================ using Pkg using Literate using DataFrames using PrettyTables # Limit DataFrame rendering during docs generation to avoid huge literal outputs. # Notes: # - Environment-variable approaches tested (`DATAFRAMES_ROWS`, `DATAFRAMES_COLUMNS`, # `LINES`, `COLUMNS`) did not constrain DataFrames output in this pipeline. # - We keep a docs-local Base.show override as a fallback and accept `kwargs...` # so explicit show(...; kwargs) calls do not error on unsupported keywords. function _env_int(name::String, default::Int) parsed = tryparse(Int, get(ENV, name, string(default))) return something(parsed, default) end const _DF_MAX_ROWS = _env_int("SIENNA_DOCS_DF_MAX_ROWS", 10) const _DF_MAX_COLS = _env_int("SIENNA_DOCS_DF_MAX_COLS", 80) function Base.show(io::IO, mime::MIME"text/plain", df::DataFrame; kwargs...) # Keep docs output bounded while allowing explicit caller kwargs. PrettyTables.pretty_table(io, df; backend = :text, maximum_number_of_rows = _DF_MAX_ROWS, maximum_number_of_columns = _DF_MAX_COLS, show_omitted_cell_summary = true, compact_printing = false, limit_printing = true, kwargs...) end function Base.show(io::IO, mime::MIME"text/html", df::DataFrame; kwargs...) PrettyTables.pretty_table(io, df; backend = :html, maximum_number_of_rows = _DF_MAX_ROWS, maximum_number_of_columns = _DF_MAX_COLS, show_omitted_cell_summary = true, compact_printing = false, limit_printing = true, kwargs...) end # Remove previously generated tutorial artifacts so a docs build only reflects # current source tutorials. # # Input: # - dir: tutorial output directory that can contain generated_*.md/ipynb. # Output: # - Deletes matching files in-place and logs each deletion. function clean_old_generated_files(dir::String) if !isdir(dir) @warn "Directory does not exist: $dir" return end generated_files = filter( f -> startswith(f, "generated_") && (endswith(f, ".md") || endswith(f, ".ipynb")), readdir(dir), ) for file in generated_files rm(joinpath(dir, file); force = true) @info "Removed old generated file: $file" end end ######################################################### # Literate post-processing functions for tutorial generation ######################################################### # Compute docs base URL from Documenter deploy context. # # Behavior: # - previews/PR123 -> .../previews/PR123 # - dev (or custom DOCUMENTER_DEVURL) -> .../dev # - tagged versions like v0.9 -> .../v0.9 # - fallback -> .../stable # # This keeps generated download/view-online links correct across preview, dev, # tagged, and stable deployments. function _compute_docs_base_url() base = "https://sienna-platform.github.io/PowerSimulations.jl" current_version = get(ENV, "DOCUMENTER_CURRENT_VERSION", "") # Preview builds (e.g. "previews/PR123") if startswith(current_version, "previews/PR") return "$base/$current_version" end # Dev builds if current_version == "dev" dev_suffix = get(ENV, "DOCUMENTER_DEVURL", "dev") return "$base/$dev_suffix" end # Tagged/versioned builds (e.g. "v0.9", "v1.2.3") if !isempty(current_version) && current_version != "stable" return "$base/$current_version" end # Default to stable return "$base/stable" end const _DOCS_BASE_URL = _compute_docs_base_url() """ Choose how tutorial download links are written in generated markdown. - **Absolute** (under `_DOCS_BASE_URL/tutorials/`): CI / Documenter context (`GITHUB_ACTIONS` or non-empty `DOCUMENTER_CURRENT_VERSION`) so previews, `dev`, and versioned URLs match `_compute_docs_base_url()`. - **Relative** (bare filenames): local/offline builds; files sit next to `generated_*.md` under `docs/src/tutorials/`. Override: `SIENNA_DOCS_DOWNLOAD_LINKS`=`absolute` or `relative`. """ function _downloads_use_absolute_urls() o = get(ENV, "SIENNA_DOCS_DOWNLOAD_LINKS", "") o == "absolute" && return true o == "relative" && return false haskey(ENV, "GITHUB_ACTIONS") && return true !isempty(get(ENV, "DOCUMENTER_CURRENT_VERSION", "")) && return true return false end # Replace APPEND_MARKDOWN("path/to/file.md") placeholders with file contents. # # Sample input: # "Before\nAPPEND_MARKDOWN(\"docs/src/tutorials/_snippet.md\")\nAfter" # Sample output: # "Before\n\nAfter" # # Notes: # - Uses a non-greedy-safe capture (`[^\"]*`) so multiple placeholders can be # replaced independently. function insert_md(content) pattern = r"APPEND_MARKDOWN\(\"([^\"]*)\"\)" if occursin(pattern, content) content = replace(content, pattern => m -> read(m.captures[1], String)) end return content end # Default display titles for Documenter admonition types when no custom title is given. # See https://documenter.juliadocs.org/stable/showcase/#Admonitions const _ADMONITION_DISPLAY_NAMES = Dict{String, String}( "note" => "Note", "info" => "Info", "tip" => "Tip", "warning" => "Warning", "danger" => "Danger", "compat" => "Compat", "todo" => "TODO", "details" => "Details", ) # Preprocess Literate source to convert Documenter-style admonitions into Jupyter-friendly # blockquotes. Used only for notebook output; markdown keeps `!!! type` and is rendered by # Documenter. Admonitions are not recognized by common mark or Jupyter; see # https://fredrikekre.github.io/Literate.jl/v2/tips/#admonitions-compatibility function preprocess_admonitions_for_notebook(str::AbstractString) lines = split(str, '\n'; keepempty = true) out = String[] i = 1 n = length(lines) admonition_start = r"^# !!! (note|info|tip|warning|danger|compat|todo|details)(?:\s+\"([^\"]*)\")?\s*$" content_line = r"^# (.*)$" # Documenter admonition body: # then 4 spaces blank_comment = r"^#\s*$" # # or # with only spaces while i <= n line = lines[i] m = match(admonition_start, line) if m !== nothing typ = lowercase(m.captures[1]) custom_title = m.captures[2] title = if custom_title !== nothing && !isempty(custom_title) custom_title else get(_ADMONITION_DISPLAY_NAMES, typ, titlecase(typ)) end push!(out, "# > *$(title)*") push!(out, "# >") i += 1 # Consume blank comment lines and content lines while i <= n l = lines[i] if match(blank_comment, l) !== nothing push!(out, "# >") i += 1 elseif (cm = match(content_line, l)) !== nothing push!(out, "# > " * cm.captures[1]) i += 1 else break end end continue end push!(out, line) i += 1 end return join(out, '\n') end # Inject a short "download tutorial files" sentence after the first markdown # heading in generated tutorial pages. # # Sample input: # "# Title\nBody..." # Sample output (conceptual): # "# Title\n\n*To follow along... [Julia script](.../tutorial.jl)...*\n\nBody..." # # Download links: # - **Deployed / CI**: absolute URLs under `_DOCS_BASE_URL` when `_downloads_use_absolute_urls()` is true. # - **Local**: bare filenames (siblings of `generated_*.md` in `docs/src/tutorials/`). function add_download_links(content, jl_file, ipynb_file) script_link, notebook_link = if _downloads_use_absolute_urls() ("$_DOCS_BASE_URL/tutorials/$(jl_file)", "$_DOCS_BASE_URL/tutorials/$(ipynb_file)") else (jl_file, ipynb_file) end download_section = """ *To follow along, you can download this tutorial as a [Julia script (.jl)]($(script_link)) or [Jupyter notebook (.ipynb)]($(notebook_link)).* """ # Insert after the first heading (which should be the title) # Match the first heading line and replace it with heading + download section m = match(r"^(#+ .+)$"m, content) if m !== nothing heading = m.match content = replace(content, r"^(#+ .+)$"m => heading * download_section; count = 1) end return content end # Insert a setup preface and captured `Pkg.status()` into the first markdown # cell of a generated notebook, immediately after the first heading. # # Sample effect: # - First markdown cell gains a "Set up" blockquote and an embedded code block # containing package versions from the docs build environment. function add_pkg_status_to_notebook(nb::Dict) cells = get(nb, "cells", []) if isempty(cells) return nb end # Find the first markdown cell first_markdown_idx = nothing for (i, cell) in enumerate(cells) if get(cell, "cell_type", "") == "markdown" first_markdown_idx = i break end end if first_markdown_idx === nothing return nb # No markdown cell found, return unchanged end first_cell = cells[first_markdown_idx] cell_source = get(first_cell, "source", []) # Convert source array to string to find the first heading source_text = join(cell_source) # Find the first heading (lines starting with #) heading_pattern = r"^(#+\s+.+?)$"m heading_match = match(heading_pattern, source_text) if heading_match === nothing return nb # No heading found, return unchanged end # Capture Pkg.status() output at build time io = IOBuffer() Pkg.status(; io = io) pkg_status_output = String(take!(io)) # Create the content to insert: blockquote "Set up" with setup instructions and pkg.status() # Blockquote title and body; hyperlinks for IJulia and create an environment preface_lines = [ "\n", "> **Set up**\n", ">\n", "> To run this notebook, first install the Julia kernel for Jupyter Notebooks using [IJulia](https://julialang.github.io/IJulia.jl/stable/manual/installation/), then [create an environment](https://pkgdocs.julialang.org/v1/environments/) for this tutorial with the packages listed with `using ` further down.\n", ">\n", "> This tutorial has demonstrated compatibility with these package versions. If you run into any errors, first check your package versions for consistency using `Pkg.status()`.\n", ">\n", ] # Format Pkg.status() output as a code block inside the blockquote pkg_status_lines = split(pkg_status_output, '\n'; keepempty = true) pkg_status_block = [" > ```\n"] for line in pkg_status_lines push!(pkg_status_block, " > " * line * "\n") end push!(pkg_status_block, " > ```\n", "\n") # Find the first heading line in the source array heading_line_idx = nothing for (i, line) in enumerate(cell_source) if match(heading_pattern, line) !== nothing heading_line_idx = i break end end if heading_line_idx === nothing return nb # Couldn't find heading line end # Build new source array new_source = String[] # Add all lines up to and including the heading line for i in 1:heading_line_idx push!(new_source, cell_source[i]) end # Add the preface and pkg.status content right after the heading append!(new_source, preface_lines) append!(new_source, pkg_status_block) # Add all remaining lines after the heading for i in (heading_line_idx + 1):length(cell_source) push!(new_source, cell_source[i]) end # Update the cell source first_cell["source"] = new_source cells[first_markdown_idx] = first_cell nb["cells"] = cells return nb end # Add italicized "view online" comment after each image from ```@raw html ... ``` (or # the raw HTML / markdown form Literate writes). Used as a postprocess in Literate.notebook. # Literate strips the backtick wrapper and outputs raw HTML; we match that multi-line block. # Sample effect: # - If a markdown cell contains one or more image fragments, append exactly one # "view online" fallback note at the end of that cell. # - If the note already exists in the cell, no change is applied. function add_image_links(nb::Dict, outputfile_base::AbstractString) tutorial_url = "$_DOCS_BASE_URL/tutorials/$(outputfile_base)/" msg = "_If image is not available when viewing in a Jupyter notebook, view the tutorial online [here]($tutorial_url)._" cells = get(nb, "cells", []) for (idx, cell) in enumerate(cells) get(cell, "cell_type", "") != "markdown" && continue source = get(cell, "source", []) isempty(source) && continue text = join(source) # Check if this cell already has the "view online" message to avoid duplicates contains(text, "If image is not available when viewing in a Jupyter notebook") && continue suffix = "\n\n" * msg * "\n" # If the cell has any of the image shapes below, we append one "view online" note. # We build one alternation pattern from sub-patterns (each line is one case). # # HTML paragraph wrapping an (Literate often emits

). # ]*> — opening

and attributes # [\s\S]*? — any chars, non-greedy, up to the first — from p_with_img_pattern = r"]*>[\s\S]*?" # Documenter @raw html chunk that Literate inlines in the notebook (backticks removed in output). # ```@raw html — start marker # [\s\S]*? — block body, non-greedy # ``` — end fence raw_html_block_pattern = r"```@raw html[\s\S]*?```" # Standard markdown image: ![alt text](url) # !\[…\] — alt in brackets; \(…\) — path in parens markdown_image_pattern = r"!\[[^\]]*\]\([^\)]*\)" # A bare not already covered by the

case above. # ]*? — attributes; /?> — self-closing or > standalone_img_pattern = r"]*?/?>" # Union of the four cases: (?: A | B | C | D ) image_fragment_pattern = Regex( "(?:" * p_with_img_pattern.pattern * "|" * raw_html_block_pattern.pattern * "|" * markdown_image_pattern.pattern * "|" * standalone_img_pattern.pattern * ")", ) if occursin(image_fragment_pattern, text) text *= suffix end # Convert back to notebook source array (lines, last without trailing \n if non-empty) lines = split(text, "\n"; keepempty = true) new_source = String[] for i in 1:length(lines) if i < length(lines) push!(new_source, lines[i] * "\n") else isempty(lines[i]) || push!(new_source, lines[i]) end end cell["source"] = new_source cells[idx] = cell end nb["cells"] = cells return nb end ######################################################### # Process tutorials with Literate ######################################################### # Generate tutorial markdown + notebook artifacts from literate .jl sources. # # Pipeline: # 1) discover tutorial .jl files (excluding helper files starting with "_") # 2) generate Documenter-flavored markdown with injected download links # 3) generate notebook with admonition conversion, setup preface, and image note function make_tutorials() tutorials_dir = abspath(joinpath(@__DIR__, "src", "tutorials")) # Exclude helper scripts that start with "_" if isdir(tutorials_dir) tutorial_files = filter( x -> endswith(x, ".jl") && !startswith(x, "_"), readdir(tutorials_dir), ) if !isempty(tutorial_files) # Clean up old generated tutorial files tutorial_outputdir = tutorials_dir clean_old_generated_files(tutorial_outputdir) for file in tutorial_files @show file infile_path = joinpath(tutorials_dir, file) execute = if occursin("EXECUTE = TRUE", uppercase(readline(infile_path))) true else false end outputfile = string("generated_", replace("$file", ".jl" => "")) # Generate markdown Literate.markdown(infile_path, tutorial_outputdir; name = outputfile, credit = false, flavor = Literate.DocumenterFlavor(), documenter = true, postprocess = ( content -> add_download_links( insert_md(content), file, string(outputfile, ".ipynb"), ) ), execute = execute) # Generate notebook (chain add_image_links after add_pkg_status_to_notebook). # preprocess_admonitions_for_notebook converts Documenter admonitions to blockquotes # so they render in Jupyter; markdown output keeps !!! style for Documenter. Literate.notebook(infile_path, tutorial_outputdir; name = outputfile, credit = false, execute = false, preprocess = preprocess_admonitions_for_notebook, postprocess = nb -> add_image_links(add_pkg_status_to_notebook(nb), outputfile)) end end end end ================================================ FILE: docs/src/api/PowerSimulations.md ================================================ ```@meta CurrentModule = PowerSimulations DocTestSetup = quote using PowerSimulations end ``` # API Reference ```@contents Pages = ["PowerSimulations.md"] Depth = 3 ``` ```@raw html     ``` ## Device Models List of structures and methods for Device models ```@docs DeviceModel ``` ### Formulations Refer to the [Formulations Page](@ref formulation_library) for each Abstract Device Formulation. HVDC formulations will be moved to its own section in future releases ### HVDC Formulations ```@docs TransportHVDCNetworkModel VoltageDispatchHVDCNetworkModel HVDCTwoTerminalLCC ``` ### Converter Formulations ```@docs QuadraticLossConverter ``` ### DC Lines Formulations ```@docs DCLossyLine ``` ### Synchronous Condenser Formulations ```@docs SynchronousCondenserBasicDispatch ``` ### Problem Templates ```@autodocs Modules = [PowerSimulations] Pages = ["problem_template.jl", "operation_problem_templates.jl", ] Order = [:type, :function] Public = true Private = false ``` ```@raw html     ``` * * * ## Decision Models ```@autodocs Modules = [PowerSimulations] Pages = ["decision_model.jl", ] Order = [:type, :function] Public = true Private = false ``` ```@raw html   ``` ```@docs GenericOpProblem ``` ```@raw html     ``` * * * ## Emulation Models ```@docs EmulationModel EmulationModel(::Type{M} where {M <: EmulationProblem}, ::ProblemTemplate, ::PSY.System, ::Union{Nothing, JuMP.Model}) build!(::EmulationModel) run!(::EmulationModel) solve!(::Int, ::EmulationModel{<:EmulationProblem}, ::Dates.DateTime, ::SimulationStore) ``` ```@raw html     ``` * * * ## Service Models List of structures and methods for Service models ```@docs ServiceModel ``` ```@raw html     ``` * * * ## Simulation Models ```@docs InitialCondition SimulationModels SimulationSequence Simulation build!(::Simulation) execute!(::Simulation) ``` ```@autodocs Modules = [PowerSimulations] Pages = ["simulation_partitions.jl", ] Order = [:type, :function] Public = true Private = false ``` ```@raw html     ``` ## Chronology Models ```@autodocs Modules = [PowerSimulations] Pages = ["initial_condition_chronologies.jl", ] Order = [:type, :function] Public = true Private = false ``` * * * ## Variables For a list of variables for each device refer to its Formulations page. ### Common Variables ```@docs ActivePowerVariable ReactivePowerVariable PiecewiseLinearCostVariable RateofChangeConstraintSlackUp RateofChangeConstraintSlackDown PostContingencyActivePowerChangeVariable ``` ### Thermal Unit Variables ```@docs OnVariable StartVariable StopVariable HotStartVariable WarmStartVariable ColdStartVariable PowerAboveMinimumVariable ``` ### Storage Unit Variables ```@docs ReservationVariable EnergyVariable ActivePowerOutVariable ActivePowerInVariable ``` ### Load Variables ```@docs ShiftUpActivePowerVariable ShiftDownActivePowerVariable ``` ### Branches and Network Variables ```@docs FlowActivePowerVariable FlowActivePowerSlackUpperBound FlowActivePowerSlackLowerBound FlowActivePowerFromToVariable FlowActivePowerToFromVariable FlowReactivePowerFromToVariable FlowReactivePowerToFromVariable PhaseShifterAngle HVDCLosses HVDCFlowDirectionVariable VoltageMagnitude VoltageAngle ``` ### Two Terminal and Multi-Terminal HVDC Variables ```@docs InterpolationBinarySquaredCurrentVariable SquaredDCVoltage DCLineCurrent InterpolationSquaredVoltageVariable InterpolationBinarySquaredVoltageVariable AuxBilinearConverterVariable AuxBilinearSquaredConverterVariable InterpolationSquaredBilinearVariable InterpolationBinarySquaredBilinearVariable InterpolationSquaredCurrentVariable DCVoltage ConverterCurrent SquaredConverterCurrent ConverterPositiveCurrent ConverterNegativeCurrent ConverterPowerDirection ``` ### Services Variables ```@docs ActivePowerReserveVariable ServiceRequirementVariable SystemBalanceSlackUp SystemBalanceSlackDown ReserveRequirementSlack InterfaceFlowSlackUp InterfaceFlowSlackDown PostContingencyActivePowerReserveDeploymentVariable ``` ### Feedforward Variables ```@docs UpperBoundFeedForwardSlack LowerBoundFeedForwardSlack ``` ```@raw html     ``` * * * ## Auxiliary Variables ### Thermal Unit Auxiliary Variables ```@docs TimeDurationOn TimeDurationOff PowerOutput ``` ### Bus Auxiliary Variables ```@docs PowerFlowVoltageAngle PowerFlowVoltageMagnitude PowerFlowLossFactors PowerFlowVoltageStabilityFactors ``` ### Branch Auxiliary Variables ```@docs PowerFlowBranchReactivePowerFromTo PowerFlowBranchReactivePowerToFrom PowerFlowBranchActivePowerFromTo PowerFlowBranchActivePowerToFrom PowerFlowBranchActivePowerLoss ``` ```@raw html     ``` * * * ## Constraints ### Common Constraints ```@docs PiecewiseLinearCostConstraint ``` ### Network Constraints ```@docs CopperPlateBalanceConstraint NodalBalanceActiveConstraint NodalBalanceReactiveConstraint AreaParticipationAssignmentConstraint ``` ### Power Variable Limit Constraints ```@docs ActivePowerVariableLimitsConstraint ReactivePowerVariableLimitsConstraint ActivePowerVariableTimeSeriesLimitsConstraint InputActivePowerVariableLimitsConstraint OutputActivePowerVariableLimitsConstraint ActivePowerInVariableTimeSeriesLimitsConstraint ActivePowerOutVariableTimeSeriesLimitsConstraint ``` ### Services Constraints ```@docs RequirementConstraint ParticipationFractionConstraint ReservePowerConstraint ``` ### Thermal Unit Constraints ```@docs ActiveRangeICConstraint CommitmentConstraint DurationConstraint RampConstraint StartupInitialConditionConstraint StartupTimeLimitTemperatureConstraint ``` ### Renewable Unit Constraints ```@docs EqualityConstraint ``` ## Source Constraints ```@docs ImportExportBudgetConstraint ``` ## Load Constraints ```@docs ShiftDownActivePowerVariableLimitsConstraint NonAnticipativityConstraint ShiftUpActivePowerVariableLimitsConstraint RealizedShiftedLoadMinimumBoundConstraint ShiftedActivePowerBalanceConstraint ``` ### Branches Constraints ```@docs FlowLimitConstraint FlowRateConstraint FlowRateConstraintFromTo FlowRateConstraintToFrom HVDCPowerBalance NetworkFlowConstraint PhaseAngleControlLimit ``` ### Two Terminal and Multi-Terminal HVDC Constraints ```@docs ConverterLossConstraint InterpolationVoltageConstraints InterpolationCurrentConstraints InterpolationBilinearConstraints CurrentAbsoluteValueConstraint ConverterPowerCalculationConstraint ConverterMcCormickEnvelopes DCLineCurrentConstraint DCCurrentBalance ``` ### Contingency Constraints ```@docs PostContingencyGenerationBalanceConstraint PostContingencyActivePowerVariableLimitsConstraint PostContingencyActivePowerReserveDeploymentVariableLimitsConstraint ``` ### Market Bid Cost Constraints ```@docs PiecewiseLinearBlockIncrementalOfferConstraint PiecewiseLinearBlockDecrementalOfferConstraint ``` ### Feedforward Constraints ```@docs FeedforwardSemiContinuousConstraint FeedforwardUpperBoundConstraint FeedforwardLowerBoundConstraint ``` ```@raw html     ``` * * * ## Parameters ### Time Series Parameters ```@docs ActivePowerTimeSeriesParameter ReactivePowerTimeSeriesParameter RequirementTimeSeriesParameter ReactivePowerOffsetParameter ActivePowerOutTimeSeriesParameter ActivePowerInTimeSeriesParameter FuelCostParameter FromToFlowLimitParameter ToFromFlowLimitParameter ``` ### Variable Value Parameters ```@docs UpperBoundValueParameter LowerBoundValueParameter OnStatusParameter FixValueParameter ``` ### Objective Function Parameters ```@docs CostFunctionParameter ``` ### Events Parameters ```@docs AvailableStatusChangeCountdownParameter AvailableStatusParameter ActivePowerOffsetParameter DynamicBranchRatingTimeSeriesParameter PostContingencyDynamicBranchRatingTimeSeriesParameter ``` ## Results ### Acessing Optimization Model ```@autodocs Modules = [PowerSimulations] Pages = ["optimization_container.jl", "optimization_debugging.jl" ] Order = [:type, :function] Public = true Private = false ``` ### Accessing Problem Results ```@autodocs Modules = [PowerSimulations] Pages = ["operation/problem_results.jl", ] Order = [:type, :function] Public = true Private = false ``` ### Accessing Simulation Results ```@autodocs Modules = [PowerSimulations] Pages = ["simulation_results.jl", "simulation_problem_results.jl", "simulation_partition_results.jl", "hdf_simulation_store.jl" ] Order = [:type, :function] Public = true Private = false ``` ## Simulation Recorder ```@autodocs Modules = [PowerSimulations] Pages = ["utils/recorder_events.jl", ] Order = [:type, :function] Public = true Private = false ``` ================================================ FILE: docs/src/api/developer.md ================================================ # Guidelines for Developers In order to contribute to `PowerSimulations.jl` repository please read the following sections of [`InfrastructureSystems.jl`](https://github.com/Sienna-Platform/InfrastructureSystems.jl) documentation in detail: 1. [Style Guide](https://nrel-Sienna.github.io/InfrastructureSystems.jl/stable/style/) 2. [Contributing Guidelines](https://github.com/Sienna-Platform/PowerSimulations.jl/blob/main/CONTRIBUTING.md) Pull requests are always welcome to fix bugs or add additional modeling capabilities. **All the code contributions need to include tests with a minimum coverage of 70%** ================================================ FILE: docs/src/api/glossary.md ================================================ # 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 tutorial on [Running a Single-Step Problem](@ref) 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. 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://sienna-platform.github.io/PowerSystems.jl/stable/modeler_guide/time_series/) ## I - *Interval*: The amount of time between updates to the decision problem. For instance, Day-Ahead problems usually have a 24-hour intervals and Real-Time problems have 5-minute intervals. Check the time [Time Series Data Section in PowerSystems.jl](https://sienna-platform.github.io/PowerSystems.jl/stable/modeler_guide/time_series/) ## R - *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://sienna-platform.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). ================================================ FILE: docs/src/api/internal.md ================================================ ```@meta CollapsedDocStrings = true ``` # Internal API ```@autodocs Modules = [PowerSimulations] Public = false ``` ================================================ FILE: docs/src/code_base_developer_guide/extending_powersimulations.md ================================================ # Extending Source Code Functionalities ## Enable other recorder events Other types of recorder events can be enabled with a possible performance impact. To do this pass in the specific recorder names to be enabled when you call build. ```julia sim = Simulation(...) recorders = [:execution] build!(sim; recorders = recorders) execute!(sim) ``` Now we can examine InitialConditionUpdateEvents for specific steps and stages. ```julia show_simulation_events( PSI.InitialConditionUpdateEvent, "./output/aggregation/1", x -> x.initial_condition_type == "DeviceStatus"; step = 2, stage = 1 ) ┌─────────────────────────────┬─────────────────────┬────────────────────────┬─────────────────┬─────────────┬─────┬──────────────┐ │ name │ simulation_time │ initial_condition_type │ device_type │ device_name │ val │ stage_number │ ├─────────────────────────────┼─────────────────────┼────────────────────────┼─────────────────┼─────────────┼─────┼──────────────┤ │ InitialConditionUpdateEvent │ 2024-01-02T00:00:00 │ DeviceStatus │ ThermalStandard │ Solitude │ 0.0 │ 1 │ │ InitialConditionUpdateEvent │ 2024-01-02T00:00:00 │ DeviceStatus │ ThermalStandard │ Park City │ 1.0 │ 1 │ │ InitialConditionUpdateEvent │ 2024-01-02T00:00:00 │ DeviceStatus │ ThermalStandard │ Alta │ 1.0 │ 1 │ │ InitialConditionUpdateEvent │ 2024-01-02T00:00:00 │ DeviceStatus │ ThermalStandard │ Brighton │ 1.0 │ 1 │ │ InitialConditionUpdateEvent │ 2024-01-02T00:00:00 │ DeviceStatus │ ThermalStandard │ Sundance │ 0.0 │ 1 │ └─────────────────────────────┴─────────────────────┴────────────────────────┴─────────────────┴─────────────┴─────┴──────────────┘ ``` ## Show the wall time with your events Sometimes you might want to see how the events line up with the wall time. ```julia show_simulation_events( PSI.InitialConditionUpdateEvent, "./output/aggregation/1", x -> x.initial_condition_type == "DeviceStatus"; step = 2, stage = 1, wall_time = true ) ┌─────────────────────────┬─────────────────────────────┬─────────────────────┬────────────────────────┬─────────────────┬─────────────┬─────┬──────────────┐ │ timestamp │ name │ simulation_time │ initial_condition_type │ device_type │ device_name │ val │ stage_number │ ├─────────────────────────┼─────────────────────────────┼─────────────────────┼────────────────────────┼─────────────────┼─────────────┼─────┼──────────────┤ │ 2020-04-07T15:08:32.711 │ InitialConditionUpdateEvent │ 2024-01-02T00:00:00 │ DeviceStatus │ ThermalStandard │ Solitude │ 0.0 │ 1 │ │ 2020-04-07T15:08:32.711 │ InitialConditionUpdateEvent │ 2024-01-02T00:00:00 │ DeviceStatus │ ThermalStandard │ Park City │ 1.0 │ 1 │ │ 2020-04-07T15:08:32.711 │ InitialConditionUpdateEvent │ 2024-01-02T00:00:00 │ DeviceStatus │ ThermalStandard │ Alta │ 1.0 │ 1 │ │ 2020-04-07T15:08:32.711 │ InitialConditionUpdateEvent │ 2024-01-02T00:00:00 │ DeviceStatus │ ThermalStandard │ Brighton │ 1.0 │ 1 │ │ 2020-04-07T15:08:32.711 │ InitialConditionUpdateEvent │ 2024-01-02T00:00:00 │ DeviceStatus │ ThermalStandard │ Sundance │ 0.0 │ 1 │ └─────────────────────────┴─────────────────────────────┴─────────────────────┴────────────────────────┴─────────────────┴─────────────┴─────┴──────────────┘ ``` ================================================ FILE: docs/src/explanation/chronologies.md ================================================ # [Chronologies](@id 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. ================================================ FILE: docs/src/explanation/feedforward.md ================================================ # [Feedforward](@id 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. 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. ================================================ FILE: docs/src/explanation/psi_structure.md ================================================ # [PowerSimulations.jl Modeling Structure](@id psi_structure) PowerSimulations enables the simulation of a sequence of power systems optimization problems and provides user control over each aspect of the simulation configuration. Specifically: - mathematical formulations can be selected for each component with [`DeviceModel`](@ref) and [`ServiceModel`](@ref) - a problem can be defined by creating model entries in a [Operations `ProblemTemplate`s](@ref op_problem_template) - models ([`DecisionModel`](@ref) or [`EmulationModel`](@ref)) can be built by applying a `ProblemTemplate` to a `System` and can be executed/solved in isolation or as part of a [`Simulation`](@ref Simulation(::SimulationSequence,::String,::Int,::SimulationModels,::AbstractString, ::Any)) - [`Simulation`](@ref Simulation(::SimulationSequence,::String,::Int,::SimulationModels,::AbstractString, ::Any))s can be defined and executed by sequencing one or more models and defining how and when data flows between models. !!! question "What is the difference between a Model and a Problem?" A "Problem" is an abstract mathematical description of how to represent power system behavior, whereas a "Model" is a concrete representation of a "Problem" applied to a dataset. I.e. once a Problem is populated with data describing all the loads, generators, lines, etc., it becomes a Model. ================================================ FILE: docs/src/explanation/sequencing.md ================================================ # [Sequencing](@id 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 = 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). ================================================ FILE: docs/src/formulation_library/Branch.md ================================================ # `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. ### 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. [`TwoTerminalLCCLine`](#TwoTerminalLCCLine) 9. [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` 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*} ``` * * * ## `PhaseAngleControl` Formulation valid for `PTDFPowerModel` Network model ```@docs 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``. * * * ## `TwoTerminalLCCLine` Formulation valid for `ACPPowerModel` Network model **Variables:** - [`HVDCRectifierDelayAngleVariable`]: + Bounds: ``(-\alpha_{r,t}^\text{min},\alpha_{r,t}^\text{max})`` + Symbol: ``\alpha_{r,t}`` - [`HVDCInverterExtinctionAngleVariable`]: + Bounds: ``(-\gamma_{i,t}^\text{min},\gamma_{i,t}^\text{max})`` + Symbol: ``\gamma_{i,t}`` - [`HVDCRectifierPowerFactorAngleVariable`]: + Bounds: ``\{0,1\}`` + Symbol: ``\phi_{r,t}`` - [`HVDCInverterPowerFactorAngleVariable`]: + Bounds: ``\{0,1\}`` + Symbol: ``\phi_{i,t}`` - [`HVDCRectifierOverlapAngleVariable`]: + Bounds: [0.0, ] + Symbol: ``\mu_{r,t}`` - [`HVDCInverterOverlapAngleVariable`]: + Bounds: [0.0, ] + Symbol: ``\mu_{i,t}`` - [`HVDCRectifierTapSettingVariable`]: + Bounds: ``(t_{r,t}^\text{min},t_{r,t}^\text{max})`` + Symbol: ``t_{r,t}`` - [`HVDCInverterTapSettingVariable`]: + Bounds: ``(t_{i,t}^\text{min},t_{i,t}^\text{max})`` + Symbol: ``t_{i,t}`` - [`HVDCRectifierDCVoltageVariable`]: + Bounds: [0.0, ] + Symbol: ``v_{r,t}^\text{dc}`` - [`HVDCInverterDCVoltageVariable`]: + Bounds: [0.0, ] + Symbol: ``v_{i,t}^\text{dc}`` - [`HVDCRectifierACCurrentVariable`]: + Bounds: [0.0, ] + Symbol: ``I_{r,t}^\text{ac}`` - [`HVDCInverterACCurrentVariable`]: + Bounds: [0.0, ] + Symbol: ``I_{i,t}^\text{ac}`` - [`DCLineCurrentFlowVariable`]: + Bounds: [0.0, ] + Symbol: ``I^\text{dc}`` - [`HVDCActivePowerReceivedFromVariable`]: + Bounds: [0.0, ] + Symbol: ``p_{r,t}^\text{ac}`` - [`HVDCActivePowerReceivedToVariable`]: + Bounds: [0.0, ] + Symbol: ``p_{i,t}^\text{ac}`` - [`HVDCReactivePowerReceivedFromVariable`]: + Bounds: [0.0, ] + Symbol: ``q_{r,t}^\text{ac}`` - [`HVDCReactivePowerReceivedToVariable`]: + Bounds: [0.0, ] + Symbol: ``q_{i,t}^\text{ac}`` **Static Parameters** - ``R^\text{dc}`` = `PowerSystems.get_r(lcc)` - ``N_r`` = `PowerSystems.get_rectifier_bridges(lcc)` - ``N_i`` = `PowerSystems.get_inverter_bridges(lcc)` - ``X_r`` = `PowerSystems.get_rectifier_xc(lcc)` - ``X_i`` = `PowerSystems.get_inverter_xc(lcc)` - ``a_r`` = `PowerSystems.get_rectifier_transformer_ratio(lcc)` - ``a_i`` = `PowerSystems.get_inverter_transformer_ratio(lcc)` - ``t_r`` = `PowerSystems.get_rectifier_tap_setting(lcc)` - ``t_i`` = `PowerSystems.get_inverter_tap_setting(lcc)` - ``t^\text{min}_r`` = `PowerSystems.get_rectifier_tap_setting(lcc).min` - ``t^\text{max}_r`` = `PowerSystems.get_rectifier_tap_setting(lcc).max` - ``t^\text{min}_i`` = `PowerSystems.get_inverter_tap_setting(lcc).min` - ``t^\text{max}_i`` = `PowerSystems.get_inverter_tap_setting(lcc).max` **Objective:** No changes to objective function **Expressions:** The variable `HVDCActivePowerReceivedFromVariable` ``p_{r,t}^\text{ac}`` is added to the nodal balance expression `ActivePowerBalance` as a negative load, since the rectifier takes power from the AC system and to injects it into the DC system. On the other hand, the variable `HVDCActivePowerReceivedToVariable` ``p_{i,t}^\text{ac}`` is added to the nodal balance expression `ActivePowerBalance` as a positive load, since it takes the power from the DC system and injects it back into the AC system. The variables `HVDCReactivePowerReceivedFromVariable` ``q_{r,t}^\text{ac}`` and `HVDCReactivePowerReceivedToVariable` ``q_{i,t}^\text{ac}`are added to the nodal balance expression`ActivePowerBalance` as positive loads, since they consume reactive power from the AC system to allow current transfer in converters during commutation. **Constraints:** - **Rectifier:** ```math \begin{aligned} & v^\text{dc}_{r,t} = \frac{3}{\pi}N_r \left( \sqrt{2}\frac{a_r v^\text{ac}_{r,t}}{t_{r,t}}\\cos{\alpha_{r,t}}-X_r I^\text{dc}_t \right)\\ & \mu_{r,t} = \arccos \left( \cos\alpha_{r,t} - \frac{\sqrt{2} I^\text{dc}_t X_r t_{r,t}}{a_r v^\text{ac}_{r,t}} \right) - \alpha_{r,t}\\ & \phi_{r,t} = \arctan \left( \frac{2\mu_{r,t} + \sin(2\alpha_{r,t}) - \sin(2\mu_{r,t} + 2\alpha_{r,t})}{\cos(2\alpha_{r,t}) - \cos(2\mu_{r,t} + 2\alpha_{r,t})} \right)\\ \end{aligned} ``` Which can be approximated as: ```math \begin{aligned} & \phi_{r,t} = arccos(\frac{1}{2}\cos\alpha_{r,t} + \frac{1}{2}\cos(\alpha_{r,t} + \mu_{r,t})) \end{aligned} ``` ```math \begin{aligned} & I^\text{ac}_{r,t} = \sqrt{6} \frac{N_r}{\pi} I^\text{dc}_t\\ & p^\text{ac}_{r,t} = \sqrt{3} I^\text{ac}_{r,t} \frac{a_r v^\text{ac}_{r,t}}{t_{r,t}}\cos{\phi_{r,t}} \\ & q^\text{ac}_{r,t} = \sqrt{3} I^\text{ac}_{r,t} \frac{a_r v^\text{ac}_{r,t}}{t_{r,t}}\sin{\phi_{r,t}} \\ \end{aligned} ``` - **Inverter:** ```math \begin{aligned} & v^\text{dc}_{i,t} = \frac{3}{\pi}N_i \left( \sqrt{2}\frac{a_i v^\text{ac}_{i,t}}{t_{i,t}}\\cos{\gamma_{i,t}}-X_i I^\text{dc}_t \right)\\ & \mu_{i,t} = \arccos \left( \cos\gamma_{i,t} - \frac{\sqrt{2} I^\text{dc}_t X_i t_{i,t}}{a_i v^\text{ac}_{i,t}} \right) - \gamma_{i,t}\\ & \phi_{i,t} = \arctan \left( \frac{2\mu_{i,t} + \sin(2\gamma_{i,t}) - \sin(2\mu_{i,t} + 2\gamma_{i,t})}{\cos(2\gamma_{i,t}) - \cos(2\mu_{r,t} + 2\gamma_{i,t})} \right)\\ \end{aligned} ``` Which can be approximated as: ```math \begin{aligned} & \phi_{i,t} = arccos(\frac{1}{2}\cos\gamma_{i,t} + \frac{1}{2}\cos(\gamma_{i,t} + \mu_{i,t})) \end{aligned} ``` ```math \begin{aligned} & I^\text{ac}_{i,t} = \sqrt{6} \frac{N_i}{\pi} I^\text{dc}_t\\ & p^\text{ac}_{i,t} = \sqrt{3} I^\text{ac}_{i,t} \frac{a_i v^\text{ac}_{i,t}}{t_{i,t}}\cos{\phi_{i,t}} \\ & q^\text{ac}_{i,t} = \sqrt{3} I^\text{ac}_{i,t} \frac{a_i v^\text{ac}_{i,t}}{t_{i,t}}\sin{\phi_{i,t}} \\ \end{aligned} ``` - **DC Transmission Line:** ```math \begin{aligned} & v^\text{dc}_{i,t} = v^\text{dc}_{r,t} - R_\text{dc}I^\text{dc}_t \end{aligned} ``` * * * ## Valid configurations Valid [`DeviceModel`](@ref)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) ``` ================================================ FILE: docs/src/formulation_library/DCModels.md ================================================ # DC Models formulations !!! note Multi-terminal DC models are still in early stages of development and future versions will add a more comprehensive list of formulations * * * ## LosslessLine `LosslessLine` models are used with `PSY.DCBranch` models. ```@docs LosslessLine ``` **Variables:** - [`FlowActivePowerVariable`](@ref): + Bounds: ``(R^\text{min},R^\text{max})`` + 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` Then, the minimum and maximum are computed as `R^\text{min} = \min(R^\text{from,min}, R^\text{to,min})` and `R^\text{max} = \min(R^\text{from,max}, R^\text{to,max})` **Objective:** No cost is added to the objective function. **Expressions:** The variable `FlowActivePowerVariable` ``f`` is added to the nodal balance expression `ActivePowerBalance` for DC Buses, by adding the flow ``f`` in the receiving DC bus and subtracting it from the sending DC bus. **Constraints:** No constraints are added to the function. * * * ## LosslessConverter Converters are used to interface the AC Buses with DC Buses. ```@docs LosslessConverter ``` **Variables:** - [`ActivePowerVariable`](@ref): + Bounds: ``(P^\text{min},P^\text{max})`` + Symbol: ``p`` **Static Parameters:** - ``P^\text{min}`` = `PowerSystems.get_active_power_limits(device).min` - ``P^\text{max}`` = `PowerSystems.get_active_power_limits(device).max` **Objective:** No cost is added to the objective function. **Expressions:** The variable `ActivePowerVariable` ``p`` is added positive to the AC balance expression `ActivePowerBalance` for AC Buses, and added negative to `ActivePowerBalance` for DC Buses, balancing both sides. **Constraints:** No constraints are added to the function. ================================================ FILE: docs/src/formulation_library/Feedforward.md ================================================ # [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*} ``` ================================================ FILE: docs/src/formulation_library/General.md ================================================ # [Formulations](@id formulation_library) Modeling formulations are created by dispatching on abstract subtypes of `PowerSimulations.AbstractDeviceFormulation` ## `FixedOutput` ```@docs FixedOutput ``` **Variables:** No variables are created for `DeviceModel(<:DeviceType, FixedOutput)` **Static Parameters:** - ThermalGen: + ``P^\text{th,max}`` = `PowerSystems.get_max_active_power(device)` + ``Q^\text{th,max}`` = `PowerSystems.get_max_reactive_power(device)` - Storage: + ``P^\text{st,max}`` = `PowerSystems.get_max_active_power(device)` + ``Q^\text{st,max}`` = `PowerSystems.get_max_reactive_power(device)` **Time Series Parameters:** ```@eval using PowerSimulations using HydroPowerSimulations using PowerSystems using DataFrames using Latexify combo_tables = [] for t in [RenewableGen, ThermalGen, HydroGen, ElectricLoad] combos = PowerSimulations.get_default_time_series_names(t, FixedOutput) combo_table = DataFrame( "Parameter" => map(x -> "[`$x`](@ref)", collect(keys(combos))), "Default Time Series Name" => map(x -> "`$x`", collect(values(combos))), ) insertcols!(combo_table, 1, "Device Type" => fill(string(t), length(combos))) push!(combo_tables, combo_table) end mdtable(vcat(combo_tables...); latex = false) ``` **Objective:** 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). **Constraints:** No constraints are created for `DeviceModel(<:DeviceType, FixedOutput)` * * * ## `FunctionData` Options PowerSimulations can represent variable costs using a variety of different methods depending on the data available in each device. The following describes the objective function terms that are populated for each variable cost option. ### `LinearFunctionData` `variable_cost = LinearFunctionData(c)`: creates a fixed marginal cost term in the objective function ```math \begin{aligned} & \text{min} \sum_{t} c * G_t \end{aligned} ``` ### `QuadraticFunctionData` and `PolynomialFunctionData` `variable_cost::QuadraticFunctionData` and `variable_cost::PolynomialFunctionData`: create a polynomial cost term in the objective function ```math \begin{aligned} & \text{min} \sum_{t} \sum_{n} C_n * G_t^n \end{aligned} ``` where - For `QuadraticFunctionData`: + ``C_0`` = `get_constant_term(variable_cost)` + ``C_1`` = `get_proportional_term(variable_cost)` + ``C_2`` = `get_quadratic_term(variable_cost)` - For `PolynomialFunctionData`: + ``C_n`` = `get_coefficients(variable_cost)[n]` ### `` and `PiecewiseLinearSlopeData` `variable_cost::PiecewiseLinearData` and `variable_cost::PiecewiseLinearSlopeData`: create a piecewise linear cost term in the objective function ```math \begin{aligned} & \text{min} \sum_{t} f(G_t) \end{aligned} ``` where - 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. * * * ## `StorageCost` Adds an objective function cost term according to: ```math \begin{aligned} & \text{min} \sum_{t} \quad [E^{surplus}_t * C^{penalty} - E^{shortage}_t * C^{value}] \end{aligned} ``` **Impact of different cost configurations:** 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 | |:--------- |:------------- |:-------------------- |:---------------------------------- |:------------------------------------------------- | | Case 1(a) | $\hat{E}=0$ | $C^{penalty}=0$ | $C^{value}=0$ | no change | | Case 1(b) | $\hat{E}=0$ | $C^{penalty}=0$ | $C^{value}<0$ | penalty for storing energy | | Case 1(c) | $\hat{E}=0$ | $C^{penalty}>0$ | $C^{value}=0$ | no penalties or incentives applied | | Case 1(d) | $\hat{E}=0$ | $C^{penalty}=0$ | $C^{value}>0$ | incentive for storing energy | | Case 1(e) | $\hat{E}=0$ | $C^{penalty}>0$ | $C^{value}<0$ | penalty for storing energy | | Case 1(f) | $\hat{E}=0$ | $C^{penalty}>0$ | $C^{value}>0$ | incentive for storing energy | | Case 2(a) | $\hat{E}>0$ | $C^{penalty}=0$ | $C^{value}=0$ | no change | | Case 2(b) | $\hat{E}>0$ | $C^{penalty}=0$ | $C^{value}<0$ | penalty on energy storage in excess of target | | Case 2(c) | $\hat{E}>0$ | $C^{penalty}>0$ | $C^{value}=0$ | penalty on energy storage short of target | | Case 2(d) | $\hat{E}>0$ | $C^{penalty}=0$ | $C^{value}>0$ | incentive on excess energy | | Case 2(e) | $\hat{E}>0$ | $C^{penalty}>0$ | $C^{value}<0$ | penalty on both excess/shortage of energy | | Case 2(f) | $\hat{E}>0$ | $C^{penalty}>0$ | $C^{value}>0$ | penalty for shortage, incentive for excess energy | ================================================ FILE: docs/src/formulation_library/Introduction.md ================================================ # [Formulations Introduction](@id formulation_intro) PowerSimulations.jl enables modularity in its formulations by assigning a [`DeviceModel`](@ref) 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`](@ref) in `PowerSimulations.jl` with three [`DeviceModel`](@ref) 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`](@ref) to each `PowerSystems.jl` with: | Type | Formulation | |:------------------- |:----------------------- | | Network | `CopperPlatePowerModel` | | `ThermalStandard` | `ThermalDispatchNoMin` | | `RenewableDispatch` | `RenewableFullDispatch` | | `PowerLoad` | `StaticPowerLoad` | Note that we did not assign any [`DeviceModel`](@ref) 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`](@ref) 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}``. ================================================ FILE: docs/src/formulation_library/Load.md ================================================ # `PowerSystems.ElectricLoad` Formulations 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. !!! 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. [`PowerLoadShift`](#PowerLoadShift) 5. [Valid configurations](#Valid-configurations) * * * ## `StaticPowerLoad` ```@docs StaticPowerLoad ``` **Variables:** 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 using DataFrames using Latexify combos = PowerSimulations.get_default_time_series_names(ElectricLoad, StaticPowerLoad) 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) ``` **Expressions:** Subtracts the parameters listed above from the respective active and reactive power balance expressions created by the selected [Network Formulations](@ref network_formulations). **Constraints:** No constraints are created * * * ## `PowerLoadInterruption` ```@docs PowerLoadInterruption ``` **Variables:** - [`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\}`` + 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:** ```@eval using PowerSimulations using PowerSystems using DataFrames using Latexify combos = PowerSimulations.get_default_time_series_names(ElectricLoad, PowerLoadInterruption) 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) ``` **Objective:** Creates an objective function term based on the [`FunctionData` Options](@ref) where the quantity term is defined as ``p^\text{ld}``. **Expressions:** - 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:** ```math \begin{aligned} & 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}))``. * * * ## `PowerLoadDispatch` ```@docs PowerLoadDispatch ``` **Variables:** - [`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:** ```@eval using PowerSimulations using PowerSystems using DataFrames using Latexify combos = PowerSimulations.get_default_time_series_names(ElectricLoad, PowerLoadDispatch) 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) ``` **Objective:** Creates an objective function term based on the [`FunctionData` Options](@ref) where the quantity term is defined as ``p^\text{ld}``. **Expressions:** - 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:** ```math \begin{aligned} & 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}))``. * * * ## `PowerLoadShift` ```@docs PowerLoadShift ``` **Variables:** - [`ShiftUpActivePowerVariable`](@ref): + Bounds: [0.0, `ShiftUpActivePowerTimeSeriesParameter`] + Default initial value: 0.0 + Symbol: ``p^\text{shift,up}`` - [`ShiftDownActivePowerVariable`](@ref): + Bounds: [0.0, `ShiftDownActivePowerTimeSeriesParameter`] + Default initial value: 0.0 + Symbol: ``p^\text{shift,dn}`` - [`ReactivePowerVariable`](@ref) *(AC network models only)*: + 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:** ```@eval using PowerSimulations using PowerSystems using DataFrames using Latexify combos = PowerSimulations.get_default_time_series_names(ShiftablePowerLoad, PowerLoadShift) 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) ``` **Expressions:** - Defines the `RealizedShiftedLoad` expression per device per time step: ```math p_t^\text{realized} = \text{ActivePowerTimeSeriesParameter}_t + p_t^\text{shift,up} - p_t^\text{shift,dn}, \quad \forall t \in \{1,\dots,T\} ``` - Subtracts ``p_t^\text{realized}`` from the active power balance expression of the selected [Network Formulations](@ref network_formulations). **Objective:** Creates objective function terms based on the [`FunctionData` Options](@ref) for both shift variables: - A cost term on ``p^\text{shift,up}`` (typically zero or negative, rewarding shifting up) - A cost term on ``p^\text{shift,dn}`` (typically positive, penalizing shifting down) **Constraints:** ```math \begin{aligned} & \sum_{t=1}^{T} \left( p_t^\text{shift,up} - p_t^\text{shift,dn} \right) = 0 \\ & \sum_{t=1}^{T_\text{sub}} \left( p_t^\text{shift,up} - p_t^\text{shift,dn} \right) = 0 \quad \text{(if \texttt{additional\_balance\_interval} is set)} \end{aligned} ``` ```math p_t^\text{realized} \ge 0, \quad \forall t \in \{1,\dots,T\} ``` ```math p_t^\text{shift,up} \le \text{ShiftUpActivePowerTimeSeriesParameter}_t, \quad \forall t \in \{1,\dots,T\} ``` ```math p_t^\text{shift,dn} \le \text{ShiftDownActivePowerTimeSeriesParameter}_t, \quad \forall t \in \{1,\dots,T\} ``` ```math \sum_{\tau=1}^{t} \left( p_\tau^\text{shift,dn} - p_\tau^\text{shift,up} \right) \ge 0, \quad \forall t \in \{1,\dots,T\} ``` ```math q_t^\text{ld} = \text{pf} \cdot p_t^\text{realized}, \quad \forall t \in \{1,\dots,T\} ``` ## Valid configurations Valid [`DeviceModel`](@ref)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) ``` ================================================ FILE: docs/src/formulation_library/Network.md ================================================ # [Network Formulations](@id network_formulations) 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. ```@docs NetworkModel ``` 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`. 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. ## `AreaPTDFPowerModel` ```@docs AreaPTDFPowerModel ``` **Variables** Slack variables are not supported. **Objective Function** No changes to the objective function. **Expressions** Creates the area-wide and nodal-wide active power balance expressions `ActivePowerBalance` to balance power based on each area independently. The flows across areas are computed based on the PTDF factors of lines connecting areas. **Constraints:** Adds the `ActivePowerBalance` constraint to balance the active power of all components available for each 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} ``` This includes the flows of lines based on the PTDF factors. ================================================ FILE: docs/src/formulation_library/Piecewise.md ================================================ # [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. ================================================ FILE: docs/src/formulation_library/README.md ================================================ # Formulation documentation guide Formulation documentation should *roughly* follow the template established by RenewableGen.md ## Auto generated items - Valid DeviceModel table: just change the device category in the filter function - Time Series Parameters: just change the device category and formulation in the `get_default_time_series_names` method call ## Linked items - Formulations in the Valid DeviceModel table must have a docstring in src/core/formulations.jl - The Formulation in the @docs block must have a docstring in src/core/formulations.jl - The Variables must have docstrings in src/core/variables.jl - The Time Series Parameters must have docstrings in src/core/parameters.jl ================================================ FILE: docs/src/formulation_library/RenewableGen.md ================================================ # `PowerSystems.RenewableGen` Formulations Renewable generation formulations define the optimization models that describe renewable units mathematical model in different operational settings, such as economic dispatch and unit commitment. !!! 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) * * * ## `RenewableFullDispatch` ```@docs RenewableFullDispatch ``` **Variables:** - [`ActivePowerVariable`](@ref): + Bounds: [0.0, ] + Symbol: ``p^\text{re}`` - [`ReactivePowerVariable`](@ref): + Bounds: [0.0, ] + Symbol: ``q^\text{re}`` **Static Parameters:** - ``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 using DataFrames using Latexify combos = PowerSimulations.get_default_time_series_names(RenewableGen, RenewableFullDispatch) 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) ``` **Objective:** 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 ``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} & 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} ``` * * * ## `RenewableConstantPowerFactor` ```@docs RenewableConstantPowerFactor ``` **Variables:** - [`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:** - ``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:** ```@eval using PowerSimulations using PowerSystems using DataFrames using Latexify combos = PowerSimulations.get_default_time_series_names( RenewableGen, RenewableConstantPowerFactor, ) 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) ``` **Objective:** 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 ``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} & 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`](@ref)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) ``` ================================================ FILE: docs/src/formulation_library/Service.md ================================================ # [`PowerSystems.Service` Formulations](@id service_formulations) `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. [`VariableMaxInterfaceFlow`](#VariableMaxInterfaceFlow) 8. [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\} ``` ## `VariableMaxInterfaceFlow` 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 VariableMaxInterfaceFlow ``` **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. **Time Series Parameters** For a `TransmissionInterface` `PowerSystems` type: ```@eval using PowerSimulations using PowerSystems using DataFrames using Latexify combos = PowerSimulations.get_default_time_series_names( TransmissionInterface, VariableMaxInterfaceFlow, ) 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 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} \cdot \text{MinInterfaceFlowLimitParameter}_t \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}\cdot \text{MaxInterfaceFlowLimitParameter}_t, \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*} ``` ================================================ FILE: docs/src/formulation_library/Source.md ================================================ # `Source` Formulations Source formulations define the optimization models that describe source or infinite bus units mathematical model in different operational settings, such as economic dispatch and unit commitment. !!! 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. [`ImportExportSourceModel`](#ImportExportSourceModel) * * * ## `ImportExportSourceModel` ```@docs ImportExportSourceModel ``` TODO ================================================ FILE: docs/src/formulation_library/ThermalGen.md ================================================ # `ThermalGen` Formulations Thermal generation formulations define the optimization models that describe thermal units mathematical model in different operational settings, such as economic dispatch and unit commitment. !!! 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) * * * ## `ThermalBasicDispatch` ```@docs ThermalBasicDispatch ``` **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` **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*} ``` * * * ## `ThermalDispatchNoMin` ```@docs ThermalDispatchNoMin ``` **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} ``` * * * ## `ThermalCompactDispatch` ```@docs ThermalCompactDispatch ``` **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 ``` **Variables:** - [`ActivePowerVariable`](@ref): + Bounds: [0.0, ] + Symbol: ``p^\text{th}`` - [`ReactivePowerVariable`](@ref): + Bounds: [0.0, ] + Symbol: ``q^\text{th}`` If Slack variables are enabled (`use_slacks = true`): - [`RateofChangeConstraintSlackUp`](@ref): + Bounds: [0.0, ] + Default initial value: 0.0 + Default proportional cost: 2e5 + Symbol: ``p^\text{sl,up}`` - [`RateofChangeConstraintSlackDown`](@ref): + Bounds: [0.0, ] + Default initial value: 0.0 + Default proportional cost: 2e5 + Symbol: ``p^\text{sl,dn}`` **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\} \\ & p_1^\text{th} - p^\text{th, init} - p_1^\text{sl,up} \le R^\text{th,up} \\ & p_t^\text{th} - p_{t-1}^\text{th} - p_t^\text{sl,up}\le R^\text{th,up}, \quad \forall t\in \{2, \dots, T\} \\ & -R^\text{th,dn} \le p_1^\text{th} - p^\text{th, init} + p_1^\text{sl,dn} \\ & -R^\text{th,dn} \le p_t^\text{th} - p_{t-1}^\text{th} + p_t^\text{sl,dn}, \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*} ``` * * * ## `ThermalBasicCompactUnitCommitment` ```@docs ThermalBasicCompactUnitCommitment ``` **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*} ``` * * * ## `ThermalCompactUnitCommitment` ```@docs ThermalCompactUnitCommitment ``` **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. 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*} ``` * * * ## `ThermalStandardUnitCommitment` ```@docs 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}`` If Slack variables are enabled (`use_slacks = true`): - [`RateofChangeConstraintSlackUp`](@ref): + Bounds: [0.0, ] + Default initial value: 0.0 + Default proportional cost: 2e5 + Symbol: ``p^\text{sl,up}`` - [`RateofChangeConstraintSlackDown`](@ref): + Bounds: [0.0, ] + Default initial value: 0.0 + Default proportional cost: 2e5 + Symbol: ``p^\text{sl,dn}`` **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\} \\ & p_1^\text{th} - p^\text{th, init} - p_1^\text{sl,up} \le R^\text{th,up} \\ & p_t^\text{th} - p_{t-1}^\text{th} - p_t^\text{sl,up} \le R^\text{th,up}, \quad \forall t\in \{2, \dots, T\} \\ & -R^\text{th,dn} \le p_1^\text{th} - p^\text{th, init} + p_1^\text{sl,dn} \\ & -R^\text{th,dn} \le p_t^\text{th} - p_{t-1}^\text{th} + p_t^\text{sl,dn}, \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*} ``` * * * ## `ThermalMultiStartUnitCommitment` ```@docs 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*} ``` * * * ## 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) ``` ================================================ FILE: docs/src/get_test_data.jl ================================================ using Cbc using PowerSimulations using PowerSystems using DataStructures using InfrastructureSystems import InfrastructureSystems as IS import PowerSimulations as PSI import PowerSystems as PSY include("../../../test/test_utils/get_test_data.jl") abstract type TestOpProblem <: PSI.DefaultDecisionProblem end system = build_c_sys5_re(; add_reserves = true) solver = optimizer_with_attributes(Cbc.Optimizer) devices = Dict{Symbol, DeviceModel}( :Generators => DeviceModel(ThermalStandard, ThermalBasicDispatch), :Loads => DeviceModel(PowerLoad, StaticPowerLoad), ) branches = Dict{Symbol, DeviceModel}( :L => DeviceModel(Line, StaticLine), :T => DeviceModel(Transformer2W, StaticBranch), :TT => DeviceModel(TapTransformer, StaticBranch), ); services = Dict{Symbol, ServiceModel}(); template = PSI.ProblemTemplate(CopperPlatePowerModel, devices, branches, services); operation_problem = PSI.DecisionModel(TestOpProblem, template, system; optimizer = solver); set_services_template!( operation_problem, Dict( :Reserve => ServiceModel(VariableReserve{ReserveUp}, RangeReserve), :Down_Reserve => ServiceModel(VariableReserve{ReserveDown}, RangeReserve), ), ) op_results = solve!(operation_problem) ================================================ FILE: docs/src/how_to/adding_new_problem_model.md ================================================ # Adding an Operations Problem Model This tutorial will show how to create a custom decision problem model. These cases are the ones where the user want to solve a fully specified problem. Some examples of custom decision models include: - Solving a custom Security Constrained Unit Commitment Problem - Solving a market agent utility maximization Problem. See examples of this functionality in HybridSystemsSimulations.jl The tutorial follows the usual steps for operational model building. First, build the decision model in isolation and second, integrate it into a simulation. In most cases there will be more than one way of achieving the same objective when it comes to implementing the model. This guide shows a general set of steps and requirements but it is by no means an exhaustive and detailed guide on developing custom decision models. !!! warning All the code in this tutorial is considered "pseudo-code". Copy-paste will likely not work out of the box. You need to develop the internals of the functions correctly for the examples below to work. ## General Rules 1. As a general rule you need to understand Julia's terminology such as multiple dispatch, parametric structs and method overloading, among others. Developing custom models for an operational simulation is a highly technical task and requires skilled development. This tutorial also requires good understanding of PowerSystems.jl data structures and features which are covered in the tutorials section of PowerSystems.jl documentation. Finally, developing a custom model decision model that will employ an optimization model under the hood requires understanding JuMP.jl. 2. Need to employ [anonymous constraints and variables in JuMP](https://jump.dev/JuMP.jl/stable/manual/variables/#anonymous_variables) and register the constraints, variables and other optimization objects into PowerSimulations.jl's optimization container. Otherwise the features to use your problem in the simulation like the coordination with other problems and post processing won't work. More on this in the section [How to develop your `build_model!` function](@ref) below. 3. Implement the required methods for your custom decision models. In some cases it will be possible to re-use some of the other methods that exist in PowerSimulations to make life easier for variable addition and constraint creation but this is not required. ## Decision Problem ### Step 1: Define a Custom Decision Problem Define a decision problem struct as a subtype of `PowerSimulations.DecisionProblem`. This requirement will enable a lot of the underlying functionality that relies on multiple dispatch. DecisionProblems are used to parameterize the behavior of [`DecisionModel`](@ref) objects which are just containers for the parameters, references and the optimization problem. It is possible to define a Custom Decision Problem that gives the user full control over the build, solve and execution process since it imposes less requirements on the developer. However, with less requirements there are also less checks and validations performed inside of PowerSimulations which might lead to unexpected errors ```julia struct MyCustomDecisionProblem <: PSI.DecisionProblem end ``` Alternatively, it is possible to define a Custom Decision Problem subtyping from `DefaultDecisionProblem` which imposes more requirements and structure onto the developer but employs more checks and validations in the process. Be aware that this route will decrease the flexibility of what can be done inside the custom model. ```julia struct MyCustomDecisionProblem <: PSI.DefaultDecisionProblem end ``` Once the problem type is defined, initialize the decision model container with your custom decision problem passing the solver and some of the settings you need for the solution of the problem. For custom problems some of the settings need manual implementation by the developer. Settings availability is also dependent on wether you choose to subtype from `PSI.DecisionProblem` or `PSI.DefaultDecisionProblem` ```julia my_model = DecisionModel{MyCustomDecisionProblem}( sys; name = "MyModel", optimizer = optimizer_with_attributes(HiGHS.Optimizer), optimizer_solve_log_print = true, ) ``` #### Mandatory Method Implementations 1. `build_model!`: This method build the `JuMP` optimization model. #### Optional Method Overloads 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!` 3. `reset!` 4. `solve_impl!` ### How to develop your `build_model!` function #### Registering a variable in the model To register a variable in the model, the developer must first allocate the container into the optimization container and then populate it. For example, it require start the build function as follows: !!! info We recommend calling `import PowerSimulations` and defining the constant `CONST PSI = PowerSimulations` to make it easier to read the code and determine which package is responsible for defining the functions. ```julia function PSI.build_model!(model::PSI.DecisionModel{MyCustomDecisionProblem}) container = PSI.get_optimization_container(model) time_steps = 1:24 PSI.set_time_steps!(container, time_steps) system = PSI.get_system(model) thermal_gens = PSY.get_components(PSY.ThermalStandard, system) thermal_gens_names = PSY.get_name.(thermal_gens) # Create the container for the variable variable = PSI.add_variable_container!( container, PSI.ActivePowerVariable(), # <- This variable is defined in PowerSimulations but the user can define their own PSY.ThermalGeneration, # <- Device type for the variable. Can be from PSY or custom defined thermal_gens_names, # <- First container dimension time_steps, # <- Second container dimension ) # Iterate over the devices and time to store the JuMP variables into the container. for t in time_steps, d in thermal_gens_names name = PSY.get_name(d) variable[name, t] = JuMP.@variable(get_jump_model(container)) # It is possible to use PSY getter functions to retrieve data from the generators JuMP.set_upper_bound(variable[name, t], UB_DATA) # <- Optional JuMP.set_lower_bound(variable[name, t], LB_DATA) # <- Optional end # Add More Variables..... return end ``` #### Registering a constraint in the model A similar pattern is used to add constraints to the model, in this example the field `meta` is used to avoid creating unnecessary duplicate constraint types. For instance to reflect upper_bound and lower_bound or upwards and downwards constraints. Meta can take any string value except for the `_` character. ```julia function PSI.build_model!(model::PSI.DecisionModel{MyCustomDecisionProblem}) container = PSI.get_optimization_container(model) time_steps = 1:24 PSI.set_time_steps!(container, time_steps) system = PSI.get_system(model) # VARIABLE ADDITION CODE # Constraint additions con_ub = PSI.add_constraints_container!( container, PSI.RangeLimitConstraint(), # <- Constraint Type defined by PSI or your own PSY.ThermalGeneration, # <- Device type for variable. Can be PSY or custom thermal_gens_names, # <- First container dimension time_steps; # <- Second container dimension meta = "ub", # <- meta allows to reuse a constraint definition for similar constraints. It only requires to be a string ) con_lb = PSI.add_constraints_container!( container, PSI.RangeLimitConstraint(), PSY.ThermalGeneration, thermal_gens_names, # <- First container dimension time_steps; # <- Second container dimension meta = "lb", # <- meta allows to reuse a constraint definition for similar constraints. It only requires to be a string ) # Retrieve a relevant variable from the container if not defined in variable = PSI.get_variable(container, PSI.ActivePowerVariable(), PSY.ThermalGeneration) for device in devices, t in time_steps ci_name = PSY.get_name(device) limits = get_min_max_limits(device) # depends on constraint type and formulation type con_ub[ci_name, t] = JuMP.@constraint(get_jump_model(container), variable[ci_name, t] >= limits.min) con_lb[ci_name, t] = JuMP.@constraint(get_jump_model(container), variable[ci_name, t] >= limits.min) end return end ``` ================================================ FILE: docs/src/how_to/debugging_infeasible_models.md ================================================ # Debugging infeasible models Getting infeasible solutions to models is a common occurrence in operations simulations, there are multiple reasons why this can happen. `PowerSimulations.jl` has several tools to help debug this situation. ## 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: ```@repl tutorial 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: ```@repl tutorial 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`](@ref). 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). ================================================ FILE: docs/src/how_to/logging.md ================================================ # Logging `PowerSimulations.jl` will output many log messages when building systems and running simulations. You may want to customize what gets logged to the console and, optionally, a file. By default all log messages of level `Logging.Info` or higher will get displayed to the console. When you run a simulation a simulation-specific logger will take over and log its messages to a file in the `logs` directory in the simulation output directory. When finished it will relinquish control back to the global logger. ## Configuring the global logger To configure the global logger in a Jupyter Notebook or REPL you may configure your own logger with the Julia Logging standard library or use the convenience function provided by PowerSimulations. This example will log messages of level `Logging.Error` to console and `Logging.Info` and higher to the file `power-simulations.log` in the current directory. ```julia import Logging using PowerSimulations logger = configure_logging(; console_level = Logging.Error, file_level = Logging.Info, filename = "power-simulations.log", ) ``` ## Configuring the simulation logger You can configure the logging level used by the simulation logger when you call `build!(simulation)`. Here is an example that increases logging verbosity: ```julia import Logging using PowerSimulations simulation = Simulation(...) build!(simulation; console_level = Logging.Info, file_level = Logging.Debug) ``` The log file will be located at `///logs/simulation.log`. ## Solver logs You can configure logging for the solver you use. Refer to the solver documentation. PowerSimulations does not redirect or intercept prints to `stdout` or `stderr` from other libraries. ## Recorder events PowerSimulations uses the `InfrastructureSystems.Recorder` to store simulation events in a log file. Refer to this [link](./simulation_recorder.md) for more information. ================================================ FILE: docs/src/how_to/parallel_simulations.md ================================================ # Parallel Simulations This section contains instructions to: - [Run a Simulation in Parallel on a local computer](@ref) - [Run a Simulation in Parallel on an HPC](@ref) ## Run a Simulation in Parallel on a local computer This page describes how to split a simulation into partitions, run each partition in parallel, and then join the results. ### Setup Create a Julia script to build and run simulations. It must meet the requirements below. A full example is in the PowerSimulations repository in `test/run_partitioned_simulation.jl`. - Call `using PowerSimulations`. - Implement a build function that matches the signature below. It must construct a `Simulation`, call `build!`, and then return the `Simulation` instance. It must throw an exception if the build fails. ``` function build_simulation( output_dir::AbstractString, simulation_name::AbstractString, partitions::SimulationPartitions, index::Union{Nothing, Integer}=nothing, ) ``` Here is example code to construct the `Simulation` with these parameters: ``` sim = Simulation( name=simulation_name, steps=partitions.num_steps, models=models, sequence=sequence, simulation_folder=output_dir, ) status = build!(sim; partitions=partitions, index=index, serialize=isnothing(index)) if status != PSI.SimulationBuildStatus.BUILT error("Failed to build simulation: status=$status") end ``` - Implement an execute function that matches the signature below. It must throw an exception if the execute fails. ``` function execute_simulation(sim, args...; kwargs...) status = execute!(sim) if status != PSI.RunStatus.SUCCESSFULLY_FINALIZED error("Simulation failed to execute: status=$status") end end ``` ### Execution After loading your script, call the function `run_parallel_simulation` as shown below. This example splits a year-long simulation into weekly partitions for a total of 53 individual jobs and then runs them four at a time. ``` julia> include("my_simulation.jl") julia> run_parallel_simulation( build_simulation, execute_simulation, script="my_simulation.jl", output_dir="my_simulation_output", name="my_simulation", num_steps=365, period=7, num_overlap_steps=1, num_parallel_processes=4, exeflags="--project=", ) ``` The final results will be in `./my_simulation_otuput/my_simulation` Note the log files and results for each partition are located in `./my_simulation_otuput/my_simulation/simulation_partitions` ## Run a Simulation in Parallel on an HPC This page describes how to split a simulation into partitions, run each partition in parallel on HPC compute nodes, and then join the results. These steps can be used on a local computer or any HPC supported by the submission software. Some steps may be specific to NREL's HPC `Eagle` cluster. *Note*: Some instructions are preliminary and will change if functionality is moved to a new Julia package. ### Setup 1. Create a conda environment and install the Python package `NREL-jade`: https://nrel.github.io/jade/installation.html. The rest of this page assumes that the environment is called `jade`. 2. Activate the environment with `conda activate jade`. 3. Locate the path to that conda environment. It will likely be `~/.conda-envs/jade` or `~/.conda/envs/jade`. 4. Load the Julia environment that you use to run simulations. Add the packages `Conda` and `PyCall`. 5. Setup Conda to use the existing `jade` environment by running these commands: ``` julia> run(`conda create -n conda_jl python conda`) julia> ENV["CONDA_JL_HOME"] = joinpath(ENV["HOME"], ".conda-envs", "jade") # change this to your path pkg> build Conda ``` 6. Copy the code below into a Julia file called `configure_parallel_simulation.jl`. This is an interface to Jade through PyCall. It will be used to create a Jade configuration. (It may eventually be moved to a separate package.) ``` function configure_parallel_simulation( script::AbstractString, num_steps::Integer, num_period_steps::Integer; num_overlap_steps::Integer=0, project_path=nothing, simulation_name="simulation", config_file="config.json", force=false, ) partitions = SimulationPartitions(num_steps, num_period_steps, num_overlap_steps) jgc = pyimport("jade.extensions.generic_command") julia_cmd = isnothing(project_path) ? "julia" : "julia --project=$project_path" setup_command = "$julia_cmd $script setup --simulation-name=$simulation_name " * "--num-steps=$num_steps --num-period-steps=$num_period_steps " * "--num-overlap-steps=$num_overlap_steps" teardown_command = "$julia_cmd $script join --simulation-name=$simulation_name" config = jgc.GenericCommandConfiguration( setup_command=setup_command, teardown_command=teardown_command, ) for i in 1:get_num_partitions(partitions) cmd = "$julia_cmd $script execute --simulation-name=$simulation_name --index=$i" job = jgc.GenericCommandParameters(command=cmd, name="execute-$i") config.add_job(job) end config.dump(config_file, indent=2) println("Created Jade configuration in $config_file. " * "Run 'jade submit-jobs [options] $config_file' to execute them.") end ``` 7. Create a Julia script to build and run simulations. It must meet the requirements below. A full example is in the PowerSimulations repository in `test/run_partitioned_simulation.jl`. - Call `using PowerSimulations`. - Implement a build function that matches the signature below. It must construct a `Simulation`, call `build!`, and then return the `Simulation` instance. It must throw an exception if the build fails. ``` function build_simulation( output_dir::AbstractString, simulation_name::AbstractString, partitions::SimulationPartitions, index::Union{Nothing, Integer}=nothing, ) ``` Here is example code to construct the `Simulation` with these parameters: ``` sim = Simulation( name=simulation_name, steps=partitions.num_steps, models=models, sequence=sequence, simulation_folder=output_dir, ) status = build!(sim; partitions=partitions, index=index, serialize=isnothing(index)) if status != PSI.SimulationBuildStatus.BUILT error("Failed to build simulation: status=$status") end ``` - Implement an execute function that matches the signature below. It must throw an exception if the execute fails. ``` function execute_simulation(sim, args...; kwargs...) status = execute!(sim) if status != PSI.RunStatus.SUCCESSFULLY_FINALIZED error("Simulation failed to execute: status=$status") end end ``` - Make the script runnable as a CLI command by including the following code at the bottom of the file. ``` function main() process_simulation_partition_cli_args(build_simulation, execute_simulation, ARGS...) end if abspath(PROGRAM_FILE) == @__FILE__ main() end ``` ### Execution 1. Create a Jade configuration that defines the partitioned simulation jobs. Load your Julia environment. This example splits a year-long simulation into weekly partitions for a total of 53 individual jobs. ``` julia> include("configure_parallel_simulation.jl") julia> num_steps = 365 julia> period = 7 julia> num_overlap_steps = 1 julia> configure_parallel_simulation( "my_simulation.jl", # this is your build/execute script num_steps, period, num_overlap_steps=1, project_path=".", # This optionally specifies the Julia project environment to load. ) Created Jade configuration in config.json. Run 'jade submit-jobs [options] config.json' to execute them. ``` Exit Julia. 2. View the configuration for accuracy. ``` $ jade config show config.json ``` 3. Start an interactive session on a debug node. *Do not submit the jobs on a login node!* The submission step will run a full build of the simulation and that may consume too many CPU and memory resources for the login node. ``` $ salloc -t 01:00:00 -N1 --account= --partition=debug ``` 4. Follow the instructions at https://nrel.github.io/jade/tutorial.html to submit the jobs. The example below will configure Jade to run each partition on its own compute node. Depending on the compute and memory constraints of your simulation, you may be able to pack more jobs on each node. Adjust the walltime as necessary. ``` $ jade config hpc -c hpc_config.toml -t slurm --walltime=04:00:00 -a $ jade submit-jobs config.json --per-node-batch-size=1 -o output ``` If you are unsure about how much memory and CPU resources your simulation consumes, add these options: ``` $ jade submit-jobs config.json --per-node-batch-size=1 -o output --resource-monitor-type periodic --resource-monitor-interval 3 ``` Jade will create HTML plots of the resource utilization in `output/stats`. You may be able to customize `--per-node-batch-size` and `--num-processes` to finish the simulations more quickly. 5. Jade will run a final command to join the simulation partitions into one unified file. You can load the results as you normally would. ``` julia> results = SimulationResults("/job-outputs/") ``` Note the log files and results for each partition are located in `/job-outputs//simulation_partitions` ================================================ FILE: docs/src/how_to/problem_templates.md ================================================ # [Operations `ProblemTemplate`s](@id op_problem_template) 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`](@ref)`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`](@ref)s and their mathematical representations, check out the [Formulation Library](@ref formulation_intro). ## Building a `ProblemTemplate` You can build a `ProblemTemplate` by adding a [`NetworkModel`](@ref), [`DeviceModel`](@ref)s, and [`ServiceModel`](@ref)s. ```julia template = ProblemTemplate() set_network_model!(template, NetworkModel(CopperPlatePowerModel)) set_device_model!(template, PowerLoad, StaticPowerLoad) set_device_model!(template, ThermalStandard, ThermalBasicUnitCommitment) set_service_model!(template, VariableReserve{ReserveUp}, RangeReserve) ``` ## Default Templates `PowerSimulations.jl` provides default templates for common operation problems. You can retrieve a default template and modify it according to your requirements. Currently supported default templates are: ```@docs; canonical=false template_economic_dispatch ``` ```@example using PowerSimulations #hide template_economic_dispatch() ``` ```@docs; canonical=false template_unit_commitment ``` ```@example using PowerSimulations #hide template_unit_commitment() ``` ================================================ FILE: docs/src/how_to/read_results.md ================================================ # [Read results](@id read_results) Once a [`DecisionModel`](@ref) 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`](@ref) 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`](@ref) 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. ================================================ FILE: docs/src/how_to/register_variable.md ================================================ # Register a variable in a custom operation model In most cases, operation problem models are optimization models. Although in `PowerSimulations.jl` it is possible to define arbitrary problems that can reflect heuristic decision rules, this is not the common case. The first aspect to consider when thinking about developing a model compatible with `PowerSimulations.jl` is that although we support all of `JuMP.jl` objects, you need to employ [anonymous constraints and variables](https://jump.dev/JuMP.jl/stable/manual/variables/#anonymous_variables) in `JuMP.jl` and register the constraints, variables and other optimization objects into `PowerSimulations.jl`'s optimization container. Otherwise the features to use your problem in the simulation like the coordination with other problems and post processing won't work. !!! info The requirements for the simulation of Power Systems operations are more strict than solving an optimization problem once with just `JuMP.jl`. The requirements imposed by `PowerSimulations.jl` to integrate your models in a simulation are designed to help with other complex operations that go beyond `JuMP.jl` scope. To register a variable in the model, the developer must first allocate the container into the optimization container and then populate it. For example, it require start the build function as follows: !!! warning All the code in this page is considered "pseudo-code". Copy-paste will likely not work out of the box. You need to develop the internals of the functions correctly for the examples below to work. ```julia using PowerSystems using PowerSimulations import PowerSystems as PSY import PowerSimulations as PSI function PSI.build_model!(model::PSI.DecisionModel{MyCustomModel}) container = PSI.get_optimization_container(model) PSI.set_time_steps!(container, 1:24) # Create the container for the variable variable = PSI.add_variable_container!( container, PSI.ActivePowerVariable(), # <- This variable is defined in PowerSimulations but the user can define their own PSY.ThermalGeneration, # <- Device type for the variable. Can be from PSY or custom defined devices_names, # <- First container dimension time_steps, # <- Second container dimension ) # Iterate over the devices and time to store the JuMP variables into the container. for t in time_steps, d in devices name = PSY.get_name(d) variable[name, t] = JuMP.@variable(get_jump_model(container)) # It is possible to use PSY getter functions to retrieve data from the generators # Any other variable property can be specified inside this loop. JuMP.set_upper_bound(variable[name, t], UB_DATA) # <- Optional JuMP.set_lower_bound(variable[name, t], LB_DATA) # <- Optional end return end ``` ================================================ FILE: docs/src/how_to/simulation_recorder.md ================================================ # Simulation Recorder PowerSimulations.jl provides the ability to record structured data as events during a simulation. These events can be post-processed to help debug problems. By default only SimulationStepEvent and ProblemExecutionEvent are recorded. Here is an example. Suppose a simulation is run in the directory `./output`. Assume that setup commands have been run: ```julia using PowerSimulations import PowerSimulations as PSI ``` Note that for all functions below you can optionally specify a function to filter events. The function must accept the event type and return true or false. ## Show all events of type PSI.SimulationStepEvent ```julia julia> show_simulation_events(PSI.SimulationStepEvent, "./output/aggregation/1") ┌─────────────────────┬─────────────────────┬──────┬────────┐ │ name │ simulation_time │ step │ status │ ├─────────────────────┼─────────────────────┼──────┼────────┤ │ SimulationStepEvent │ 2024-01-01T00:00:00 │ 1 │ start │ │ SimulationStepEvent │ 2024-01-01T23:00:00 │ 1 │ done │ │ SimulationStepEvent │ 2024-01-01T23:00:00 │ 2 │ start │ │ SimulationStepEvent │ 2024-01-02T23:00:00 │ 2 │ done │ └─────────────────────┴─────────────────────┴──────┴────────┘ ``` ## Show events of type PSI.ProblemExecutionEvent for a specific step and stage. ```julia show_simulation_events( PSI.ProblemExecutionEvent, "./output/aggregation/1", x -> x.step == 1 && x.stage == 2 && x.status == "start" ) ┌──────────────────────┬─────────────────────┬──────┬───────┬────────┐ │ name │ simulation_time │ step │ stage │ status │ ├──────────────────────┼─────────────────────┼──────┼───────┼────────┤ │ ProblemExecutionEvent │ 2024-01-01T00:00:00 │ 1 │ 2 │ start │ │ ProblemExecutionEvent │ 2024-01-01T00:00:00 │ 1 │ 2 │ start │ │ ProblemExecutionEvent │ 2024-01-01T01:00:00 │ 1 │ 2 │ start │ │ ProblemExecutionEvent │ 2024-01-01T02:00:00 │ 1 │ 2 │ start │ │ ProblemExecutionEvent │ 2024-01-01T03:00:00 │ 1 │ 2 │ start │ │ ProblemExecutionEvent │ 2024-01-01T04:00:00 │ 1 │ 2 │ start │ │ ProblemExecutionEvent │ 2024-01-01T05:00:00 │ 1 │ 2 │ start │ │ ProblemExecutionEvent │ 2024-01-01T06:00:00 │ 1 │ 2 │ start │ │ ProblemExecutionEvent │ 2024-01-01T07:00:00 │ 1 │ 2 │ start │ │ ProblemExecutionEvent │ 2024-01-01T08:00:00 │ 1 │ 2 │ start │ │ ProblemExecutionEvent │ 2024-01-01T09:00:00 │ 1 │ 2 │ start │ │ ProblemExecutionEvent │ 2024-01-01T10:00:00 │ 1 │ 2 │ start │ │ ProblemExecutionEvent │ 2024-01-01T11:00:00 │ 1 │ 2 │ start │ │ ProblemExecutionEvent │ 2024-01-01T12:00:00 │ 1 │ 2 │ start │ │ ProblemExecutionEvent │ 2024-01-01T13:00:00 │ 1 │ 2 │ start │ │ ProblemExecutionEvent │ 2024-01-01T14:00:00 │ 1 │ 2 │ start │ │ ProblemExecutionEvent │ 2024-01-01T15:00:00 │ 1 │ 2 │ start │ │ ProblemExecutionEvent │ 2024-01-01T16:00:00 │ 1 │ 2 │ start │ │ ProblemExecutionEvent │ 2024-01-01T17:00:00 │ 1 │ 2 │ start │ │ ProblemExecutionEvent │ 2024-01-01T18:00:00 │ 1 │ 2 │ start │ │ ProblemExecutionEvent │ 2024-01-01T19:00:00 │ 1 │ 2 │ start │ │ ProblemExecutionEvent │ 2024-01-01T20:00:00 │ 1 │ 2 │ start │ │ ProblemExecutionEvent │ 2024-01-01T21:00:00 │ 1 │ 2 │ start │ │ ProblemExecutionEvent │ 2024-01-01T22:00:00 │ 1 │ 2 │ start │ └──────────────────────┴─────────────────────┴──────┴───────┴────────┘ ``` ================================================ FILE: docs/src/index.md ================================================ # PowerSimulations.jl ```@meta CurrentModule = PowerSimulations ``` ## Overview `PowerSimulations.jl` is a power system operations simulation tool developed as a flexible and open source software for quasi-static power systems simulations including Production Cost Models. `PowerSimulations.jl` tackles the issues of developing a simulation model in a modular way providing tools for the formulation of decision models and emulation models that can be solved independently or in an interconnected fashion. `PowerSimulations.jl` supports the workflows to develop simulations by separating the development of operations models and simulation models. - **Operation Models**: Optimization model used to find the solution of an operation problem. - **Simulations Models**: Defined the requirements to find solutions to a sequence of operation problems in a way that resembles the procedures followed by operators. The most common Simulation Model is the solution of a Unit Commitment and Economic Dispatch sequence of problems. This model is used in commercial Production Cost Modeling tools, but it has a limited scope of analysis. `PowerSimulations.jl` is an active project under development, and we welcome your feedback, suggestions, and bug reports. ## About Sienna `PowerSimulations.jl` is part of the National Renewable Energy Laboratory's [Sienna ecosystem](https://sienna-platform.github.io/Sienna/), an open source framework for power system modeling, simulation, and optimization. The Sienna ecosystem can be [found on Github](https://github.com/Sienna-Platform/Sienna). It contains three applications: - [Sienna\Data](https://sienna-platform.github.io/Sienna/pages/applications/sienna_data.html) enables efficient data input, analysis, and transformation - [Sienna\Ops](https://sienna-platform.github.io/Sienna/pages/applications/sienna_ops.html) enables enables system scheduling simulations by formulating and solving optimization problems - [Sienna\Dyn](https://sienna-platform.github.io/Sienna/pages/applications/sienna_dyn.html) enables system transient analysis including small signal stability and full system dynamic simulations Each application uses multiple packages in the [`Julia`](http://www.julialang.org) programming language. ## Installation and Quick Links - [Sienna installation page](https://sienna-platform.github.io/Sienna/SiennaDocs/docs/build/how-to/install/): Instructions to install `PowerSimulations.jl` and other Sienna\Ops packages - [`JuMP.jl` solver's page](https://jump.dev/JuMP.jl/stable/installation/#Install-a-solver): An appropriate optimization solver is required for running `PowerSimulations.jl` models. Refer to this page to select and install a solver for your application. - [Sienna Documentation Hub](https://sienna-platform.github.io/Sienna/SiennaDocs/docs/build/index.html): Links to other Sienna packages' documentation ## How To Use This Documentation There are five main sections containing different information: - **Tutorials** - Detailed walk-throughs to help you *learn* how to use `PowerSimulations.jl` - **How to...** - Directions to help *guide* your work for a particular task - **Explanation** - Additional details and background information to help you *understand* `PowerSimulations.jl`, its structure, and how it works behind the scenes - **Reference** - Technical references and API for a quick *look-up* during your work - **Formulation Library** - Technical reference for the variables, parameters, and equations that PowerSimulations.jl uses to define device behavior `PowerSimulations.jl` strives to follow the [Diataxis](https://diataxis.fr/) documentation framework. ================================================ FILE: docs/src/tutorials/decision_problem.jl ================================================ #!nb # ```@meta #!nb # EditURL = "decision_problem.jl" #!nb # ``` #!nb # # # Running a Single-Step Problem # # ## Introduction # # `PowerSimulations.jl` supports the construction and solution of optimal power system # scheduling problems (Operations Problems). Operations problems form the fundamental # building blocks for sequential simulations. This example shows how to specify and customize # the mathematics that will be applied to the data with a [`ProblemTemplate`](@ref), # build and execute a [`DecisionModel`](@ref), and access the results. using PowerSystems using PowerSimulations using HydroPowerSimulations using PowerSystemCaseBuilder using HiGHS # solver using Dates # ## Data # # !!! note # # [PowerSystemCaseBuilder.jl](https://github.com/Sienna-Platform/PowerSystemCaseBuilder.jl) # is a helper library that makes it easier to reproduce examples in the documentation # and tutorials. Normally you would pass your local files to create the system data # instead of calling the function `build_system`. # For more details visit # [PowerSystemCaseBuilder Documentation](https://sienna-platform.github.io/PowerSystems.jl/stable/how_to/powersystembuilder/) sys = build_system(PSISystems, "modified_RTS_GMLC_DA_sys") # ## Define a problem specification with a `ProblemTemplate` # # You can create an empty template with: template_uc = ProblemTemplate() # Now, you can add a [`DeviceModel`](@ref) for each device type to create an assignment # between PowerSystems device types and the subtypes of `AbstractDeviceFormulation`. # PowerSimulations has a variety of different `AbstractDeviceFormulation` subtypes # that can be applied to different PowerSystems device types, each dispatching to different # methods for populating optimization problem objectives, variables, and constraints. # Documentation on the formulation options for various devices can be found in the # [formulation library docs](https://sienna-platform.github.io/PowerSimulations.jl/latest/formulation_library/General/#formulation_library) # ### Branch Formulations # # Here is an example of relatively standard branch formulations. Other formulations allow # for selective enforcement of transmission limits and greater control on transformer settings. set_device_model!(template_uc, Line, StaticBranch) set_device_model!(template_uc, Transformer2W, StaticBranch) set_device_model!(template_uc, TapTransformer, StaticBranch) # ### Injection Device Formulations # # Here we define template entries for all devices that inject or withdraw power on the # 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 `RenewableNonDispatch` devices. 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, RenewableNonDispatch, FixedOutput) # ### Service Formulations # # We have two `VariableReserve` types, parameterized by their direction. So, similar to # creating [`DeviceModel`](@ref)s, we can create [`ServiceModel`](@ref)s. The primary difference being # that [`DeviceModel`](@ref) objects define how constraints get created, while [`ServiceModel`](@ref) objects # define how constraints get modified. set_service_model!(template_uc, VariableReserve{ReserveUp}, RangeReserve) set_service_model!(template_uc, VariableReserve{ReserveDown}, RangeReserve) # ### Network Formulations # # Finally, we can define the transmission network specification that we'd like to model. # For simplicity, we'll choose a copper plate formulation. But there are dozens of # specifications available through an integration with # [PowerModels.jl](https://lanl-ansi.github.io/PowerModels.jl/stable/). # # *Note that many formulations will require appropriate data and may be computationally intractable* set_network_model!(template_uc, NetworkModel(CopperPlatePowerModel)) # ## `DecisionModel` # # Now that we have a `System` and a [`ProblemTemplate`](@ref), we can put the two together # to create a [`DecisionModel`](@ref) that we solve. # ### Optimizer # # It's most convenient to define an optimizer instance upfront and pass it into the # [`DecisionModel`](@ref) constructor. For this example, we can use the free HiGHS solver # with a relatively relaxed MIP gap (`ratioGap`) setting to improve speed. solver = optimizer_with_attributes(HiGHS.Optimizer, "mip_rel_gap" => 0.5) # ### Build a `DecisionModel` # # The construction of a [`DecisionModel`](@ref) essentially applies a [`ProblemTemplate`](@ref) # to `System` data to create a JuMP model. problem = DecisionModel(template_uc, sys; optimizer = solver, horizon = Hour(24)) build!(problem; output_dir = mktempdir()) # !!! tip # # The principal component of the [`DecisionModel`](@ref) is the JuMP model. # But you can serialize to a file using the following command: # # ```julia # serialize_optimization_model(problem, save_path) # ``` # # Keep in mind that if the setting `"store_variable_names"` is set to `False` then # the file won't show the model's names. # ### Solve a `DecisionModel` solve!(problem) # ## Results Inspection # # PowerSimulations collects the [`DecisionModel`](@ref) results into a # `OptimizationProblemResults` struct: res = OptimizationProblemResults(problem) # ### Optimizer Stats # # The optimizer summary is included get_optimizer_stats(res) # ### Objective Function Value get_objective_value(res) # ### Variable, Parameter, Auxiliary Variable, Dual, and Expression Values # # The solution value data frames for variables, parameters, auxiliary variables, duals, and # expressions can be accessed using the `read_` methods: read_variables(res) # Or, you can read a single parameter value for parameters that exist in the results. list_parameter_names(res) read_parameter(res, "ActivePowerTimeSeriesParameter__RenewableDispatch") # ## Plotting # # Take a look at the plotting capabilities in # [PowerGraphics.jl](https://sienna-platform.github.io/PowerGraphics.jl/stable/) ================================================ FILE: docs/src/tutorials/pcm_simulation.jl ================================================ #!nb # ```@meta #!nb # EditURL = "pcm_simulation.jl" #!nb # ``` #!nb # # # Running a Multi-Stage Production Cost Simulation # # ## Introduction # # PowerSimulations.jl supports simulations that consist of sequential optimization problems # where results from previous problems inform subsequent problems in a variety of ways. This # example demonstrates some of these capabilities to represent electricity market clearing. # This example is intended to be an extension of the tutorial on # [Running a Single-Step Problem](@ref). # # ### Load Packages using PowerSystems using PowerSimulations using HydroPowerSimulations import PowerSimulations as PSI using PowerSystemCaseBuilder using Dates using HiGHS #solver # ### Optimizer # # It's most convenient to define an optimizer instance upfront and pass it into the # [`DecisionModel`](@ref) constructor. For this example, we can use the free HiGHS solver with a # relatively relaxed MIP gap (`ratioGap`) setting to improve speed. solver = optimizer_with_attributes(HiGHS.Optimizer, "mip_rel_gap" => 0.5) # !!! note # # Defining a solver upfront ensures that only one license is requested when using a license-limited solver, such as Gurobi. We can create a environment variable and pass it to the optimizer constructor for shared license use if using such a solver # # ```julia # using Gurobi # # gurobi_env = Gurobi.Env() # # solver = optimizer_with_attributes(() -> Gurobi.Optimizer(gurobi_env),"MIPGap" => 0.01) # ``` # # Conversely, if a unique optimizer constructor is defined within the SimulationModels for each stage, a separate license will be obtained for each stage. # # ### Hourly day-ahead system # # First, we'll create a `System` with hourly data to represent day-ahead forecasted wind, # solar, and load profiles: sys_DA = build_system(PSISystems, "modified_RTS_GMLC_DA_sys"; skip_serialization = true) # ### 5-Minute system # # The RTS data also includes 5-minute resolution time series data. So, we can create another # `System` to represent 15 minute ahead forecasted data for a "real-time" market: sys_RT = build_system(PSISystems, "modified_RTS_GMLC_RT_sys"; skip_serialization = true) # ## `ProblemTemplate`s define stages # # Sequential simulations in PowerSimulations are created by defining `OperationsProblems` # that represent stages, and how information flows between executions of a stage and # between different stages. # # Let's start by defining a two stage simulation that might look like a typical day-Ahead # and real-time electricity market clearing process. # # ### Day-ahead unit commitment stage # # First, we can define a unit commitment template for the day ahead problem. We can use the # included UC template, but in this example, we'll replace the `ThermalBasicUnitCommitment` # with the slightly more complex `ThermalStandardUnitCommitment` for the thermal generators. template_uc = template_unit_commitment() set_device_model!(template_uc, ThermalStandard, ThermalStandardUnitCommitment) set_device_model!(template_uc, HydroDispatch, HydroDispatchRunOfRiver) # ### Define the reference model for the real-time economic dispatch # # In addition to the manual specification process demonstrated in the OperationsProblem # example, PSI also provides pre-specified templates for some standard problems: template_ed = template_economic_dispatch(; network = NetworkModel(PTDFPowerModel; use_slacks = true), ) # ### Define the `SimulationModels` # # [`DecisionModel`](@ref)`s define the problems that are executed in the simulation. The actual problem will change as the stage gets updated to represent different time periods, but the formulations applied to the components is constant within a stage. In this case, we want to define two stages with the `ProblemTemplate`s and the `System`s that we've already created. models = SimulationModels(; decision_models = [ DecisionModel(template_uc, sys_DA; optimizer = solver, name = "UC"), DecisionModel(template_ed, sys_RT; optimizer = solver, name = "ED"), ], ) # ### `SimulationSequence` # # Similar to a `ProblemTemplate`, the `SimulationSequence` provides a template of # how to execute a sequential set of operations problems. # # Let's review some of the `SimulationSequence` arguments. # # ### 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. # # ### `FeedForward` # # The definition of exactly what information is passed using the defined chronologies is # accomplished with `FeedForward`. Specifically, `FeedForward` is used # to define what to do with information being passed with an inter-stage chronology. Let's # define a `FeedForward` that affects the semi-continuous range constraints of thermal generators # in the economic dispatch problems based on the value of the unit-commitment variables. feedforward = Dict( "ED" => [ SemiContinuousFeedforward(; component_type = ThermalStandard, source = OnVariable, affected_values = [ActivePowerVariable], ), ], ) # ### Sequencing # # The stage problem length, look-ahead, and other details surrounding the temporal Sequencing # of stages are controlled using the structure of the time series data in the `System`s. # So, to define a typical day-ahead - real-time sequence: # # - Day ahead problems should represent 48 hours, advancing 24 hours after each execution (24-hour look-ahead) # - Real time problems should represent 1 hour (12 5-minute periods), advancing 15 min after each execution (15 min look-ahead) # # We can adjust the time series data to reflect this structure in each `System`: # # - `transform_single_time_series!(sys_DA, Hour(48), Hour(24))` # - `transform_single_time_series!(sys_RT, Minute(60), Minute(15))` # # Now we can put it all together to define a `SimulationSequence` DA_RT_sequence = SimulationSequence(; models = models, ini_cond_chronology = InterProblemChronology(), feedforwards = feedforward, ) # ## `Simulation` # # Now, we can build and execute a simulation using the `SimulationSequence` and `Stage`s # that we've defined. path = mkdir(joinpath(".", "rts-store")) #hide sim = Simulation(; name = "rts-test", steps = 2, models = models, sequence = DA_RT_sequence, simulation_folder = joinpath(".", "rts-store"), ) # ### Build simulation build!(sim) # ### Execute simulation # # the following command returns the status of the simulation (0: is proper execution) and # stores the results in a set of HDF5 files on disk. execute!(sim; enable_progress_bar = false) # ## Results # # To access the results, we need to load the simulation result metadata and then make # requests to the specific data of interest. This allows you to efficiently access the # results of interest without overloading resources. results = SimulationResults(sim); uc_results = get_decision_problem_results(results, "UC"); # UC stage result metadata ed_results = get_decision_problem_results(results, "ED"); # ED stage result metadata # We can read all the result variables read_variables(uc_results) # or all the parameters read_parameters(uc_results) # We can just list the variable names contained in `uc_results`: list_variable_names(uc_results) # and a number of parameters (this pattern also works for aux_variables, expressions, and duals) list_parameter_names(uc_results) # Now we can read the specific results of interest for a specific problem, time window (optional), # and set of variables, duals, or parameters (optional) Dict([ v => read_variable(uc_results, v) for v in [ "ActivePowerVariable__RenewableDispatch", "ActivePowerVariable__HydroDispatch", "StopVariable__ThermalStandard", ] ]) # Or if we want the result of just one variable, parameter, or dual (must be defined in the # problem definition), we can use: read_parameter( ed_results, "ActivePowerTimeSeriesParameter__RenewableNonDispatch"; initial_time = DateTime("2020-01-01T06:00:00"), count = 5, ) # !!! 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_*`: 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://sienna-platform.github.io/PowerGraphics.jl/stable/) ================================================ FILE: scripts/formatter/Project.toml ================================================ uuid = "c6367ca8-164d-4469-afe3-c91cf8860505" authors = ["Jose Daniel Lara "] [deps] JuliaFormatter = "98e50ef6-434e-11e9-1051-2b60c6c9e899" Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" [compat] JuliaFormatter = "1.0" julia = "^1.7" ================================================ FILE: scripts/formatter/formatter_code.jl ================================================ using Pkg Pkg.activate(@__DIR__) Pkg.instantiate() Pkg.update() using JuliaFormatter main_paths = ["."] for main_path in main_paths for (root, dir, files) in walkdir(main_path) for f in files @show file_path = abspath(root, f) !((occursin(".jl", f) || occursin(".md", f))) && continue format(file_path; whitespace_ops_in_indices = true, remove_extra_newlines = true, verbose = true, always_for_in = true, whitespace_typedefs = true, conditional_to_if = true, join_lines_based_on_source = true, separate_kwargs_with_semicolon = true, format_markdown = true, # extending_powersimulations causes format failures but is not current public ignore = [ "README.md", "index.md", "extending_powersimulations.md", "simulation_recorder.md", "how_to/install.md", ], # always_use_return = true. # Disabled since it throws a lot of false positives ) end end end ================================================ FILE: src/PowerSimulations.jl ================================================ isdefined(Base, :__precompile__) && __precompile__() module PowerSimulations ################################################################################# # Exports # Base Models export Simulation export DecisionModel export EmulationModel export ProblemTemplate export InitialCondition export SimulationModels export SimulationSequence export SimulationResults export SimulationPartitions export SimulationPartitionResults export TableFormat # Network Relevant Exports export NetworkModel export PTDFPowerModel export CopperPlatePowerModel export AreaBalancePowerModel export AreaPTDFPowerModel # HVDC Network Relevant exports export TransportHVDCNetworkModel export VoltageDispatchHVDCNetworkModel ######## Device Models ######## export DeviceModel export FixedOutput ####### Event Models ######## export EventModel ######## Service Models ######## export ServiceModel export RangeReserve export RampReserve export StepwiseCostReserve export NonSpinningReserve export PIDSmoothACE export GroupReserve export ConstantMaxInterfaceFlow export VariableMaxInterfaceFlow ######## Branch Models ######## export StaticBranch export StaticBranchBounds export StaticBranchUnbounded export HVDCTwoTerminalLossless export HVDCTwoTerminalDispatch export HVDCTwoTerminalUnbounded export HVDCTwoTerminalLCC export PhaseAngleControl export PTDFBranchFlow ######## HVDC models ######## export LosslessConverter export QuadraticLossConverter export LosslessLine export DCLossyLine ######## Load Models ######## export StaticPowerLoad export PowerLoadInterruption export PowerLoadDispatch export PowerLoadShift ######## Renewable Formulations ######## export RenewableFullDispatch export RenewableConstantPowerFactor ######## Thermal Formulations ######## export ThermalStandardUnitCommitment export ThermalBasicUnitCommitment export ThermalBasicCompactUnitCommitment export ThermalBasicDispatch export ThermalStandardDispatch export ThermalDispatchNoMin export ThermalMultiStartUnitCommitment export ThermalCompactUnitCommitment export ThermalCompactDispatch ###### Regulation Device Formulation ####### export DeviceLimitedRegulation export ReserveLimitedRegulation ###### Source Formulations ###### export ImportExportSourceModel ###### SynCons Formulations ###### export SynchronousCondenserBasicDispatch # feedforward models export UpperBoundFeedforward export LowerBoundFeedforward export SemiContinuousFeedforward export FixValueFeedforward # InitialConditions chrons export InterProblemChronology export IntraProblemChronology # Initial Conditions Quantities export DevicePower export DeviceStatus export InitialTimeDurationOn export InitialTimeDurationOff export InitialEnergyLevel # operation_models export GenericOpProblem export UnitCommitmentProblem export EconomicDispatchProblem # export OptimalPowerFlow # Functions export build! ## Op Model Exports export get_initial_conditions export serialize_results export serialize_optimization_model ## Decision Model Export export solve! ## Emulation Model Exports export run! ## Sim Model Exports export execute! export get_simulation_model export run_parallel_simulation ## Template Exports export template_economic_dispatch export template_unit_commitment export EconomicDispatchProblem export UnitCommitmentProblem export AGCReserveDeployment export set_device_model! export set_service_model! export set_network_model! export get_network_formulation export get_hvdc_network_model export set_hvdc_network_model! ## Results interfaces export SimulationResultsExport export export_results export export_realized_results export export_optimizer_stats export get_variable_values export get_dual_values export get_parameter_values export get_aux_variable_values export get_expression_values export get_timestamps export get_model_name export get_decision_problem_results export get_emulation_problem_results export get_system export get_system! export set_system! export list_variable_keys export list_dual_keys export list_parameter_keys export list_aux_variable_keys export list_expression_keys export list_variable_names export list_dual_names export list_parameter_names export list_aux_variable_names export list_expression_names export list_decision_problems export list_supported_formats export load_results! export read_variable export read_dual export read_parameter export read_aux_variable export read_expression export read_variables export read_duals export read_parameters export read_aux_variables export read_expressions export read_realized_variable export read_realized_dual export read_realized_parameter export read_realized_aux_variable export read_realized_expression export read_realized_variables export read_realized_duals export read_realized_parameters export read_realized_aux_variables export read_realized_expressions export get_realized_timestamps export get_problem_base_power export get_objective_value 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 export get_variable_index export list_recorder_events export show_recorder_events export list_simulation_events export show_simulation_events export get_num_partitions # Variables export ActivePowerVariable export ActivePowerInVariable export ActivePowerOutVariable export HotStartVariable export WarmStartVariable export ColdStartVariable export EnergyVariable export LiftVariable export OnVariable export ReactivePowerVariable export ReservationVariable export ActivePowerReserveVariable export ServiceRequirementVariable export StartVariable export StopVariable export SteadyStateFrequencyDeviation export AreaMismatchVariable export DeltaActivePowerUpVariable export DeltaActivePowerDownVariable export AdditionalDeltaActivePowerUpVariable export AdditionalDeltaActivePowerDownVariable export SmoothACE export SystemBalanceSlackUp export SystemBalanceSlackDown export ReserveRequirementSlack export VoltageMagnitude export VoltageAngle export FlowActivePowerVariable export FlowActivePowerSlackUpperBound export FlowActivePowerSlackLowerBound export FlowActivePowerFromToVariable export FlowActivePowerToFromVariable export FlowReactivePowerFromToVariable export FlowReactivePowerToFromVariable export PowerAboveMinimumVariable export PhaseShifterAngle export UpperBoundFeedForwardSlack export LowerBoundFeedForwardSlack export InterfaceFlowSlackUp export InterfaceFlowSlackDown export PiecewiseLinearCostVariable export RateofChangeConstraintSlackUp export RateofChangeConstraintSlackDown export PostContingencyActivePowerChangeVariable export PostContingencyActivePowerReserveDeploymentVariable export DCVoltage export DCLineCurrent export ConverterPowerDirection export ConverterCurrent export SquaredConverterCurrent export InterpolationSquaredCurrentVariable export InterpolationBinarySquaredCurrentVariable export ConverterPositiveCurrent export ConverterNegativeCurrent export SquaredDCVoltage export InterpolationSquaredVoltageVariable export InterpolationBinarySquaredVoltageVariable export AuxBilinearConverterVariable export AuxBilinearSquaredConverterVariable export InterpolationSquaredBilinearVariable export InterpolationBinarySquaredBilinearVariable export ShiftUpActivePowerVariable export ShiftDownActivePowerVariable # Auxiliary variables export TimeDurationOn export TimeDurationOff export PowerOutput export PowerFlowVoltageAngle export PowerFlowVoltageMagnitude export PowerFlowBranchReactivePowerFromTo, PowerFlowBranchReactivePowerToFrom export PowerFlowBranchActivePowerFromTo, PowerFlowBranchActivePowerToFrom export PowerFlowBranchActivePowerLoss export PowerFlowLossFactors export PowerFlowVoltageStabilityFactors # Constraints export AbsoluteValueConstraint export ActivePowerVariableTimeSeriesLimitsConstraint export LineFlowBoundConstraint export ActivePowerVariableLimitsConstraint export ActivePowerInVariableTimeSeriesLimitsConstraint export ActivePowerOutVariableTimeSeriesLimitsConstraint export ActiveRangeICConstraint export AreaParticipationAssignmentConstraint export BalanceAuxConstraint export CommitmentConstraint export CopperPlateBalanceConstraint export DurationConstraint export EnergyBalanceConstraint export EqualityConstraint export FeedforwardSemiContinuousConstraint export FeedforwardUpperBoundConstraint export FeedforwardLowerBoundConstraint export FeedforwardIntegralLimitConstraint export FlowLimitConstraint export FlowLimitFromToConstraint export FlowLimitToFromConstraint export FrequencyResponseConstraint export HVDCPowerBalance export HVDCLosses export HVDCFlowDirectionVariable export InputActivePowerVariableLimitsConstraint export InterfaceFlowLimit export NetworkFlowConstraint export NodalBalanceActiveConstraint export NodalBalanceReactiveConstraint export OutputActivePowerVariableLimitsConstraint export PiecewiseLinearCostConstraint export ParticipationAssignmentConstraint export ParticipationFractionConstraint export PhaseAngleControlLimit export RampConstraint export RampLimitConstraint export RangeLimitConstraint export FlowRateConstraint export FlowRateConstraintFromTo export FlowRateConstraintToFrom export PostContingencyEmergencyRateLimitConstraint export ReactivePowerVariableLimitsConstraint export RegulationLimitsConstraint export RequirementConstraint export ReserveEnergyCoverageConstraint export ReservePowerConstraint export SACEPIDAreaConstraint export StartTypeConstraint export StartupInitialConditionConstraint export StartupTimeLimitTemperatureConstraint export PostContingencyActivePowerVariableLimitsConstraint export PostContingencyActivePowerReserveDeploymentVariableLimitsConstraint export PostContingencyGenerationBalanceConstraint export PostContingencyRampConstraint export ImportExportBudgetConstraint export PiecewiseLinearBlockIncrementalOfferConstraint export PiecewiseLinearBlockDecrementalOfferConstraint export NodalBalanceCurrentConstraint export DCLineCurrentConstraint export ConverterPowerCalculationConstraint export ConverterMcCormickEnvelopes export InterpolationVoltageConstraints export InterpolationCurrentConstraints export InterpolationBilinearConstraints export ConverterLossConstraint export CurrentAbsoluteValueConstraint export ShiftedActivePowerBalanceConstraint export ShiftUpActivePowerVariableLimitsConstraint export ShiftDownActivePowerVariableLimitsConstraint export RealizedShiftedLoadMinimumBoundConstraint export NonAnticipativityConstraint # Parameters # Time Series Parameters export ActivePowerTimeSeriesParameter export ActivePowerOutTimeSeriesParameter export ActivePowerInTimeSeriesParameter export ReactivePowerTimeSeriesParameter export DynamicBranchRatingTimeSeriesParameter export FuelCostParameter export PostContingencyDynamicBranchRatingTimeSeriesParameter export RequirementTimeSeriesParameter export FromToFlowLimitParameter export ToFromFlowLimitParameter # Cost Parameters export CostFunctionParameter # Feedforward Parameters export OnStatusParameter export UpperBoundValueParameter export LowerBoundValueParameter export FixValueParameter # Event Parameters export AvailableStatusParameter export AvailableStatusChangeCountdownParameter export ActivePowerOffsetParameter export ReactivePowerOffsetParameter # Expressions export SystemBalanceExpressions export RangeConstraintLBExpressions export RangeConstraintUBExpressions export CostExpressions export ConstituentCostExpression export ActivePowerBalance export ReactivePowerBalance export EmergencyUp export EmergencyDown export RawACE export ProductionCostExpression export FuelCostExpression export StartUpCostExpression export ShutDownCostExpression export FixedCostExpression export VOMCostExpression export CurtailmentCostExpression export FuelConsumptionExpression export ActivePowerRangeExpressionLB export ActivePowerRangeExpressionUB export PostContingencyBranchFlow export PostContingencyActivePowerGeneration export PostContingencyActivePowerBalance export NetActivePower export DCCurrentBalance ################################################################################# # Imports import DataStructures: OrderedDict, Deque, SortedDict import Logging import Serialization # Modeling Imports import JuMP # so that users do not need to import JuMP to use a solver with PowerModels import JuMP: optimizer_with_attributes import JuMP.Containers: DenseAxisArray, SparseAxisArray export optimizer_with_attributes import MathOptInterface as MOI import LinearAlgebra import SparseArrays import JSON3 import PowerSystems as PSY import InfrastructureSystems as IS import PowerFlows import PowerNetworkMatrices as PNM import PowerNetworkMatrices: PTDF, VirtualPTDF, LODF, VirtualLODF export PTDF export VirtualPTDF export LODF export VirtualLODF import InfrastructureSystems: @assert_op, TableFormat, 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 import InfrastructureSystems.Optimization: get_source_data # IS.Optimization imports that stay private, may or may not be additional methods in PowerSimulations # PowerSystems imports import PowerSystems: get_components, get_component, get_available_components, get_available_component, get_groups, get_available_groups import PowerSystems: StartUpStages export get_name export get_model_base_power export get_optimizer_stats export get_timestamps export get_resolution import PowerModels as PM import TimerOutputs import ProgressMeter import Distributed import Distributions: Bernoulli, Geometric import Random import Random: AbstractRNG, rand # Base Imports import Base.getindex import Base.isempty import Base.length import Base.first import InteractiveUtils: methodswith # TimeStamp Management Imports import Dates import TimeSeries # I/O Imports import DataFrames import DataFrames: DataFrame, DataFrameRow, Not, innerjoin import DataFramesMeta: @chain, @orderby, @rename, @select, @subset, @transform import CSV import HDF5 import PrettyTables # PowerModels exports export ACPPowerModel export ACRPowerModel export ACTPowerModel export DCPPowerModel export NFAPowerModel export DCPLLPowerModel export LPACCPowerModel export SOCWRPowerModel export SOCWRConicPowerModel export QCRMPowerModel export QCLSPowerModel export process_simulation_partition_cli_args ################################################################################ # Type Alias From other Packages const PSI = PowerSimulations const ISOPT = IS.Optimization const MOIU = MOI.Utilities const MOPFM = MOI.FileFormats.Model const PFS = PowerFlows const TS = TimeSeries ################################################################################ function progress_meter_enabled() return isa(stderr, Base.TTY) && (get(ENV, "CI", nothing) != "true") && (get(ENV, "RUNNING_PSI_TESTS", nothing) != "true") end using DocStringExtensions @template DEFAULT = """ $(TYPEDSIGNATURES) $(DOCSTRING) """ # Includes include("core/definitions.jl") # Core components include("core/formulations.jl") include("core/network_formulations.jl") include("core/abstract_simulation_store.jl") include("core/operation_model_abstract_types.jl") include("core/abstract_feedforward.jl") include("core/variables.jl") include("core/network_reductions.jl") include("core/parameters.jl") include("core/service_model.jl") include("core/event_keys.jl") include("core/event_model.jl") include("core/device_model.jl") include("core/network_model.jl") include("core/auxiliary_variables.jl") include("core/constraints.jl") include("core/expressions.jl") include("core/initial_conditions.jl") include("core/settings.jl") include("core/cache_utils.jl") include("core/dataset.jl") include("core/dataset_container.jl") include("core/results_by_time.jl") # Order Required include("operation/problem_template.jl") include("core/power_flow_data_wrapper.jl") include("core/optimization_container.jl") include("core/dual_processing.jl") include("core/store_common.jl") include("initial_conditions/initial_condition_chronologies.jl") include("operation/operation_model_interface.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("simulation/simulation_info.jl") include("operation/operation_model_types.jl") include("operation/template_validation.jl") include("operation/decision_model.jl") include("operation/emulation_model.jl") include("operation/problem_results.jl") include("operation/time_series_interface.jl") include("operation/optimization_debugging.jl") include("operation/model_numerical_analysis_utils.jl") include("initial_conditions/add_initial_condition.jl") include("initial_conditions/update_initial_conditions.jl") include("initial_conditions/calculate_initial_condition.jl") include("feedforward/feedforwards.jl") include("feedforward/feedforward_arguments.jl") include("feedforward/feedforward_constraints.jl") include("contingency_model/contingency.jl") include("contingency_model/contingency_arguments.jl") include("contingency_model/contingency_constraints.jl") include("parameters/add_parameters.jl") include("simulation/optimization_output_cache.jl") include("simulation/optimization_output_caches.jl") include("simulation/simulation_models.jl") include("simulation/simulation_state.jl") include("simulation/initial_condition_update_simulation.jl") include("simulation/simulation_store_params.jl") include("simulation/hdf_simulation_store.jl") include("simulation/in_memory_simulation_store.jl") include("simulation/simulation_problem_results.jl") include("simulation/get_components_interface.jl") include("simulation/decision_model_simulation_results.jl") include("simulation/emulation_model_simulation_results.jl") include("simulation/realized_meta.jl") include("simulation/simulation_partitions.jl") include("simulation/simulation_partition_results.jl") include("simulation/simulation_sequence.jl") include("simulation/simulation_internal.jl") include("simulation/simulation.jl") include("simulation/simulation_events.jl") include("simulation/simulation_results_export.jl") include("simulation/simulation_results.jl") include("operation/operation_model_simulation_interface.jl") include("parameters/update_container_parameter_values.jl") include("parameters/update_cost_parameters.jl") include("parameters/update_parameters.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/objective_function/import_export.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") include("devices_models/devices/common/add_constraint_dual.jl") include("devices_models/devices/common/rateofchange_constraints.jl") include("devices_models/devices/common/duration_constraints.jl") include("devices_models/devices/common/get_time_series.jl") include("devices_models/devices/common/add_pwl_methods.jl") # Device Modeling components include("devices_models/devices/default_interface_methods.jl") include("devices_models/devices/common/add_to_expression.jl") include("devices_models/devices/common/set_expression.jl") 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/source.jl") include("devices_models/devices/reactivepower_device.jl") #include("devices_models/devices/regulation_device.jl") # Services Models #include("services_models/agc.jl") include("services_models/reserves.jl") include("services_models/reserve_group.jl") include("services_models/transmission_interface.jl") include("services_models/service_slacks.jl") include("services_models/services_constructor.jl") # Network models include("network_models/copperplate_model.jl") include("network_models/powermodels_interface.jl") include("network_models/pm_translator.jl") include("network_models/network_slack_variables.jl") include("network_models/area_balance_model.jl") include("network_models/hvdc_networks.jl") include("network_models/power_flow_evaluation.jl") include("initial_conditions/initialization.jl") # Device constructors include("devices_models/device_constructors/thermalgeneration_constructor.jl") 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/source_constructor.jl") include("devices_models/device_constructors/reactivepowerdevice_constructor.jl") #include("devices_models/device_constructors/regulationdevice_constructor.jl") # Network constructors include("network_models/network_constructor.jl") include("network_models/hvdc_network_constructor.jl") # Templates for Operation Problems include("operation/operation_problem_templates.jl") # Utils include("utils/indexing.jl") @static if pkgversion(PrettyTables).major == 2 # When PrettyTables v3 is more widely adopted in the ecosystem, we can remove this file. # In this case, we should also update the compat bounds in Project.toml to list only # PrettyTables v3. include("utils/print_pt_v2.jl") else include("utils/print_pt_v3.jl") end include("utils/file_utils.jl") include("utils/logging.jl") include("utils/dataframes_utils.jl") include("utils/jump_utils.jl") include("utils/powersystems_utils.jl") include("utils/time_series_utils.jl") include("utils/recorder_events.jl") include("utils/datetime_utils.jl") include("utils/generate_valid_formulations.jl") end ================================================ FILE: src/contingency_model/contingency.jl ================================================ #! format: off # This value could change depending on the event modeling choices get_parameter_multiplier(::EventParameter, ::PSY.Device, ::EventModel) = 1.0 get_initial_parameter_value(::ActivePowerOffsetParameter, ::PSY.Device, ::EventModel) = 0.0 get_initial_parameter_value(::ReactivePowerOffsetParameter, ::PSY.Device, ::EventModel) = 0.0 get_initial_parameter_value(::AvailableStatusChangeCountdownParameter, ::PSY.Device, ::EventModel) = 0.0 get_initial_parameter_value(::AvailableStatusParameter, ::PSY.Device, ::EventModel) = 1.0 supports_outages(::Type{T}) where {T <: PSY.StaticInjection} = false supports_outages(::Type{T}) where {T <: PSY.ThermalStandard} = true supports_outages(::Type{T}) where {T <: PSY.RenewableGen} = true supports_outages(::Type{T}) where {T <: PSY.ElectricLoad} = true supports_outages(::Type{T}) where {T <: PSY.Storage} = true supports_outages(::Type{T}) where {T <: PSY.HydroGen} = true #! format: on ================================================ FILE: src/contingency_model/contingency_arguments.jl ================================================ function add_event_arguments!( container::OptimizationContainer, devices::T, device_model::DeviceModel{U, V}, network_model::NetworkModel, ) where { T <: Union{Vector{U}, IS.FlattenIteratorWrapper{U}}, V <: AbstractDeviceFormulation, } where {U <: PSY.StaticInjection} for (key, event_model) in get_events(device_model) event_type = get_entry_type(key) devices_with_attributes = [d for d in devices if PSY.has_supplemental_attributes(d, event_type)] isempty(devices_with_attributes) && error("no devices found with a supplemental attribute for event $event_type") for p_type in [AvailableStatusChangeCountdownParameter, AvailableStatusParameter] add_parameters!( container, p_type, devices_with_attributes, device_model, event_model, ) end end return end function add_event_arguments!( container::OptimizationContainer, devices::T, device_model::DeviceModel{U, V}, network_model::NetworkModel{<:PM.AbstractActivePowerModel}, ) where { T <: Union{Vector{U}, IS.FlattenIteratorWrapper{U}}, V <: Union{StaticPowerLoad, PowerLoadDispatch, PowerLoadInterruption}, } where {U <: PSY.PowerLoad} for (key, event_model) in get_events(device_model) event_type = get_entry_type(key) devices_with_attributes = [d for d in devices if PSY.has_supplemental_attributes(d, event_type)] isempty(devices_with_attributes) && error("no devices found with a supplemental attribute for event $event_type") for p_type in [AvailableStatusChangeCountdownParameter, AvailableStatusParameter] add_parameters!( container, p_type, devices_with_attributes, device_model, event_model, ) end add_parameters!( container, ActivePowerOffsetParameter, devices_with_attributes, device_model, event_model, ) add_to_expression!( container, ActivePowerBalance, ActivePowerOffsetParameter, devices_with_attributes, device_model, network_model, ) end return end function add_event_arguments!( container::OptimizationContainer, devices::T, device_model::DeviceModel{U, V}, network_model::NetworkModel{<:PM.AbstractPowerModel}, ) where { T <: Union{Vector{U}, IS.FlattenIteratorWrapper{U}}, V <: Union{StaticPowerLoad, PowerLoadDispatch, PowerLoadInterruption}, } where {U <: PSY.PowerLoad} for (key, event_model) in get_events(device_model) event_type = get_entry_type(key) devices_with_attributes = [d for d in devices if PSY.has_supplemental_attributes(d, event_type)] isempty(devices_with_attributes) && error("no devices found with a supplemental attribute for event $event_type") for p_type in [AvailableStatusChangeCountdownParameter, AvailableStatusParameter] add_parameters!( container, p_type, devices_with_attributes, device_model, event_model, ) end add_parameters!( container, ActivePowerOffsetParameter, devices_with_attributes, device_model, event_model, ) add_to_expression!( container, ActivePowerBalance, ActivePowerOffsetParameter, devices_with_attributes, device_model, network_model, ) add_parameters!( container, ReactivePowerOffsetParameter, devices_with_attributes, device_model, event_model, ) add_to_expression!( container, ReactivePowerBalance, ReactivePowerOffsetParameter, devices_with_attributes, device_model, network_model, ) end return end function add_event_arguments!( container::OptimizationContainer, devices::T, device_model::DeviceModel{U, FixedOutput}, network_model::NetworkModel{<:PM.AbstractActivePowerModel}, ) where { T <: Union{Vector{U}, IS.FlattenIteratorWrapper{U}}, } where {U <: PSY.StaticInjection} for (key, event_model) in get_events(device_model) event_type = get_entry_type(key) devices_with_attributes = [d for d in devices if PSY.has_supplemental_attributes(d, event_type)] isempty(devices_with_attributes) && error("no devices found with a supplemental attribute for event $event_type") for p_type in [AvailableStatusChangeCountdownParameter, AvailableStatusParameter] add_parameters!( container, p_type, devices_with_attributes, device_model, event_model, ) end add_parameters!( container, ActivePowerOffsetParameter, devices_with_attributes, device_model, event_model, ) add_to_expression!( container, ActivePowerBalance, ActivePowerOffsetParameter, devices_with_attributes, device_model, network_model, ) end return end function add_event_arguments!( container::OptimizationContainer, devices::T, device_model::DeviceModel{U, FixedOutput}, network_model::NetworkModel{<:PM.AbstractPowerModel}, ) where { T <: Union{Vector{U}, IS.FlattenIteratorWrapper{U}}, } where {U <: PSY.StaticInjection} for (key, event_model) in get_events(device_model) event_type = get_entry_type(key) devices_with_attributes = [d for d in devices if PSY.has_supplemental_attributes(d, event_type)] isempty(devices_with_attributes) && error("no devices found with a supplemental attribute for event $event_type") for p_type in [AvailableStatusChangeCountdownParameter, AvailableStatusParameter] add_parameters!( container, p_type, devices_with_attributes, device_model, event_model, ) end add_parameters!( container, ActivePowerOffsetParameter, devices_with_attributes, device_model, event_model, ) add_to_expression!( container, ActivePowerBalance, ActivePowerOffsetParameter, devices_with_attributes, device_model, network_model, ) add_parameters!( container, ReactivePowerOffsetParameter, devices_with_attributes, device_model, event_model, ) add_to_expression!( container, ReactivePowerBalance, ReactivePowerOffsetParameter, devices_with_attributes, device_model, network_model, ) end return end function _add_parameters!( container, ::T, devices::Vector{U}, device_model::DeviceModel{U, W}, event_model::EventModel{V, X}, ) where { T <: EventParameter, U <: PSY.Component, V <: PSY.Contingency, W <: AbstractDeviceFormulation, X <: AbstractEventCondition, } @debug "adding" T U V _group = LOG_GROUP_OPTIMIZATION_CONTAINER time_steps = get_time_steps(container) parameter_container = add_param_container!( container, T(), U, V, PSY.get_name.(devices), time_steps, ) jump_model = get_jump_model(container) parent_mult = get_multiplier_array_data(parameter_container) parent_param = get_parameter_array_data(parameter_container) for (i, d) in enumerate(devices) ini_val = get_initial_parameter_value(T(), d, event_model) name = PSY.get_name(d) _set_multiplier_at!( parent_mult, get_parameter_multiplier(T(), d, event_model), i, ) for t in time_steps _set_parameter_at!(parent_param, jump_model, ini_val, i, t) end end return end """ Default implementation to add parameters to SystemBalanceExpressions """ function add_to_expression!( container::OptimizationContainer, ::Type{T}, ::Type{U}, devices::Union{Vector{V}, IS.FlattenIteratorWrapper{V}}, device_model::DeviceModel{V, W}, network_model::NetworkModel{X}, ) where { T <: SystemBalanceExpressions, U <: EventParameter, V <: PSY.StaticInjection, W <: AbstractDeviceFormulation, X <: CopperPlatePowerModel, } param_container = get_parameter(container, U(), V) multiplier = get_multiplier_array(param_container) expression = get_expression(container, T(), PSY.System) for d in devices device_bus = PSY.get_bus(d) ref_bus = get_reference_bus(network_model, device_bus) name = PSY.get_name(d) for t in get_time_steps(container) _add_to_jump_expression!( expression[ref_bus, t], get_parameter_array(param_container)[name, t], multiplier[name, t], ) end end return end """ Default implementation to add parameters to SystemBalanceExpressions """ function add_to_expression!( container::OptimizationContainer, ::Type{T}, ::Type{U}, devices::Union{Vector{V}, IS.FlattenIteratorWrapper{V}}, device_model::DeviceModel{V, W}, network_model::NetworkModel{X}, ) where { T <: SystemBalanceExpressions, U <: EventParameter, V <: PSY.StaticInjection, W <: AbstractDeviceFormulation, X <: AbstractPTDFModel, } param_container = get_parameter(container, U(), V) multiplier = get_multiplier_array(param_container) sys_expr = get_expression(container, T(), _system_expression_type(X)) nodal_expr = get_expression(container, T(), PSY.ACBus) network_reduction = get_network_reduction(network_model) for d in devices name = PSY.get_name(d) device_bus = PSY.get_bus(d) bus_no_ = PSY.get_number(device_bus) bus_no = PNM.get_mapped_bus_number(network_reduction, bus_no_) ref_index = _ref_index(network_model, device_bus) for t in get_time_steps(container) param = get_parameter_array(param_container)[name, t] _add_to_jump_expression!(sys_expr[ref_index, t], param, multiplier[name, t]) _add_to_jump_expression!(nodal_expr[bus_no, t], param, multiplier[name, t]) end end return end """ Default implementation to add parameters to SystemBalanceExpressions """ function add_to_expression!( container::OptimizationContainer, ::Type{T}, ::Type{U}, devices::Union{Vector{V}, IS.FlattenIteratorWrapper{V}}, model::DeviceModel{V, W}, network_model::NetworkModel{X}, ) where { T <: SystemBalanceExpressions, U <: EventParameter, V <: PSY.Device, W <: AbstractDeviceFormulation, X <: PM.AbstractPowerModel, } param_container = get_parameter(container, U(), V) multiplier = get_multiplier_array(param_container) network_reduction = get_network_reduction(network_model) for d in devices, t in get_time_steps(container) bus_no = PNM.get_mapped_bus_number(network_reduction, PSY.get_bus(d)) name = PSY.get_name(d) _add_to_jump_expression!( get_expression(container, T(), PSY.ACBus)[bus_no, t], get_parameter_array(param_container)[name, t], multiplier[name, t], ) end return end function add_to_expression!( container::OptimizationContainer, ::Type{T}, ::Type{U}, devices::Union{Vector{V}, IS.FlattenIteratorWrapper{V}}, model::DeviceModel{V, W}, network_model::NetworkModel{AreaBalancePowerModel}, ) where { T <: SystemBalanceExpressions, U <: EventParameter, 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_array(param_container)[name, t], multiplier[name, t], ) end return end ================================================ FILE: src/contingency_model/contingency_constraints.jl ================================================ function add_event_constraints!( container::OptimizationContainer, devices::T, device_model::DeviceModel{U, V}, network_model::NetworkModel{W}, ) where { T <: Union{Vector{U}, IS.FlattenIteratorWrapper{U}}, V <: AbstractDeviceFormulation, W <: PM.AbstractActivePowerModel, } where {U <: PSY.ThermalGen} for (key, event_model) in get_events(device_model) event_type = get_entry_type(key) devices_with_attributes = [d for d in devices if PSY.has_supplemental_attributes(d, event_type)] isempty(devices_with_attributes) && error("no devices found with a supplemental attribute for event $event_type") add_parameterized_upper_bound_range_constraints( container, ActivePowerOutageConstraint, ActivePowerRangeExpressionUB, AvailableStatusParameter, devices_with_attributes, device_model, W, ) end return end function add_event_constraints!( container::OptimizationContainer, devices::T, device_model::DeviceModel{U, V}, network_model::NetworkModel{W}, ) where { T <: Union{Vector{U}, IS.FlattenIteratorWrapper{U}}, V <: AbstractDeviceFormulation, W <: PM.AbstractPowerModel, } where {U <: PSY.ThermalGen} for (key, event_model) in get_events(device_model) event_type = get_entry_type(key) devices_with_attributes = [d for d in devices if PSY.has_supplemental_attributes(d, event_type)] isempty(devices_with_attributes) && error("no devices found with a supplemental attribute for event $event_type") add_parameterized_upper_bound_range_constraints( container, ActivePowerOutageConstraint, ActivePowerRangeExpressionUB, AvailableStatusParameter, devices_with_attributes, device_model, W, ) add_reactive_power_contingency_constraint( container, ReactivePowerOutageConstraint, ReactivePowerVariable, AvailableStatusParameter, devices_with_attributes, device_model, W, ) end return end function add_event_constraints!( container::OptimizationContainer, devices::T, device_model::DeviceModel{U, V}, network_model::NetworkModel{W}, ) where { T <: Union{Vector{U}, IS.FlattenIteratorWrapper{U}}, V <: AbstractDeviceFormulation, W <: PM.AbstractActivePowerModel, } where {U <: PSY.RenewableGen} for (key, event_model) in get_events(device_model) event_type = get_entry_type(key) devices_with_attributes = [d for d in devices if PSY.has_supplemental_attributes(d, event_type)] isempty(devices_with_attributes) && error("no devices found with a supplemental attribute for event $event_type") if has_service_model(device_model) lhs_type = ActivePowerRangeExpressionUB else lhs_type = ActivePowerVariable end add_parameterized_upper_bound_range_constraints( container, ActivePowerOutageConstraint, lhs_type, AvailableStatusParameter, devices_with_attributes, device_model, W, ) end return end function add_event_constraints!( container::OptimizationContainer, devices::T, device_model::DeviceModel{U, V}, network_model::NetworkModel{W}, ) where { T <: Union{Vector{U}, IS.FlattenIteratorWrapper{U}}, V <: AbstractDeviceFormulation, W <: PM.AbstractPowerModel, } where {U <: PSY.RenewableGen} for (key, event_model) in get_events(device_model) event_type = get_entry_type(key) devices_with_attributes = [d for d in devices if PSY.has_supplemental_attributes(d, event_type)] isempty(devices_with_attributes) && error("no devices found with a supplemental attribute for event $event_type") if has_service_model(device_model) lhs_type = ActivePowerRangeExpressionUB else lhs_type = ActivePowerVariable end add_parameterized_upper_bound_range_constraints( container, ActivePowerOutageConstraint, lhs_type, AvailableStatusParameter, devices_with_attributes, device_model, W, ) add_reactive_power_contingency_constraint( container, ReactivePowerOutageConstraint, ReactivePowerVariable, AvailableStatusParameter, devices_with_attributes, device_model, W, ) end return end function add_event_constraints!( container::OptimizationContainer, devices::T, device_model::DeviceModel{U, V}, network_model::NetworkModel{W}, ) where { T <: Union{Vector{U}, IS.FlattenIteratorWrapper{U}}, V <: AbstractDeviceFormulation, W <: PM.AbstractActivePowerModel, } where {U <: PSY.ElectricLoad} for (key, event_model) in get_events(device_model) event_type = get_entry_type(key) devices_with_attributes = [d for d in devices if PSY.has_supplemental_attributes(d, event_type)] isempty(devices_with_attributes) && error("no devices found with a supplemental attribute for event $event_type") add_parameterized_upper_bound_range_constraints( container, ActivePowerOutageConstraint, ActivePowerVariable, AvailableStatusParameter, devices_with_attributes, device_model, W, ) end return end function add_event_constraints!( container::OptimizationContainer, devices::T, device_model::DeviceModel{U, V}, network_model::NetworkModel{W}, ) where { T <: Union{Vector{U}, IS.FlattenIteratorWrapper{U}}, V <: AbstractDeviceFormulation, W <: PM.AbstractPowerModel, } where {U <: PSY.ElectricLoad} for (key, event_model) in get_events(device_model) event_type = get_entry_type(key) devices_with_attributes = [d for d in devices if PSY.has_supplemental_attributes(d, event_type)] isempty(devices_with_attributes) && error("no devices found with a supplemental attribute for event $event_type") add_parameterized_upper_bound_range_constraints( container, ActivePowerOutageConstraint, ActivePowerVariable, AvailableStatusParameter, devices_with_attributes, device_model, W, ) add_reactive_power_contingency_constraint( container, ReactivePowerOutageConstraint, ReactivePowerVariable, AvailableStatusParameter, devices_with_attributes, device_model, W, ) end return end function add_reactive_power_contingency_constraint( container::OptimizationContainer, ::Type{ReactivePowerOutageConstraint}, ::Type{ReactivePowerVariable}, ::Type{AvailableStatusParameter}, devices::Union{Vector{V}, IS.FlattenIteratorWrapper{V}}, model::DeviceModel{V, W}, ::Type{X}, ) where { V <: PSY.Component, W <: AbstractDeviceFormulation, X <: PM.AbstractPowerModel, } array_reactive = get_variable(container, ReactivePowerVariable(), V) _add_reactive_power_contingency_constraint_impl!( container, ReactivePowerOutageConstraint, array_reactive, AvailableStatusParameter(), devices, model, ) return end function _add_reactive_power_contingency_constraint_impl!( container::OptimizationContainer, ::Type{ReactivePowerOutageConstraint}, array_reactive, param::AvailableStatusParameter, devices::Union{Vector{V}, IS.FlattenIteratorWrapper{V}}, model::DeviceModel{V, W}, ) where { V <: PSY.Component, W <: AbstractDeviceFormulation, } time_steps = get_time_steps(container) names = PSY.get_name.(devices) constraint_container = add_constraints_container!( container, ReactivePowerOutageConstraint(), V, names, time_steps; meta = "ub", ) param_array = get_parameter_array(container, param, V) param_multiplier = get_parameter_multiplier_array(container, AvailableStatusParameter(), V) jump_model = get_jump_model(container) time_steps = axes(constraint_container)[2] for device in devices, t in time_steps name = PSY.get_name(device) ub = _get_reactive_power_upper_bound(device) constraint_container[name, t] = JuMP.@constraint( jump_model, (array_reactive[name, t])^2 <= (ub * param_array[name, t]) ) end return end function _get_reactive_power_upper_bound(device::PSY.StaticInjection) return maximum([ PSY.get_reactive_power_limits(device).max^2, PSY.get_reactive_power_limits(device).min^2, ]) end function _get_reactive_power_upper_bound(device::PSY.ElectricLoad) return PSY.get_max_reactive_power(device)^2 end ================================================ FILE: src/core/abstract_feedforward.jl ================================================ abstract type AbstractAffectFeedforward end get_device_type(x::AbstractAffectFeedforward) = x.device_type ================================================ FILE: src/core/abstract_simulation_store.jl ================================================ """ Provides storage of simulation data """ abstract type SimulationStore end # Required methods: # - open_store # - Base.isopen(store::SimulationStore) # - Base.close(store::SimulationStore) # - Base.flush(store::SimulationStore) # - get_params(store::SimulationStore) # - initialize_problem_storage! # - list_decision_model_keys(store::SimulationStore, problem::Symbol, container_type::Symbol) # - list_emulation_model_keys(store::SimulationStore, container_type::Symbol) # - list_decision_models(store::SimulationStore) # - log_cache_hit_percentages(store::SimulationStore) # - write_result! # - read_result! # - read_results # - write_optimizer_stats! # - read_optimizer_stats # - get_dm_data # - get_em_data # - get_container_key_lookup get_dm_data(store::SimulationStore) = store.dm_data get_em_data(store::SimulationStore) = store.em_data ================================================ FILE: src/core/auxiliary_variables.jl ================================================ """ Auxiliary Variable for Thermal Generation Models to keep track of time elapsed on """ struct TimeDurationOn <: AuxVariableType end """ Auxiliary Variable for Thermal Generation Models to keep track of time elapsed off """ struct TimeDurationOff <: AuxVariableType end """ Auxiliary Variable for Thermal Generation Models that solve for power above min """ struct PowerOutput <: AuxVariableType end """ Auxiliary Variable of DC Current Variables for DC Lines formulations Docs abbreviation: ``p_l^{loss}`` """ struct DCLineLosses <: AuxVariableType end """ Auxiliary Variables that are calculated using a `PowerFlowEvaluationModel` """ abstract type PowerFlowAuxVariableType <: AuxVariableType end """ Auxiliary Variable for the bus angle results from power flow evaluation """ struct PowerFlowVoltageAngle <: PowerFlowAuxVariableType end """ Auxiliary Variable for the bus voltage magnitued results from power flow evaluation """ struct PowerFlowVoltageMagnitude <: PowerFlowAuxVariableType end """ Auxiliary Variable for line power flow results from power flow evaluation """ abstract type BranchFlowAuxVariableType <: PowerFlowAuxVariableType end """ Auxiliary Variable for the line reactive flow in the from -> to direction from power flow evaluation """ struct PowerFlowBranchReactivePowerFromTo <: BranchFlowAuxVariableType end """ Auxiliary Variable for the line reactive flow in the to -> from direction from power flow evaluation """ struct PowerFlowBranchReactivePowerToFrom <: BranchFlowAuxVariableType end """ Auxiliary Variable for the line active flow in the from -> to direction from power flow evaluation """ struct PowerFlowBranchActivePowerFromTo <: BranchFlowAuxVariableType end """ Auxiliary Variable for the line active flow in the to -> from direction from power flow evaluation """ struct PowerFlowBranchActivePowerToFrom <: BranchFlowAuxVariableType end """ Auxiliary Variable for the loss factors from AC power flow evaluation that are calculated using the Jacobian matrix """ struct PowerFlowLossFactors <: PowerFlowAuxVariableType end """ Auxiliary Variable for the voltage stability factors from AC power flow evaluation that are calculated using the Jacobian matrix """ struct PowerFlowVoltageStabilityFactors <: PowerFlowAuxVariableType end # should this be a subtype of BranchFlowAuxVariableType? It's line-related but has no flow direction. """ Auxiliary Variable for the active power loss on a line from AC power flow evaluation. """ struct PowerFlowBranchActivePowerLoss <: PowerFlowAuxVariableType end # TODO reactive loss? convert_result_to_natural_units(::Type{PowerOutput}) = true convert_result_to_natural_units( ::Type{ <:Union{ PowerFlowBranchReactivePowerFromTo, PowerFlowBranchReactivePowerToFrom, PowerFlowBranchActivePowerFromTo, PowerFlowBranchActivePowerToFrom, PowerFlowBranchActivePowerLoss, }, }, ) = true "Whether the auxiliary variable is calculated using a `PowerFlowEvaluationModel`" is_from_power_flow(::Type{<:AuxVariableType}) = false is_from_power_flow(::Type{<:PowerFlowAuxVariableType}) = true ================================================ FILE: src/core/cache_utils.jl ================================================ struct OptimizationResultCacheKey model::Symbol key::OptimizationContainerKey end struct CacheFlushRule keep_in_cache::Bool end CacheFlushRule() = CacheFlushRule(false) const DEFAULT_SIMULATION_STORE_CACHE_SIZE_MiB = 1024 const DEFAULT_SIMULATION_STORE_CACHE_SIZE = DEFAULT_SIMULATION_STORE_CACHE_SIZE_MiB * MiB const MIN_CACHE_FLUSH_SIZE_MiB = 1 const MIN_CACHE_FLUSH_SIZE = MIN_CACHE_FLUSH_SIZE_MiB * MiB """ Informs the flusher on what data to keep in cache. """ struct CacheFlushRules data::Dict{OptimizationResultCacheKey, CacheFlushRule} min_flush_size::Int max_size::Int end function CacheFlushRules(; max_size = DEFAULT_SIMULATION_STORE_CACHE_SIZE, min_flush_size = MIN_CACHE_FLUSH_SIZE, ) return CacheFlushRules( Dict{OptimizationResultCacheKey, CacheFlushRule}(), min_flush_size, max_size, ) end function add_rule!( rules::CacheFlushRules, model_name, op_container_key, keep_in_cache::Bool, ) key = OptimizationResultCacheKey(model_name, op_container_key) rules.data[key] = CacheFlushRule(keep_in_cache) return end function get_rule(x::CacheFlushRules, model, op_container_key) return get_rule(x, OptimizationResultCacheKey(model, op_container_key)) end get_rule(x::CacheFlushRules, key::OptimizationResultCacheKey) = x.data[key] mutable struct CacheStats hits::Int misses::Int end CacheStats() = CacheStats(0, 0) function get_cache_hit_percentage(x::CacheStats) total = x.hits + x.misses total == 0 && return 0.0 return x.hits / (total) * 100 end ================================================ FILE: src/core/constraints.jl ================================================ abstract type PostContingencyConstraintType <: ConstraintType end 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 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 constraint to balance active power. For more information check [ThermalGen Formulations](@ref ThermalGen-Formulations). The specified constraint is generally formulated as: ```math \\sum_{g \\in \\mathcal{G}_c} p_{g,t} &= \\sum_{g \\in \\mathcal{G}} \\Delta p_{g, c, t} &\\quad \\forall c \\in \\mathcal{C} \\ \\forall t \\in \\{1, \\dots, T\\} ``` """ struct PostContingencyGenerationBalanceConstraint <: PostContingencyConstraintType 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 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 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 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 abstract type AbstractPiecewiseLinearBlockOfferConstraint <: ConstraintType end """ Struct to create the PiecewiseLinearBlockIncrementalOfferConstraint associated with a specified variable. See [Piecewise linear cost functions](@ref pwl_cost) for more information. """ struct PiecewiseLinearBlockIncrementalOfferConstraint <: AbstractPiecewiseLinearBlockOfferConstraint end """ Struct to create the PiecewiseLinearBlockDecrementalOfferConstraint associated with a specified variable. See [Piecewise linear cost functions](@ref pwl_cost) for more information. """ struct PiecewiseLinearBlockDecrementalOfferConstraint <: AbstractPiecewiseLinearBlockOfferConstraint 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 PostContingencyRampConstraint <: PostContingencyConstraintType end struct RampLimitConstraint <: ConstraintType end struct RangeLimitConstraint <: ConstraintType end """ Struct to create the constraint that set the AC flow limits through AC branches and HVDC two-terminal 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 FlowRateConstraint <: ConstraintType end struct PostContingencyEmergencyRateLimitConstraint <: PostContingencyConstraintType end """ Struct to create the constraint for branch flow rate limits from the 'from' bus to the 'to' bus. For more information check [Branch Formulations](@ref PowerSystems.Branch-Formulations). """ struct FlowRateConstraintFromTo <: ConstraintType end """ Struct to create the constraint for branch flow rate limits from the 'to' bus to the 'from' bus. For more information check [Branch Formulations](@ref PowerSystems.Branch-Formulations). """ struct FlowRateConstraintToFrom <: 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 InterfaceFlowLimit <: ConstraintType end struct HVDCFlowCalculationConstraint <: ConstraintType end """ Struct to create the constraint that calculates the Rectifier DC line voltage. ```math v_d^r = \\frac{3}{\\pi}N^r \\left( \\sqrt{2}\frac{a^r v_\\text{ac}^r}{t^r}\\cos{\\alpha^r}-X^r I_d \\right) ``` """ struct HVDCRectifierDCLineVoltageConstraint <: ConstraintType end """ Struct to create the constraint that calculates the Inverter DC line voltage. ```math v_d^i = \\frac{3}{\\pi}N^i \\left( \\sqrt{2}\frac{a^i v_\\text{ac}^i}{t^i}\\cos{\\gamma^i}-X^i I_d \\right) ``` """ struct HVDCInverterDCLineVoltageConstraint <: ConstraintType end """ Struct to create the constraint that calculates the Rectifier Overlap Angle. ```math \\mu^r = \\arccos \\left( \\cos\\alpha^r - \\frac{\\sqrt{2} I_d X^r t^r}{a^r v_\\text{ac}^r} \\right) - \\alpha^r ``` """ struct HVDCRectifierOverlapAngleConstraint <: ConstraintType end """ Struct to create the constraint that calculates the Inverter Overlap Angle. ```math \\mu^i = \\arccos \\left( \\cos\\gamma^i - \\frac{\\sqrt{2} I_d X^i t^r}{a^i v_\\text{ac}^i} \\right) - \\gamma^i ``` """ struct HVDCInverterOverlapAngleConstraint <: ConstraintType end """ Struct to create the constraint that calculates the Rectifier Power Factor Angle. ```math \\phi^r = \\arctan \\left( \\frac{2\\mu^r + \\sin(2\\alpha^r) - \\sin(2(\\mu^r + \\alpha^r))}{\\cos(2\alpha^r) - \\cos(2(\\mu^r + \\alpha^r))} \\right) ``` """ struct HVDCRectifierPowerFactorAngleConstraint <: ConstraintType end """ Struct to create the constraint that calculates the Inverter Power Factor Angle. ```math \\phi^i = \\arctan \\left( \\frac{2\\mu^i + \\sin(2\\gamma^i) - \\sin(2(\\mu^i + \\gamma^i))}{\\cos(2\\gamma^i) - \\cos(2(\\mu^i + \\gamma^i))} \\right) ``` """ struct HVDCInverterPowerFactorAngleConstraint <: ConstraintType end """ Struct to create the constraint that calculates the AC Current flowing into the AC side of the rectifier. ```math i_\text{ac}^r = \\sqrt{6} \\frac{N^r}{\\pi}I_d ``` """ struct HVDCRectifierACCurrentFlowConstraint <: ConstraintType end """ Struct to create the constraint that calculates the AC Current flowing into the AC side of the inverter. ```math i_\text{ac}^i = \\sqrt{6} \\frac{N^i}{\\pi}I_d ``` """ struct HVDCInverterACCurrentFlowConstraint <: ConstraintType end """ Struct to create the constraint that calculates the AC Power injection at the AC side of the rectifier. ```math \\begin{align*} p_\\text{ac}^r = \\sqrt{3} i_\\text{ac}^r \\frac{a^r v_\\text{ac}^r}{t^r}\\cos{\\phi^r} \\\\ q_\\text{ac}^r = \\sqrt{3} i_\\text{ac}^r \\frac{a^r v_\\text{ac}^r}{t^r}\\sin{\\phi^r} \\\\ \\end{align*} ``` """ struct HVDCRectifierPowerCalculationConstraint <: ConstraintType end """ Struct to create the constraint that calculates the AC Power injection at the AC side of the inverter. ```math \\begin{align*} p_\\text{ac}^i = \\sqrt{3} i_\\text{ac}^i \\frac{a^i v_\\text{ac}^i}{t^i}\\cos{\\phi^i} \\\\ q_\\text{ac}^i = \\sqrt{3} i_\\text{ac}^i \\frac{a^i v_\\text{ac}^i}{t^i}\\sin{\\phi^i} \\\\ \\end{align*} ``` """ struct HVDCInverterPowerCalculationConstraint <: ConstraintType end """ Struct to create the constraint that links the AC and DC side of the network. ```math v_d^i = v_d^r - R_d I_d ``` """ struct HVDCTransmissionDCLineConstraint <: 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\\} ``` """ abstract type PostContingencyVariableLimitsConstraint <: PowerVariableLimitsConstraint 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 post-contingency 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 + \\Delta p_{c, t} \\le P^\\text{max}, \\quad \\forall c \\in \\mathcal{C} \\ \\forall t \\in \\{1,\\dots,T\\} ``` """ struct PostContingencyActivePowerVariableLimitsConstraint <: PostContingencyVariableLimitsConstraint end """ Struct to create the constraint to limit post-contingency active power reserve deploymentexpressions. 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 \\Delta rsv_{r, c, t} \\le rsv_{r, c, t}, \\quad \\forall r \\in \\mathcal{R} \\ \\forall c \\in \\mathcal{C} \\ \\forall t \\in \\{1,\\dots,T\\} ``` """ struct PostContingencyActivePowerReserveDeploymentVariableLimitsConstraint <: PostContingencyVariableLimitsConstraint 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 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^{out} \\le \\text{ActivePowerTimeSeriesParameter}_t, \\quad \\forall t \\in \\{1,\\dots,T\\} ``` """ struct ActivePowerOutVariableTimeSeriesLimitsConstraint <: 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^{in} \\le \\text{ActivePowerTimeSeriesParameter}_t, \\quad \\forall t \\in \\{1,\\dots,T\\} ``` """ struct ActivePowerInVariableTimeSeriesLimitsConstraint <: PowerVariableLimitsConstraint end """ Struct to create the constraint to limit the import and exports in a determined period. For more information check [Device Formulations](@ref formulation_intro). """ struct ImportExportBudgetConstraint <: ConstraintType end struct LineFlowBoundConstraint <: ConstraintType end abstract type EventConstraint <: ConstraintType end struct ActivePowerOutageConstraint <: EventConstraint end struct ReactivePowerOutageConstraint <: EventConstraint end ############################################################ ########## Multi-Terminal Converter Constraints ############ ############################################################ """ Struct to create the constraints that set the current flowing through a DC line. ```math \\begin{align*} & i_l^{dc} = \\frac{1}{r_l} (v_{from,l} - v_{to,l}), \\quad \\forall t \\in \\{1,\\dots, T\\} \\end{align*} ``` """ struct DCLineCurrentConstraint <: ConstraintType end struct NodalBalanceCurrentConstraint <: ConstraintType end """ Struct to create the constraints that compute the converter DC power based on current and voltage. The specified constraints are formulated as: ```math \\begin{align*} & p_c = 0.5 * (γ^sq - v^sq - i^sq), \\quad \\forall t \\in \\{1,\\dots, T\\} \\\\ & γ_c = v_c + i_c, \\quad \\forall t \\in \\{1,\\dots, T\\} \\\\ \\end{align*} ``` """ struct ConverterPowerCalculationConstraint <: ConstraintType end """ Struct to create the constraints that decide the balance of AC and DC power of the converter. The specified constraints are formulated as: ```math \\begin{align*} & p_ac = p_dc - loss_t \\quad \\forall t \\in \\{1,\\dots, T\\} \\\\ & loss_t = a i_c^2 + b i_c + c \\\\ \\end{align*} ``` """ struct ConverterLossConstraint <: ConstraintType end """ Struct to create the McCormick envelopes constraints that decide the bounds on the DC active power. The specified constraints are formulated as: ```math \\begin{align*} & p_c >= V^{min} i_c + v_c I^{min} - I^{min}V^{min}, \\quad \\forall t \\in \\{1,\\dots, T\\} \\\\ & p_c >= V^{max} i_c + v_c I^{max} - I^{max}V^{max}, \\quad \\forall t \\in \\{1,\\dots, T\\} \\\\ & p_c <= V^{max} i_c + v_c I^{min} - I^{min}V^{max}, \\quad \\forall t \\in \\{1,\\dots, T\\} \\\\ & p_c <= V^{min} i_c + v_c I^{max} - I^{max}V^{min}, \\quad \\forall t \\in \\{1,\\dots, T\\} \\\\ \\end{align*} ``` """ struct ConverterMcCormickEnvelopes <: ConstraintType end """ Struct to create the Quadratic PWL interpolation constraints that decide square value of the voltage. In this case x = voltage and y = squared_voltage. The specified constraints are formulated as: ```math \\begin{align*} & x = x_0 + \\sum_{k=1}^K (x_{k} - x_{k-1}) \\delta_k, \\quad \\forall t \\in \\{1,\\dots, T\\} \\\\ & y = y_0 + \\sum_{k=1}^K (x_{k} - x_{k-1}) \\delta_k, \\quad \\forall t \\in \\{1,\\dots, T\\} \\\\ & z_k \\le \\delta_k, \\quad \\forall t \\in \\{1,\\dots, T\\}, \\forall k \\in \\{1,\\dots, K-1\\} \\\\ & z_k \\ge \\delta_{k+1}, \\quad \\forall t \\in \\{1,\\dots, T\\}, \\forall k \\in \\{1,\\dots, K-1\\} \\\\ \\end{align*} ``` """ struct InterpolationVoltageConstraints <: ConstraintType end """ Struct to create the Quadratic PWL interpolation constraints that decide square value of the current. In this case x = current and y = squared_current. The specified constraints are formulated as: ```math \\begin{align*} & x = x_0 + \\sum_{k=1}^K (x_{k} - x_{k-1}) \\delta_k, \\quad \\forall t \\in \\{1,\\dots, T\\} \\\\ & y = y_0 + \\sum_{k=1}^K (x_{k} - x_{k-1}) \\delta_k, \\quad \\forall t \\in \\{1,\\dots, T\\} \\\\ & z_k \\le \\delta_k, \\quad \\forall t \\in \\{1,\\dots, T\\}, \\forall k \\in \\{1,\\dots, K-1\\} \\\\ & z_k \\ge \\delta_{k+1}, \\quad \\forall t \\in \\{1,\\dots, T\\}, \\forall k \\in \\{1,\\dots, K-1\\} \\\\ \\end{align*} ``` """ struct InterpolationCurrentConstraints <: ConstraintType end """ Struct to create the Quadratic PWL interpolation constraints that decide square value of the bilinear variable γ. In this case x = γ and y = squared_γ. The specified constraints are formulated as: ```math \\begin{align*} & x = x_0 + \\sum_{k=1}^K (x_{k} - x_{k-1}) \\delta_k, \\quad \\forall t \\in \\{1,\\dots, T\\} \\\\ & y = y_0 + \\sum_{k=1}^K (x_{k} - x_{k-1}) \\delta_k, \\quad \\forall t \\in \\{1,\\dots, T\\} \\\\ & z_k \\le \\delta_k, \\quad \\forall t \\in \\{1,\\dots, T\\}, \\forall k \\in \\{1,\\dots, K-1\\} \\\\ & z_k \\ge \\delta_{k+1}, \\quad \\forall t \\in \\{1,\\dots, T\\}, \\forall k \\in \\{1,\\dots, K-1\\} \\\\ \\end{align*} ``` """ struct InterpolationBilinearConstraints <: ConstraintType end """ Struct to create the constraints that set the absolute value for the current to use in losses through a lossy Interconnecting Power Converter. The specified constraint is formulated as: ```math \\begin{align*} & i_c^{dc} = i_c^+ - i_c^-, \\quad \\forall t \\in \\{1,\\dots, T\\} \\\\ & i_c^+ \\le I_{max} \\cdot \\nu_c, \\quad \\forall t \\in \\{1,\\dots, T\\} \\\\ & i_c^+ \\le I_{max} \\cdot (1 - \\nu_c), \\quad \\forall t \\in \\{1,\\dots, T\\} \\end{align*} ``` """ struct CurrentAbsoluteValueConstraint <: ConstraintType end """ Struct to create the constraint to balance shifted power over the user-defined time horizons. For more information check the [`PowerLoadShift`](@ref) formulation. The specified constraints are formulated as: ```math \\sum_{t \\in \\text{time horizon}_k } p_t^\\text{shift,up} - p_t^\\text{shift,dn} = 0 , \\quad \\forall k \\text{ time horizons} ``` """ struct ShiftedActivePowerBalanceConstraint <: ConstraintType end """ Struct to create the constraint to balance shifted power over the user-defined time horizons. For more information check the [`PowerLoadShift`](@ref) formulation. The specified constraints are formulated as: ```math p_t^\\text{realized} \\ge 0.0 , \\quad \\forall k \\text{ time horizons} ``` """ struct RealizedShiftedLoadMinimumBoundConstraint <: ConstraintType end """ Struct to create the non-anticipativity constraint for the [`PowerLoadShift`](@ref) formulation. This enforces that shift up can only occur after an equal or greater amount of shift down has already been committed, preventing the optimizer from shifting load up before it has been shifted down. The constraint is formulated as: ```math \\sum_{\\tau=1}^{t} \\left( p_\\tau^\\text{shift,dn} - p_\\tau^\\text{shift,up} \\right) \\ge 0, \\quad \\forall t \\in \\{1,\\dots,T\\} ``` """ struct NonAnticipativityConstraint <: ConstraintType end """ Struct to create the constraint to limit shifted power active power between upper and lower bounds. For more information check the [`PowerLoadShift`](@ref) formulation. The specified constraints are formulated as: ```math 0 \\le p_t^\\text{shift, up} \\le P_t^\\text{upper}, \\quad \\forall t \\in \\{1,\\dots,T\\} ``` """ struct ShiftUpActivePowerVariableLimitsConstraint <: PowerVariableLimitsConstraint end """ Struct to create the constraint to limit shifted power active power between upper and lower bounds. For more information check the [`PowerLoadShift`](@ref) formulation. The specified constraints are formulated as: ```math 0 \\le p_t^\\text{shift, dn} \\le P_t^\\text{lower}, \\quad \\forall t \\in \\{1,\\dots,T\\} ``` """ struct ShiftDownActivePowerVariableLimitsConstraint <: PowerVariableLimitsConstraint end ================================================ FILE: src/core/dataset.jl ================================================ abstract type AbstractDataset end get_data_resolution(s::AbstractDataset)::Dates.Millisecond = s.resolution get_last_recorded_row(s::AbstractDataset) = s.last_recorded_row """ Return the timestamp from the data used in the last update """ get_update_timestamp(s::AbstractDataset) = s.update_timestamp function set_last_recorded_row!(s::AbstractDataset, val::Int) s.last_recorded_row = val return end function set_update_timestamp!(s::AbstractDataset, val::Dates.DateTime) s.update_timestamp = val return end # Values field is accessed with dot syntax to avoid type instability mutable struct InMemoryDataset{N} <: AbstractDataset "Data with dimensions (N column names, row indexes)" values::DenseAxisArray{Float64, N} # We use Array here to allow for overwrites when updating the state timestamps::Vector{Dates.DateTime} # Resolution is needed because AbstractDataset might have just one row resolution::Dates.Millisecond end_of_step_index::Int last_recorded_row::Int update_timestamp::Dates.DateTime end function InMemoryDataset( values::DenseAxisArray{Float64, N}, timestamps::Vector{Dates.DateTime}, resolution::Dates.Millisecond, end_of_step_index::Int, ) where {N} return InMemoryDataset{N}( values, timestamps, resolution, end_of_step_index, 0, UNSET_INI_TIME, ) end function InMemoryDataset(values::DenseAxisArray{Float64, N}) where {N} return InMemoryDataset{N}( values, Vector{Dates.DateTime}(), Dates.Second(0.0), 1, 0, UNSET_INI_TIME, ) end # Helper method for one dimensional cases function InMemoryDataset( fill_val::Float64, initial_time::Dates.DateTime, resolution::Dates.Millisecond, end_of_step_index::Int, row_count::Int, column_names::Vector{String}) return InMemoryDataset( fill_val, initial_time, resolution, end_of_step_index, row_count, (column_names,), ) end function InMemoryDataset( fill_val::Float64, initial_time::Dates.DateTime, resolution::Dates.Millisecond, end_of_step_index::Int, row_count::Int, column_names::NTuple{N, <:Any}) where {N} return InMemoryDataset( fill!( DenseAxisArray{Float64}(undef, column_names..., 1:row_count), fill_val, ), collect( range( initial_time; step = resolution, length = row_count, ), ), resolution, end_of_step_index, ) end get_num_rows(s::InMemoryDataset{N}) where {N} = size(s.values)[N] function make_system_state( timestamp::Dates.DateTime, resolution::Dates.Millisecond, columns::NTuple{N, <:Any}, ) where {N} return InMemoryDataset(NaN, timestamp, resolution, 0, 1, columns) end function get_dataset_value( s::T, date::Dates.DateTime, ) where {T <: Union{InMemoryDataset{1}, InMemoryDataset{2}}} s_index = find_timestamp_index(s.timestamps, date) if isnothing(s_index) error("Request time stamp $date not in the state") end return s.values[:, s_index] end function get_dataset_value(s::InMemoryDataset{3}, date::Dates.DateTime) s_index = find_timestamp_index(s.timestamps, date) if isnothing(s_index) error("Request time stamp $date not in the state") end return s.values[:, :, s_index] end function get_column_names(k::OptimizationContainerKey, s::InMemoryDataset) return get_column_names_from_axis_array(k, s.values) end function get_last_recorded_value(s::InMemoryDataset{2}) if get_last_recorded_row(s) == 0 error("The Dataset hasn't been written yet") end return s.values[:, get_last_recorded_row(s)] end function get_last_recorded_value(s::InMemoryDataset{3}) if get_last_recorded_row(s) == 0 error("The Dataset hasn't been written yet") end return s.values[:, :, get_last_recorded_row(s)] end function get_end_of_step_timestamp(s::InMemoryDataset) return s.timestamps[s.end_of_step_index] end """ Return the timestamp from most recent data row updated in the dataset. This value may not be the same as the result from `get_update_timestamp` """ function get_last_updated_timestamp(s::InMemoryDataset) last_recorded_row = get_last_recorded_row(s) if last_recorded_row == 0 return UNSET_INI_TIME end return s.timestamps[last_recorded_row] end function get_value_timestamp(s::InMemoryDataset, date::Dates.DateTime) s_index = find_timestamp_index(s.timestamps, date) if isnothing(s_index) error("Request time stamp $date not in the state") end return s.timestamps[s_index] end # These set_value! methods expect a single time_step value because they are used to update #the state so the incoming vals will have one dimension less than the DataSet. The exception # is for vals of Dimension 1 which are still stored in DataSets of dimension 2. function set_value!(s::InMemoryDataset{2}, vals::DenseAxisArray{Float64, 2}, index::Int) s.values[:, index] = vals[:, index] return end function set_value!(s::InMemoryDataset{2}, vals::DenseAxisArray{Float64, 1}, index::Int) s.values[:, index] = vals return end function set_value!(s::InMemoryDataset{3}, vals::DenseAxisArray{Float64, 2}, index::Int) s.values[:, :, index] = vals return end function set_value!(s::InMemoryDataset{2}, vals::Array{Float64, 1}, index::Int) s.values[:, index] = vals return end # HDF5Dataset does not account of overwrites in the data. Values are written sequentially. mutable struct HDF5Dataset{N} <: AbstractDataset values::HDF5.Dataset column_dataset::HDF5.Dataset write_index::Int last_recorded_row::Int resolution::Dates.Millisecond initial_timestamp::Dates.DateTime update_timestamp::Dates.DateTime column_names::NTuple{N, Vector{String}} function HDF5Dataset{N}(values, column_dataset, write_index, last_recorded_row, resolution, initial_timestamp, update_timestamp, column_names::NTuple{N, Vector{String}}, ) where {N} new{N}(values, column_dataset, write_index, last_recorded_row, resolution, initial_timestamp, update_timestamp, column_names) end end function HDF5Dataset{1}( values::HDF5.Dataset, column_dataset::HDF5.Dataset, ::NTuple{1, Int}, resolution::Dates.Millisecond, initial_time::Dates.DateTime, ) HDF5Dataset{1}( values, column_dataset, 1, 0, resolution, initial_time, UNSET_INI_TIME, (column_dataset[:],), ) end function HDF5Dataset{2}( values::HDF5.Dataset, column_dataset::HDF5.Dataset, column_lengths::NTuple{2, Int}, resolution::Dates.Period, initial_time::Dates.DateTime, ) # The indexing is done in this way because we save all the names in an # adjacent column entry in the HDF5 Datatset. The indexes for each column # are known because we know how many elements are in each dimension. # the names for the first column are store in the 1:first_column_number_of_elements. col1 = column_dataset[1:column_lengths[1]] # the names for the second column are store in the first_column_number_of elements + 1:end of the column with the names. col2 = column_dataset[(column_lengths[1] + 1):end] HDF5Dataset{2}( values, column_dataset, 1, 0, resolution, initial_time, UNSET_INI_TIME, (col1, col2), ) end function get_column_names(::OptimizationContainerKey, s::HDF5Dataset) return s.column_names end """ Return the timestamp from most recent data row updated in the dataset. This value may not be the same as the result from `get_update_timestamp` """ function get_last_updated_timestamp(s::HDF5Dataset) last_recorded_row = get_last_recorded_row(s) if last_recorded_row == 0 return UNSET_INI_TIME end return s.initial_timestamp + s.resolution * (last_recorded_row - 1) end function get_value_timestamp(s::HDF5Dataset, date::Dates.DateTime) error("Not implemented for HDF5Dataset. Required if it is used for simulation state.") # TODO: This code is broken because timestamps is not a field. #s_index = find_timestamp_index(s.timestamps, date) #if isnothing(s_index) # error("Request time stamp $date not in the state") #end #return s.initial_timestamp + s.resolution * (s_index - 1) 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) return end ================================================ FILE: src/core/dataset_container.jl ================================================ struct DatasetContainer{T} duals::Dict{ConstraintKey, T} aux_variables::Dict{AuxVarKey, T} variables::Dict{VariableKey, T} parameters::Dict{ParameterKey, T} expressions::Dict{ExpressionKey, T} end function DatasetContainer{T}() where {T <: AbstractDataset} return DatasetContainer( Dict{ConstraintKey, T}(), Dict{AuxVarKey, T}(), Dict{VariableKey, T}(), Dict{ParameterKey, T}(), Dict{ExpressionKey, T}(), ) end function Base.empty!(container::DatasetContainer) for field in fieldnames(DatasetContainer) field_dict = getfield(container, field) for val in values(field_dict) empty!(val) end end @debug "Emptied the store" _group = LOG_GROUP_SIMULATION_STORE return end function get_duals_values(container::DatasetContainer{InMemoryDataset}) return container.duals end function get_aux_variables_values(container::DatasetContainer{InMemoryDataset}) return container.aux_variables end function get_variables_values(container::DatasetContainer{InMemoryDataset}) return container.variables end function get_parameters_values(container::DatasetContainer{InMemoryDataset}) return container.parameters end function get_expression_values(container::DatasetContainer{InMemoryDataset}) return container.expressions end function get_duals_values(::DatasetContainer{HDF5Dataset}) error("Operation not allowed on a HDF5Dataset.") end function get_aux_variables_values(::DatasetContainer{HDF5Dataset}) error("Operation not allowed on a HDF5Dataset.") end function get_variables_values(::DatasetContainer{HDF5Dataset}) error("Operation not allowed on a HDF5Dataset.") end function get_parameters_values(::DatasetContainer{HDF5Dataset}) error("Operation not allowed on a HDF5Dataset.") end function get_expression_values(::DatasetContainer{HDF5Dataset}) error("Operation not allowed on a HDF5Dataset.") end function get_dataset_keys(container::DatasetContainer) return Iterators.flatten( keys(getfield(container, f)) for f in fieldnames(DatasetContainer) ) end function get_dataset(container::DatasetContainer, key::OptimizationContainerKey) datasets = getfield(container, get_store_container_type(key)) return datasets[key] end function set_dataset!( container::DatasetContainer{T}, key::OptimizationContainerKey, val::T, ) where {T <: AbstractDataset} datasets = getfield(container, get_store_container_type(key)) datasets[key] = val return end function has_dataset(container::DatasetContainer, key::OptimizationContainerKey) datasets = getfield(container, get_store_container_type(key)) return haskey(datasets, key) end function get_dataset( container::DatasetContainer, ::T, ::Type{U}, ) where {T <: ConstraintType, U <: Union{PSY.Component, PSY.System}} return get_dataset(container, ConstraintKey(T, U)) end function get_dataset( container::DatasetContainer, ::T, ::Type{U}, ) where {T <: VariableType, U <: Union{PSY.Component, PSY.System}} return get_dataset(container, VariableKey(T, U)) end function get_dataset( container::DatasetContainer, ::T, ::Type{U}, ) where {T <: AuxVariableType, U <: Union{PSY.Component, PSY.System}} return get_dataset(container, AuxVarKey(T, U)) end function get_dataset( container::DatasetContainer, ::T, ::Type{U}, ) where {T <: ParameterType, U <: Union{PSY.Component, PSY.System}} return get_dataset(container, ParameterKey(T, U)) end function get_dataset( container::DatasetContainer, ::T, ::Type{U}, ) where {T <: ExpressionType, U <: Union{PSY.Component, PSY.System}} return get_dataset(container, ExpressionKey(T, U)) end function get_dataset_values(container::DatasetContainer, key::OptimizationContainerKey) return get_dataset(container, key).values end function get_dataset_values( container::DatasetContainer, ::T, ::Type{U}, ) where {T <: ConstraintType, U <: Union{PSY.Component, PSY.System}} return get_dataset_values(container, ConstraintKey(T, U)) end function get_dataset_values( container::DatasetContainer, ::T, ::Type{U}, ) where {T <: VariableType, U <: Union{PSY.Component, PSY.System}} return get_dataset_values(container, VariableKey(T, U)) end function get_dataset_values( container::DatasetContainer, ::T, ::Type{U}, ) where {T <: AuxVariableType, U <: Union{PSY.Component, PSY.System}} return get_dataset_values(container, AuxVarKey(T, U)) end function get_dataset_values( container::DatasetContainer, ::T, ::Type{U}, ) where {T <: ParameterType, U <: Union{PSY.Component, PSY.System}} return get_dataset_values(container, ParameterKey(T, U)) end function get_dataset_values( container::DatasetContainer, ::T, ::Type{U}, ) where {T <: ExpressionType, U <: Union{PSY.Component, PSY.System}} return get_dataset_values(container, ExpressionKey(T, U)) end function get_dataset_values( container::DatasetContainer, key::OptimizationContainerKey, date::Dates.DateTime, ) return get_dataset_value(get_dataset(container, key), date) end function get_last_recorded_row( container::DatasetContainer, key::OptimizationContainerKey, ) return get_last_recorded_row(get_dataset(container, key)) end """ Return the timestamp from the data used in the last update """ function get_update_timestamp(container::DatasetContainer, key::OptimizationContainerKey) return get_update_timestamp(get_dataset(container, key)) end """ Return the timestamp from most recent data row updated in the dataset. This value may not be the same as the result from `get_update_timestamp` """ function get_last_updated_timestamp( container::DatasetContainer, key::OptimizationContainerKey, ) return get_last_updated_timestamp(get_dataset(container, key)) end function get_last_update_value( container::DatasetContainer, key::OptimizationContainerKey, ) return get_last_recorded_value(get_dataset(container, key)) end function set_dataset_values!( container::DatasetContainer, key::OptimizationContainerKey, index::Int, vals, ) set_value!(get_dataset(container, key), vals, index) return end ================================================ FILE: src/core/definitions.jl ================================================ ################################################################################# # Type Alias for long type signatures const MinMax = NamedTuple{(:min, :max), NTuple{2, Float64}} const NamedMinMax = Tuple{String, MinMax} const UpDown = NamedTuple{(:up, :down), NTuple{2, Float64}} const InOut = NamedTuple{(:in, :out), NTuple{2, Float64}} const BUILD_PROBLEMS_TIMER = TimerOutputs.TimerOutput() const RUN_OPERATION_MODEL_TIMER = TimerOutputs.TimerOutput() const RUN_SIMULATION_TIMER = TimerOutputs.TimerOutput() # Type Alias for JuMP containers const JuMPOrFloat = Union{JuMP.AbstractJuMPScalar, Float64} const GAE = JuMP.GenericAffExpr{Float64, JuMP.VariableRef} const JuMPAffineExpressionArray = Matrix{GAE} const JuMPAffineExpressionVector = Vector{GAE} const JuMPConstraintArray = DenseAxisArray{JuMP.ConstraintRef} const JuMPAffineExpressionDArrayStringInt = JuMP.Containers.DenseAxisArray{ JuMP.AffExpr, 2, Tuple{Vector{String}, UnitRange{Int64}}, Tuple{ JuMP.Containers._AxisLookup{Dict{String, Int64}}, JuMP.Containers._AxisLookup{Tuple{Int64, Int64}}, }, } const JuMPAffineExpressionDArrayIntInt = 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 JuMPVariableTensor{N} = DenseAxisArray{ JuMP.VariableRef, N, <:Tuple{Vector{String}, Vararg{Any}}, # last element is always UnitRange{Int64} but we cannot specify anything after the Vararg <:Tuple{ JuMP.Containers._AxisLookup{Dict{String, Int64}}, Vararg{JuMP.Containers._AxisLookup{<:Any}}, # last element is always JuMP.Containers._AxisLookup{Tuple{Int64, Int64}} but we cannot specify anything after the Vararg }, } const JuMPFloatMatrix = DenseAxisArray{Float64, 2} const JuMPFloatArray = DenseAxisArray{Float64} const JuMPVariableArray = DenseAxisArray{JuMP.VariableRef} const JumpSupportedLiterals = Union{Number, Vector{<:Tuple{Number, Number}}, Tuple{Vararg{Number}}} # Settings constants const UNSET_HORIZON = Dates.Millisecond(0) const UNSET_RESOLUTION = Dates.Millisecond(0) const UNSET_INTERVAL = 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 PTDF_ZERO_TOL = 1e-9 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 HOURS_IN_WEEK = 168.0 const MAX_START_STAGES = 3 const OBJECTIVE_FUNCTION_POSITIVE = 1.0 const OBJECTIVE_FUNCTION_NEGATIVE = -1.0 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 DEFAULT_INTERPOLATION_LENGTH = 4 const KiB = 1024 const MiB = KiB * KiB const GiB = MiB * KiB const PSI_NAME_DELIMITER = "__" const M_VALUE = 1e6 const NO_SERVICE_NAME_PROVIDED = "" const UPPER_BOUND = "ub" const LOWER_BOUND = "lb" const MAX_OPTIMIZE_TRIES = 2 # File Names definitions const PROBLEM_LOG_FILENAME = "operation_problem.log" const SIMULATION_LOG_FILENAME = "simulation.log" const REQUIRED_RECORDERS = (:simulation_status, :execution) const KNOWN_SIMULATION_PATHS = [ "data_store", "logs", "models_json", "problems", "recorder", "results", "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 ModelBuildStatus = ISOPT.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 = (ModelBuildStatus, SimulationBuildStatus, RunStatus, SOSStatusVariable) const ENUM_MAPPINGS = Dict() for enum in ENUMS ENUM_MAPPINGS[enum] = Dict() for value in instances(enum) ENUM_MAPPINGS[enum][lowercase(string(value))] = value 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. """ function get_enum_value(enum, value::String) if !haskey(ENUM_MAPPINGS, enum) throw(ArgumentError("enum=$enum is not valid")) end val = lowercase(value) if !haskey(ENUM_MAPPINGS[enum], val) throw(ArgumentError("enum=$enum does not have value=$val")) end return ENUM_MAPPINGS[enum][val] end 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) ================================================ FILE: src/core/device_model.jl ================================================ """ Formulation type to augment the power balance constraint expression with a time series parameter """ struct FixedOutput <: AbstractDeviceFormulation end function _check_device_formulation( ::Type{D}, ) where {D <: Union{AbstractDeviceFormulation, PSY.Device}} if !isconcretetype(D) throw( ArgumentError( "The device model must contain only concrete types, $(D) is an Abstract Type", ), ) end end """ DeviceModel( ::Type{D}, ::Type{B}, feedforwards::Vector{<:AbstractAffectFeedforward} use_slacks::Bool, duals::Vector{DataType}, services::Vector{ServiceModel} attributes::Dict{String, Any} ) Establishes the model for a particular device specified by type. Uses the keyword argument feedforward to enable passing values between operation model at simulation time # Arguments - `::Type{D} where D<:PSY.Device`: Power System Device Type - `::Type{B} where B<:AbstractDeviceFormulation`: Abstract Device Formulation - `feedforward::Array{<:AbstractAffectFeedforward} = Vector{AbstractAffectFeedforward}()` : use to pass parameters between models - `use_slacks::Bool = false` : Add slacks to the device model. Implementation is model dependent and not all models feature slacks - `duals::Vector{DataType} = Vector{DataType}()`: use to pass constraint type to calculate the duals. The DataType needs to be a valid ConstraintType - `time_series_names::Dict{Type{<:TimeSeriesParameter}, String} = get_default_time_series_names(D, B)` : use to specify time series names associated to the device` - `attributes::Dict{String, Any} = get_default_attributes(D, B)` : use to specify attributes to the device # Example ```julia thermal_gens = DeviceModel(ThermalStandard, ThermalBasicUnitCommitment) ``` """ mutable struct DeviceModel{D <: PSY.Device, B <: AbstractDeviceFormulation} feedforwards::Vector{<:AbstractAffectFeedforward} use_slacks::Bool duals::Vector{DataType} services::Vector{ServiceModel} time_series_names::Dict{Type{<:ParameterType}, String} attributes::Dict{String, Any} subsystem::Union{Nothing, String} events::Dict{EventKey, EventModel} device_cache::Vector{D} function DeviceModel( ::Type{D}, ::Type{B}; feedforwards = Vector{AbstractAffectFeedforward}(), use_slacks = false, duals = Vector{DataType}(), time_series_names = get_default_time_series_names(D, B), attributes = Dict{String, Any}(), ) where {D <: PSY.Device, B <: AbstractDeviceFormulation} attributes_ = get_default_attributes(D, B) for (k, v) in attributes attributes_[k] = v end _check_device_formulation(D) _check_device_formulation(B) new{D, B}( feedforwards, use_slacks, duals, Vector{ServiceModel}(), time_series_names, attributes_, nothing, Dict{EventKey, EventModel}(), Vector{D}(), ) end end get_component_type( ::DeviceModel{D, B}, ) where {D <: PSY.Device, B <: AbstractDeviceFormulation} = D get_events(m::DeviceModel) = m.events get_formulation( ::DeviceModel{D, B}, ) where {D <: PSY.Device, B <: AbstractDeviceFormulation} = B get_feedforwards(m::DeviceModel) = m.feedforwards get_services(m::DeviceModel) = m.services get_services(::Nothing) = nothing get_use_slacks(m::DeviceModel) = m.use_slacks get_duals(m::DeviceModel) = m.duals get_time_series_names(m::DeviceModel) = m.time_series_names get_attributes(m::DeviceModel) = m.attributes get_attribute(::Nothing, ::String) = nothing get_attribute(m::DeviceModel, key::String) = get(m.attributes, key, nothing) get_subsystem(m::DeviceModel) = m.subsystem get_device_cache(m::DeviceModel) = m.device_cache set_subsystem!(m::DeviceModel, id::String) = m.subsystem = id function _set_model!( dict::Dict, model::DeviceModel{D, B}, ) where {D <: PSY.Device, B <: AbstractDeviceFormulation} key = Symbol(D) if haskey(dict, key) @warn "Overwriting $(D) existing model" end dict[key] = model return end has_service_model(model::DeviceModel) = !isempty(get_services(model)) function set_event_model!( model::DeviceModel{D, B}, key::EventKey, event_model::EventModel, ) where {D <: PSY.Device, B <: AbstractDeviceFormulation} if haskey(model.events, key) error("EventModel $key already exists in model for device $D") end model.events[key] = event_model return end ================================================ FILE: src/core/dual_processing.jl ================================================ struct VarRestoreInfo{A <: AbstractArray} lb::Union{Nothing, A} ub::Union{Nothing, A} fixed_int_value::Union{Nothing, A} is_integer::Bool end _first_element(v::DenseAxisArray) = first(v) _first_element(v::SparseAxisArray) = first(values(v.data)) function _round_cache_values!(cache::DenseAxisArray) cache.data .= round.(cache.data) return end function _round_cache_values!(cache::SparseAxisArray) for k in keys(cache.data) cache.data[k] = round(cache.data[k]) end return end function process_duals(container::OptimizationContainer, lp_optimizer) var_container = get_variables(container) for (k, v) in var_container container.primal_values_cache.variables_cache[k] = jump_value.(v) end for (k, v) in get_expressions(container) container.primal_values_cache.expressions_cache[k] = jump_value.(v) end var_cache = container.primal_values_cache.variables_cache cache = sizehint!(Dict{VariableKey, VarRestoreInfo}(), length(var_container)) for (key, variable) in var_container is_integer_flag = false elem = _first_element(variable) if JuMP.is_binary(elem) JuMP.unset_binary.(variable) elseif JuMP.is_integer(elem) JuMP.unset_integer.(variable) is_integer_flag = true else continue end lb = if JuMP.has_lower_bound(elem) JuMP.lower_bound.(variable) else nothing end ub = if JuMP.has_upper_bound(elem) JuMP.upper_bound.(variable) else nothing end fixed_int_value = if JuMP.is_fixed(elem) && is_integer_flag jump_value.(variable) else nothing end cache[key] = VarRestoreInfo{typeof(var_cache[key])}(lb, ub, fixed_int_value, is_integer_flag) # Round cached values in-place to nearest integer to avoid infeasibilities # from MIP solver numerical tolerances (e.g. 0.9999997 instead of 1.0) _round_cache_values!(var_cache[key]) JuMP.fix.(variable, var_cache[key]; force = true) end @assert !isempty(cache) jump_model = get_jump_model(container) if JuMP.mode(jump_model) != JuMP.DIRECT JuMP.set_optimizer(jump_model, lp_optimizer) else @debug("JuMP model set in direct mode during dual calculation") end JuMP.optimize!(jump_model) model_status = JuMP.primal_status(jump_model) if model_status ∉ ( MOI.FEASIBLE_POINT::MOI.ResultStatusCode, MOI.NEARLY_FEASIBLE_POINT::MOI.ResultStatusCode, ) @error "Optimizer returned $model_status during dual calculation" return RunStatus.FAILED end if JuMP.has_duals(jump_model) for (key, dual) in get_duals(container) constraint = get_constraint(container, key) dual.data .= jump_value.(constraint).data end end for (key, variable) in var_container if !haskey(cache, key) continue end info = cache[key] JuMP.unfix.(variable) if info.lb !== nothing JuMP.set_lower_bound.(variable, info.lb) end if info.ub !== nothing JuMP.set_upper_bound.(variable, info.ub) end if info.is_integer JuMP.set_integer.(variable) else JuMP.set_binary.(variable) end if info.fixed_int_value !== nothing JuMP.fix.(variable, info.fixed_int_value) end end return RunStatus.SUCCESSFULLY_FINALIZED end ================================================ FILE: src/core/event_keys.jl ================================================ struct EventKey{T <: PSY.Contingency, U <: Union{PSY.Component, PSY.System}} meta::String end function EventKey( ::Type{T}, ::Type{U}, ) where {T <: PSY.Contingency, U <: Union{PSY.Component, PSY.System}} if isabstracttype(U) error("Type $U can't be abstract") end return EventKey{T, U}("") end get_entry_type( ::EventKey{T, U}, ) where {T <: PSY.Contingency, U <: Union{PSY.Component, PSY.System}} = T get_component_type( ::EventKey{T, U}, ) where {T <: PSY.Contingency, U <: Union{PSY.Component, PSY.System}} = U ================================================ FILE: src/core/event_model.jl ================================================ abstract type AbstractEventCondition end """ ContinuousCondition() Establishes an event condition that is triggered at all timesteps. """ struct ContinuousCondition <: AbstractEventCondition end """ PresetTimeCondition(time_stamps::Vector{Dates.DateTime}) Establishes an event condition that is triggered at pre-determined times. # Arguments - `time_stamps::Vector{Dates.DateTime}`: times when event is triggered """ struct PresetTimeCondition <: AbstractEventCondition time_stamps::Vector{Dates.DateTime} end get_time_stamps(c::PresetTimeCondition) = c.time_stamps """ StateVariableValueCondition( variable_type::Type{<:VariableType} device_type::Type{<:PSY.Device} device_name::String value::Float64 ) Establishes an event condition that is triggered if a variable of type `variable_type` for a device of type `device_type` and name `device_name` is equal to `value`. and name # Arguments - `variable_type::Type{<:VariableType}`: variable to be monitored - `device_type::Type{<:PSY.Device}`: device type to be monitored - `device_name::String`: name of monitored device - `value::Float64`: value to compare to in p.u. """ struct StateVariableValueCondition <: AbstractEventCondition variable_type::VariableType device_type::Type{<:PSY.Device} device_name::String value::Float64 end get_variable_type(c::StateVariableValueCondition) = c.variable_type get_device_type(c::StateVariableValueCondition) = c.device_type get_device_name(c::StateVariableValueCondition) = c.device_name get_value(c::StateVariableValueCondition) = c.value """ DiscreteEventCondition(condition_function::Function) Establishes an event condition that is triggered if when a user defined function evaluates to true. The function should take SimulationState as its only arguement and return true when the event should be triggered and false otherwise. # Arguments - `condition_function::Function`: user defined function `f(::SimulationState)`to determine if event is triggered. """ struct DiscreteEventCondition <: AbstractEventCondition condition_function::Function end get_condition_function(c::DiscreteEventCondition) = c.condition_function mutable struct EventModel{D <: PSY.Contingency, B <: AbstractEventCondition} condition::B timeseries_mapping::Dict{Symbol, Union{String, Nothing}} attribute_device_map::Dict{Symbol, Dict{Base.UUID, Dict{DataType, Set{String}}}} attributes::Dict{String, Any} function EventModel( contingency_type::Type{D}, condition::B; timeseries_mapping = get_empty_timeseries_mapping(contingency_type), attributes = Dict{String, Any}(), ) where {D <: PSY.Contingency, B <: AbstractEventCondition} new{D, B}( condition, timeseries_mapping, Dict{Symbol, Dict{Base.UUID, Dict{DataType, Set{String}}}}(), attributes, ) end end function get_empty_timeseries_mapping( ::Type{PSY.FixedForcedOutage}, ) return Dict{Symbol, Union{String, Nothing}}( :outage_status => nothing, ) end function get_empty_timeseries_mapping( ::Type{PSY.GeometricDistributionForcedOutage}, ) return Dict{Symbol, Union{String, Nothing}}( :mean_time_to_recovery => nothing, :outage_transition_probability => nothing, ) end get_event_type( ::EventModel{D, B}, ) where {D <: PSY.Contingency, B <: AbstractEventCondition} = D get_event_condition( e::EventModel{D, B}, ) where {D <: PSY.Contingency, B <: AbstractEventCondition} = e.condition get_attribute_device_map(e::EventModel) = e.attribute_device_map ================================================ FILE: src/core/expressions.jl ================================================ abstract type SystemBalanceExpressions <: ExpressionType end abstract type RangeConstraintLBExpressions <: ExpressionType end abstract type RangeConstraintUBExpressions <: ExpressionType end abstract type CostExpressions <: ExpressionType end abstract type PostContingencyExpressions <: ExpressionType end abstract type PostContingencySystemBalanceExpressions <: SystemBalanceExpressions end struct ActivePowerBalance <: SystemBalanceExpressions end struct PostContingencyActivePowerBalance <: PostContingencySystemBalanceExpressions end struct ReactivePowerBalance <: SystemBalanceExpressions end struct EmergencyUp <: ExpressionType end struct EmergencyDown <: ExpressionType end struct RawACE <: ExpressionType end struct ProductionCostExpression <: CostExpressions end abstract type ConstituentCostExpression <: CostExpressions end struct FuelCostExpression <: ConstituentCostExpression end struct StartUpCostExpression <: ConstituentCostExpression end struct ShutDownCostExpression <: ConstituentCostExpression end struct FixedCostExpression <: ConstituentCostExpression end struct VOMCostExpression <: ConstituentCostExpression end struct CurtailmentCostExpression <: CostExpressions end struct FuelConsumptionExpression <: ExpressionType end struct ActivePowerRangeExpressionLB <: RangeConstraintLBExpressions end struct ActivePowerRangeExpressionUB <: RangeConstraintUBExpressions end struct ComponentReserveUpBalanceExpression <: ExpressionType end struct ComponentReserveDownBalanceExpression <: ExpressionType end struct InterfaceTotalFlow <: ExpressionType end struct PTDFBranchFlow <: ExpressionType end struct PostContingencyBranchFlow <: PostContingencyExpressions end struct PostContingencyActivePowerGeneration <: PostContingencyExpressions end struct PostContingencyNodalActivePowerDeployment <: PostContingencyExpressions end struct NetActivePower <: ExpressionType end struct RealizedShiftedLoad <: ExpressionType end """ Struct for DC current balance in multi-terminal DC networks """ struct DCCurrentBalance <: ExpressionType end should_write_resulting_value(::Type{<:CostExpressions}) = true should_write_resulting_value(::Type{FuelConsumptionExpression}) = 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 should_write_resulting_value(::Type{DCCurrentBalance}) = true should_write_resulting_value(::Type{PTDFBranchFlow}) = true should_write_resulting_value(::Type{RealizedShiftedLoad}) = true #should_write_resulting_value(::Type{PostContingencyBranchFlow}) = true #should_write_resulting_value(::Type{PostContingencyActivePowerGeneration}) = true convert_result_to_natural_units(::Type{InterfaceTotalFlow}) = true convert_result_to_natural_units(::Type{PostContingencyBranchFlow}) = true convert_result_to_natural_units(::Type{PostContingencyActivePowerGeneration}) = true convert_result_to_natural_units(::Type{PTDFBranchFlow}) = true convert_result_to_natural_units(::Type{RealizedShiftedLoad}) = true ================================================ FILE: src/core/formulations.jl ================================================ """ Abstract type for Device Formulations (a.k.a Models) # Example import PowerSimulations as PSI struct MyCustomDeviceFormulation <: PSI.AbstractDeviceFormulation """ abstract type AbstractDeviceFormulation end ########################### Thermal Generation Formulations ################################ abstract type AbstractThermalFormulation <: AbstractDeviceFormulation end abstract type AbstractThermalDispatchFormulation <: AbstractThermalFormulation end abstract type AbstractThermalUnitCommitment <: AbstractThermalFormulation end abstract type AbstractStandardUnitCommitment <: AbstractThermalUnitCommitment end abstract type AbstractCompactUnitCommitment <: AbstractThermalUnitCommitment end """ Formulation type to enable basic unit commitment representation without any intertemporal (ramp, min on/off time) constraints """ struct ThermalBasicUnitCommitment <: AbstractStandardUnitCommitment end """ Formulation type to enable standard unit commitment with intertemporal constraints and simplified startup profiles """ struct ThermalStandardUnitCommitment <: AbstractStandardUnitCommitment end """ Formulation type to enable basic dispatch without any intertemporal (ramp) constraints """ struct ThermalBasicDispatch <: AbstractThermalDispatchFormulation end """ Formulation type to enable standard dispatch with a range and enforce intertemporal ramp constraints """ struct ThermalStandardDispatch <: AbstractThermalDispatchFormulation end """ 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 """ Formulation type to enable pg-lib commitment formulation with startup/shutdown profiles """ struct ThermalMultiStartUnitCommitment <: AbstractCompactUnitCommitment end """ Formulation type to enable thermal compact commitment """ struct ThermalCompactUnitCommitment <: AbstractCompactUnitCommitment end """ Formulation type to enable thermal compact commitment without intertemporal (ramp, min on/off time) constraints """ struct ThermalBasicCompactUnitCommitment <: AbstractCompactUnitCommitment end """ Formulation type to enable thermal compact dispatch """ struct ThermalCompactDispatch <: AbstractThermalDispatchFormulation end ############################# Electric Load Formulations ################################### abstract type AbstractLoadFormulation <: AbstractDeviceFormulation end abstract type AbstractControllablePowerLoadFormulation <: AbstractLoadFormulation end """ Formulation type to add a time series parameter for non-dispatchable `ElectricLoad` withdrawals to power balance constraints """ struct StaticPowerLoad <: AbstractLoadFormulation end """ Formulation type to enable (binary) load interruptions """ struct PowerLoadInterruption <: AbstractControllablePowerLoadFormulation end """ Formulation type to enable (continuous) load interruption dispatch """ struct PowerLoadDispatch <: AbstractControllablePowerLoadFormulation end """ Formulation type to enable load shifting """ struct PowerLoadShift <: AbstractControllablePowerLoadFormulation end ############################ Regulation Device Formulations ################################ abstract type AbstractRegulationFormulation <: AbstractDeviceFormulation end struct ReserveLimitedRegulation <: AbstractRegulationFormulation end struct DeviceLimitedRegulation <: AbstractRegulationFormulation end ########################### Renewable Generation Formulations ############################## abstract type AbstractRenewableFormulation <: AbstractDeviceFormulation end abstract type AbstractRenewableDispatchFormulation <: AbstractRenewableFormulation end """ Formulation type to add injection variables constrained by a maximum injection time series for `RenewableGen` """ struct RenewableFullDispatch <: AbstractRenewableDispatchFormulation end """ Formulation type to add real and reactive injection variables with constant power factor with maximum real power injections constrained by a time series for `RenewableGen` """ struct RenewableConstantPowerFactor <: AbstractRenewableDispatchFormulation end ########################### Source Formulations ############################## abstract type AbstractSourceFormulation <: AbstractDeviceFormulation end """ Formulation type to add import and export model for `Source` """ struct ImportExportSourceModel <: AbstractSourceFormulation end ########################### Reactive Power Device Formulations ############################## abstract type AbstractReactivePowerDeviceFormulation <: AbstractDeviceFormulation end """ Formulation type to add reactive power dispatch variables for `SynchronousCondenser` """ struct SynchronousCondenserBasicDispatch <: AbstractReactivePowerDeviceFormulation end """ Abstract type for Branch Formulations (a.k.a Models) # Example import PowerSimulations as PSI struct MyCustomBranchFormulation <: PSI.AbstractDeviceFormulation """ # Generic Branch Models abstract type AbstractBranchFormulation <: AbstractDeviceFormulation end ############################### AC/DC Branch Formulations ##################################### """ Branch type to add unbounded flow variables and use flow constraints """ struct StaticBranch <: AbstractBranchFormulation end """ Branch type to add bounded flow variables and use flow constraints """ struct StaticBranchBounds <: AbstractBranchFormulation end """ Branch type to avoid flow constraints """ struct StaticBranchUnbounded <: AbstractBranchFormulation end """ Branch formulation for PhaseShiftingTransformer flow control """ struct PhaseAngleControl <: AbstractBranchFormulation end ############################### DC Branch Formulations ##################################### abstract type AbstractTwoTerminalDCLineFormulation <: AbstractBranchFormulation end """ Branch type to avoid flow constraints """ struct HVDCTwoTerminalUnbounded <: AbstractTwoTerminalDCLineFormulation end """ Branch type to represent lossless power flow on DC lines """ struct HVDCTwoTerminalLossless <: AbstractTwoTerminalDCLineFormulation end """ Branch type to represent lossy power flow on DC lines """ struct HVDCTwoTerminalDispatch <: AbstractTwoTerminalDCLineFormulation end """ Branch type to represent piecewise lossy power flow on two terminal DC lines """ struct HVDCTwoTerminalPiecewiseLoss <: AbstractTwoTerminalDCLineFormulation end """ Branch type to represent non-linear LCC (line commutated converter) model on two-terminal DC lines """ struct HVDCTwoTerminalLCC <: AbstractTwoTerminalDCLineFormulation end # Not Implemented # struct VoltageSourceDC <: AbstractTwoTerminalDCLineFormulation end ############################### AC/DC Converter Formulations ##################################### abstract type AbstractConverterFormulation <: AbstractDeviceFormulation end """ Lossless InterconnectingConverter Model """ struct LosslessConverter <: AbstractConverterFormulation end """ Linear Loss InterconnectingConverter Model """ struct LinearLossConverter <: AbstractConverterFormulation end """ Quadratic Loss InterconnectingConverter Model """ struct QuadraticLossConverter <: AbstractConverterFormulation end ############################## HVDC Lines Formulations ################################## abstract type AbstractDCLineFormulation <: AbstractBranchFormulation end """ Lossless Line Abstract Model """ struct DCLosslessLine <: AbstractDCLineFormulation end """ Lossy Line Abstract Model """ struct DCLossyLine <: AbstractDCLineFormulation end """ Lossless Line struct formulation """ struct LosslessLine <: AbstractDCLineFormulation end ############################## HVDC Network Model Formulations ################################## abstract type AbstractHVDCNetworkModel end """ Transport Lossless HVDC network model. No DC voltage variables are added and DC lines are modeled as lossless power transport elements """ struct TransportHVDCNetworkModel <: AbstractHVDCNetworkModel end """ DC Voltage HVDC network model, where currents are solved based on DC voltage difference between DC buses """ struct VoltageDispatchHVDCNetworkModel <: AbstractHVDCNetworkModel end """ Abstract type for Service Formulations (a.k.a Models) # Example import PowerSimulations as PSI struct MyServiceFormulation <: PSI.AbstractServiceFormulation """ abstract type AbstractServiceFormulation end abstract type AbstractReservesFormulation <: AbstractServiceFormulation end abstract type AbstractSecurityConstrainedReservesFormulation <: AbstractReservesFormulation end 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 ================================================ FILE: src/core/initial_conditions.jl ================================================ """ Container for the initial condition data """ mutable struct InitialCondition{ T <: InitialConditionType, U <: Union{JuMP.VariableRef, Float64, Nothing}, } component::PSY.Component value::U end function InitialCondition( ::Type{T}, component::PSY.Component, value::U, ) where {T <: InitialConditionType, U <: Union{JuMP.VariableRef, Float64}} return InitialCondition{T, U}(component, value) end function InitialCondition( ::InitialConditionKey{T, U}, component::U, value::V, ) where { T <: InitialConditionType, U <: PSY.Component, V <: Union{JuMP.VariableRef, Float64}, } return InitialCondition{T, U}(component, value) end function get_condition(p::InitialCondition{T, Float64}) where {T <: InitialConditionType} return p.value end function get_condition( p::InitialCondition{T, JuMP.VariableRef}, ) where {T <: InitialConditionType} return jump_value(p.value) end function get_condition( ::InitialCondition{T, Nothing}, ) where {T <: InitialConditionType} return nothing end get_component(ic::InitialCondition) = ic.component get_value(ic::InitialCondition) = ic.value get_component_name(ic::InitialCondition) = PSY.get_name(ic.component) get_component_type(ic::InitialCondition) = typeof(ic.component) get_ic_type( ::Type{InitialCondition{T, U}}, ) where {T <: InitialConditionType, U <: Union{JuMP.VariableRef, Float64, Nothing}} = T get_ic_type( ::InitialCondition{T, U}, ) where {T <: InitialConditionType, U <: Union{JuMP.VariableRef, Float64, Nothing}} = T """ Stores data to populate initial conditions before the build call """ mutable struct InitialConditionsData duals::Dict{ConstraintKey, AbstractArray} parameters::Dict{ParameterKey, AbstractArray} variables::Dict{VariableKey, AbstractArray} aux_variables::Dict{AuxVarKey, AbstractArray} end function InitialConditionsData() return InitialConditionsData( Dict{ConstraintKey, AbstractArray}(), Dict{ParameterKey, AbstractArray}(), Dict{VariableKey, AbstractArray}(), Dict{AuxVarKey, AbstractArray}(), ) end function get_initial_condition_value( ic_data::InitialConditionsData, ::T, ::Type{U}, ) where {T <: VariableType, U <: Union{PSY.Component, PSY.System}} return ic_data.variables[VariableKey(T, U)] end function get_initial_condition_value( ic_data::InitialConditionsData, ::T, ::Type{U}, ) where {T <: AuxVariableType, U <: Union{PSY.Component, PSY.System}} return ic_data.aux_variables[AuxVarKey(T, U)] end function get_initial_condition_value( ic_data::InitialConditionsData, ::T, ::Type{U}, ) where {T <: ConstraintType, U <: Union{PSY.Component, PSY.System}} return ic_data.duals[ConstraintKey(T, U)] end function get_initial_condition_value( ic_data::InitialConditionsData, ::T, ::Type{U}, ) where {T <: ParameterType, U <: Union{PSY.Component, PSY.System}} return ic_data.parameters[ParameterKey(T, U)] end function has_initial_condition_value( ic_data::InitialConditionsData, ::T, ::Type{U}, ) where {T <: VariableType, U <: Union{PSY.Component, PSY.System}} return haskey(ic_data.variables, VariableKey(T, U)) end function has_initial_condition_value( ic_data::InitialConditionsData, ::T, ::Type{U}, ) where {T <: AuxVariableType, U <: Union{PSY.Component, PSY.System}} return haskey(ic_data.aux_variables, AuxVarKey(T, U)) end function has_initial_condition_value( ic_data::InitialConditionsData, ::T, ::Type{U}, ) where {T <: ConstraintType, U <: Union{PSY.Component, PSY.System}} return haskey(ic_data.duals, ConstraintKey(T, U)) end function has_initial_condition_value( ic_data::InitialConditionsData, ::T, ::Type{U}, ) where {T <: ParameterType, U <: Union{PSY.Component, PSY.System}} return haskey(ic_data.parameters, ParameterKey(T, U)) end ######################### Initial Conditions Definitions##################################### struct DevicePower <: InitialConditionType end struct DeviceAboveMinPower <: InitialConditionType end struct DeviceStatus <: InitialConditionType end 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 ================================================ FILE: src/core/model_store_params.jl ================================================ struct ModelStoreParams <: ISOPT.AbstractModelStoreParams num_executions::Int horizon_count::Int interval::Dates.Millisecond resolution::Dates.Millisecond base_power::Float64 system_uuid::Base.UUID container_metadata::ISOPT.OptimizationContainerMetadata function ModelStoreParams( num_executions::Int, horizon_count::Int, interval::Dates.Millisecond, resolution::Dates.Millisecond, base_power::Float64, system_uuid::Base.UUID, container_metadata = ISOPT.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 = ISOPT.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) ================================================ FILE: src/core/network_formulations.jl ================================================ ############################## 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://sienna-platform.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://sienna-platform.github.io/PowerNetworkMatrices.jl/stable/tutorials/tutorial_PTDF_matrix/) matrix. Balancing areas as well as synchronous regions. """ struct AreaPTDFPowerModel <: AbstractPTDFModel end #================================================ # 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 ================================================# ##### Exact Non-Convex Models ##### import PowerModels: ACPPowerModel import PowerModels: ACRPowerModel import PowerModels: ACTPowerModel ##### Linear Approximations ##### import PowerModels: DCPPowerModel import PowerModels: NFAPowerModel ##### Quadratic Approximations ##### import PowerModels: DCPLLPowerModel import PowerModels: LPACCPowerModel ##### Quadratic Relaxations ##### import PowerModels: SOCWRPowerModel import PowerModels: SOCWRConicPowerModel import PowerModels: QCRMPowerModel import PowerModels: QCLSPowerModel supports_branch_filtering(::Type{<:PM.AbstractPowerModel}) = false supports_branch_filtering(::Type{<:AbstractPTDFModel}) = true ignores_branch_filtering(::Type{<:PM.AbstractPowerModel}) = false ignores_branch_filtering(::Type{CopperPlatePowerModel}) = true ignores_branch_filtering(::Type{AreaBalancePowerModel}) = true requires_all_branch_models(::Type{<:PM.AbstractPowerModel}) = true requires_all_branch_models(::Type{<:AbstractPTDFModel}) = false requires_all_branch_models(::Type{CopperPlatePowerModel}) = false requires_all_branch_models(::Type{AreaBalancePowerModel}) = false ================================================ FILE: src/core/network_model.jl ================================================ const DeviceModelForBranches = DeviceModel{<:PSY.Branch, <:AbstractDeviceFormulation} const BranchModelContainer = Dict{Symbol, DeviceModelForBranches} function _check_pm_formulation(::Type{T}) where {T <: PM.AbstractPowerModel} if !isconcretetype(T) throw( ArgumentError( "The network model must contain only concrete types, $(T) is an Abstract Type", ), ) end end _maybe_flatten_pfem(pfem::Vector{PFS.PowerFlowEvaluationModel}) = pfem _maybe_flatten_pfem(pfem::PFS.PowerFlowEvaluationModel) = PFS.flatten_power_flow_evaluation_model(pfem) """ Establishes the NetworkModel for a given PowerModels formulation type. # Arguments - `::Type{T}` where `T <: PM.AbstractPowerModel`: the power-system formulation type. # Accepted keyword arguments - `use_slacks::Bool` = false Adds slack buses to the network modeling. - `PTDF_matrix::Union{PNM.PowerNetworkMatrix, Nothing}` = nothing PTDF/VirtualPTDF matrix produced by PowerNetworkMatrices (optional). - `LODF_matrix::Union{PNM.PowerNetworkMatrix, Nothing}` = nothing LODF/VirtualLODF matrix produced by PowerNetworkMatrices (optional). - `reduce_radial_branches::Bool` = false Enable radial branch reduction when building network matrices. - `reduce_degree_two_branches::Bool` = false Enable degree-two branch reduction when building network matrices. - `subnetworks::Dict{Int, Set{Int}}` = Dict() Optional mapping of reference bus → set of mapped buses. If not provided, subnetworks are inferred from PTDF/VirtualPTDF or discovered from the system. - `duals::Vector{DataType}` = Vector{DataType}() Constraint types for which duals should be recorded. - `power_flow_evaluation::Union{PFS.PowerFlowEvaluationModel, Vector{PFS.PowerFlowEvaluationModel}}` Power-flow evaluation model(s). A single model is flattened to a vector internally. # Notes - `modeled_ac_branch_types` and `reduced_branch_tracker` are internal fields managed by the model. - `subsystem` can be set after construction via `set_subsystem!(model, id)`. - PTDF/LODF inputs are validated against the requested reduction flags and may raise a ConflictingInputsError if they are inconsistent with `reduce_radial_branches` or `reduce_degree_two_branches`. # Examples ptdf = PNM.VirtualPTDF(system) nw = NetworkModel(PTDFPowerModel; PTDF_matrix = ptdf, reduce_radial_branches = true, power_flow_evaluation = PFS.PowerFlowEvaluationModel()) nw2 = NetworkModel(CopperPlatePowerModel; subnetworks = Dict(1 => Set([1,2,3]))) """ mutable struct NetworkModel{T <: PM.AbstractPowerModel} use_slacks::Bool PTDF_matrix::Union{Nothing, PNM.PowerNetworkMatrix} LODF_matrix::Union{Nothing, PNM.PowerNetworkMatrix} subnetworks::Dict{Int, Set{Int}} bus_area_map::Dict{PSY.ACBus, Int} duals::Vector{DataType} network_reduction::PNM.NetworkReductionData reduce_radial_branches::Bool reduce_degree_two_branches::Bool power_flow_evaluation::Vector{PFS.PowerFlowEvaluationModel} subsystem::Union{Nothing, String} hvdc_network_model::Union{Nothing, AbstractHVDCNetworkModel} modeled_ac_branch_types::Vector{DataType} reduced_branch_tracker::BranchReductionOptimizationTracker function NetworkModel( ::Type{T}; use_slacks = false, PTDF_matrix = nothing, LODF_matrix = nothing, reduce_radial_branches = false, reduce_degree_two_branches = false, subnetworks = Dict{Int, Set{Int}}(), duals = Vector{DataType}(), power_flow_evaluation::Union{ PFS.PowerFlowEvaluationModel, Vector{PFS.PowerFlowEvaluationModel}, } = PFS.PowerFlowEvaluationModel[], hvdc_network_model = nothing, ) where {T <: PM.AbstractPowerModel} _check_pm_formulation(T) new{T}( use_slacks, PTDF_matrix, LODF_matrix, subnetworks, Dict{PSY.ACBus, Int}(), duals, PNM.NetworkReductionData(), reduce_radial_branches, reduce_degree_two_branches, _maybe_flatten_pfem(power_flow_evaluation), nothing, hvdc_network_model, Vector{DataType}(), BranchReductionOptimizationTracker(), ) end end get_use_slacks(m::NetworkModel) = m.use_slacks get_PTDF_matrix(m::NetworkModel) = m.PTDF_matrix get_LODF_matrix(m::NetworkModel) = m.LODF_matrix get_reduce_radial_branches(m::NetworkModel) = m.reduce_radial_branches get_network_reduction(m::NetworkModel) = m.network_reduction get_duals(m::NetworkModel) = m.duals get_network_formulation(::NetworkModel{T}) where {T} = T get_reduced_branch_tracker(m::NetworkModel) = m.reduced_branch_tracker get_reference_buses(m::NetworkModel{T}) where {T <: PM.AbstractPowerModel} = collect(keys(m.subnetworks)) get_subnetworks(m::NetworkModel) = m.subnetworks get_bus_area_map(m::NetworkModel) = m.bus_area_map get_power_flow_evaluation(m::NetworkModel) = m.power_flow_evaluation has_subnetworks(m::NetworkModel) = !isempty(m.bus_area_map) get_subsystem(m::NetworkModel) = m.subsystem get_hvdc_network_model(m::NetworkModel) = m.hvdc_network_model set_subsystem!(m::NetworkModel, id::String) = m.subsystem = id set_hvdc_network_model!(m::NetworkModel, val::Union{Nothing, AbstractHVDCNetworkModel}) = m.hvdc_network_model = val function add_dual!(model::NetworkModel, dual) dual in model.duals && error("dual = $dual is already stored") push!(model.duals, dual) @debug "Added dual" dual _group = LOG_GROUP_NETWORK_CONSTRUCTION return end function _get_filters(branch_models::BranchModelContainer) filters = Dict{DataType, Function}() for v in values(branch_models) filter_func = get_attribute(v, "filter_function") if filter_func !== nothing filters[get_component_type(v)] = filter_func end end return filters end function _get_irreducible_buses_due_to_dlrs( sys::PSY.System, network_model::NetworkModel, branch_models::BranchModelContainer, ) @debug "Identifying buses that are irreducible due to dynamic line ratings" irreducible_buses = Set{Int64}() for branch_type in network_model.modeled_ac_branch_types device_model = branch_models[Symbol(branch_type)] if !haskey( get_time_series_names(device_model), DynamicBranchRatingTimeSeriesParameter, ) continue end if branch_type == PSY.ThreeWindingTransformer @warn "Dynamic branch ratings for ThreeWindingTransformers are not implemented yet. Skipping it." continue end ts_name = get_time_series_names(device_model)[DynamicBranchRatingTimeSeriesParameter] ts_type = PSY.Deterministic #TODO workaround since we dont have the container branches = PSY.get_available_components(branch_type, sys) for branch in branches if !PSY.has_time_series(branch, ts_type, ts_name) continue end bus_to = PSY.get_number(PSY.get_to(PSY.get_arc(branch))) bus_from = PSY.get_number(PSY.get_from(PSY.get_arc(branch))) push!(irreducible_buses, bus_to) push!(irreducible_buses, bus_from) end end return collect(irreducible_buses) end function instantiate_network_model!( model::NetworkModel{T}, branch_models::BranchModelContainer, number_of_steps::Int, sys::PSY.System, ) where {T <: PM.AbstractPowerModel} if isempty(model.subnetworks) model.subnetworks = PNM.find_subnetworks(sys) end irreducible_buses = _get_irreducible_buses_due_to_dlrs( sys, model, branch_models, ) if model.reduce_radial_branches && model.reduce_degree_two_branches @info "Applying both radial and degree two reductions" ybus = PNM.Ybus( sys; network_reductions = PNM.NetworkReduction[ PNM.RadialReduction(; irreducible_buses = irreducible_buses), PNM.DegreeTwoReduction(; irreducible_buses = irreducible_buses), ], ) elseif model.reduce_radial_branches @info "Applying radial reduction" if !isempty(irreducible_buses) @warn "Irreducible buses identified due to DLRs. The reduction of any radial branch between 2 irreducible buses wil be ignored" end ybus = PNM.Ybus( sys; network_reductions = PNM.NetworkReduction[PNM.RadialReduction(; irreducible_buses = irreducible_buses, )], ) elseif model.reduce_degree_two_branches @info "Applying degree two reduction" ybus = PNM.Ybus( sys; network_reductions = PNM.NetworkReduction[PNM.DegreeTwoReduction(; irreducible_buses = irreducible_buses, )], ) else ybus = PNM.Ybus(sys) end model.network_reduction = deepcopy(PNM.get_network_reduction_data(ybus)) #if !isempty(model.network_reductionget_net_reduction_data) # TODO: Network reimplement this when it becomes necessary. We don't have any # reductions that are incompatible right now. # check_network_reduction_compatibility(T) #end PNM.populate_branch_maps_by_type!(model.network_reduction, _get_filters(branch_models)) empty!(model.reduced_branch_tracker) set_number_of_steps!(model.reduced_branch_tracker, number_of_steps) return end function instantiate_network_model!( model::NetworkModel{AreaBalancePowerModel}, branch_models::BranchModelContainer, number_of_steps::Int, sys::PSY.System, ) PNM.populate_branch_maps_by_type!(model.network_reduction) empty!(model.reduced_branch_tracker) set_number_of_steps!(model.reduced_branch_tracker, number_of_steps) return end function instantiate_network_model!( model::NetworkModel{CopperPlatePowerModel}, branch_models::BranchModelContainer, number_of_steps::Int, sys::PSY.System, ) if isempty(model.subnetworks) model.subnetworks = PNM.find_subnetworks(sys) end if length(model.subnetworks) > 1 @debug "System Contains Multiple Subnetworks. Assigning buses to subnetworks." model.network_reduction = deepcopy(PNM.get_network_reduction_data(PNM.Ybus(sys))) _assign_subnetworks_to_buses(model, sys) end empty!(model.reduced_branch_tracker) set_number_of_steps!(model.reduced_branch_tracker, number_of_steps) return end function instantiate_network_model!( model::NetworkModel{<:AbstractPTDFModel}, branch_models::BranchModelContainer, number_of_steps::Int, sys::PSY.System, ) irreducible_buses = _get_irreducible_buses_due_to_dlrs( sys, model, branch_models, ) if get_PTDF_matrix(model) === nothing || !isempty(irreducible_buses) if get_PTDF_matrix(model) !== nothing @warn "Provided PTDF Matrix is being ignored since irreducible buses were identified because of DLRs. Recalculating PTDF Matrix with PowerNetworkMatrices.VirtualPTDF and the identified irreducible buses." else @info "No PTDF Matrix provided. Calculating using PowerNetworkMatrices.VirtualPTDF" end if model.reduce_radial_branches && model.reduce_degree_two_branches @info "Applying both radial and degree two reductions" ptdf = PNM.VirtualPTDF( sys; tol = PTDF_ZERO_TOL, network_reductions = PNM.NetworkReduction[ PNM.RadialReduction(; irreducible_buses = irreducible_buses), PNM.DegreeTwoReduction(; irreducible_buses = irreducible_buses, ), ], ) elseif model.reduce_radial_branches @info "Applying radial reduction" if !isempty(irreducible_buses) @warn "Irreducible buses identified due to DLRs. The reduction of any radial branch between 2 irreducible buses wil be ignored" end ptdf = PNM.VirtualPTDF( sys; tol = PTDF_ZERO_TOL, network_reductions = PNM.NetworkReduction[PNM.RadialReduction(; irreducible_buses = irreducible_buses, )], ) elseif model.reduce_degree_two_branches @info "Applying degree two reduction" ptdf = PNM.VirtualPTDF( sys; tol = PTDF_ZERO_TOL, network_reductions = PNM.NetworkReduction[PNM.DegreeTwoReduction(; irreducible_buses = irreducible_buses, )], ) else ptdf = PNM.VirtualPTDF(sys; tol = PTDF_ZERO_TOL) end model.PTDF_matrix = ptdf model.network_reduction = deepcopy(ptdf.network_reduction_data) else model.network_reduction = deepcopy(model.PTDF_matrix.network_reduction_data) end if !model.reduce_radial_branches && PNM.has_radial_reduction( PNM.get_reductions(model.PTDF_matrix.network_reduction_data), ) throw( IS.ConflictingInputsError( "The provided PTDF Matrix has reduced radial branches and mismatches the network \ model specification reduce_radial_branches = false. Set the keyword argument \ reduce_radial_branches = true in your network model"), ) end if !model.reduce_degree_two_branches && PNM.has_degree_two_reduction( PNM.get_reductions(model.PTDF_matrix.network_reduction_data), ) throw( IS.ConflictingInputsError( "The provided PTDF Matrix has reduced degree two branches and mismatches the network \ model specification reduce_degree_two_branches = false. Set the keyword argument \ reduce_degree_two_branches = true in your network model"), ) end if model.reduce_radial_branches && PNM.has_ward_reduction(PNM.get_reductions(model.PTDF_matrix.network_reduction_data)) throw( IS.ConflictingInputsError( "The provided PTDF Matrix has a ward reduction specified and the keyword argument \\ reduce_radial_branches = true. Set the keyword argument reduce_radial_branches = false \\ or provide a modified PTDF Matrix without the Ward reduction."), ) end if model.reduce_radial_branches @assert !isempty(model.PTDF_matrix.network_reduction_data) end model.subnetworks = _make_subnetworks_from_subnetwork_axes(model.PTDF_matrix) if length(model.subnetworks) > 1 @debug "System Contains Multiple Subnetworks. Assigning buses to subnetworks." _assign_subnetworks_to_buses(model, sys) end PNM.populate_branch_maps_by_type!(model.network_reduction, _get_filters(branch_models)) empty!(model.reduced_branch_tracker) set_number_of_steps!(model.reduced_branch_tracker, number_of_steps) return end function _make_subnetworks_from_subnetwork_axes(ptdf::PNM.PTDF) subnetworks = Dict{Int, Set{Int}}() for (ref_bus, ptdf_axes) in ptdf.subnetwork_axes subnetworks[ref_bus] = Set(ptdf_axes[1]) end return subnetworks end function _make_subnetworks_from_subnetwork_axes(ptdf::PNM.VirtualPTDF) subnetworks = Dict{Int, Set{Int}}() for (ref_bus, ptdf_axes) in ptdf.subnetwork_axes subnetworks[ref_bus] = Set(ptdf_axes[2]) end return subnetworks end function _assign_subnetworks_to_buses( model::NetworkModel{T}, sys::PSY.System, ) where {T <: Union{CopperPlatePowerModel, AbstractPTDFModel}} subnetworks = model.subnetworks temp_bus_map = Dict{Int, Int}() network_reduction = PSI.get_network_reduction(model) for bus in PSI.get_available_components(model, PSY.ACBus, sys) bus_no = PSY.get_number(bus) mapped_bus_no = PNM.get_mapped_bus_number(network_reduction, bus) mapped_bus_no ∈ network_reduction.removed_buses && continue if haskey(temp_bus_map, bus_no) model.bus_area_map[bus] = temp_bus_map[bus_no] continue else bus_mapped = false for (subnet, bus_set) in subnetworks if mapped_bus_no ∈ bus_set temp_bus_map[bus_no] = subnet model.bus_area_map[bus] = subnet bus_mapped = true break end end end if !bus_mapped error( "Bus $(PSY.summary(bus)) not mapped to any reference bus: Mapped bus number: $(mapped_bus_no)", ) end end return end _assign_subnetworks_to_buses( ::NetworkModel{T}, ::PSY.System, ) where {T <: PM.AbstractPowerModel} = nothing function get_reference_bus( model::NetworkModel{T}, b::PSY.ACBus, )::Int where {T <: PM.AbstractPowerModel} if isempty(model.bus_area_map) return first(keys(model.subnetworks)) else return model.bus_area_map[b] end end ================================================ FILE: src/core/network_reductions.jl ================================================ mutable struct BranchReductionOptimizationTracker variable_dict::Dict{ Type{<:ISOPT.VariableType}, Dict{Tuple{Int, Int}, Vector{JuMP.VariableRef}}, } parameter_dict::Dict{ Type{<:ISOPT.ParameterType}, Dict{Tuple{Int, Int}, Vector{Union{Float64, JuMP.VariableRef}}}, } constraint_dict::Dict{Type{<:ISOPT.ConstraintType}, Set{Tuple{Int, Int}}} constraint_map_by_type::Dict{ Type{<:ISOPT.ConstraintType}, Dict{ Type{<:PSY.ACTransmission}, SortedDict{String, Tuple{Tuple{Int, Int}, String}}, }, } number_of_steps::Int end get_variable_dict(reduction_tracker::BranchReductionOptimizationTracker) = reduction_tracker.variable_dict get_parameter_dict(reduction_tracker::BranchReductionOptimizationTracker) = reduction_tracker.parameter_dict get_constraint_dict(reduction_tracker::BranchReductionOptimizationTracker) = reduction_tracker.constraint_dict get_constraint_map_by_type(reduction_tracker::BranchReductionOptimizationTracker) = reduction_tracker.constraint_map_by_type get_number_of_steps(reduction_tracker::BranchReductionOptimizationTracker) = reduction_tracker.number_of_steps set_number_of_steps!(reduction_tracker, number_of_steps) = reduction_tracker.number_of_steps = number_of_steps Base.isempty( reduction_tracker::BranchReductionOptimizationTracker, ) = isempty(reduction_tracker.variable_dict) && isempty(reduction_tracker.parameter_dict) && isempty(reduction_tracker.constraint_dict) Base.empty!( reduction_tracker::BranchReductionOptimizationTracker, ) = begin empty!(reduction_tracker.variable_dict) empty!(reduction_tracker.parameter_dict) empty!(reduction_tracker.constraint_dict) end function BranchReductionOptimizationTracker() return BranchReductionOptimizationTracker(Dict(), Dict(), Dict(), Dict(), 0) end function _make_empty_variable_tracker_dict( arc_tuple::Tuple{Int, Int}, num_steps::Int, ) return Dict{Tuple{Int, Int}, Vector{JuMP.VariableRef}}( arc_tuple => Vector{JuMP.VariableRef}(undef, num_steps), ) end function _make_empty_parameter_tracker_dict( arc_tuple::Tuple{Int, Int}, num_steps::Int, ) return Dict{Tuple{Int, Int}, Vector{Union{Float64, JuMP.VariableRef}}}( arc_tuple => Vector{Union{Float64, JuMP.VariableRef}}(undef, num_steps), ) end """Look up (or register) the tracker entry for `arc_tuple` and `VariableType` T. Returns `(has_entry, tracker_vector)` where `has_entry` is `true` when the arc was already registered by a previous call (i.e. a parallel/reduced branch of a different device type already created the variable).""" function search_for_reduced_branch_variable!( tracker::BranchReductionOptimizationTracker, arc_tuple::Tuple{Int, Int}, ::Type{T}, ) where {T <: ISOPT.VariableType} variable_dict = tracker.variable_dict time_steps = get_number_of_steps(tracker) if !haskey(variable_dict, T) variable_dict[T] = _make_empty_variable_tracker_dict(arc_tuple, time_steps) return (false, variable_dict[T][arc_tuple]) else if haskey(variable_dict[T], arc_tuple) return (true, variable_dict[T][arc_tuple]) else variable_dict[T][arc_tuple] = Vector{JuMP.VariableRef}(undef, time_steps) return (false, variable_dict[T][arc_tuple]) end end end """Look up (or register) the tracker entry for `arc_tuple` and `ParameterType` T. Stores `Float64` values when `built_for_recurrent_solves` is `false`, or `JuMP.VariableRef` objects (JuMP parameters) when `true`, so that shared arcs across different branch types reuse the same underlying parameter object. Returns `(has_entry, tracker_vector)`.""" function search_for_reduced_branch_parameter!( tracker::BranchReductionOptimizationTracker, arc_tuple::Tuple{Int, Int}, ::Type{T}, ) where {T <: ISOPT.ParameterType} parameter_dict = tracker.parameter_dict time_steps = get_number_of_steps(tracker) if !haskey(parameter_dict, T) parameter_dict[T] = _make_empty_parameter_tracker_dict(arc_tuple, time_steps) return (false, parameter_dict[T][arc_tuple]) else if haskey(parameter_dict[T], arc_tuple) return (true, parameter_dict[T][arc_tuple]) else parameter_dict[T][arc_tuple] = Vector{Union{Float64, JuMP.VariableRef}}(undef, time_steps) return (false, parameter_dict[T][arc_tuple]) end end end # Backwards-compatible dispatcher: routes to the correctly typed dict based on T. function search_for_reduced_branch_argument!( tracker::BranchReductionOptimizationTracker, arc_tuple::Tuple{Int, Int}, ::Type{T}, ) where {T <: ISOPT.VariableType} return search_for_reduced_branch_variable!(tracker, arc_tuple, T) end function search_for_reduced_branch_argument!( tracker::BranchReductionOptimizationTracker, arc_tuple::Tuple{Int, Int}, ::Type{T}, ) where {T <: ISOPT.ParameterType} return search_for_reduced_branch_parameter!(tracker, arc_tuple, T) end function get_branch_argument_parameter_axes( net_reduction_data::PNM.NetworkReductionData, ::IS.FlattenIteratorWrapper{T}, ::Type{V}, ts_name::String; interval::Dates.Millisecond = UNSET_INTERVAL, ) where {T <: PSY.ACTransmission, V <: PSY.TimeSeriesData} return get_branch_argument_parameter_axes( net_reduction_data, T, V, ts_name; interval = interval, ) end function get_branch_argument_parameter_axes( net_reduction_data::PNM.NetworkReductionData, ::Type{T}, ::Type{V}, ts_name::String; interval::Dates.Millisecond = UNSET_INTERVAL, ) where {T <: PSY.ACTransmission, V <: PSY.TimeSeriesData} is_interval = _to_is_interval(interval) name_axis = Vector{String}() ts_uuid_axis = Vector{String}() for (name, (arc, reduction)) in net_reduction_data.name_to_arc_map[T] reduction_entry = net_reduction_data.all_branch_maps_by_type[reduction][T][arc] device_with_time_series = PNM.get_device_with_time_series(reduction_entry, V, ts_name) if device_with_time_series !== nothing push!(name_axis, name) push!( ts_uuid_axis, string( IS.get_time_series_uuid( V, device_with_time_series, ts_name; interval = is_interval, ), ), ) end end return name_axis, ts_uuid_axis end function get_branch_argument_variable_axis( net_reduction_data::PNM.NetworkReductionData, ::IS.FlattenIteratorWrapper{T}, ) where {T <: PSY.ACTransmission} return get_branch_argument_variable_axis(net_reduction_data, T) end function get_branch_argument_variable_axis( net_reduction_data::PNM.NetworkReductionData, ::Type{T}, ) where {T <: PSY.ACTransmission} name_axis = net_reduction_data.name_to_arc_map[T] return collect(keys(name_axis)) end #= function get_branch_argument_variable_axis( net_reduction_data::PNM.NetworkReductionData, ::Type{PNM.ThreeWindingTransformerWinding{T}}, ) where {T <: PSY.ThreeWindingTransformer} name_axis = net_reduction_data.name_to_arc_map[T] return collect(keys(name_axis)) end =# function get_branch_argument_constraint_axis( net_reduction_data::PNM.NetworkReductionData, reduced_branch_tracker::BranchReductionOptimizationTracker, ::IS.FlattenIteratorWrapper{T}, ::Type{U}, ) where {T <: PSY.ACTransmission, U <: ISOPT.ConstraintType} return get_branch_argument_constraint_axis( net_reduction_data, reduced_branch_tracker, T, U, ) end function get_branch_argument_constraint_axis( net_reduction_data::PNM.NetworkReductionData, reduced_branch_tracker::BranchReductionOptimizationTracker, ::Type{T}, ::Type{U}, ) where {T <: PSY.ACTransmission, U <: ISOPT.ConstraintType} constraint_tracker = get_constraint_dict(reduced_branch_tracker) constraint_map_by_type = get_constraint_map_by_type(reduced_branch_tracker) name_axis = net_reduction_data.name_to_arc_map[T] arc_tuples_with_constraints = get!(constraint_tracker, U, Set{Tuple{Int, Int}}()) constraint_map = get!( constraint_map_by_type, U, Dict{ Type{<:PSY.ACTransmission}, SortedDict{String, Tuple{Tuple{Int, Int}, String}}, }(), ) constraint_submap = get!(constraint_map, T, SortedDict{String, Tuple{Tuple{Int, Int}, String}}()) for (branch_name, name_axis_tuple) in name_axis arc_tuple = name_axis_tuple[1] if !(arc_tuple in arc_tuples_with_constraints) constraint_submap[branch_name] = name_axis_tuple push!(arc_tuples_with_constraints, arc_tuple) end end return collect(keys(constraint_submap)) end ================================================ FILE: src/core/operation_model_abstract_types.jl ================================================ """ Abstract type for Decision Model and Emulation Model. OperationModel structs are parameterized with DecisionProblem or Emulation Problem structs """ abstract type OperationModel end #TODO: Document the required interfaces for custom types """ Abstract type for Decision Problems # Example import PowerSimulations as PSI struct MyCustomProblem <: PSI.DecisionProblem """ abstract type DecisionProblem end """ Abstract type for Emulation Problems # Example import PowerSimulations as PSI struct MyCustomEmulator <: PSI.EmulationProblem """ abstract type EmulationProblem end ================================================ FILE: src/core/optimization_container.jl ================================================ struct PrimalValuesCache variables_cache::Dict{VariableKey, AbstractArray} expressions_cache::Dict{ExpressionKey, AbstractArray} end function PrimalValuesCache() return PrimalValuesCache( Dict{VariableKey, AbstractArray}(), Dict{ExpressionKey, AbstractArray}(), ) end function Base.isempty(pvc::PrimalValuesCache) return isempty(pvc.variables_cache) && isempty(pvc.expressions_cache) end mutable struct ObjectiveFunction invariant_terms::JuMP.AbstractJuMPScalar variant_terms::GAE synchronized::Bool sense::MOI.OptimizationSense function ObjectiveFunction(invariant_terms::JuMP.AbstractJuMPScalar, variant_terms::GAE, synchronized::Bool, sense::MOI.OptimizationSense = MOI.MIN_SENSE) new(invariant_terms, variant_terms, synchronized, sense) end end get_invariant_terms(v::ObjectiveFunction) = v.invariant_terms get_variant_terms(v::ObjectiveFunction) = v.variant_terms function get_objective_expression(v::ObjectiveFunction) if iszero(v.variant_terms) return v.invariant_terms else # JuMP doesn't support expression conversion from Affn to QuadExpressions if isa(v.invariant_terms, JuMP.GenericQuadExpr) # Avoid mutation of invariant term temp_expr = JuMP.QuadExpr() JuMP.add_to_expression!(temp_expr, v.invariant_terms) return JuMP.add_to_expression!(temp_expr, v.variant_terms) else # This will mutate the variant terms, but these are reseted at each step. return JuMP.add_to_expression!(v.variant_terms, v.invariant_terms) end end end get_sense(v::ObjectiveFunction) = v.sense is_synchronized(v::ObjectiveFunction) = v.synchronized 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 function ObjectiveFunction() return ObjectiveFunction( zero(JuMP.GenericAffExpr{Float64, JuMP.VariableRef}), zero(JuMP.AffExpr), true, ) end mutable struct OptimizationContainer <: ISOPT.AbstractOptimizationContainer JuMPmodel::JuMP.Model time_steps::UnitRange{Int} settings::Settings variables::OrderedDict{VariableKey, AbstractArray} aux_variables::OrderedDict{AuxVarKey, AbstractArray} duals::OrderedDict{ConstraintKey, AbstractArray} constraints::OrderedDict{ConstraintKey, AbstractArray} objective_function::ObjectiveFunction expressions::OrderedDict{ExpressionKey, AbstractArray} parameters::OrderedDict{ParameterKey, ParameterContainer} primal_values_cache::PrimalValuesCache initial_conditions::OrderedDict{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::ISOPT.OptimizationContainerMetadata default_time_series_type::Type{<:PSY.TimeSeriesData} power_flow_evaluation_data::Vector{PowerFlowEvaluationData} end function OptimizationContainer( sys::PSY.System, settings::Settings, jump_model::Union{Nothing, JuMP.Model}, ::Type{T}, ) where {T <: PSY.TimeSeriesData} if isabstracttype(T) error("Default Time Series Type $V can't be abstract") end if jump_model !== nothing && get_direct_mode_optimizer(settings) throw( IS.ConflictingInputsError( "Externally provided JuMP models are not compatible with the direct model keyword argument. Use JuMP.direct_model before passing the custom model", ), ) end return OptimizationContainer( jump_model === nothing ? JuMP.Model() : jump_model, 1:1, settings, OrderedDict{VariableKey, AbstractArray}(), OrderedDict{AuxVarKey, AbstractArray}(), OrderedDict{ConstraintKey, AbstractArray}(), OrderedDict{ConstraintKey, AbstractArray}(), ObjectiveFunction(), OrderedDict{ExpressionKey, AbstractArray}(), OrderedDict{ParameterKey, ParameterContainer}(), PrimalValuesCache(), OrderedDict{InitialConditionKey, Vector{InitialCondition}}(), InitialConditionsData(), Dict{Symbol, Array}(), nothing, PSY.get_base_power(sys), OptimizerStats(), false, ISOPT.OptimizationContainerMetadata(), T, Vector{PowerFlowEvaluationData}[], ) end built_for_recurrent_solves(container::OptimizationContainer) = container.built_for_recurrent_solves get_aux_variables(container::OptimizationContainer) = container.aux_variables get_base_power(container::OptimizationContainer) = container.base_power get_constraints(container::OptimizationContainer) = container.constraints function cost_function_unsynch(container::OptimizationContainer) 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 function get_container_keys(container::OptimizationContainer) return Iterators.flatten(keys(getfield(container, f)) for f in STORE_CONTAINERS) end get_default_time_series_type(container::OptimizationContainer) = container.default_time_series_type get_duals(container::OptimizationContainer) = container.duals get_expressions(container::OptimizationContainer) = container.expressions get_infeasibility_conflict(container::OptimizationContainer) = container.infeasibility_conflict get_initial_conditions(container::OptimizationContainer) = container.initial_conditions get_initial_conditions_data(container::OptimizationContainer) = container.initial_conditions_data get_initial_time(container::OptimizationContainer) = get_initial_time(container.settings) get_jump_model(container::OptimizationContainer) = container.JuMPmodel get_metadata(container::OptimizationContainer) = container.metadata get_optimizer_stats(container::OptimizationContainer) = container.optimizer_stats get_parameters(container::OptimizationContainer) = container.parameters get_power_flow_evaluation_data(container::OptimizationContainer) = container.power_flow_evaluation_data 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 set_initial_conditions_data!(container::OptimizationContainer, data) = container.initial_conditions_data = data get_objective_expression(container::OptimizationContainer) = container.objective_function is_synchronized(container::OptimizationContainer) = container.objective_function.synchronized set_time_steps!(container::OptimizationContainer, time_steps::UnitRange{Int64}) = container.time_steps = time_steps function reset_power_flow_is_solved!(container::OptimizationContainer) for pf_e_data in get_power_flow_evaluation_data(container) pf_e_data.is_solved = false end end function has_container_key( container::OptimizationContainer, ::Type{T}, ::Type{U}, meta = ISOPT.CONTAINER_KEY_EMPTY_META, ) where {T <: ExpressionType, U <: Union{PSY.Component, PSY.System}} key = ExpressionKey(T, U, meta) return haskey(container.expressions, key) end function has_container_key( container::OptimizationContainer, ::Type{T}, ::Type{U}, meta = ISOPT.CONTAINER_KEY_EMPTY_META, ) where {T <: VariableType, U <: Union{PSY.Component, PSY.System}} key = VariableKey(T, U, meta) return haskey(container.variables, key) end function has_container_key( container::OptimizationContainer, ::Type{T}, ::Type{U}, meta = ISOPT.CONTAINER_KEY_EMPTY_META, ) where {T <: AuxVariableType, U <: Union{PSY.Component, PSY.System}} key = AuxVarKey(T, U, meta) return haskey(container.aux_variables, key) end function has_container_key( container::OptimizationContainer, ::Type{T}, ::Type{U}, meta = ISOPT.CONTAINER_KEY_EMPTY_META, ) where {T <: ConstraintType, U <: Union{PSY.Component, PSY.System}} key = ConstraintKey(T, U, meta) return haskey(container.constraints, key) end function has_container_key( container::OptimizationContainer, ::Type{T}, ::Type{U}, meta = ISOPT.CONTAINER_KEY_EMPTY_META, ) where {T <: ParameterType, U <: Union{PSY.Component, PSY.System}} key = ParameterKey(T, U, meta) return haskey(container.parameters, key) end function has_container_key( container::OptimizationContainer, ::Type{T}, ::Type{U}, meta = ISOPT.CONTAINER_KEY_EMPTY_META, ) where {T <: InitialConditionType, U <: Union{PSY.Component, PSY.System}} key = InitialConditionKey(T, U, meta) return haskey(container.initial_conditions, key) end function is_milp(container::OptimizationContainer)::Bool !supports_milp(container) && return false if !isempty( JuMP.all_constraints( PSI.get_jump_model(container), JuMP.VariableRef, JuMP.MOI.ZeroOne, ), ) return true end return false end function supports_milp(container::OptimizationContainer) jump_model = get_jump_model(container) return supports_milp(jump_model) end function _validate_warm_start_support(JuMPmodel::JuMP.Model, warm_start_enabled::Bool) !warm_start_enabled && return warm_start_enabled solver_supports_warm_start = MOI.supports(JuMP.backend(JuMPmodel), MOI.VariablePrimalStart(), MOI.VariableIndex) if !solver_supports_warm_start solver_name = JuMP.solver_name(JuMPmodel) @warn("$(solver_name) does not support warm start") end return solver_supports_warm_start end function _finalize_jump_model!(container::OptimizationContainer, settings::Settings) @debug "Instantiating the JuMP model" _group = LOG_GROUP_OPTIMIZATION_CONTAINER if built_for_recurrent_solves(container) && get_optimizer(settings) === nothing throw( IS.ConflictingInputsError( "Optimizer can not be nothing when building for recurrent solves", ), ) end if get_direct_mode_optimizer(settings) optimizer = () -> MOI.instantiate(get_optimizer(settings)) container.JuMPmodel = JuMP.direct_model(optimizer()) elseif get_optimizer(settings) === nothing @debug "The optimization model has no optimizer attached" _group = LOG_GROUP_OPTIMIZATION_CONTAINER else JuMP.set_optimizer(PSI.get_jump_model(container), get_optimizer(settings)) end JuMPmodel = PSI.get_jump_model(container) warm_start_enabled = get_warm_start(settings) solver_supports_warm_start = _validate_warm_start_support(JuMPmodel, warm_start_enabled) set_warm_start!(settings, solver_supports_warm_start) JuMP.set_string_names_on_creation(JuMPmodel, get_store_variable_names(settings)) @debug begin JuMP.set_string_names_on_creation(JuMPmodel, true) end if get_optimizer_solve_log_print(settings) JuMP.unset_silent(JuMPmodel) @debug "optimizer unset to silent" _group = LOG_GROUP_OPTIMIZATION_CONTAINER else JuMP.set_silent(JuMPmodel) @debug "optimizer set to silent" _group = LOG_GROUP_OPTIMIZATION_CONTAINER end return end function init_optimization_container!( container::OptimizationContainer, network_model::NetworkModel{T}, sys::PSY.System, ) where {T <: PM.AbstractPowerModel} PSY.set_units_base_system!(sys, "SYSTEM_BASE") # The order of operations matter settings = get_settings(container) if get_initial_time(settings) == UNSET_INI_TIME if get_default_time_series_type(container) <: PSY.AbstractDeterministic set_initial_time!(settings, PSY.get_forecast_initial_timestamp(sys)) elseif get_default_time_series_type(container) <: PSY.SingleTimeSeries ini_time, _ = PSY.check_time_series_consistency(sys, PSY.SingleTimeSeries) set_initial_time!(settings, ini_time) end end if get_resolution(settings) == UNSET_RESOLUTION error("Resolution not set in the model. Can't continue with the build.") end 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 = length(get_available_components(network_model, PSY.Device, sys)) else total_number_of_devices = length(get_available_components(network_model, PSY.Device, sys)) total_number_of_devices += length(get_available_components(network_model, PSY.ACBranch, sys)) end # The 10e6 limit is based on the sizes of the lp benchmark problems http://plato.asu.edu/ftp/lpcom.html # The maximum numbers of constraints and variables in the benchmark problems is 1,918,399 and 1,259,121, # respectively. See also https://prod-ng.sandia.gov/techlib-noauth/access-control.cgi/2013/138847.pdf variable_count_estimate = length(container.time_steps) * total_number_of_devices if variable_count_estimate > 10e6 @warn( "The lower estimate of total number of variables that will be created in the model is $(variable_count_estimate). \\ The total number of variables might be larger than 10e6 and could lead to large build or solve times." ) end stats = get_optimizer_stats(container) stats.detailed_stats = get_detailed_optimizer_stats(settings) _finalize_jump_model!(container, settings) return end function reset_optimization_model!(container::OptimizationContainer) for field in [:variables, :aux_variables, :constraints, :expressions, :duals] empty!(getfield(container, field)) end container.initial_conditions_data = InitialConditionsData() container.objective_function = ObjectiveFunction() container.primal_values_cache = PrimalValuesCache() JuMP.empty!(PSI.get_jump_model(container)) return end function check_parameter_multiplier_values(multiplier_array::DenseAxisArray) return !all(isnan.(multiplier_array.data)) end function check_parameter_multiplier_values(multiplier_array::SparseAxisArray) return !all(isnan.(values(multiplier_array.data))) end function check_optimization_container(container::OptimizationContainer) for (k, param_container) in container.parameters valid = check_parameter_multiplier_values(param_container.multiplier_array) if !valid error("The model container has invalid values in $(encode_key_as_string(k))") end end return end function get_problem_size(container::OptimizationContainer) model = get_jump_model(container) vars = JuMP.num_variables(model) cons = 0 for (exp, c_type) in JuMP.list_of_constraint_types(model) cons += JuMP.num_constraints(model, exp, c_type) end return "The current total number of variables is $(vars) and total number of constraints is $(cons)" end function _make_container_array(ax...) return remove_undef!(DenseAxisArray{GAE}(undef, ax...)) end function _make_system_expressions!( container::OptimizationContainer, subnetworks::Dict{Int, Set{Int}}, ::Vector{Int}, ::Type{<:PM.AbstractPowerModel}, 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( ExpressionKey(ActivePowerBalance, PSY.ACBus) => _make_container_array(ac_bus_numbers, time_steps), ExpressionKey(ReactivePowerBalance, PSY.ACBus) => _make_container_array(ac_bus_numbers, time_steps), ) return end function _make_system_expressions!( container::OptimizationContainer, subnetworks::Dict{Int, Set{Int}}, ::Vector{Int}, ::Type{<:PM.AbstractActivePowerModel}, 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( ExpressionKey(ActivePowerBalance, PSY.ACBus) => _make_container_array(ac_bus_numbers, time_steps), ) return end function _make_system_expressions!( container::OptimizationContainer, subnetworks::Dict{Int, Set{Int}}, ::Vector{Int}, ::Type{CopperPlatePowerModel}, bus_reduction_map::Dict{Int64, Set{Int64}}, ) time_steps = get_time_steps(container) subnetworks_ref_buses = collect(keys(subnetworks)) container.expressions = Dict( ExpressionKey(ActivePowerBalance, PSY.System) => _make_container_array(subnetworks_ref_buses, time_steps), ) return end function _make_system_expressions!( container::OptimizationContainer, subnetworks::Dict{Int, Set{Int}}, ::Vector{Int}, ::Type{T}, bus_reduction_map::Dict{Int64, Set{Int64}}, ) where {T <: Union{PTDFPowerModel}} 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 subnetworks = collect(keys(subnetworks)) container.expressions = Dict( ExpressionKey(ActivePowerBalance, PSY.System) => _make_container_array(subnetworks, 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), ) return end function _make_system_expressions!( container::OptimizationContainer, subnetworks::Dict{Int, Set{Int}}, ::Type{AreaBalancePowerModel}, areas::IS.FlattenIteratorWrapper{PSY.Area}, ) 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}}, ::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 return end function initialize_system_expressions!( container::OptimizationContainer, network_model::NetworkModel{T}, subnetworks::Dict{Int, Set{Int}}, ::BranchModelContainer, system::PSY.System, bus_reduction_map::Dict{Int64, Set{Int64}}, ) where {T <: PM.AbstractPowerModel} 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 _verify_area_subnetwork_topology(sys::PSY.System, subnetworks::Dict{Int, Set{Int}}) if length(subnetworks) < 1 @debug "Only one subnetwork detected in the system. Area - Subnetwork topology check is valid." return end @warn "More than one subnetwork detected in AreaBalancePowerModel. Topology consistency checks must be conducted." area_map = PSY.get_aggregation_topology_mapping(PSY.Area, sys) for (area, buses) in area_map bus_numbers = [ PSY.get_number(b) for b in buses if PSY.get_bustype(b) != PSY.ACBusTypes.ISOLATED ] subnets = Int[] for (subnet, subnet_bus_numbers) in subnetworks if !isdisjoint(bus_numbers, subnet_bus_numbers) push!(subnets, subnet) end end if length(subnets) > 1 @error "Area $(PSY.get_name(area)) is connected to multiple subnetworks $(subnets)." throw( IS.ConflictingInputsError( "AreaBalancePowerModel doesn't support systems with Areas distributed across multiple asynchronous areas", )) end end return end function initialize_system_expressions!( container::OptimizationContainer, network_model::NetworkModel{AreaBalancePowerModel}, subnetworks::Dict{Int, Set{Int}}, branch_models::BranchModelContainer, 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 area_interchanges = PSY.get_available_components(PSY.AreaInterchange, system) if isempty(area_interchanges) || !haskey(branch_models, :AreaInterchange) @warn "The system does not contain any AreaInterchanges. The model won't have any power flowing between the areas." end if !isempty(area_interchanges) && !haskey(branch_models, :AreaInterchange) @warn "AreaInterchanges are not included in the model template. The model won't have any power flowing between the areas." end _verify_area_subnetwork_topology(system, subnetworks) _make_system_expressions!(container, subnetworks, AreaBalancePowerModel, areas) return end function initialize_system_expressions!( container::OptimizationContainer, network_model::NetworkModel{T}, subnetworks::Dict{Int, Set{Int}}, ::BranchModelContainer, system::PSY.System, bus_reduction_map::Dict{Int64, Set{Int64}}, ) where {T <: AreaPTDFPowerModel} 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 initialize_hvdc_system!( container::OptimizationContainer, network_model::NetworkModel{T}, dc_model::Nothing, system::PSY.System, ) where {T <: PM.AbstractPowerModel} dc_buses = get_available_components(network_model, PSY.DCBus, system) if !isempty(dc_buses) @warn "HVDC Network Model is set to 'Nothing' but DC Buses are present in the system. \ Consider adding an HVDC Network Model or removing DC Buses from the system." end return end function initialize_hvdc_system!( container::OptimizationContainer, network_model::NetworkModel{T}, dc_model::U, system::PSY.System, ) where {T <: PM.AbstractPowerModel, U <: TransportHVDCNetworkModel} dc_buses = get_available_components(network_model, PSY.DCBus, system) @assert !isempty(dc_buses) "No DC buses found in the system. Consider adding DC Buses or removing HVDC network model." dc_bus_numbers = sort(PSY.get_number.(dc_buses)) container.expressions[ExpressionKey(ActivePowerBalance, PSY.DCBus)] = _make_container_array(dc_bus_numbers, get_time_steps(container)) return end function initialize_hvdc_system!( container::OptimizationContainer, network_model::NetworkModel{T}, dc_model::U, system::PSY.System, ) where {T <: PM.AbstractPowerModel, U <: VoltageDispatchHVDCNetworkModel} dc_buses = get_available_components(network_model, PSY.DCBus, system) @assert !isempty(dc_buses) "No DC buses found in the system. Consider adding DC Buses or removing HVDC network model." dc_bus_numbers = sort(PSY.get_number.(dc_buses)) container.expressions[ExpressionKey(DCCurrentBalance, PSY.DCBus)] = _make_container_array(dc_bus_numbers, get_time_steps(container)) add_variable!(container, DCVoltage(), dc_buses, dc_model) return end function build_impl!( container::OptimizationContainer, template::ProblemTemplate, sys::PSY.System, ) transmission = get_network_formulation(template) transmission_model = get_network_model(template) hvdc_model = get_hvdc_network_model(template) initialize_system_expressions!( container, get_network_model(template), transmission_model.subnetworks, get_branch_models(template), sys, transmission_model.network_reduction.bus_reduction_map) initialize_hvdc_system!( container, transmission_model, hvdc_model, sys, ) # Order is required for device_model in values(template.devices) @debug "Building Arguments for $(get_component_type(device_model)) with $(get_formulation(device_model)) formulation" _group = LOG_GROUP_OPTIMIZATION_CONTAINER TimerOutputs.@timeit BUILD_PROBLEMS_TIMER "$(get_component_type(device_model))" begin construct_device!( container, sys, ArgumentConstructStage(), device_model, transmission_model, ) @debug "Problem size:" get_problem_size(container) _group = LOG_GROUP_OPTIMIZATION_CONTAINER end end TimerOutputs.@timeit BUILD_PROBLEMS_TIMER "Services" begin construct_services!( container, sys, ArgumentConstructStage(), get_service_models(template), get_device_models(template), transmission_model, ) end # # Sort branch models so that those with time series parameters (e.g. DLR) come first. # # This ensures that DLR-aware constraint builders claim shared arcs before static builders, # # preventing static constraints from overriding DLR constraints for parallel branches of # # different types sharing the same arc. # sorted_branch_models = sort( # collect(values(template.branches)); # by = b -> isempty(get_time_series_names(b)) ? 1 : 0, # ) for branch_model in values(template.branches) @debug "Building Arguments for $(get_component_type(branch_model)) with $(get_formulation(branch_model)) formulation" _group = LOG_GROUP_OPTIMIZATION_CONTAINER TimerOutputs.@timeit BUILD_PROBLEMS_TIMER "$(get_component_type(branch_model))" begin construct_device!( container, sys, ArgumentConstructStage(), branch_model, transmission_model, ) @debug "Problem size:" get_problem_size(container) _group = LOG_GROUP_OPTIMIZATION_CONTAINER end 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 TimerOutputs.@timeit BUILD_PROBLEMS_TIMER "$(get_component_type(device_model))" begin construct_device!( container, sys, ModelConstructStage(), device_model, transmission_model, ) @debug "Problem size:" get_problem_size(container) _group = LOG_GROUP_OPTIMIZATION_CONTAINER end end # This function should be called after construct_device ModelConstructStage TimerOutputs.@timeit BUILD_PROBLEMS_TIMER "$(transmission)" begin @debug "Building $(transmission) network formulation" _group = LOG_GROUP_OPTIMIZATION_CONTAINER construct_network!(container, sys, transmission_model, template) construct_hvdc_network!(container, sys, transmission_model, hvdc_model, template) @debug "Problem size:" get_problem_size(container) _group = LOG_GROUP_OPTIMIZATION_CONTAINER end for branch_model in values(template.branches) @debug "Building Model for $(get_component_type(branch_model)) with $(get_formulation(branch_model)) formulation" _group = LOG_GROUP_OPTIMIZATION_CONTAINER TimerOutputs.@timeit BUILD_PROBLEMS_TIMER "$(get_component_type(branch_model))" begin construct_device!( container, sys, ModelConstructStage(), branch_model, transmission_model, ) @debug "Problem size:" get_problem_size(container) _group = LOG_GROUP_OPTIMIZATION_CONTAINER 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) end @debug "Total operation count $(PSI.get_jump_model(container).operator_counter)" _group = LOG_GROUP_OPTIMIZATION_CONTAINER TimerOutputs.@timeit BUILD_PROBLEMS_TIMER "Power Flow Initialization" begin add_power_flow_data!(container, get_power_flow_evaluation(transmission_model), sys) end check_optimization_container(container) return end function update_objective_function!(container::OptimizationContainer) JuMP.@objective( get_jump_model(container), get_sense(container.objective_function), get_objective_expression(container.objective_function) ) return end """ Default solve method for OptimizationContainer """ function solve_impl!(container::OptimizationContainer, system::PSY.System) optimizer_stats = get_optimizer_stats(container) jump_model = get_jump_model(container) model_status = MOI.NO_SOLUTION::MOI.ResultStatusCode conflict_status = MOI.COMPUTE_CONFLICT_NOT_CALLED try_count = 0 while model_status != MOI.FEASIBLE_POINT::MOI.ResultStatusCode _, optimizer_stats.timed_solve_time, optimizer_stats.solve_bytes_alloc, optimizer_stats.sec_in_gc = @timed JuMP.optimize!(jump_model) model_status = JuMP.primal_status(jump_model) if model_status != MOI.FEASIBLE_POINT::MOI.ResultStatusCode if get_calculate_conflict(get_settings(container)) @warn "Optimizer returned $model_status computing conflict" conflict_status = compute_conflict!(container) if conflict_status == MOI.CONFLICT_FOUND return RunStatus.FAILED end else @warn "Optimizer returned $model_status trying optimize! again" end try_count += 1 if try_count > MAX_OPTIMIZE_TRIES @error "Optimizer returned $model_status after $MAX_OPTIMIZE_TRIES optimize! attempts" return RunStatus.FAILED end end end # Order is important because if a dual is needed then it could move the results to the # temporary primal container _, optimizer_stats.timed_calculate_aux_variables = @timed calculate_aux_variables!(container, system) # Needs to be called here to avoid issues when getting duals from MILPs write_optimizer_stats!(container) _, optimizer_stats.timed_calculate_dual_variables = @timed calculate_dual_variables!(container, system, is_milp(container)) return RunStatus.SUCCESSFULLY_FINALIZED end function compute_conflict!(container::OptimizationContainer) jump_model = get_jump_model(container) settings = get_settings(container) JuMP.unset_silent(jump_model) jump_model.is_model_dirty = false conflict = container.infeasibility_conflict try JuMP.compute_conflict!(jump_model) conflict_status = MOI.get(jump_model, MOI.ConflictStatus()) if conflict_status != MOI.CONFLICT_FOUND @error "No conflict could be found for the model. Status: $conflict_status" if !get_optimizer_solve_log_print(settings) JuMP.set_silent(jump_model) end return conflict_status end for (key, field_container) in get_constraints(container) conflict_indices = check_conflict_status(jump_model, field_container) if isempty(conflict_indices) @info "Conflict Index returned empty for $key" continue else conflict[ISOPT.encode_key(key)] = conflict_indices end end msg = IOBuffer() for (k, v) in conflict PrettyTables.pretty_table(msg, v; header = [k]) end @error "Constraints participating in conflict basis (IIS) \n\n$(String(take!(msg)))" return conflict_status catch e jump_model.is_model_dirty = true if isa(e, MethodError) @info "Can't compute conflict, check that your optimizer supports conflict refining/IIS" else @error "Can't compute conflict" exception = (e, catch_backtrace()) end end return MOI.NO_CONFLICT_EXISTS end function write_optimizer_stats!(container::OptimizationContainer) write_optimizer_stats!(get_optimizer_stats(container), get_jump_model(container)) return end """ Exports the OpModel JuMP object in MathOptFormat """ function serialize_optimization_model(container::OptimizationContainer, save_path::String) serialize_jump_optimization_model(get_jump_model(container), save_path) return end function serialize_metadata!(container::OptimizationContainer, output_dir::String) for key in Iterators.flatten(( keys(container.constraints), keys(container.duals), keys(container.parameters), keys(container.variables), keys(container.aux_variables), keys(container.expressions), )) encoded_key = encode_key_as_string(key) if ISOPT.has_container_key(container.metadata, encoded_key) # Constraints and Duals can store the same key. IS.@assert_op key == ISOPT.get_container_key(container.metadata, encoded_key) end ISOPT.add_container_key!(container.metadata, encoded_key, key) end filename = ISOPT._make_metadata_filename(output_dir) Serialization.serialize(filename, container.metadata) @debug "Serialized container keys to $filename" _group = IS.LOG_GROUP_SERIALIZATION end function deserialize_metadata!( container::OptimizationContainer, output_dir::String, model_name, ) merge!( container.metadata.container_key_lookup, deserialize_metadata( ISOPT.OptimizationContainerMetadata, output_dir, model_name, ), ) return end # PERF: compilation hotspot. from string conversion at the container[key] = value line? function _assign_container!(container::OrderedDict, key::OptimizationContainerKey, value) if haskey(container, key) @error "$(ISOPT.encode_key(key)) is already stored" sort!( ISOPT.encode_key.(keys(container)), ) throw(IS.InvalidValue("$key is already stored")) end container[key] = value @debug "Added container entry $(typeof(key)) $(ISOPT.encode_key(key))" _group = LOG_GROUP_OPTIMZATION_CONTAINER return end ####################################### Variable Container ################################# function _add_variable_container!( container::OptimizationContainer, var_key::VariableKey{T, U}, sparse::Bool, axs..., ) where {T <: VariableType, U <: Union{PSY.Component, PSY.System}} if sparse var_container = sparse_container_spec(JuMP.VariableRef, axs...) else var_container = container_spec(JuMP.VariableRef, axs...) end _assign_container!(container.variables, var_key, var_container) return var_container end function add_variable_container!( container::OptimizationContainer, ::T, ::Type{U}, axs...; sparse = false, meta = ISOPT.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...) end function add_variable_container!( container::OptimizationContainer, ::T, ::Type{U}, meta::String, axs...; sparse = false, ) where {T <: VariableType, U <: Union{PSY.Component, PSY.System}} var_key = VariableKey(T, U, meta) return _add_variable_container!(container, var_key, sparse, axs...) end function _get_pwl_variables_container() contents = Dict{Tuple{String, Int, Int}, Any}() return SparseAxisArray(contents) end function add_variable_container!( container::OptimizationContainer, ::T, ::Type{U}; meta = ISOPT.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] end function get_variable_keys(container::OptimizationContainer) return collect(keys(container.variables)) end function get_variable(container::OptimizationContainer, key::VariableKey) var = get(container.variables, key, nothing) if var === nothing name = ISOPT.encode_key(key) keys = ISOPT.encode_key.(get_variable_keys(container)) throw(IS.InvalidValue("variable $name is not stored. $keys")) end return var end function get_variable( container::OptimizationContainer, ::T, ::Type{U}, meta::String = ISOPT.CONTAINER_KEY_EMPTY_META, ) where {T <: VariableType, U <: Union{PSY.Component, PSY.System}} return get_variable(container, VariableKey(T, U, meta)) end ##################################### AuxVariable Container ################################ function add_aux_variable_container!( container::OptimizationContainer, ::T, ::Type{U}, axs...; sparse = false, meta = ISOPT.CONTAINER_KEY_EMPTY_META, ) where {T <: AuxVariableType, U <: PSY.Component} var_key = AuxVarKey(T, U, meta) if sparse aux_variable_container = sparse_container_spec(Float64, axs...) else aux_variable_container = container_spec(Float64, axs...) end _assign_container!(container.aux_variables, var_key, aux_variable_container) return aux_variable_container end function get_aux_variable_keys(container::OptimizationContainer) return collect(keys(container.aux_variables)) end function get_aux_variable(container::OptimizationContainer, key::AuxVarKey) aux = get(container.aux_variables, key, nothing) if aux === nothing name = ISOPT.encode_key(key) keys = ISOPT.encode_key.(get_aux_variable_keys(container)) throw(IS.InvalidValue("Auxiliary variable $name is not stored. $keys")) end return aux end function get_aux_variable( container::OptimizationContainer, ::T, ::Type{U}, meta::String = ISOPT.CONTAINER_KEY_EMPTY_META, ) where {T <: AuxVariableType, U <: PSY.Component} return get_aux_variable(container, AuxVarKey(T, U, meta)) end ##################################### DualVariable Container ################################ function add_dual_container!( container::OptimizationContainer, ::Type{T}, ::Type{U}, axs...; sparse = false, meta = ISOPT.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, \\ dual value retrieval requires solving an additional Linear Program \\ which increases simulation time and the results could be innacurate.") end const_key = ConstraintKey(T, U, meta) if sparse dual_container = sparse_container_spec(Float64, axs...) else dual_container = container_spec(Float64, axs...) end _assign_container!(container.duals, const_key, dual_container) return dual_container end function get_dual_keys(container::OptimizationContainer) return collect(keys(container.duals)) end ##################################### Constraint Container ################################# function _add_constraints_container!( container::OptimizationContainer, cons_key::ConstraintKey, axs...; sparse = false, ) if sparse cons_container = sparse_container_spec(JuMP.ConstraintRef, axs...) else cons_container = container_spec(JuMP.ConstraintRef, axs...) end _assign_container!(container.constraints, cons_key, cons_container) return cons_container end function add_constraints_container!( container::OptimizationContainer, ::T, ::Type{U}, axs...; sparse = false, meta = ISOPT.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) end function get_constraint_keys(container::OptimizationContainer) return collect(keys(container.constraints)) end function get_constraint(container::OptimizationContainer, key::ConstraintKey) var = get(container.constraints, key, nothing) if var === nothing name = ISOPT.encode_key(key) keys = ISOPT.encode_key.(get_constraint_keys(container)) throw(IS.InvalidValue("constraint $name is not stored. $keys")) end return var end function get_constraint( container::OptimizationContainer, ::T, ::Type{U}, meta::String = ISOPT.CONTAINER_KEY_EMPTY_META, ) where {T <: ConstraintType, U <: Union{PSY.Component, PSY.System}} return get_constraint(container, ConstraintKey(T, U, meta)) end function read_duals(container::OptimizationContainer) return Dict(k => to_dataframe(jump_value.(v), k) for (k, v) in get_duals(container)) end ##################################### Parameter Container ################################## function _add_param_container!( container::OptimizationContainer, key::ParameterKey{T, U}, attribute::VariableValueAttributes{<:OptimizationContainerKey}, param_type::DataType, axs...; sparse = false, ) where {T <: VariableValueParameter, U <: PSY.Component} if sparse param_array = sparse_container_spec(param_type, axs...) multiplier_array = sparse_container_spec(Float64, axs...) else param_array = DenseAxisArray{param_type}(undef, axs...) multiplier_array = fill!(DenseAxisArray{Float64}(undef, axs...), NaN) end param_container = ParameterContainer(attribute, param_array, multiplier_array) _assign_container!(container.parameters, key, param_container) return param_container end function _add_param_container!( container::OptimizationContainer, key::ParameterKey{T, U}, attribute::VariableValueAttributes{<:OptimizationContainerKey}, axs...; sparse = false, ) where {T <: VariableValueParameter, U <: PSY.Component} if built_for_recurrent_solves(container) && !get_rebuild_model(get_settings(container)) param_type = JuMP.VariableRef else param_type = Float64 end return _add_param_container!( container, key, attribute, param_type, axs...; sparse = sparse, ) end function _add_param_container!( container::OptimizationContainer, key::ParameterKey{T, U}, attribute::TimeSeriesAttributes{V}, param_axs, multiplier_axs, additional_axs, time_steps::UnitRange{Int}; sparse = false, ) where {T <: TimeSeriesParameter, U <: PSY.Component, V <: PSY.TimeSeriesData} if built_for_recurrent_solves(container) && !get_rebuild_model(get_settings(container)) param_type = JuMP.VariableRef else param_type = Float64 end if sparse param_array = sparse_container_spec(param_type, param_axs, additional_axs..., time_steps) multiplier_array = sparse_container_spec(Float64, multiplier_axs, additional_axs..., time_steps) else param_array = DenseAxisArray{param_type}(undef, param_axs, additional_axs..., time_steps) multiplier_array = fill!( DenseAxisArray{Float64}( undef, multiplier_axs, additional_axs..., time_steps, ), NaN, ) end param_container = ParameterContainer(attribute, param_array, multiplier_array) _assign_container!(container.parameters, key, param_container) return param_container end function _add_param_container!( container::OptimizationContainer, key::ParameterKey{T, U}, attribute::EventParametersAttributes{V, T}, param_axs, time_steps; sparse = false, ) where {T <: EventParameter, U <: PSY.Component, V <: PSY.Contingency} if built_for_recurrent_solves(container) && !get_rebuild_model(get_settings(container)) param_type = JuMP.VariableRef else param_type = Float64 end if sparse error("Sparse parameter container is not supported for $V") else param_array = DenseAxisArray{param_type}(undef, param_axs, time_steps) multiplier_array = fill!(DenseAxisArray{Float64}(undef, param_axs, time_steps), NaN) end param_container = ParameterContainer(attribute, param_array, multiplier_array) _assign_container!(container.parameters, key, param_container) return param_container end function _add_param_container!( container::OptimizationContainer, key::ParameterKey{T, U}, attributes::CostFunctionAttributes{R}, axs...; sparse = false, ) where {R, T <: ObjectiveFunctionParameter, U <: PSY.Component} if sparse param_array = sparse_container_spec(R, axs...) multiplier_array = sparse_container_spec(Float64, axs...) else param_array = DenseAxisArray{R}(undef, axs...) multiplier_array = fill!(DenseAxisArray{Float64}(undef, axs...), NaN) end param_container = ParameterContainer(attributes, param_array, multiplier_array) _assign_container!(container.parameters, key, param_container) return param_container end function add_param_container!( container::OptimizationContainer, ::T, ::Type{U}, ::Type{V}, name::String, param_axs, multiplier_axs, additional_axs, time_steps::UnitRange{Int}; sparse = false, meta = ISOPT.CONTAINER_KEY_EMPTY_META, ) where {T <: TimeSeriesParameter, U <: PSY.Component, V <: PSY.TimeSeriesData} param_key = ParameterKey(T, U, meta) if isabstracttype(V) error("$V can't be abstract: $param_key") end attributes = TimeSeriesAttributes(V, name) return _add_param_container!( container, param_key, attributes, param_axs, multiplier_axs, additional_axs, time_steps; sparse = sparse, ) end function add_param_container!( container::OptimizationContainer, ::T, ::Type{U}, variable_types::Tuple{Vararg{Type}}, sos_variable::SOSStatusVariable = NO_VARIABLE, uses_compact_power::Bool = false, data_type::DataType = Float64, axs...; sparse = false, meta = ISOPT.CONTAINER_KEY_EMPTY_META, ) where {T <: ObjectiveFunctionParameter, U <: PSY.Component} param_key = ParameterKey(T, U, meta) attributes = CostFunctionAttributes{data_type}(variable_types, sos_variable, uses_compact_power) return _add_param_container!(container, param_key, attributes, axs...; sparse = sparse) end function add_param_container!( container::OptimizationContainer, ::T, ::Type{U}, ::Type{V}, axs...; sparse = false, meta = ISOPT.CONTAINER_KEY_EMPTY_META, ) where {T <: EventParameter, U <: PSY.Component, V <: PSY.Contingency} param_key = ParameterKey(T, U, meta) attributes = EventParametersAttributes{V, T}(U[]) return _add_param_container!(container, param_key, attributes, axs...; sparse = sparse) end function add_param_container!( container::OptimizationContainer, ::T, ::Type{U}, source_key::V, axs...; sparse = false, meta = ISOPT.CONTAINER_KEY_EMPTY_META, ) where {T <: VariableValueParameter, U <: PSY.Component, V <: OptimizationContainerKey} param_key = ParameterKey(T, U, meta) attributes = VariableValueAttributes(source_key) return _add_param_container!(container, param_key, attributes, axs...; sparse = sparse) end # FixValue parameters are created using Float64 since we employ JuMP.fix to fix the downstream # variables. function add_param_container!( container::OptimizationContainer, ::T, ::Type{U}, source_key::V, axs...; sparse = false, meta = ISOPT.CONTAINER_KEY_EMPTY_META, ) where {T <: FixValueParameter, U <: PSY.Component, V <: OptimizationContainerKey} if meta == ISOPT.CONTAINER_KEY_EMPTY_META error("$T parameters require passing the VariableType to the meta field") end param_key = ParameterKey(T, U, meta) attributes = VariableValueAttributes(source_key) return _add_param_container!( container, param_key, attributes, Float64, axs...; sparse = sparse, ) end function get_parameter_keys(container::OptimizationContainer) return collect(keys(container.parameters)) end function get_parameter(container::OptimizationContainer, key::ParameterKey) param_container = get(container.parameters, key, nothing) if param_container === nothing name = ISOPT.encode_key(key) throw( IS.InvalidValue( "parameter $name is not stored. $(collect(keys(container.parameters)))", ), ) end return param_container end function get_parameter( container::OptimizationContainer, ::T, ::Type{U}, meta = ISOPT.CONTAINER_KEY_EMPTY_META, ) where {T <: ParameterType, U <: Union{PSY.Component, PSY.System}} return get_parameter(container, ParameterKey(T, U, meta)) end function get_parameter_array(container::OptimizationContainer, key) return get_parameter_array(get_parameter(container, key)) end function get_parameter_array( container::OptimizationContainer, key::ParameterKey{T, U}, ) where {T <: ParameterType, U <: Union{PSY.Component, PSY.System}} return get_parameter_array(get_parameter(container, key)) end function get_parameter_multiplier_array( container::OptimizationContainer, key::ParameterKey{T, U}, ) where {T <: ParameterType, U <: Union{PSY.Component, PSY.System}} return get_multiplier_array(get_parameter(container, key)) end function get_parameter_attributes( container::OptimizationContainer, key::ParameterKey{T, U}, ) where {T <: ParameterType, U <: Union{PSY.Component, PSY.System}} return get_attributes(get_parameter(container, key)) end function get_parameter_array( container::OptimizationContainer, ::T, ::Type{U}, meta = ISOPT.CONTAINER_KEY_EMPTY_META, ) where {T <: ParameterType, U <: Union{PSY.Component, PSY.System}} return get_parameter_array(container, ParameterKey(T, U, meta)) end function get_parameter_multiplier_array( container::OptimizationContainer, ::T, ::Type{U}, meta = ISOPT.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 function get_parameter_attributes( container::OptimizationContainer, ::T, ::Type{U}, meta = ISOPT.CONTAINER_KEY_EMPTY_META, ) where {T <: ParameterType, U <: Union{PSY.Component, PSY.System}} return get_attributes(get_parameter(container, ParameterKey(T, U, meta))) end # Slow implementation not to be used in hot loops function read_parameters(container::OptimizationContainer) params_dict = Dict{ParameterKey, DenseAxisArray}() parameters = get_parameters(container) (parameters === nothing || isempty(parameters)) && return params_dict for (k, v) in parameters # TODO: all functions similar to calculate_parameter_values should be in one # place and be consistent in behavior. #params_dict[k] = to_dataframe(calculate_parameter_values(v)) param_array = get_parameter_values(v) multiplier_array = get_multiplier_array(v) params_dict[k] = _calculate_parameter_values(k, param_array, multiplier_array) end return params_dict end function _calculate_parameter_values( ::ParameterKey{<:ParameterType, <:PSY.Component}, param_array, multiplier_array, ) return param_array .* multiplier_array end function _calculate_parameter_values( ::ParameterKey{<:ObjectiveFunctionParameter, <:PSY.Component}, param_array, multiplier_array, ) return param_array end ##################################### Expression Container ################################# function _add_expression_container!( container::OptimizationContainer, expr_key::ExpressionKey, ::Type{T}, axs...; sparse = false, ) where {T <: JuMP.AbstractJuMPScalar} if sparse expr_container = sparse_container_spec(T, axs...) else expr_container = container_spec(T, axs...) end remove_undef!(expr_container) _assign_container!(container.expressions, expr_key, expr_container) return expr_container end function add_expression_container!( container::OptimizationContainer, ::T, ::Type{U}, axs...; expr_type = GAE, sparse = false, meta = ISOPT.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, expr_type, axs...; sparse = sparse, ) end function add_expression_container!( container::OptimizationContainer, ::T, ::Type{U}, axs...; sparse = false, meta = ISOPT.CONTAINER_KEY_EMPTY_META, ) where {T <: CostExpressions, U <: Union{PSY.Component, PSY.System}} expr_key = ExpressionKey(T, U, meta) expr_type = JuMP.QuadExpr return _add_expression_container!( container, expr_key, expr_type, axs...; sparse = sparse, ) end function get_expression_keys(container::OptimizationContainer) return collect(keys(container.expressions)) end function get_expression(container::OptimizationContainer, key::ExpressionKey) var = get(container.expressions, key, nothing) if var === nothing throw( IS.InvalidValue( "expression $key is not stored. $(collect(keys(container.expressions)))", ), ) end return var end function get_expression( container::OptimizationContainer, ::T, ::Type{U}, meta = ISOPT.CONTAINER_KEY_EMPTY_META, ) where {T <: ExpressionType, U <: Union{PSY.Component, PSY.System}} return get_expression(container, ExpressionKey(T, U, meta)) end function read_expressions(container::OptimizationContainer) return Dict( k => to_dataframe(jump_value.(v), k) for (k, v) in get_expressions(container) if !(get_entry_type(k) <: SystemBalanceExpressions) ) end ###################################Initial Conditions Containers############################ function _add_initial_condition_container!( container::OptimizationContainer, 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)) ini_conds = Vector{ Union{InitialCondition{T, JuMP.VariableRef}, InitialCondition{T, Nothing}}, }( undef, length_devices, ) else ini_conds = Vector{Union{InitialCondition{T, Float64}, InitialCondition{T, Nothing}}}( undef, length_devices, ) end _assign_container!(container.initial_conditions, ic_key, ini_conds) return ini_conds end function add_initial_condition_container!( container::OptimizationContainer, ::T, ::Type{U}, axs; meta = ISOPT.CONTAINER_KEY_EMPTY_META, ) where {T <: InitialConditionType, U <: Union{PSY.Component, PSY.System}} 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 function get_initial_condition( container::OptimizationContainer, ::T, ::Type{D}, ) where {T <: InitialConditionType, D <: PSY.Component} return get_initial_condition(container, InitialConditionKey(T, D)) end 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)")) end return initial_conditions end function get_initial_conditions_keys(container::OptimizationContainer) return collect(keys(container.initial_conditions)) end function write_initial_conditions_data!( container::OptimizationContainer, ic_container::OptimizationContainer, ) for field in STORE_CONTAINERS ic_container_dict = getfield(ic_container, field) if field == STORE_CONTAINER_PARAMETERS ic_container_dict = read_parameters(ic_container) end if field == STORE_CONTAINER_EXPRESSIONS continue end isempty(ic_container_dict) && continue ic_data_dict = getfield(get_initial_conditions_data(container), field) for (key, field_container) in ic_container_dict @debug "Adding $(encode_key_as_string(key)) to InitialConditionsData" _group = LOG_GROUP_SERVICE_CONSTUCTORS if field == STORE_CONTAINER_PARAMETERS ic_data_dict[key] = ic_container_dict[key] else ic_data_dict[key] = jump_value.(field_container) end end end return end # Note: These methods aren't passing the potential meta fields in the keys function get_initial_conditions_variable( container::OptimizationContainer, type::VariableType, ::Type{T}, ) where {T <: Union{PSY.Component, PSY.System}} return get_initial_conditions_variable(get_initial_conditions_data(container), type, T) end function get_initial_conditions_aux_variable( container::OptimizationContainer, type::AuxVariableType, ::Type{T}, ) where {T <: Union{PSY.Component, PSY.System}} return get_initial_conditions_aux_variable( get_initial_conditions_data(container), type, T, ) end function get_initial_conditions_dual( container::OptimizationContainer, type::ConstraintType, ::Type{T}, ) where {T <: Union{PSY.Component, PSY.System}} return get_initial_conditions_dual(get_initial_conditions_data(container), type, T) end function get_initial_conditions_parameter( container::OptimizationContainer, type::ParameterType, ::Type{T}, ) where {T <: Union{PSY.Component, PSY.System}} return get_initial_conditions_parameter(get_initial_conditions_data(container), type, T) end function add_to_objective_invariant_expression!( container::OptimizationContainer, cost_expr::T, ) where {T <: JuMP.AbstractJuMPScalar} T_cf = typeof(container.objective_function.invariant_terms) if T_cf <: JuMP.GenericAffExpr && T <: JuMP.GenericQuadExpr container.objective_function.invariant_terms += cost_expr else JuMP.add_to_expression!(container.objective_function.invariant_terms, cost_expr) end return end function add_to_objective_variant_expression!( container::OptimizationContainer, cost_expr::JuMP.AffExpr, ) JuMP.add_to_expression!(container.objective_function.variant_terms, cost_expr) return end function deserialize_key(container::OptimizationContainer, name::AbstractString) return deserialize_key(container.metadata, name) end function calculate_aux_variables!(container::OptimizationContainer, system::PSY.System) aux_var_keys = keys(get_aux_variables(container)) pf_aux_var_keys = filter(is_from_power_flow ∘ get_entry_type, aux_var_keys) non_pf_aux_var_keys = setdiff(aux_var_keys, pf_aux_var_keys) # We should only have power flow aux vars if we have power flow evaluators @assert isempty(pf_aux_var_keys) || !isempty(get_power_flow_evaluation_data(container)) TimerOutputs.@timeit RUN_SIMULATION_TIMER "Power Flow Evaluation" begin reset_power_flow_is_solved!(container) # Power flow-related aux vars get calculated once per power flow for (i, pf_e_data) in enumerate(get_power_flow_evaluation_data(container)) @debug "Processing power flow $i" solve_power_flow!(pf_e_data, container, system) for key in pf_aux_var_keys calculate_aux_variable_value!(container, key, system) end end end # Other aux vars get calculated once at the end for key in non_pf_aux_var_keys calculate_aux_variable_value!(container, key, system) end return RunStatus.SUCCESSFULLY_FINALIZED end function _calculate_dual_variable_value!( container::OptimizationContainer, key::ConstraintKey{CopperPlateBalanceConstraint, PSY.System}, ::PSY.System, ) constraint_container = get_constraint(container, key) dual_variable_container = get_duals(container)[key] for subnet in axes(constraint_container)[1], t in axes(constraint_container)[2] # See https://jump.dev/JuMP.jl/stable/manual/solutions/#Dual-solution-values dual_variable_container[subnet, t] = jump_value(constraint_container[subnet, t]) end return end function _calculate_dual_variable_value!( container::OptimizationContainer, key::ConstraintKey{T, D}, ::PSY.System, ) where {T <: ConstraintType, D <: Union{PSY.Component, PSY.System}} constraint_duals = jump_value.(get_constraint(container, key)) dual_variable_container = get_duals(container)[key] # Needs to loop since the container ordering might not match in the DenseAxisArray for index in Iterators.product(axes(constraint_duals)...) dual_variable_container[index...] = constraint_duals[index...] end return end function _calculate_dual_variables_continous_model!( container::OptimizationContainer, system::PSY.System, ) duals_vars = get_duals(container) for key in keys(duals_vars) _calculate_dual_variable_value!(container, key, system) end return RunStatus.SUCCESSFULLY_FINALIZED end function _calculate_dual_variables_discrete_model!( container::OptimizationContainer, ::PSY.System, ) return process_duals(container, container.settings.optimizer) end function calculate_dual_variables!( container::OptimizationContainer, sys::PSY.System, is_milp::Bool, ) isempty(get_duals(container)) && return RunStatus.SUCCESSFULLY_FINALIZED if is_milp status = _calculate_dual_variables_discrete_model!(container, sys) else status = _calculate_dual_variables_continous_model!(container, sys) end return end ########################### Helper Functions to get keys ################################### function get_optimization_container_key( ::T, ::Type{U}, meta::String, ) where {T <: AuxVariableType, U <: PSY.Component} return AuxVarKey(T, U, meta) end function get_optimization_container_key( ::T, ::Type{U}, meta::String, ) where {T <: VariableType, U <: PSY.Component} return VariableKey(T, U, meta) end function get_optimization_container_key( ::T, ::Type{U}, meta::String, ) where {T <: ParameterType, U <: PSY.Component} return ParameterKey(T, U, meta) end function get_optimization_container_key( ::T, ::Type{U}, meta::String, ) where {T <: ConstraintType, U <: PSY.Component} return ConstraintKey(T, U, meta) end function lazy_container_addition!( container::OptimizationContainer, var::T, ::Type{U}, axs...; kwargs..., ) where {T <: VariableType, U <: Union{PSY.Component, PSY.System}} if !has_container_key(container, T, U) var_container = add_variable_container!(container, var, U, axs...; kwargs...) else var_container = get_variable(container, var, U) end return var_container end function lazy_container_addition!( container::OptimizationContainer, constraint::T, ::Type{U}, axs...; kwargs..., ) where {T <: ConstraintType, U <: Union{PSY.Component, PSY.System}} meta = get(kwargs, :meta, ISOPT.CONTAINER_KEY_EMPTY_META) if !has_container_key(container, T, U, meta) cons_container = add_constraints_container!(container, constraint, U, axs...; kwargs...) else cons_container = get_constraint(container, constraint, U, meta) end return cons_container end function lazy_container_addition!( container::OptimizationContainer, expression::T, ::Type{U}, axs...; kwargs..., ) where {T <: ExpressionType, U <: Union{PSY.Component, PSY.System}} meta = get(kwargs, :meta, IS.Optimization.CONTAINER_KEY_EMPTY_META) if !has_container_key(container, T, U, meta) expr_container = add_expression_container!(container, expression, U, axs...; kwargs...) else expr_container = get_expression(container, expression, U, meta) end return expr_container end function get_time_series_initial_values!( container::OptimizationContainer, ::Type{T}, component::PSY.Component, time_series_name::AbstractString; interval::Dates.Millisecond = UNSET_INTERVAL, resolution::Dates.Millisecond = UNSET_RESOLUTION, ) where {T <: PSY.TimeSeriesData} initial_time = get_initial_time(container) time_steps = get_time_steps(container) forecast = PSY.get_time_series( T, component, time_series_name; start_time = initial_time, count = 1, interval = _to_is_interval(interval), resolution = _to_is_resolution(resolution), ) ts_values = IS.get_time_series_values( component, forecast; start_time = initial_time, len = length(time_steps), ignore_scaling_factors = true, ) return ts_values end """ Get the column names for the specified container in the OptimizationContainer. # Arguments - `container::OptimizationContainer`: The optimization container. - `field::Symbol`: The field for which to retrieve the column names. - `key::OptimizationContainerKey`: The key for which to retrieve the column names. # Returns - `Tuple`: Tuple of Vector{String}. """ function get_column_names( ::OptimizationContainer, field::Symbol, subcontainer, key::OptimizationContainerKey, ) return if field == :parameters # Parameters are stored in ParameterContainer. get_column_names(key, subcontainer) else # The others are in DenseAxisArrays. get_column_names_from_axis_array(key, subcontainer) end end lookup_value(container::OptimizationContainer, key::VariableKey) = get_variable(container, key) lookup_value(container::OptimizationContainer, key::ParameterKey) = calculate_parameter_values(get_parameter(container, key)) lookup_value(container::OptimizationContainer, key::AuxVarKey) = get_aux_variable(container, key) lookup_value(container::OptimizationContainer, key::ExpressionKey) = get_expression(container, key) lookup_value(container::OptimizationContainer, key::ConstraintKey) = get_constraint(container, key) ================================================ FILE: src/core/parameters.jl ================================================ abstract type ParameterAttributes end struct NoAttributes end struct TimeSeriesAttributes{T <: PSY.TimeSeriesData} <: ParameterAttributes name::String multiplier_id::Base.RefValue{Int} component_name_to_ts_uuid::Dict{String, String} subsystem::Base.RefValue{String} end function TimeSeriesAttributes( ::Type{T}, name::String, multiplier_id::Int = 1, component_name_to_ts_uuid = Dict{String, String}(), ) where {T <: PSY.TimeSeriesData} return TimeSeriesAttributes{T}( name, Base.RefValue{Int}(multiplier_id), component_name_to_ts_uuid, Base.RefValue{String}(""), ) end get_time_series_type(::TimeSeriesAttributes{T}) where {T <: PSY.TimeSeriesData} = T get_time_series_name(attr::TimeSeriesAttributes) = attr.name get_time_series_multiplier_id(attr::TimeSeriesAttributes) = attr.multiplier_id[] get_subsystem(attr::TimeSeriesAttributes) = attr.subsystem[] function set_subsystem!(attr::TimeSeriesAttributes, val::String) attr.subsystem[] = val return end set_subsystem!(::TimeSeriesAttributes, ::Nothing) = nothing function add_component_name!(attr::TimeSeriesAttributes, name::String, uuid::String) if haskey(attr.component_name_to_ts_uuid, name) throw(ArgumentError("$name is already stored")) end attr.component_name_to_ts_uuid[name] = uuid return end get_component_names(attr::TimeSeriesAttributes) = keys(attr.component_name_to_ts_uuid) function _get_ts_uuid(attr::TimeSeriesAttributes, name::String) if !haskey(attr.component_name_to_ts_uuid, name) throw( ArgumentError( "No time series UUID found for in attributes for component $name: available names are $(keys(attr.component_name_to_ts_uuid))", ), ) end return attr.component_name_to_ts_uuid[name] end struct VariableValueAttributes{T <: OptimizationContainerKey} <: ParameterAttributes attribute_key::T affected_keys::Set end function VariableValueAttributes(key::T) where {T <: OptimizationContainerKey} return VariableValueAttributes{T}(key, Set()) end get_attribute_key(attr::VariableValueAttributes) = attr.attribute_key struct CostFunctionAttributes{T} <: ParameterAttributes variable_types::Tuple{Vararg{Type}} sos_status::SOSStatusVariable uses_compact_power::Bool end get_sos_status(attr::CostFunctionAttributes) = attr.sos_status get_variable_types(attr::CostFunctionAttributes) = attr.variable_types get_uses_compact_power(attr::CostFunctionAttributes) = attr.uses_compact_power struct EventParametersAttributes{T <: PSY.Outage, U <: ParameterType} <: ParameterAttributes affected_devices::Vector{<:PSY.Component} end function get_param_type( ::EventParametersAttributes{T, U}, ) where {T <: PSY.Outage, U <: ParameterType} return U end struct ParameterContainer{T <: AbstractArray, U <: AbstractArray} attributes::ParameterAttributes parameter_array::T multiplier_array::U end function ParameterContainer(parameter_array, multiplier_array) return ParameterContainer(NoAttributes(), parameter_array, multiplier_array) end function calculate_parameter_values(container::ParameterContainer) return calculate_parameter_values( container.attributes, container.parameter_array, container.multiplier_array, ) end function calculate_parameter_values( attributes::ParameterAttributes, param_array::DenseAxisArray, multiplier_array::DenseAxisArray, ) return get_parameter_values(attributes, param_array, multiplier_array) .* multiplier_array end function calculate_parameter_values( ::ParameterAttributes, param_array::SparseAxisArray, multiplier_array::SparseAxisArray, ) p_array = jump_value.(to_matrix(param_array)) m_array = to_matrix(multiplier_array) return p_array .* m_array end function get_parameter_column_refs(container::ParameterContainer, column::AbstractString) return get_parameter_column_refs( container.attributes, container.parameter_array, column, ) end function get_parameter_column_refs(::ParameterAttributes, param_array, column) return param_array end function get_parameter_column_refs( attributes::TimeSeriesAttributes{T}, param_array::DenseAxisArray, column, ) where {T <: PSY.TimeSeriesData} expand_ixs((_get_ts_uuid(attributes, column),), param_array) return param_array[expand_ixs((_get_ts_uuid(attributes, column),), param_array)...] end function get_parameter_column_values(container::ParameterContainer, column::AbstractString) return jump_value.(get_parameter_column_refs(container, column)) end function get_parameter_values(container::ParameterContainer) return get_parameter_values( container.attributes, container.parameter_array, container.multiplier_array, ) end # TODO: SparseAxisArray versions of these functions function get_parameter_values( ::ParameterAttributes, param_array::DenseAxisArray, multiplier_array::DenseAxisArray, ) return (.*).(jump_value.(param_array), multiplier_array) end function get_parameter_values( attr::EventParametersAttributes, param_array::DenseAxisArray, multiplier_array::DenseAxisArray, ) return jump_value.(param_array) end function get_parameter_values( attributes::TimeSeriesAttributes{T}, param_array::DenseAxisArray, multiplier_array::DenseAxisArray, ) where {T <: PSY.TimeSeriesData} exploded_param_array = DenseAxisArray{Float64}(undef, axes(multiplier_array)...) for name in axes(multiplier_array)[1] param_col = param_array[_get_ts_uuid(attributes, name), axes(param_array)[2:end]...] device_axes = axes(multiplier_array)[2:end] exploded_param_array[name, device_axes...] = jump_value.(param_col) end return exploded_param_array end get_parameter_array(c::ParameterContainer) = c.parameter_array # Underlying dense storage of the parameter array. `parent` on a JuMP `DenseAxisArray` # returns the array itself, so reach for `.data` directly to bypass the axis-keyed lookup. get_parameter_array_data(c::ParameterContainer) = get_parameter_array(c).data get_multiplier_array(c::ParameterContainer) = c.multiplier_array # Same shortcut for the multiplier array — used by the integer-indexed fast path. get_multiplier_array_data(c::ParameterContainer) = get_multiplier_array(c).data get_attributes(c::ParameterContainer) = c.attributes Base.length(c::ParameterContainer) = length(c.parameter_array) Base.size(c::ParameterContainer) = size(c.parameter_array) function get_column_names(key::ParameterKey, c::ParameterContainer) return get_column_names_from_axis_array(key, get_multiplier_array(c)) end const ValidDataParamEltypes = Union{Float64, Tuple{Vararg{Float64}}} function _set_parameter!( array::AbstractArray{T}, ::JuMP.Model, value::Union{T, AbstractVector{T}}, ixs::Tuple, ) where {T <: ValidDataParamEltypes} assign_maybe_broadcast!(array, value, ixs) return end function _set_parameter!( array::AbstractArray{JuMP.VariableRef}, model::JuMP.Model, value::Union{T, AbstractVector{T}}, ixs::Tuple, ) where {T <: ValidDataParamEltypes} assign_maybe_broadcast!(array, add_jump_parameter.(Ref(model), value), ixs) return end function _set_parameter!( array::SparseAxisArray{Union{Nothing, JuMP.VariableRef}}, model::JuMP.Model, value::Union{T, AbstractVector{T}}, ixs::Tuple, ) where {T <: ValidDataParamEltypes} assign_maybe_broadcast!(array, add_jump_parameter.(Ref(model), value), ixs) return end function set_multiplier!(container::ParameterContainer, multiplier::Float64, ixs...) assign_maybe_broadcast!(get_multiplier_array(container), multiplier, ixs) return end function set_parameter!( container::ParameterContainer, jump_model::JuMP.Model, parameter::Union{ValidDataParamEltypes, AbstractVector{<:ValidDataParamEltypes}}, ixs..., ) param_array = get_parameter_array(container) _set_parameter!(param_array, jump_model, parameter, ixs) return end # Overload for when a JuMP parameter VariableRef is passed directly (recurrent-solve # path where a parallel branch type reuses the VariableRef created by the first type). function set_parameter!( container::ParameterContainer, ::JuMP.Model, parameter::JuMP.VariableRef, ixs..., ) assign_maybe_broadcast!(get_parameter_array(container), parameter, ixs) return end # Fast-path setters that skip DenseAxisArray's string-keyed axis lookup. Callers pass # `get_parameter_array_data(container)` once, then write into the underlying Array # by integer indices. The (i, t) layout matches the canonical (component, time) axis # order produced by `add_param_container!`. # # 2D scalar path: covers Float64 and Tuple{Vararg{Float64}} eltypes (the latter is # used by piecewise-cost MarketBid parameters whose storage is a Matrix of tuples). @inline function _set_parameter_at!( parent_param::Array{T, 2}, ::JuMP.Model, value::T, i::Int, t::Int, ) where {T <: ValidDataParamEltypes} parent_param[i, t] = value return end # 2D recurrent-rebuild paths: param storage is `Array{JuMP.VariableRef, 2}`. Either we # need a fresh JuMP parameter (Float64 input) or we reuse one created by an earlier # parallel branch type (VariableRef input). @inline function _set_parameter_at!( parent_param::Array{JuMP.VariableRef, 2}, jump_model::JuMP.Model, value::Float64, i::Int, t::Int, ) parent_param[i, t] = add_jump_parameter(jump_model, value) return end @inline function _set_parameter_at!( parent_param::Array{JuMP.VariableRef, 2}, ::JuMP.Model, parameter::JuMP.VariableRef, i::Int, t::Int, ) parent_param[i, t] = parameter return end # 3D fast paths (parameter container with a middle additional axis, e.g. piecewise # tranches). The supplied `value` is a length-(size(parent_param, 2)) vector that fills # the middle axis at position (i, :, t). Eltype constrained to `ValidDataParamEltypes` # so tuples-of-floats are also accepted (piecewise breakpoint storage). @inline function _set_parameter_at!( parent_param::Array{T, 3}, ::JuMP.Model, value::AbstractVector{<:T}, i::Int, t::Int, ) where {T <: ValidDataParamEltypes} @views parent_param[i, :, t] .= value return end @inline function _set_parameter_at!( parent_param::Array{JuMP.VariableRef, 3}, jump_model::JuMP.Model, value::AbstractVector{Float64}, i::Int, t::Int, ) for k in 1:size(parent_param, 2) parent_param[i, k, t] = add_jump_parameter(jump_model, value[k]) end return end # Fast-path setters for the multiplier array, mirroring `_set_parameter_at!`. # Multipliers are always Float64-valued (or tuples-of-floats for piecewise # parameters), so a single typed family covers every call site. Callers should # hoist `parent_mult = get_multiplier_array_data(parameter_container)` once # above the device loop and pass the integer device row index. # 2D row fill: assigns `value` across the whole time slice for component `i` # (the canonical pattern at parameter-creation time, where the multiplier is # constant per device). @inline function _set_multiplier_at!( parent_mult::Array{T, 2}, value::T, i::Int, ) where {T <: ValidDataParamEltypes} @views parent_mult[i, :] .= value return end # 2D scalar write at a single (component, time) cell. @inline function _set_multiplier_at!( parent_mult::Array{T, 2}, value::T, i::Int, t::Int, ) where {T <: ValidDataParamEltypes} parent_mult[i, t] = value return end # 3D row fill: assigns `value` across all (tranche, time) for component `i`. @inline function _set_multiplier_at!( parent_mult::Array{T, 3}, value::T, i::Int, ) where {T <: ValidDataParamEltypes} @views parent_mult[i, :, :] .= value return end # 3D point write at a single (component, tranche, time) cell. @inline function _set_multiplier_at!( parent_mult::Array{T, 3}, value::T, i::Int, j::Int, t::Int, ) where {T <: ValidDataParamEltypes} parent_mult[i, j, t] = value return end # Fast-path setters for the simulation-step parameter-VALUE update path. # Used by `_update_parameter_values!` and `_fix_parameter_value!` overloads # where the parameter container already exists and we are pushing new values # into it from upstream model results or time series. Caller hoists # `parent_param = get_parameter_array_data(parameter_container)` (and an # optional `parent_mult` / `parent_var`) above the device loop, then writes # by integer (i, t) — bypassing DenseAxisArray's string-keyed axis lookup. # # For Float64-typed storage we write directly; for `JuMP.VariableRef` storage # we update the JuMP parameter's bound via `JuMP.fix(...; force=true)`. @inline function _set_param_value_at!( parent_param::Array{T, 2}, value::T, i::Int, t::Int, ) where {T <: ValidDataParamEltypes} @inbounds parent_param[i, t] = value return end @inline function _set_param_value_at!( parent_param::Array{JuMP.VariableRef, 2}, value::Float64, i::Int, t::Int, ) @inbounds JuMP.fix(parent_param[i, t], value; force = true) return end # 3D paths for piecewise-tranche updates. @inline function _set_param_value_at!( parent_param::Array{T, 3}, value::AbstractVector{<:T}, i::Int, t::Int, ) where {T <: ValidDataParamEltypes} @inbounds @views parent_param[i, :, t] .= value return end @inline function _set_param_value_at!( parent_param::Array{JuMP.VariableRef, 3}, value::AbstractVector{Float64}, i::Int, t::Int, ) @inbounds for k in 1:size(parent_param, 2) JuMP.fix(parent_param[i, k, t], value[k]; force = true) end return end """ Parameter to define active power time series """ struct ActivePowerTimeSeriesParameter <: TimeSeriesParameter end """ Parameter to define reactive power time series """ struct ReactivePowerTimeSeriesParameter <: TimeSeriesParameter end """ Parameter to define active power out time series """ struct ActivePowerOutTimeSeriesParameter <: TimeSeriesParameter end """ Parameter to define active power in time series """ struct ActivePowerInTimeSeriesParameter <: TimeSeriesParameter end """ Parameter to define active power in time series """ struct ShiftUpActivePowerTimeSeriesParameter <: TimeSeriesParameter end """ Parameter to define active power in time series """ struct ShiftDownActivePowerTimeSeriesParameter <: TimeSeriesParameter end """ Parameter to define requirement time series """ struct RequirementTimeSeriesParameter <: TimeSeriesParameter end """ Abstract type for dynamic ratings of AC branches """ abstract type AbstractDynamicBranchRatingTimeSeriesParameter <: TimeSeriesParameter end """ Parameter to define the dynamic rating time series of a branch """ struct DynamicBranchRatingTimeSeriesParameter <: AbstractDynamicBranchRatingTimeSeriesParameter end """ Parameter to define the dynamic ratings time series of an AC branch for post-contingency condition """ struct PostContingencyDynamicBranchRatingTimeSeriesParameter <: AbstractDynamicBranchRatingTimeSeriesParameter 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 """ Parameter to define variable upper bound """ struct UpperBoundValueParameter <: VariableValueParameter end """ Parameter to define variable lower bound """ struct LowerBoundValueParameter <: VariableValueParameter end """ Parameter to define unit commitment status updated from the system state """ struct OnStatusParameter <: VariableValueParameter end """ Parameter to FixValueParameter """ struct FixValueParameter <: VariableValueParameter end """ Parameter to define cost function coefficient """ struct CostFunctionParameter <: ObjectiveFunctionParameter end """ Parameter to define fuel cost time series """ struct FuelCostParameter <: ObjectiveFunctionParameter end "Parameter to define startup cost time series" struct StartupCostParameter <: ObjectiveFunctionParameter end "Parameter to define shutdown cost time series" struct ShutdownCostParameter <: ObjectiveFunctionParameter end "Parameters to define the cost at the minimum available power" abstract type AbstractCostAtMinParameter <: ObjectiveFunctionParameter end "[`AbstractCostAtMinParameter`](@ref) for the incremental case (power source)" struct IncrementalCostAtMinParameter <: AbstractCostAtMinParameter end "[`AbstractCostAtMinParameter`](@ref) for the decremental case (power sink)" struct DecrementalCostAtMinParameter <: AbstractCostAtMinParameter end "Parameters to define the slopes of a piecewise linear cost function" abstract type AbstractPiecewiseLinearSlopeParameter <: ObjectiveFunctionParameter end "[`AbstractPiecewiseLinearSlopeParameter`](@ref) for the incremental case (power source)" struct IncrementalPiecewiseLinearSlopeParameter <: AbstractPiecewiseLinearSlopeParameter end "[`AbstractPiecewiseLinearSlopeParameter`](@ref) for the decremental case (power sink)" struct DecrementalPiecewiseLinearSlopeParameter <: AbstractPiecewiseLinearSlopeParameter end "Parameters to define the breakpoints of a piecewise linear function" abstract type AbstractPiecewiseLinearBreakpointParameter <: TimeSeriesParameter end "[`AbstractPiecewiseLinearBreakpointParameter`](@ref) for the incremental case (power source)" struct IncrementalPiecewiseLinearBreakpointParameter <: AbstractPiecewiseLinearBreakpointParameter end "[`AbstractPiecewiseLinearBreakpointParameter`](@ref) for the decremental case (power sink)" struct DecrementalPiecewiseLinearBreakpointParameter <: AbstractPiecewiseLinearBreakpointParameter end abstract type AuxVariableValueParameter <: RightHandSideParameter end abstract type EventParameter <: ParameterType end """ Parameter to define component availability status updated from the system state """ struct AvailableStatusParameter <: EventParameter end """ Parameter to define active power offset during an event. """ struct ActivePowerOffsetParameter <: EventParameter end """ Parameter to define reactive power offset during an event. """ struct ReactivePowerOffsetParameter <: EventParameter end """ Parameter to record that the component changed in the availability status """ struct AvailableStatusChangeCountdownParameter <: EventParameter end should_write_resulting_value(::Type{<:RightHandSideParameter}) = true should_write_resulting_value(::Type{<:EventParameter}) = true should_write_resulting_value(::Type{<:FuelCostParameter}) = true should_write_resulting_value(::Type{<:ShutdownCostParameter}) = true should_write_resulting_value(::Type{<:AbstractCostAtMinParameter}) = true should_write_resulting_value(::Type{<:AbstractPiecewiseLinearSlopeParameter}) = true should_write_resulting_value(::Type{<:AbstractPiecewiseLinearBreakpointParameter}) = true convert_result_to_natural_units(::Type{DynamicBranchRatingTimeSeriesParameter}) = true convert_result_to_natural_units( ::Type{PostContingencyDynamicBranchRatingTimeSeriesParameter}, ) = true convert_result_to_natural_units(::Type{ActivePowerTimeSeriesParameter}) = true convert_result_to_natural_units(::Type{ReactivePowerTimeSeriesParameter}) = true convert_result_to_natural_units(::Type{ActivePowerOutTimeSeriesParameter}) = true convert_result_to_natural_units(::Type{ActivePowerInTimeSeriesParameter}) = true convert_result_to_natural_units(::Type{RequirementTimeSeriesParameter}) = true convert_result_to_natural_units(::Type{UpperBoundValueParameter}) = true convert_result_to_natural_units(::Type{LowerBoundValueParameter}) = true convert_result_to_natural_units(::Type{ShiftUpActivePowerTimeSeriesParameter}) = true convert_result_to_natural_units(::Type{ShiftDownActivePowerTimeSeriesParameter}) = true ================================================ FILE: src/core/power_flow_data_wrapper.jl ================================================ mutable struct PowerFlowEvaluationData{T <: PFS.PowerFlowContainer} power_flow_data::T """ Records which PSI keys are read as input to the power flow and how the data are mapped. The Symbol is a category of data: `:active_power`, `:reactive_power`, etc. The `OptimizationContainerKey` is a source of that data in the `OptimizationContainer`. For `PowerFlowData`, leaf values are `Dict{String, Int64}` mapping component name to matrix index of bus; for `SystemPowerFlowContainer`, leaf values are Dict{Union{String, Int64}, Union{String, Int64}} mapping component name/bus number to component name/bus number. """ input_key_map::Dict{Symbol, <:Dict{<:OptimizationContainerKey, <:Any}} is_solved::Bool end check_network_reduction(::PFS.SystemPowerFlowContainer) = nothing function check_network_reduction(pfd::PFS.PowerFlowData) nrd = PFS.get_network_reduction_data(pfd) if !isempty(PNM.get_reductions(nrd)) throw( IS.NotImplementedError( "Power flow in-the-loop on reduced networks isn't supported. Network " * "reductions of types $(PNM.get_reductions(nrd)) present.", ), ) end return end function PowerFlowEvaluationData(power_flow_data::T) where {T <: PFS.PowerFlowContainer} check_network_reduction(power_flow_data) return PowerFlowEvaluationData{T}( power_flow_data, Dict{Symbol, Dict{OptimizationContainerKey, <:Any}}(), false, ) end get_power_flow_data(ped::PowerFlowEvaluationData) = ped.power_flow_data get_input_key_map(ped::PowerFlowEvaluationData) = ped.input_key_map ================================================ FILE: src/core/results_by_time.jl ================================================ mutable struct ResultsByTime{T, N} key::OptimizationContainerKey data::SortedDict{Dates.DateTime, T} resolution::Dates.Period column_names::NTuple{N, Vector{String}} end function ResultsByTime( key::OptimizationContainerKey, data::SortedDict{Dates.DateTime, T}, resolution::Dates.Period, column_names, ) where {T} _check_column_consistency(data, column_names) ResultsByTime(key, data, resolution, column_names) end function _check_column_consistency( data::SortedDict{Dates.DateTime, DenseAxisArray{Float64, 2}}, cols::Tuple{Vector{String}}, ) for val in values(data) if axes(val)[1] != cols[1] error("Mismatch in DenseAxisArray column names: $(axes(val)[1]) $cols") end end end function _check_column_consistency( data::SortedDict{Dates.DateTime, Matrix{Float64}}, cols::Tuple{Vector{String}}, ) for val in values(data) if size(val)[2] != length(cols[1]) error( "Mismatch in length of Matrix columns: $(size(val)[2]) $(length(cols[1]))", ) end end end function _check_column_consistency( data::SortedDict{Dates.DateTime, DenseAxisArray{Float64, 2}}, cols::NTuple{N, Vector{String}}, ) where {N} # TODO: end function _check_column_consistency( data::SortedDict{Dates.DateTime, DataFrame}, cols::NTuple{N, Vector{String}}, ) where {N} for df in values(data) if DataFrames.ncol(df) != length(cols[1]) error( "Mismatch in length of DataFrame columns: $(DataFrames.ncol(df)) $(length(cols[1]))", ) end end end # TODO: Implement consistency check for other sizes # This struct behaves like a dict, delegating to its 'data' field. Base.length(res::ResultsByTime) = length(res.data) Base.iterate(res::ResultsByTime) = iterate(res.data) Base.iterate(res::ResultsByTime, state) = iterate(res.data, state) Base.getindex(res::ResultsByTime, i) = getindex(res.data, i) Base.setindex!(res::ResultsByTime, v, i) = setindex!(res.data, v, i) Base.firstindex(res::ResultsByTime) = firstindex(res.data) Base.lastindex(res::ResultsByTime) = lastindex(res.data) get_column_names(x::ResultsByTime) = x.column_names get_num_rows(::ResultsByTime{DenseAxisArray{Float64, 2}}, data) = size(data, 2) get_num_rows(::ResultsByTime{DenseAxisArray{Float64, 3}}, data) = size(data, 3) get_num_rows(::ResultsByTime{Matrix{Float64}}, data) = size(data, 1) get_num_rows(::ResultsByTime{DataFrame}, data) = DataFrames.nrow(data) function _add_timestamps!( df::DataFrames.DataFrame, results::ResultsByTime, timestamp::Dates.DateTime, data, ) time_col = _get_timestamps(results, timestamp, get_num_rows(results, data)) if !isnothing(time_col) DataFrames.insertcols!(df, 1, :DateTime => time_col) end return end function _get_timestamps(results::ResultsByTime, timestamp::Dates.DateTime, len::Int) if results.resolution == Dates.Period(Dates.Millisecond(0)) return nothing end return range(timestamp; length = len, step = results.resolution) end function make_dataframe( results::ResultsByTime{DenseAxisArray{Float64, 2}}, timestamp::Dates.DateTime; table_format::TableFormat = TableFormat.LONG, ) array = results.data[timestamp] timestamps = _get_timestamps(results, timestamp, get_num_rows(results, array)) return to_results_dataframe(array, timestamps, Val(table_format)) end function make_dataframe( results::ResultsByTime{DenseAxisArray{Float64, 3}}, timestamp::Dates.DateTime; table_format::TableFormat = TableFormat.LONG, ) array = results.data[timestamp] num_timestamps = get_num_rows(results, array) timestamps = _get_timestamps(results, timestamp, num_timestamps) return to_results_dataframe(array, timestamps, Val(table_format)) end function make_dataframe( results::ResultsByTime{Matrix{Float64}}, timestamp::Dates.DateTime; table_format::TableFormat = TableFormat.LONG, ) array = results.data[timestamp] df_wide = DataFrames.DataFrame(array, results.column_names) _add_timestamps!(df_wide, results, timestamp, array) return if table_format == TableFormat.LONG measure_vars = [x for x in names(df_wide) if x != "DateTime"] DataFrames.stack( df_wide, measure_vars; variable_name = :name, value_name = :value, ) elseif table_format == TableFormat.WIDE df_wide else error("Unsupported table format: $table_format") end end function make_dataframes(results::ResultsByTime; table_format::TableFormat = table_format) return SortedDict( k => make_dataframe(results, k; table_format = table_format) for k in keys(results.data) ) end struct ResultsByKeyAndTime "Contains all keys stored in the model." result_keys::Vector{OptimizationContainerKey} "Contains the results that have been read from the store and cached." cached_results::Dict{OptimizationContainerKey, ResultsByTime} end ResultsByKeyAndTime(result_keys) = ResultsByKeyAndTime( collect(result_keys), Dict{OptimizationContainerKey, ResultsByTime}(), ) Base.empty!(res::ResultsByKeyAndTime) = empty!(res.cached_results) ================================================ FILE: src/core/service_model.jl ================================================ function _check_service_formulation( ::Type{D}, ) where {D <: Union{AbstractServiceFormulation, PSY.Service}} if !isconcretetype(D) throw( ArgumentError( "The service model must contain only concrete types, $(D) is an Abstract Type", ), ) end end """ Establishes the model for a particular services specified by type. Uses the keyword argument `use_service_name` to assign the model to a service with the same name as the name in the template. Uses the keyword argument feedforward to enable passing values between operation model at simulation time # Arguments -`::Type{D}`: Power System Service Type -`::Type{B}`: Abstract Service Formulation # Accepted Key Words - `feedforward::Array{<:AbstractAffectFeedforward}` : use to pass parameters between models - `use_service_name::Bool` : use the name as the name for the service # Example reserves = ServiceModel(PSY.VariableReserve{PSY.ReserveUp}, RangeReserve) """ mutable struct ServiceModel{D <: PSY.Service, B <: AbstractServiceFormulation} feedforwards::Vector{<:AbstractAffectFeedforward} service_name::String use_slacks::Bool duals::Vector{DataType} time_series_names::Dict{Type{<:TimeSeriesParameter}, String} attributes::Dict{String, Any} contributing_devices_map::Dict{Type{<:PSY.Component}, Vector{<:PSY.Component}} subsystem::Union{Nothing, String} function ServiceModel( ::Type{D}, ::Type{B}, service_name::String; use_slacks = false, feedforwards = Vector{AbstractAffectFeedforward}(), duals = Vector{DataType}(), time_series_names = get_default_time_series_names(D, B), attributes = Dict{String, Any}(), contributing_devices_map = Dict{Type{<:PSY.Component}, Vector{<:PSY.Component}}(), ) where {D <: PSY.Service, B <: AbstractServiceFormulation} attributes_for_model = get_default_attributes(D, B) for (k, v) in attributes attributes_for_model[k] = v end _check_service_formulation(D) _check_service_formulation(B) new{D, B}( feedforwards, service_name, use_slacks, duals, time_series_names, attributes_for_model, contributing_devices_map, nothing, ) end end get_component_type( ::ServiceModel{D, B}, ) where {D <: PSY.Service, B <: AbstractServiceFormulation} = D get_formulation( ::ServiceModel{D, B}, ) where {D <: PSY.Service, B <: AbstractServiceFormulation} = B get_feedforwards(m::ServiceModel) = m.feedforwards get_service_name(m::ServiceModel) = m.service_name get_use_slacks(m::ServiceModel) = m.use_slacks get_duals(m::ServiceModel) = m.duals get_time_series_names(m::ServiceModel) = m.time_series_names get_attributes(m::ServiceModel) = m.attributes get_attribute(m::ServiceModel, key::String) = get(m.attributes, key, nothing) get_contributing_devices_map(m::ServiceModel) = m.contributing_devices_map get_contributing_devices_map(m::ServiceModel, key) = get(m.contributing_devices_map, key, nothing) get_contributing_devices(m::ServiceModel) = [z for x in values(m.contributing_devices_map) for z in x] get_subsystem(m::ServiceModel) = m.subsystem set_subsystem!(m::ServiceModel, id::String) = m.subsystem = id function ServiceModel( service_type::Type{D}, formulation_type::Type{B}; use_slacks = false, feedforwards = Vector{AbstractAffectFeedforward}(), duals = Vector{DataType}(), time_series_names = get_default_time_series_names(D, B), attributes = get_default_attributes(D, B), ) where {D <: PSY.Service, B <: AbstractServiceFormulation} # If more attributes are used later, move free form string to const and organize # attributes attributes_for_model = get_default_attributes(D, B) for (k, v) in attributes attributes_for_model[k] = v end if !haskey(attributes_for_model, "aggregated_service_model") push!(attributes_for_model, "aggregated_service_model" => true) end return ServiceModel( service_type, formulation_type, NO_SERVICE_NAME_PROVIDED; use_slacks, feedforwards, duals, time_series_names, attributes = attributes_for_model, ) end function _set_model!(dict::Dict, key::Tuple{String, Symbol}, model::ServiceModel) if haskey(dict, key) @warn "Overwriting $(key) existing model" end dict[key] = model return end function _set_model!( dict::Dict, model::ServiceModel{D, B}, ) where {D <: PSY.Service, B <: AbstractServiceFormulation} _set_model!(dict, (get_service_name(model), Symbol(D)), model) return end ================================================ FILE: src/core/settings.jl ================================================ struct Settings horizon::Base.RefValue{Dates.Millisecond} resolution::Base.RefValue{Dates.Millisecond} interval::Base.RefValue{Dates.Millisecond} time_series_cache_size::Int warm_start::Base.RefValue{Bool} initial_time::Base.RefValue{Dates.DateTime} optimizer::Union{Nothing, MOI.OptimizerWithAttributes} direct_mode_optimizer::Bool optimizer_solve_log_print::Bool detailed_optimizer_stats::Bool calculate_conflict::Bool check_components::Bool initialize_model::Bool initialization_file::String deserialize_initial_conditions::Bool export_pwl_vars::Bool allow_fails::Bool rebuild_model::Bool export_optimization_model::Bool store_variable_names::Bool check_numerical_bounds::Bool ext::Dict{String, Any} end function Settings( sys; initial_time::Dates.DateTime = UNSET_INI_TIME, time_series_cache_size::Int = IS.TIME_SERIES_CACHE_SIZE_BYTES, warm_start::Bool = true, horizon::Dates.Period = UNSET_HORIZON, resolution::Dates.Period = UNSET_RESOLUTION, interval::Dates.Period = UNSET_INTERVAL, optimizer = nothing, direct_mode_optimizer::Bool = false, optimizer_solve_log_print::Bool = false, detailed_optimizer_stats::Bool = false, calculate_conflict::Bool = false, check_components::Bool = true, initialize_model::Bool = true, initialization_file = "", deserialize_initial_conditions::Bool = false, export_pwl_vars::Bool = false, allow_fails::Bool = false, check_numerical_bounds = true, rebuild_model = false, export_optimization_model = false, store_variable_names = false, ext = Dict{String, Any}(), ) 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 if isa(optimizer, MOI.OptimizerWithAttributes) || optimizer === nothing optimizer_ = optimizer elseif isa(optimizer, DataType) optimizer_ = MOI.OptimizerWithAttributes(optimizer) else error( "The provided input for optimizer is invalid. Provide a JuMP.OptimizerWithAttributes object or a valid Optimizer constructor (e.g. HiGHS.Optimizer).", ) end return Settings( Ref(IS.time_period_conversion(horizon)), Ref(IS.time_period_conversion(resolution)), Ref(IS.time_period_conversion(interval)), time_series_cache_size, Ref(warm_start), Ref(initial_time), optimizer_, direct_mode_optimizer, optimizer_solve_log_print, detailed_optimizer_stats, calculate_conflict, check_components, initialize_model, initialization_file, deserialize_initial_conditions, export_pwl_vars, allow_fails, rebuild_model, export_optimization_model, store_variable_names, check_numerical_bounds, ext, ) end function log_values(settings::Settings) text = Vector{String}() for (name, type) in zip(fieldnames(Settings), fieldtypes(Settings)) val = getfield(settings, name) if type <: Base.RefValue val = val[] end push!(text, "$name = $val") end @debug "Settings: $(join(text, ", "))" _group = LOG_GROUP_OPTIMIZATION_CONTAINER end get_horizon(settings::Settings) = settings.horizon[] get_resolution(settings::Settings) = settings.resolution[] get_interval(settings::Settings) = settings.interval[] get_initial_time(settings::Settings)::Dates.DateTime = settings.initial_time[] get_optimizer(settings::Settings) = settings.optimizer get_ext(settings::Settings) = settings.ext get_warm_start(settings::Settings) = settings.warm_start[] get_check_components(settings::Settings) = settings.check_components get_initialize_model(settings::Settings) = settings.initialize_model get_initialization_file(settings::Settings) = settings.initialization_file get_deserialize_initial_conditions(settings::Settings) = settings.deserialize_initial_conditions get_export_pwl_vars(settings::Settings) = settings.export_pwl_vars get_check_numerical_bounds(settings::Settings) = settings.check_numerical_bounds get_allow_fails(settings::Settings) = settings.allow_fails get_optimizer_solve_log_print(settings::Settings) = settings.optimizer_solve_log_print get_calculate_conflict(settings::Settings) = settings.calculate_conflict get_detailed_optimizer_stats(settings::Settings) = settings.detailed_optimizer_stats get_direct_mode_optimizer(settings::Settings) = settings.direct_mode_optimizer get_store_variable_names(settings::Settings) = settings.store_variable_names get_rebuild_model(settings::Settings) = settings.rebuild_model get_export_optimization_model(settings::Settings) = settings.export_optimization_model use_time_series_cache(settings::Settings) = settings.time_series_cache_size > 0 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 function set_interval!(settings::Settings, interval::Dates.TimePeriod) settings.interval[] = IS.time_period_conversion(interval) return end function set_initial_time!(settings::Settings, initial_time::Dates.DateTime) settings.initial_time[] = initial_time return end function set_warm_start!(settings::Settings, warm_start::Bool) settings.warm_start[] = warm_start return end ================================================ FILE: src/core/store_common.jl ================================================ # 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 const EmulationModelIndexType = Int function write_results!( store, model::OperationModel, index::Union{DecisionModelIndexType, EmulationModelIndexType}, update_timestamp::Dates.DateTime; exports = nothing, ) if exports !== nothing export_params = Dict{Symbol, Any}( :exports => exports, :exports_path => joinpath(exports.path, string(get_name(model))), :file_type => get_export_file_type(exports), :resolution => get_resolution(model), :horizon_count => get_horizon(get_settings(model)) ÷ get_resolution(model), ) else export_params = nothing end write_model_dual_results!(store, model, index, update_timestamp, export_params) write_model_parameter_results!(store, model, index, update_timestamp, export_params) write_model_variable_results!(store, model, index, update_timestamp, export_params) write_model_aux_variable_results!(store, model, index, update_timestamp, export_params) write_model_expression_results!(store, model, index, update_timestamp, export_params) return end function write_model_dual_results!( store, model::T, index::Union{DecisionModelIndexType, EmulationModelIndexType}, update_timestamp::Dates.DateTime, export_params::Union{Dict{Symbol, Any}, Nothing}, ) where {T <: OperationModel} container = get_optimization_container(model) model_name = get_name(model) if export_params !== nothing exports_path = joinpath(export_params[:exports_path], "duals") mkpath(exports_path) end for (key, constraint) in get_duals(container) !should_write_resulting_value(key) && continue data = jump_value.(constraint) write_result!(store, model_name, key, index, update_timestamp, data) if export_params !== nothing && should_export_dual(export_params[:exports], update_timestamp, model_name, key) 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_count, step = resolution) DataFrames.insertcols!(df, 1, :DateTime => time_col) ISOPT.export_result(file_type, exports_path, key, index, df) end end return end function write_model_parameter_results!( store, model::T, index::Union{DecisionModelIndexType, EmulationModelIndexType}, update_timestamp::Dates.DateTime, export_params::Union{Dict{Symbol, Any}, Nothing}, ) where {T <: OperationModel} container = get_optimization_container(model) model_name = get_name(model) if export_params !== nothing exports_path = joinpath(export_params[:exports_path], "parameters") mkpath(exports_path) 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 !should_write_resulting_value(key) && continue data = calculate_parameter_values(container) write_result!(store, model_name, key, index, update_timestamp, data) if export_params !== nothing && should_export_parameter( export_params[:exports], update_timestamp, model_name, key, ) resolution = export_params[:resolution] file_type = export_params[:file_type] df = to_dataframe(data, key) time_col = range(index; length = horizon_count, step = resolution) DataFrames.insertcols!(df, 1, :DateTime => time_col) ISOPT.export_result(file_type, exports_path, key, index, df) end end return end function write_model_variable_results!( store, model::T, index::Union{DecisionModelIndexType, EmulationModelIndexType}, update_timestamp::Dates.DateTime, export_params::Union{Dict{Symbol, Any}, Nothing}, ) where {T <: OperationModel} container = get_optimization_container(model) model_name = get_name(model) if export_params !== nothing exports_path = joinpath(export_params[:exports_path], "variables") mkpath(exports_path) end if !isempty(container.primal_values_cache) variables = container.primal_values_cache.variables_cache else variables = get_variables(container) end for (key, variable) in variables !should_write_resulting_value(key) && continue data = jump_value.(variable) write_result!(store, model_name, key, index, update_timestamp, data) if export_params !== nothing && should_export_variable( export_params[:exports], update_timestamp, model_name, key, ) 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_count, step = resolution) DataFrames.insertcols!(df, 1, :DateTime => time_col) ISOPT.export_result(file_type, exports_path, key, index, df) end end return end function write_model_aux_variable_results!( store, model::T, index::Union{DecisionModelIndexType, EmulationModelIndexType}, update_timestamp::Dates.DateTime, export_params::Union{Dict{Symbol, Any}, Nothing}, ) where {T <: OperationModel} container = get_optimization_container(model) model_name = get_name(model) if export_params !== nothing exports_path = joinpath(export_params[:exports_path], "aux_variables") mkpath(exports_path) end for (key, variable) in get_aux_variables(container) !should_write_resulting_value(key) && continue data = jump_value.(variable) write_result!(store, model_name, key, index, update_timestamp, data) if export_params !== nothing && should_export_aux_variable( export_params[:exports], update_timestamp, model_name, key, ) 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_count, step = resolution) DataFrames.insertcols!(df, 1, :DateTime => time_col) ISOPT.export_result(file_type, exports_path, key, index, df) end end return end function write_model_expression_results!( store, model::T, index::Union{DecisionModelIndexType, EmulationModelIndexType}, update_timestamp::Dates.DateTime, export_params::Union{Dict{Symbol, Any}, Nothing}, ) where {T <: OperationModel} container = get_optimization_container(model) model_name = get_name(model) if export_params !== nothing exports_path = joinpath(export_params[:exports_path], "expressions") mkpath(exports_path) end if !isempty(container.primal_values_cache) expressions = container.primal_values_cache.expressions_cache else expressions = get_expressions(container) end for (key, expression) in expressions !should_write_resulting_value(key) && continue data = jump_value.(expression) write_result!(store, model_name, key, index, update_timestamp, data) if export_params !== nothing && should_export_expression( export_params[:exports], update_timestamp, model_name, key, ) 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_count, step = resolution) DataFrames.insertcols!(df, 1, :DateTime => time_col) ISOPT.export_result(file_type, exports_path, key, index, df) end end return end ================================================ FILE: src/core/variables.jl ================================================ abstract type AbstractContingencyVariableType <: VariableType end abstract type SparseVariableType <: VariableType end abstract type InterpolationVariableType <: SparseVariableType end abstract type BinaryInterpolationVariableType <: SparseVariableType end """ Struct to dispatch the creation of Active Power Variables Docs abbreviation: ``p`` """ struct ActivePowerVariable <: VariableType end """ Struct to dispatch the creation of Post-Contingency Active Power Change Variables. Docs abbreviation: ``\\Delta p_{g,c}`` """ struct PostContingencyActivePowerChangeVariable <: AbstractContingencyVariableType end """ Struct to dispatch the creation of Post-Contingency Active Power Deployment Variable for mapping reserves deployment under contingencies. Docs abbreviation: ``\\Delta rsv_{r,g,c}`` """ struct PostContingencyActivePowerReserveDeploymentVariable <: AbstractContingencyVariableType end """ Struct to dispatch the creation of Active Power Variables above minimum power for Thermal Compact formulations 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: ``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: ``p^\\text{out}`` """ struct ActivePowerOutVariable <: VariableType end "Multi-start startup variables" abstract type MultiStartVariable <: VariableType end """ Struct to dispatch the creation of Hot Start Variable for Thermal units with temperature considerations Docs abbreviation: ``z^\\text{th}`` """ struct HotStartVariable <: MultiStartVariable end """ Struct to dispatch the creation of Warm Start Variable for Thermal units with temperature considerations Docs abbreviation: ``y^\\text{th}`` """ struct WarmStartVariable <: MultiStartVariable end """ Struct to dispatch the creation of Cold Start Variable for Thermal units with temperature considerations Docs abbreviation: ``x^\\text{th}`` """ struct ColdStartVariable <: MultiStartVariable end """ Struct to dispatch the creation of a variable for energy storage level (state of charge) Docs abbreviation: ``e`` """ struct EnergyVariable <: VariableType end struct LiftVariable <: VariableType end """ Struct to dispatch the creation of a binary commitment status variable Docs abbreviation: ``u`` """ struct OnVariable <: VariableType end """ Struct to dispatch the creation of Reactive Power Variables Docs abbreviation: ``q`` """ struct ReactivePowerVariable <: VariableType end """ Struct to dispatch the creation of binary storage charge reservation variable Docs abbreviation: ``u^\\text{st}`` """ struct ReservationVariable <: VariableType end """ Struct to dispatch the creation of Active Power Reserve Variables 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: ``v`` """ struct StartVariable <: VariableType end """ Struct to dispatch the creation of Binary Stop Variables Docs abbreviation: ``w`` """ struct StopVariable <: VariableType end struct SteadyStateFrequencyDeviation <: VariableType end struct AreaMismatchVariable <: VariableType end struct DeltaActivePowerUpVariable <: VariableType end struct DeltaActivePowerDownVariable <: VariableType end struct AdditionalDeltaActivePowerUpVariable <: VariableType end 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 Voltage Magnitude Variables for AC formulations Docs abbreviation: ``v`` """ struct VoltageMagnitude <: VariableType end """ Struct to dispatch the creation of Voltage Angle Variables for AC/DC formulations Docs abbreviation: ``\\theta`` """ struct VoltageAngle <: VariableType end ######################################### ###### Power Load Shift Variables ####### ######################################### """ Struct to dispatch the creation of Shifted Active Power Variables Docs abbreviation: ``p^\\text{shift,up}`` """ struct ShiftUpActivePowerVariable <: VariableType end """ Struct to dispatch the creation of Shifted Down Active Power Variables Docs abbreviation: ``p^\\text{shift,dn}`` """ struct ShiftDownActivePowerVariable <: VariableType end ######################################### ####### DC Converter Variables ########## ######################################### """ Struct to dispatch the variable of DC Current Variables for DC Lines formulations Docs abbreviation: ``i_l^{dc}`` """ struct DCLineCurrent <: VariableType end """ Struct to dispatch the creation of Voltage Variables for DC formulations Docs abbreviation: ``v^{dc}`` """ struct DCVoltage <: VariableType end """ Struct to dispatch the creation of Squared Voltage Variables for DC formulations Docs abbreviation: ``v^{sq,dc}`` """ struct SquaredDCVoltage <: VariableType end """ Struct to dispatch the creation of DC Converter Current Variables for DC formulations Docs abbreviation: ``i_c^{dc}`` """ struct ConverterCurrent <: VariableType end """ Struct to dispatch the creation of DC Converter Power Variables for DC formulations Docs abbreviation: ``p_c^{dc}`` """ struct ConverterDCPower <: VariableType end """ Struct to dispatch the creation of Squared DC Converter Current Variables for DC formulations Docs abbreviation: ``i_c^{sq,dc}`` """ struct SquaredConverterCurrent <: VariableType end """ Struct to dispatch the creation of DC Converter Positive Term Current Variables for DC formulations Docs abbreviation: ``i_c^{+,dc}`` """ struct ConverterPositiveCurrent <: VariableType end """ Struct to dispatch the creation of DC Converter Negative Term Current Variables for DC formulations Docs abbreviation: ``i_c^{-,dc}`` """ struct ConverterNegativeCurrent <: VariableType end """ Struct to dispatch the creation of DC Converter Binary for Absolute Value Current Variables for DC formulations Docs abbreviation: `\\nu_c`` """ struct ConverterCurrentDirection <: VariableType end """ Struct to dispatch the creation of Binary Variable for Converter Power Direction Docs abbreviation: ``\\kappa_c^{dc}`` """ struct ConverterPowerDirection <: VariableType end """ Struct to dispatch the creation of Auxiliary Variable for Converter Bilinear term: v * i Docs abbreviation: ``\\gamma_c^{dc}`` """ struct AuxBilinearConverterVariable <: VariableType end """ Struct to dispatch the creation of Auxiliary Variable for Squared Converter Bilinear term: v * i Docs abbreviation: ``\\gamma_c^{sq,dc}`` """ struct AuxBilinearSquaredConverterVariable <: VariableType end """ Struct to dispatch the creation of Continuous Interpolation Variable for Squared Converter Voltage Docs abbreviation: ``\\delta_c^{v}`` """ struct InterpolationSquaredVoltageVariable <: InterpolationVariableType end """ Struct to dispatch the creation of Binary Interpolation Variable for Squared Converter Voltage Docs abbreviation: ``z_c^{v}`` """ struct InterpolationBinarySquaredVoltageVariable <: BinaryInterpolationVariableType end """ Struct to dispatch the creation of Continuous Interpolation Variable for Squared Converter Current Docs abbreviation: ``\\delta_c^{i}`` """ struct InterpolationSquaredCurrentVariable <: InterpolationVariableType end """ Struct to dispatch the creation of Binary Interpolation Variable for Squared Converter Current Docs abbreviation: ``z_c^{i}`` """ struct InterpolationBinarySquaredCurrentVariable <: BinaryInterpolationVariableType end """ Struct to dispatch the creation of Continuous Interpolation Variable for Squared Converter AuxVar Docs abbreviation: ``\\delta_c^{\\gamma}`` """ struct InterpolationSquaredBilinearVariable <: InterpolationVariableType end """ Struct to dispatch the creation of Binary Interpolation Variable for Squared Converter AuxVar Docs abbreviation: ``z_c^{\\gamma}`` """ struct InterpolationBinarySquaredBilinearVariable <: BinaryInterpolationVariableType end ######################################################### ######################################################### abstract type AbstractACActivePowerFlow <: VariableType end abstract type AbstractACReactivePowerFlow <: VariableType end """ Struct to dispatch the creation of bidirectional Active Power Flow Variables Docs abbreviation: ``f`` """ struct FlowActivePowerVariable <: AbstractACActivePowerFlow end # This Variable Type doesn't make sense since there are no lossless NetworkModels with ReactivePower. # struct FlowReactivePowerVariable <: VariableType end """ Struct to dispatch the creation of unidirectional Active Power Flow Variables Docs abbreviation: ``f^\\text{from-to}`` """ struct FlowActivePowerFromToVariable <: AbstractACActivePowerFlow end """ Struct to dispatch the creation of unidirectional Active Power Flow Variables Docs abbreviation: ``f^\\text{to-from}`` """ struct FlowActivePowerToFromVariable <: AbstractACActivePowerFlow end """ Struct to dispatch the creation of unidirectional Reactive Power Flow Variables Docs abbreviation: ``f^\\text{q,from-to}`` """ struct FlowReactivePowerFromToVariable <: AbstractACReactivePowerFlow end """ Struct to dispatch the creation of unidirectional Reactive Power Flow Variables Docs abbreviation: ``f^\\text{q,to-from}`` """ struct FlowReactivePowerToFromVariable <: AbstractACReactivePowerFlow 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 <: AbstractACActivePowerFlow 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 <: AbstractACActivePowerFlow end """ Struct to dispatch the creation of Phase Shifters Variables Docs abbreviation: ``\\theta^\\text{shift}`` """ struct PhaseShifterAngle <: VariableType end # Necessary as a work around for HVDCTwoTerminal models with losses """ Struct to dispatch the creation of HVDC Losses Auxiliary Variables Docs abbreviation: ``\\ell`` """ struct HVDCLosses <: VariableType end """ Struct to dispatch the creation of HVDC Flow Direction Auxiliary Variables Docs abbreviation: ``u^\\text{dir}`` """ struct HVDCFlowDirectionVariable <: VariableType end """ Struct to dispatch the creation of HVDC Received Flow at From Bus Variables for PWL formulations Docs abbreviation: ``x`` """ struct HVDCActivePowerReceivedFromVariable <: VariableType end """ Struct to dispatch the creation of HVDC Received Flow at To Bus Variables for PWL formulations Docs abbreviation: ``y`` """ struct HVDCActivePowerReceivedToVariable <: VariableType end """ Struct to dispatch the creation of HVDC Received Reactive Flow From Bus Variables Docs abbreviation: ``x^r`` """ struct HVDCReactivePowerReceivedFromVariable <: VariableType end """ Struct to dispatch the creation of HVDC Received Reactive Flow To Bus Variables Docs abbreviation: ``y^i`` """ struct HVDCReactivePowerReceivedToVariable <: VariableType end """ Struct to define the creation of HVDC Rectifier Delay Angle Variable Docs abbreviation: ``\\alpha^r`` """ struct HVDCRectifierDelayAngleVariable <: VariableType end """ Struct to define the creation of HVDC Inverter Extinction Angle Variable Docs abbreviation: ``\\gamma^i`` """ struct HVDCInverterExtinctionAngleVariable <: VariableType end """ Struct to define the creation of HVDC Rectifier Power Factor Angle Variable Docs abbreviation: ``\\phi^r`` """ struct HVDCRectifierPowerFactorAngleVariable <: VariableType end """ Struct to define the creation of HVDC Inverter Power Factor Angle Variable Docs abbreviation: ``\\phi^i`` """ struct HVDCInverterPowerFactorAngleVariable <: VariableType end """ Struct to define the creation of HVDC Rectifier Overlap Angle Variable Docs abbreviation: ``\\mu^r`` """ struct HVDCRectifierOverlapAngleVariable <: VariableType end """ Struct to define the creation of HVDC Inverter Overlap Angle Variable Docs abbreviation: ``\\mu^i`` """ struct HVDCInverterOverlapAngleVariable <: VariableType end """ Struct to define the creation of HVDC DC Line Voltage at Rectifier Side Docs abbreviation: ``\\v_{d}^r`` """ struct HVDCRectifierDCVoltageVariable <: VariableType end """ Struct to define the creation of HVDC DC Line Voltage at Inverter Side Docs abbreviation: ``\\v_{d}^i`` """ struct HVDCInverterDCVoltageVariable <: VariableType end """ Struct to define the creation of HVDC AC Line Current flowing into the AC side of Rectifier Docs abbreviation: ``\\i_{ac}^r`` """ struct HVDCRectifierACCurrentVariable <: VariableType end """ Struct to define the creation of HVDC AC Line Current flowing into the AC side of Inverter Docs abbreviation: ``\\i_{ac}^i`` """ struct HVDCInverterACCurrentVariable <: VariableType end """ Struct to define the creation of HVDC DC Line Current Flow Docs abbreviation: ``\\i_{d}`` """ struct DCLineCurrentFlowVariable <: VariableType end """ Struct to define the creation of HVDC Tap Setting at Rectifier Transformer Docs abbreviation: ``\\t^r`` """ struct HVDCRectifierTapSettingVariable <: VariableType end """ Struct to define the creation of HVDC Tap Setting at Inverter Transformer Docs abbreviation: ``\\t^i`` """ struct HVDCInverterTapSettingVariable <: VariableType end """ Struct to dispatch the creation of HVDC Piecewise Loss Variables Docs abbreviation: ``h`` or ``w`` """ struct HVDCPiecewiseLossVariable <: SparseVariableType end """ Struct to dispatch the creation of HVDC Piecewise Binary Loss Variables Docs abbreviation: ``z`` """ struct HVDCPiecewiseBinaryLossVariable <: SparseVariableType end """ Struct to dispatch the creation of piecewise linear cost variables for objective function Docs abbreviation: ``\\delta`` """ struct PiecewiseLinearCostVariable <: SparseVariableType end abstract type AbstractPiecewiseLinearBlockOffer <: SparseVariableType end """ Struct to dispatch the creation of piecewise linear block incremental offer variables for objective function Docs abbreviation: ``\\delta`` """ struct PiecewiseLinearBlockIncrementalOffer <: AbstractPiecewiseLinearBlockOffer end """ Struct to dispatch the creation of piecewise linear block decremental offer variables for objective function Docs abbreviation: ``\\delta_d`` """ struct PiecewiseLinearBlockDecrementalOffer <: AbstractPiecewiseLinearBlockOffer 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 """ Struct to dispatch the creation of Slack variables for rate of change constraints up limits Docs abbreviation: ``p^\\text{sl,up}`` """ struct RateofChangeConstraintSlackUp <: VariableType end """ Struct to dispatch the creation of Slack variables for rate of change constraints down limits Docs abbreviation: ``p^\\text{sl,dn}`` """ struct RateofChangeConstraintSlackDown <: VariableType end const MULTI_START_VARIABLES = Tuple(IS.get_all_concrete_subtypes(PSI.MultiStartVariable)) should_write_resulting_value(::Type{PiecewiseLinearCostVariable}) = false should_write_resulting_value(::Type{PiecewiseLinearBlockIncrementalOffer}) = false should_write_resulting_value(::Type{PiecewiseLinearBlockDecrementalOffer}) = false should_write_resulting_value(::Type{HVDCPiecewiseLossVariable}) = false should_write_resulting_value(::Type{HVDCPiecewiseBinaryLossVariable}) = false function should_write_resulting_value( ::Type{T}, ) where {T <: Union{InterpolationVariableType, BinaryInterpolationVariableType}} return false end convert_result_to_natural_units(::Type{ActivePowerVariable}) = true convert_result_to_natural_units(::Type{PostContingencyActivePowerChangeVariable}) = true convert_result_to_natural_units(::Type{PowerAboveMinimumVariable}) = true convert_result_to_natural_units(::Type{ActivePowerInVariable}) = true convert_result_to_natural_units(::Type{ActivePowerOutVariable}) = true convert_result_to_natural_units(::Type{EnergyVariable}) = true convert_result_to_natural_units(::Type{ReactivePowerVariable}) = true convert_result_to_natural_units(::Type{ActivePowerReserveVariable}) = true convert_result_to_natural_units( ::Type{PostContingencyActivePowerReserveDeploymentVariable}, ) = true convert_result_to_natural_units(::Type{ServiceRequirementVariable}) = true convert_result_to_natural_units(::Type{RateofChangeConstraintSlackUp}) = true convert_result_to_natural_units(::Type{RateofChangeConstraintSlackDown}) = true convert_result_to_natural_units(::Type{AreaMismatchVariable}) = true convert_result_to_natural_units(::Type{DeltaActivePowerUpVariable}) = true convert_result_to_natural_units(::Type{DeltaActivePowerDownVariable}) = true convert_result_to_natural_units(::Type{AdditionalDeltaActivePowerUpVariable}) = true convert_result_to_natural_units(::Type{AdditionalDeltaActivePowerDownVariable}) = true convert_result_to_natural_units(::Type{SmoothACE}) = true convert_result_to_natural_units(::Type{SystemBalanceSlackUp}) = true convert_result_to_natural_units(::Type{SystemBalanceSlackDown}) = true convert_result_to_natural_units(::Type{ReserveRequirementSlack}) = true convert_result_to_natural_units(::Type{FlowActivePowerVariable}) = true convert_result_to_natural_units(::Type{FlowActivePowerFromToVariable}) = true convert_result_to_natural_units(::Type{FlowActivePowerToFromVariable}) = true convert_result_to_natural_units(::Type{FlowReactivePowerFromToVariable}) = true convert_result_to_natural_units(::Type{FlowReactivePowerToFromVariable}) = true convert_result_to_natural_units(::Type{HVDCLosses}) = true convert_result_to_natural_units(::Type{InterfaceFlowSlackUp}) = true convert_result_to_natural_units(::Type{InterfaceFlowSlackDown}) = true ================================================ FILE: src/devices_models/device_constructors/branch_constructor.jl ================================================ ################################# Generic AC Branch Models ################################ # These 3 methods are defined on concrete formulations of the branches to avoid ambiguity function construct_device!( ::OptimizationContainer, ::PSY.System, ::ArgumentConstructStage, ::DeviceModel{T, StaticBranch}, ::Union{ NetworkModel{CopperPlatePowerModel}, NetworkModel{AreaBalancePowerModel}, }, ) where {T <: PSY.ACTransmission} @debug "No argument construction needed for CopperPlatePowerModel or AreaBalancePowerModel and DeviceModel{$T, StaticBranch}" _group = LOG_GROUP_BRANCH_CONSTRUCTIONS return end function construct_device!( ::OptimizationContainer, ::PSY.System, ::ModelConstructStage, ::DeviceModel{T, StaticBranch}, ::Union{ NetworkModel{CopperPlatePowerModel}, NetworkModel{AreaBalancePowerModel}, }, ) where {T <: PSY.ACTransmission} @debug "No model construction needed for CopperPlatePowerModel or AreaBalancePowerModel and DeviceModel{$T, StaticBranch}" _group = LOG_GROUP_BRANCH_CONSTRUCTIONS return end function construct_device!( ::OptimizationContainer, ::PSY.System, ::ArgumentConstructStage, ::DeviceModel{T, StaticBranchBounds}, ::Union{ NetworkModel{CopperPlatePowerModel}, NetworkModel{AreaBalancePowerModel}, }, ) where {T <: PSY.ACTransmission} @debug "No argument construction needed for CopperPlatePowerModel or AreaBalancePowerModel and DeviceModel{$T, StaticBranchBounds}" _group = LOG_GROUP_BRANCH_CONSTRUCTIONS return end function construct_device!( ::OptimizationContainer, ::PSY.System, ::ModelConstructStage, ::DeviceModel{T, StaticBranchBounds}, ::Union{ NetworkModel{CopperPlatePowerModel}, NetworkModel{AreaBalancePowerModel}, }, ) where {T <: PSY.ACTransmission} @debug "No model construction needed for CopperPlatePowerModel or AreaBalancePowerModel and DeviceModel{$T, StaticBranchBounds}" _group = LOG_GROUP_BRANCH_CONSTRUCTIONS return end construct_device!( ::OptimizationContainer, ::PSY.System, ::ArgumentConstructStage, ::DeviceModel{<:PSY.ACTransmission, StaticBranchUnbounded}, ::NetworkModel{<:PM.AbstractPowerModel}, ) = nothing construct_device!( ::OptimizationContainer, ::PSY.System, ::ModelConstructStage, ::DeviceModel{<:PSY.ACTransmission, StaticBranchUnbounded}, ::NetworkModel{<:PM.AbstractPowerModel}, ) = nothing # For DC Power only. Implements constraints function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ArgumentConstructStage, device_model::DeviceModel{T, StaticBranch}, network_model::NetworkModel{<:PM.AbstractActivePowerModel}, ) where {T <: PSY.ACTransmission} devices = get_available_components(device_model, sys) if get_use_slacks(device_model) add_variables!( container, FlowActivePowerSlackUpperBound, network_model, devices, StaticBranch(), ) add_variables!( container, FlowActivePowerSlackLowerBound, network_model, devices, StaticBranch(), ) end add_feedforward_arguments!(container, device_model, devices) return end # For DC Power only. Implements constraints function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ModelConstructStage, device_model::DeviceModel{T, StaticBranch}, network_model::NetworkModel{U}, ) where {T <: PSY.ACTransmission, U <: PM.AbstractActivePowerModel} @debug "construct_device" _group = LOG_GROUP_BRANCH_CONSTRUCTIONS devices = get_available_components(device_model, sys) add_constraints!(container, FlowRateConstraint, 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 # For DC Power only function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ArgumentConstructStage, device_model::DeviceModel{T, StaticBranch}, network_model::NetworkModel{<:AbstractPTDFModel}, ) where {T <: PSY.ACTransmission} devices = get_available_components(device_model, sys) if get_use_slacks(device_model) add_variables!( container, FlowActivePowerSlackUpperBound, network_model, devices, StaticBranch(), ) add_variables!( container, FlowActivePowerSlackLowerBound, network_model, devices, StaticBranch(), ) end if haskey(get_time_series_names(device_model), DynamicBranchRatingTimeSeriesParameter) add_branch_parameters!( container, DynamicBranchRatingTimeSeriesParameter, devices, device_model, network_model, ) end if haskey( get_time_series_names(device_model), PostContingencyDynamicBranchRatingTimeSeriesParameter, ) add_branch_parameters!( container, PostContingencyDynamicBranchRatingTimeSeriesParameter, devices, device_model, network_model, ) end add_feedforward_arguments!(container, device_model, devices) return end function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ModelConstructStage, device_model::DeviceModel{T, StaticBranch}, network_model::NetworkModel{<:AbstractPTDFModel}, ) where {T <: PSY.ACTransmission} devices = get_available_components(device_model, sys) # The order of these methods is important. The add_expressions! must be before the constraints add_expressions!( container, PTDFBranchFlow, devices, device_model, network_model, ) if haskey(get_time_series_names(device_model), DynamicBranchRatingTimeSeriesParameter) add_flow_rate_constraint_with_parameters!( container, FlowRateConstraint, devices, device_model, network_model, ) else add_constraints!( container, FlowRateConstraint, devices, device_model, network_model, ) end add_feedforward_constraints!(container, device_model, devices) objective_function!(container, devices, device_model, PTDFPowerModel) add_constraint_dual!(container, sys, device_model) return end function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ArgumentConstructStage, device_model::DeviceModel{T, StaticBranchBounds}, network_model::NetworkModel{<:AbstractPTDFModel}, ) where {T <: PSY.ACTransmission} devices = get_available_components(device_model, sys) add_variables!( container, FlowActivePowerVariable, network_model, devices, StaticBranchBounds(), ) if get_use_slacks(device_model) add_variables!( container, FlowActivePowerSlackUpperBound, network_model, devices, StaticBranch(), ) add_variables!( container, FlowActivePowerSlackLowerBound, network_model, devices, StaticBranch(), ) end add_feedforward_arguments!(container, device_model, devices) return end function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ModelConstructStage, device_model::DeviceModel{T, StaticBranchBounds}, network_model::NetworkModel{<:AbstractPTDFModel}, ) where {T <: PSY.ACTransmission} devices = get_available_components(device_model, sys) # The order of these methods is important. The add_expressions! must be before the constraints add_expressions!( container, PTDFBranchFlow, devices, device_model, network_model, ) branch_rate_bounds!(container, device_model, network_model) add_constraints!(container, NetworkFlowConstraint, devices, device_model, network_model) add_feedforward_constraints!(container, device_model, devices) objective_function!(container, devices, device_model, PTDFPowerModel) add_constraint_dual!(container, sys, device_model) return end function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ArgumentConstructStage, device_model::DeviceModel{T, StaticBranchUnbounded}, network_model::NetworkModel{<:AbstractPTDFModel}, ) where {T <: PSY.ACTransmission} devices = get_available_components(device_model, sys) add_feedforward_arguments!(container, device_model, devices) return end function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ModelConstructStage, device_model::DeviceModel{T, StaticBranchUnbounded}, network_model::NetworkModel{<:AbstractPTDFModel}, ) where {T <: PSY.ACTransmission} devices = get_available_components(device_model, sys) # The order of these methods is important. The add_expressions! must be before the constraints add_expressions!( container, PTDFBranchFlow, devices, device_model, network_model, ) add_feedforward_constraints!(container, device_model, devices) add_constraints!(container, NetworkFlowConstraint, devices, device_model, network_model) add_constraint_dual!(container, sys, device_model) return end # For AC Power only. Implements Bounds on the active power and rating constraints on the aparent power function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ArgumentConstructStage, device_model::DeviceModel{T, StaticBranch}, network_model::NetworkModel{<:PM.AbstractPowerModel}, ) where {T <: PSY.ACTransmission} 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, network_model, devices, StaticBranch(), ) end add_feedforward_arguments!(container, device_model, devices) return end function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ModelConstructStage, device_model::DeviceModel{T, StaticBranch}, network_model::NetworkModel{<:PM.AbstractPowerModel}, ) where {T <: PSY.ACTransmission} devices = get_available_components(device_model, sys) add_feedforward_constraints!(container, device_model, devices) add_constraints!( container, FlowRateConstraintFromTo, devices, device_model, network_model, ) add_constraints!( container, FlowRateConstraintToFrom, devices, device_model, network_model, ) add_constraint_dual!(container, sys, device_model) return end function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ArgumentConstructStage, device_model::DeviceModel{T, StaticBranchBounds}, ::NetworkModel{U}, ) where {T <: PSY.ACTransmission, U <: PM.AbstractPowerModel} if get_use_slacks(device_model) throw( ArgumentError( "StaticBranchBounds formulation and $U is not compatible with the use of slacks", ), ) end devices = get_available_components(device_model, sys) add_feedforward_arguments!(container, device_model, devices) return end function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ModelConstructStage, device_model::DeviceModel{T, StaticBranchBounds}, network_model::NetworkModel{<:PM.AbstractPowerModel}, ) where {T <: PSY.ACTransmission} devices = get_available_components(device_model, sys) branch_rate_bounds!(container, device_model, network_model) add_constraints!( container, FlowRateConstraintFromTo, devices, device_model, network_model, ) add_constraints!( container, FlowRateConstraintToFrom, devices, device_model, network_model, ) add_constraint_dual!(container, sys, device_model) add_feedforward_constraints!(container, device_model, devices) return end function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ModelConstructStage, device_model::DeviceModel{T, StaticBranchBounds}, network_model::NetworkModel{<:PM.AbstractActivePowerModel}, ) where {T <: PSY.ACTransmission} devices = get_available_components(device_model, sys) branch_rate_bounds!(container, device_model, network_model) add_constraint_dual!(container, sys, device_model) add_feedforward_constraints!(container, device_model, devices) return end ################################### TwoTerminal HVDC Line Models ################################### function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ArgumentConstructStage, device_model::DeviceModel{T, HVDCTwoTerminalLossless}, network_model::NetworkModel{CopperPlatePowerModel}, ) where {T <: PSY.TwoTerminalHVDC} if has_subnetworks(network_model) devices = get_available_components(device_model, sys) add_variables!( container, FlowActivePowerVariable, network_model, devices, HVDCTwoTerminalLossless(), ) add_to_expression!( container, ActivePowerBalance, FlowActivePowerVariable, devices, device_model, network_model, ) add_feedforward_arguments!(container, device_model, devices) end return end function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ModelConstructStage, device_model::DeviceModel{T, HVDCTwoTerminalLossless}, network_model::NetworkModel{CopperPlatePowerModel}, ) where {T <: PSY.TwoTerminalHVDC} if has_subnetworks(network_model) devices = get_available_components(device_model, sys) add_constraints!( container, FlowRateConstraint, devices, device_model, network_model, ) add_constraint_dual!(container, sys, device_model) add_feedforward_constraints!(container, device_model, devices) end return end function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ArgumentConstructStage, device_model::DeviceModel{T, HVDCTwoTerminalUnbounded}, ::NetworkModel{<:PM.AbstractPowerModel}, ) where {T <: PSY.TwoTerminalHVDC} devices = get_available_components(device_model, sys) add_feedforward_arguments!(container, device_model, devices) return end function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ModelConstructStage, device_model::DeviceModel{<:PSY.TwoTerminalHVDC, HVDCTwoTerminalUnbounded}, ::NetworkModel{<:PM.AbstractPowerModel}, ) devices = get_available_components(device_model, sys) add_constraint_dual!(container, sys, device_model) add_feedforward_constraints!(container, device_model, devices) return end function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ArgumentConstructStage, device_model::DeviceModel{T, HVDCTwoTerminalUnbounded}, network_model::NetworkModel{CopperPlatePowerModel}, ) where {T <: PSY.TwoTerminalHVDC} devices = get_available_components(device_model, sys) add_variables!(container, FlowActivePowerVariable, devices, HVDCTwoTerminalUnbounded()) add_to_expression!( container, ActivePowerBalance, FlowActivePowerVariable, devices, device_model, network_model, ) add_feedforward_arguments!(container, devicemodel, devices) return end function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ModelConstructStage, device_model::DeviceModel{<:PSY.TwoTerminalHVDC, HVDCTwoTerminalUnbounded}, ::NetworkModel{CopperPlatePowerModel}, ) devices = get_available_components(device_model, sys) add_constraint_dual!(container, sys, device_model) add_feedforward_constraints!(container, device_model, devices) return end function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ArgumentConstructStage, device_model::DeviceModel{T, HVDCTwoTerminalDispatch}, ::NetworkModel{AreaBalancePowerModel}, ) where {T <: PSY.TwoTerminalHVDC} @warn "AreaBalancePowerModel doesn't model individual line flows for $T. Arguments not built" return end function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ModelConstructStage, device_model::DeviceModel{T, HVDCTwoTerminalDispatch}, ::NetworkModel{AreaBalancePowerModel}, ) where {T <: PSY.TwoTerminalHVDC} @warn "AreaBalancePowerModel doesn't model individual line flows for $T. Model not built" return end # Repeated method to avoid ambiguity between HVDCTwoTerminalUnbounded, HVDCTwoTerminalLossless and HVDCTwoTerminalDispatch function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ArgumentConstructStage, device_model::DeviceModel{T, HVDCTwoTerminalUnbounded}, ::NetworkModel{AreaBalancePowerModel}, ) where {T <: PSY.TwoTerminalHVDC} @warn "AreaBalancePowerModel doesn't model individual line flows for $T. Arguments not built" return end function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ModelConstructStage, device_model::DeviceModel{T, HVDCTwoTerminalUnbounded}, ::NetworkModel{AreaBalancePowerModel}, ) where {T <: PSY.TwoTerminalHVDC} @warn "AreaBalancePowerModel doesn't model individual line flows for $T. Model not built" return end # Repeated method to avoid ambiguity between HVDCTwoTerminalUnbounded, HVDCTwoTerminalLossless and HVDCTwoTerminalDispatch function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ArgumentConstructStage, device_model::DeviceModel{T, HVDCTwoTerminalLossless}, ::NetworkModel{AreaBalancePowerModel}, ) where {T <: PSY.TwoTerminalHVDC} @warn "AreaBalancePowerModel doesn't model individual line flows for $T. Arguments not built" return end function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ModelConstructStage, device_model::DeviceModel{T, HVDCTwoTerminalLossless}, ::NetworkModel{AreaBalancePowerModel}, ) where {T <: PSY.TwoTerminalHVDC} @warn "AreaBalancePowerModel doesn't model individual line flows for $T. Model not built" return end # Repeated method to avoid ambiguity between HVDCTwoTerminalUnbounded and HVDCTwoTerminalLossless function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ArgumentConstructStage, device_model::DeviceModel{T, HVDCTwoTerminalUnbounded}, network_model::NetworkModel{<:AbstractPTDFModel}, ) where {T <: PSY.TwoTerminalHVDC} devices = get_available_components(device_model, sys) add_variables!(container, FlowActivePowerVariable, devices, HVDCTwoTerminalUnbounded()) add_to_expression!( container, ActivePowerBalance, FlowActivePowerVariable, devices, device_model, network_model, ) add_feedforward_arguments!(container, device_model, devices) return end # Repeated method to avoid ambiguity between HVDCTwoTerminalUnbounded and HVDCTwoTerminalLossless function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ModelConstructStage, device_model::DeviceModel{<:PSY.TwoTerminalHVDC, HVDCTwoTerminalUnbounded}, network_model::NetworkModel{<:AbstractPTDFModel}, ) devices = get_available_components(device_model, sys) add_constraint_dual!(container, sys, device_model) add_feedforward_constraints!(container, device_model, devices) return end function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ArgumentConstructStage, device_model::DeviceModel{T, HVDCTwoTerminalLossless}, ::NetworkModel{<:PM.AbstractPowerModel}, ) where {T <: PSY.TwoTerminalHVDC} devices = get_available_components(device_model, sys) add_feedforward_arguments!(container, device_model, devices) return end function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ModelConstructStage, device_model::DeviceModel{T, HVDCTwoTerminalLossless}, network_model::NetworkModel{<:PM.AbstractPowerModel}, ) where {T <: PSY.TwoTerminalHVDC} devices = get_available_components(device_model, sys) add_constraints!(container, FlowRateConstraint, devices, device_model, network_model) add_constraint_dual!(container, sys, device_model) add_feedforward_constraints!(container, device_model, devices) return end # Repeated method to avoid ambiguity between HVDCTwoTerminalUnbounded and HVDCTwoTerminalLossless function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ArgumentConstructStage, device_model::DeviceModel{T, HVDCTwoTerminalLossless}, network_model::NetworkModel{<:AbstractPTDFModel}, ) where {T <: PSY.TwoTerminalHVDC} devices = get_available_components(device_model, sys) add_variables!(container, FlowActivePowerVariable, devices, HVDCTwoTerminalLossless()) add_to_expression!( container, ActivePowerBalance, FlowActivePowerVariable, devices, device_model, network_model, ) add_feedforward_arguments!(container, device_model, devices) return end # Repeated method to avoid ambiguity between HVDCTwoTerminalUnbounded and HVDCTwoTerminalLossless function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ModelConstructStage, device_model::DeviceModel{T, HVDCTwoTerminalLossless}, network_model::NetworkModel{PTDFPowerModel}, ) where { T <: PSY.TwoTerminalHVDC, } devices = get_available_components(device_model, sys) add_constraints!(container, FlowRateConstraint, devices, device_model, network_model) add_constraint_dual!(container, sys, device_model) add_feedforward_constraints!(container, device_model, devices) return end function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ArgumentConstructStage, device_model::DeviceModel{T, HVDCTwoTerminalDispatch}, network_model::NetworkModel{<:AbstractPTDFModel}, ) where {T <: PSY.TwoTerminalHVDC} devices = get_available_components(device_model, sys) add_variables!( container, FlowActivePowerToFromVariable, devices, HVDCTwoTerminalDispatch(), ) add_variables!( container, FlowActivePowerFromToVariable, devices, HVDCTwoTerminalDispatch(), ) add_variables!(container, HVDCLosses, devices, HVDCTwoTerminalDispatch()) add_variables!(container, HVDCFlowDirectionVariable, devices, HVDCTwoTerminalDispatch()) add_to_expression!( container, ActivePowerBalance, FlowActivePowerToFromVariable, devices, device_model, network_model, ) add_to_expression!( container, ActivePowerBalance, FlowActivePowerFromToVariable, devices, device_model, network_model, ) add_to_expression!( container, ActivePowerBalance, HVDCLosses, devices, device_model, network_model, ) add_feedforward_arguments!(container, device_model, devices) return end function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ModelConstructStage, device_model::DeviceModel{T, HVDCTwoTerminalDispatch}, network_model::NetworkModel{<:AbstractPTDFModel}, ) where {T <: PSY.TwoTerminalHVDC} devices = get_available_components(device_model, sys) add_constraints!( container, FlowRateConstraintFromTo, devices, device_model, network_model, ) add_constraints!( container, FlowRateConstraintToFrom, devices, device_model, network_model, ) add_constraints!(container, HVDCPowerBalance, devices, device_model, network_model) add_constraint_dual!(container, sys, device_model) add_feedforward_constraints!(container, device_model, devices) return end function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ArgumentConstructStage, device_model::DeviceModel{T, HVDCTwoTerminalDispatch}, network_model::NetworkModel{<:PM.AbstractActivePowerModel}, ) where {T <: PSY.TwoTerminalHVDC} devices = get_available_components(device_model, sys) add_variables!( container, FlowActivePowerToFromVariable, devices, HVDCTwoTerminalDispatch(), ) add_variables!( container, FlowActivePowerFromToVariable, devices, HVDCTwoTerminalDispatch(), ) add_variables!(container, HVDCFlowDirectionVariable, devices, HVDCTwoTerminalDispatch()) add_variables!(container, HVDCLosses, devices, HVDCTwoTerminalDispatch()) add_to_expression!( container, ActivePowerBalance, FlowActivePowerToFromVariable, devices, device_model, network_model, ) add_to_expression!( container, ActivePowerBalance, FlowActivePowerFromToVariable, devices, device_model, network_model, ) add_feedforward_arguments!(container, device_model, devices) return end function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ModelConstructStage, device_model::DeviceModel{T, HVDCTwoTerminalDispatch}, network_model::NetworkModel{CopperPlatePowerModel}, ) where {T <: PSY.TwoTerminalHVDC} devices = get_available_components(device_model, sys) @warn "CopperPlatePowerModel models with HVDC ignores inter-area losses" add_constraints!( container, FlowRateConstraintFromTo, devices, device_model, network_model, ) add_constraints!( container, FlowRateConstraintToFrom, devices, device_model, network_model, ) add_constraint_dual!(container, sys, device_model) add_feedforward_constraints!(container, device_model, devices) return end function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ArgumentConstructStage, device_model::DeviceModel{T, U}, network_model::NetworkModel{<:AbstractPTDFModel}, ) where { T <: PSY.TwoTerminalHVDC, U <: HVDCTwoTerminalPiecewiseLoss, } devices = get_available_components(device_model, sys) add_variables!( container, HVDCActivePowerReceivedFromVariable, devices, HVDCTwoTerminalPiecewiseLoss(), ) add_variables!( container, HVDCActivePowerReceivedToVariable, devices, HVDCTwoTerminalPiecewiseLoss(), ) _add_sparse_pwl_loss_variables!(container, devices, device_model) add_to_expression!( container, ActivePowerBalance, HVDCActivePowerReceivedFromVariable, devices, device_model, network_model, ) add_to_expression!( container, ActivePowerBalance, HVDCActivePowerReceivedToVariable, devices, device_model, network_model, ) add_feedforward_arguments!(container, device_model, devices) return end function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ModelConstructStage, device_model::DeviceModel{T, U}, network_model::NetworkModel{<:AbstractPTDFModel}, ) where { T <: PSY.TwoTerminalHVDC, U <: HVDCTwoTerminalPiecewiseLoss, } devices = get_available_components(device_model, sys) add_constraints!( container, FlowRateConstraintFromTo, devices, device_model, network_model, ) add_constraints!( container, FlowRateConstraintToFrom, devices, device_model, network_model, ) add_constraints!( container, HVDCFlowCalculationConstraint, devices, device_model, network_model, ) add_feedforward_constraints!(container, device_model, devices) return end # TODO: Other models besides PTDF #= function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ArgumentConstructStage, model::DeviceModel{T, HVDCTwoTerminalPiecewiseLoss}, network_model::NetworkModel{<:PM.AbstractActivePowerModel}, ) where {T <: PSY.TwoTerminalHVDC} devices = get_available_components(model, sys) add_variables!( container, FlowActivePowerToFromVariable, devices, HVDCTwoTerminalDispatch(), ) add_variables!( container, FlowActivePowerFromToVariable, devices, HVDCTwoTerminalDispatch(), ) add_variables!(container, HVDCFlowDirectionVariable, devices, HVDCTwoTerminalDispatch()) add_to_expression!( container, ActivePowerBalance, FlowActivePowerToFromVariable, devices, model, network_model, ) add_to_expression!( container, ActivePowerBalance, FlowActivePowerFromToVariable, devices, model, network_model, ) add_feedforward_arguments!(container, model, devices) return end function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ModelConstructStage, model::DeviceModel{T, HVDCTwoTerminalPiecewiseLoss}, network_model::NetworkModel{CopperPlatePowerModel}, ) where {T <: PSY.TwoTerminalHVDC} devices = get_available_components(model, sys) @warn "CopperPlatePowerModel models with HVDC ignores inter-area losses" add_constraints!(container, FlowRateConstraintFromTo, devices, model, network_model) add_constraints!(container, FlowRateConstraintToFrom, devices, model, network_model) add_constraint_dual!(container, sys, model) return end =# function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ModelConstructStage, device_model::DeviceModel{T, HVDCTwoTerminalDispatch}, network_model::NetworkModel{<:PM.AbstractActivePowerModel}, ) where {T <: PSY.TwoTerminalHVDC} devices = get_available_components(device_model, sys) add_constraints!( container, FlowRateConstraintFromTo, devices, device_model, network_model, ) add_constraints!( container, FlowRateConstraintToFrom, devices, device_model, network_model, ) add_constraints!(container, HVDCPowerBalance, devices, device_model, network_model) add_constraint_dual!(container, sys, device_model) add_feedforward_constraints!(container, device_model, devices) return end ############################# NEW LCC HVDC NON-LINEAR MODEL ############################# function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ArgumentConstructStage, device_model::DeviceModel{T, HVDCTwoTerminalLCC}, network_model::NetworkModel{<:PM.ACPPowerModel}, ) where {T <: PSY.TwoTerminalLCCLine} devices = get_available_components(device_model, sys) # Variables add_variables!( container, HVDCActivePowerReceivedFromVariable, devices, HVDCTwoTerminalLCC(), ) add_variables!( container, HVDCActivePowerReceivedToVariable, devices, HVDCTwoTerminalLCC(), ) add_variables!( container, HVDCReactivePowerReceivedFromVariable, devices, HVDCTwoTerminalLCC(), ) add_variables!( container, HVDCReactivePowerReceivedToVariable, devices, HVDCTwoTerminalLCC(), ) add_variables!( container, HVDCRectifierDelayAngleVariable, devices, HVDCTwoTerminalLCC(), ) add_variables!( container, HVDCInverterExtinctionAngleVariable, devices, HVDCTwoTerminalLCC(), ) add_variables!( container, HVDCRectifierPowerFactorAngleVariable, devices, HVDCTwoTerminalLCC(), ) add_variables!( container, HVDCInverterPowerFactorAngleVariable, devices, HVDCTwoTerminalLCC(), ) add_variables!( container, HVDCRectifierOverlapAngleVariable, devices, HVDCTwoTerminalLCC(), ) add_variables!( container, HVDCInverterOverlapAngleVariable, devices, HVDCTwoTerminalLCC(), ) add_variables!( container, HVDCRectifierDCVoltageVariable, devices, HVDCTwoTerminalLCC(), ) add_variables!( container, HVDCInverterDCVoltageVariable, devices, HVDCTwoTerminalLCC(), ) add_variables!( container, HVDCRectifierACCurrentVariable, devices, HVDCTwoTerminalLCC(), ) add_variables!( container, HVDCInverterACCurrentVariable, devices, HVDCTwoTerminalLCC(), ) add_variables!( container, DCLineCurrentFlowVariable, devices, HVDCTwoTerminalLCC(), ) add_variables!( container, HVDCRectifierTapSettingVariable, devices, HVDCTwoTerminalLCC(), ) add_variables!( container, HVDCInverterTapSettingVariable, devices, HVDCTwoTerminalLCC(), ) # Expressions add_to_expression!( container, ActivePowerBalance, HVDCActivePowerReceivedFromVariable, devices, device_model, network_model, ) add_to_expression!( container, ActivePowerBalance, HVDCActivePowerReceivedToVariable, devices, device_model, network_model, ) add_to_expression!( container, ReactivePowerBalance, HVDCReactivePowerReceivedFromVariable, devices, device_model, network_model, ) add_to_expression!( container, ReactivePowerBalance, HVDCReactivePowerReceivedToVariable, devices, device_model, network_model, ) add_feedforward_arguments!(container, device_model, devices) return end function construct_device!( ::OptimizationContainer, ::PSY.System, ::ArgumentConstructStage, ::DeviceModel{T, HVDCTwoTerminalLCC}, ::NetworkModel{N}, ) where {T <: PSY.TwoTerminalLCCLine, N <: PM.AbstractPowerModel} throw( ArgumentError( "HVDCTwoTerminalLCC formulation requires ACPPowerModel network. " * "Got $N. Use HVDCTwoTerminalLossless, HVDCTwoTerminalDispatch, or " * "HVDCTwoTerminalPiecewiseLoss for DC/PTDF networks.", ), ) end function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ModelConstructStage, device_model::DeviceModel{T, HVDCTwoTerminalLCC}, network_model::NetworkModel{<:PM.ACPPowerModel}, ) where {T <: PSY.TwoTerminalLCCLine} devices = get_available_components(device_model, sys) add_constraints!( container, HVDCRectifierDCLineVoltageConstraint, devices, device_model, network_model, ) add_constraints!( container, HVDCInverterDCLineVoltageConstraint, devices, device_model, network_model, ) add_constraints!( container, HVDCRectifierOverlapAngleConstraint, devices, device_model, network_model, ) add_constraints!( container, HVDCInverterOverlapAngleConstraint, devices, device_model, network_model, ) add_constraints!( container, HVDCRectifierPowerFactorAngleConstraint, devices, device_model, network_model, ) add_constraints!( container, HVDCInverterPowerFactorAngleConstraint, devices, device_model, network_model, ) add_constraints!( container, HVDCRectifierACCurrentFlowConstraint, devices, device_model, network_model, ) add_constraints!( container, HVDCInverterACCurrentFlowConstraint, devices, device_model, network_model, ) add_constraints!( container, HVDCRectifierPowerCalculationConstraint, devices, device_model, network_model, ) add_constraints!( container, HVDCInverterPowerCalculationConstraint, devices, device_model, network_model, ) add_constraints!( container, HVDCTransmissionDCLineConstraint, devices, device_model, network_model, ) return end ############################# Phase Shifter Transformer Models ############################# function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ArgumentConstructStage, device_model::DeviceModel{PSY.PhaseShiftingTransformer, PhaseAngleControl}, network_model::NetworkModel{PM.DCPPowerModel}, ) devices = get_available_components(device_model, sys) add_variables!(container, FlowActivePowerVariable, devices, PhaseAngleControl()) add_variables!(container, PhaseShifterAngle, devices, PhaseAngleControl()) add_to_expression!( container, ActivePowerBalance, FlowActivePowerVariable, devices, device_model, network_model, ) add_feedforward_arguments!(container, device_model, devices) return end function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ArgumentConstructStage, device_model::DeviceModel{PSY.PhaseShiftingTransformer, PhaseAngleControl}, network_model::NetworkModel{<:AbstractPTDFModel}, ) devices = get_available_components(device_model, sys) add_variables!(container, FlowActivePowerVariable, devices, PhaseAngleControl()) add_variables!(container, PhaseShifterAngle, devices, PhaseAngleControl()) add_to_expression!( container, ActivePowerBalance, PhaseShifterAngle, devices, device_model, network_model, ) add_feedforward_arguments!(container, device_model, devices) return end function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ModelConstructStage, device_model::DeviceModel{PSY.PhaseShiftingTransformer, PhaseAngleControl}, network_model::NetworkModel{PM.DCPPowerModel}, ) devices = get_available_components(device_model, sys) add_constraints!(container, FlowLimitConstraint, devices, device_model, network_model) add_constraints!( container, PhaseAngleControlLimit, devices, device_model, network_model, ) add_constraints!(container, NetworkFlowConstraint, devices, device_model, network_model) add_constraint_dual!(container, sys, device_model) add_feedforward_constraints!(container, device_model, devices) return end function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ModelConstructStage, device_model::DeviceModel{PSY.PhaseShiftingTransformer, PhaseAngleControl}, network_model::NetworkModel{<:AbstractPTDFModel}, ) devices = get_available_components(device_model, sys) add_constraints!(container, FlowLimitConstraint, devices, device_model, network_model) add_constraints!( container, PhaseAngleControlLimit, devices, device_model, network_model, ) add_constraints!(container, NetworkFlowConstraint, devices, device_model, network_model) add_constraint_dual!(container, sys, device_model) add_feedforward_constraints!(container, device_model, devices) return end ################################# AreaInterchange Models ################################ function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ArgumentConstructStage, device_model::DeviceModel{PSY.AreaInterchange, U}, network_model::NetworkModel{CopperPlatePowerModel}, ) where {U <: Union{StaticBranchUnbounded, StaticBranch}} devices = get_available_components(device_model, sys) add_feedforward_arguments!(container, device_model, devices) return end function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ModelConstructStage, device_model::DeviceModel{PSY.AreaInterchange, StaticBranchUnbounded}, network_model::NetworkModel{T}, ) where {T <: PM.AbstractActivePowerModel} devices = get_available_components(device_model, sys) add_feedforward_constraints!(container, device_model, devices) return end function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ArgumentConstructStage, device_model::DeviceModel{PSY.AreaInterchange, T}, network_model::NetworkModel{U}, ) where { T <: Union{StaticBranchUnbounded, StaticBranch}, U <: PM.AbstractActivePowerModel, } devices = get_available_components(device_model, sys) has_ts = PSY.has_time_series.(devices) if get_use_slacks(device_model) add_variables!( container, FlowActivePowerSlackUpperBound, network_model, devices, T(), ) add_variables!( container, FlowActivePowerSlackLowerBound, network_model, devices, T(), ) end 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, device_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, device_model) add_parameters!(container, ToFromFlowLimitParameter, devices, device_model) end add_feedforward_arguments!(container, device_model, devices) return end function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ModelConstructStage, device_model::DeviceModel{PSY.AreaInterchange, StaticBranch}, network_model::NetworkModel{T}, ) where {T <: PM.AbstractActivePowerModel} devices = get_available_components(device_model, sys) add_constraints!(container, FlowLimitConstraint, devices, device_model, network_model) add_feedforward_constraints!(container, device_model, devices) return end function _get_branch_map(network_model::NetworkModel) @assert !isempty(network_model.modeled_ac_branch_types) net_reduction_data = get_network_reduction(network_model) all_branch_maps_by_type = net_reduction_data.all_branch_maps_by_type inter_area_branch_map = # This method uses ACBranch to support HVDC Dict{Tuple{String, String}, Dict{DataType, Vector{String}}}() name_to_arc_maps = PNM.get_name_to_arc_maps(net_reduction_data) for br_type in network_model.modeled_ac_branch_types !haskey(name_to_arc_maps, br_type) && continue name_to_arc_map = PNM.get_name_to_arc_map(net_reduction_data, br_type) for (name, (arc, reduction)) in name_to_arc_map reduction_entry = all_branch_maps_by_type[reduction][br_type][arc] area_from, area_to = _get_area_from_to(reduction_entry) if area_from != area_to branch_typed_dict = get!( inter_area_branch_map, (PSY.get_name(area_from), PSY.get_name(area_to)), Dict{DataType, Vector{String}}(), ) _add_to_branch_map!(branch_typed_dict, reduction_entry, name) end end end return inter_area_branch_map end function _add_to_branch_map!( branch_typed_dict::Dict{DataType, Vector{String}}, ::T, name::String, ) where {T <: PSY.ACBranch} if !haskey(branch_typed_dict, T) branch_typed_dict[T] = [name] else push!(branch_typed_dict[T], name) end end function _add_to_branch_map!( branch_typed_dict::Dict{DataType, Vector{String}}, reduction_entry::Union{PNM.BranchesParallel, PNM.BranchesSeries}, name::String, ) _add_to_branch_map!(branch_typed_dict, first(reduction_entry), name) end # This method uses ACBranch to support 2T - HVDC function _get_area_from_to(reduction_entry::PSY.ACBranch) area_from = PSY.get_area(PSY.get_arc(reduction_entry).from) area_to = PSY.get_area(PSY.get_arc(reduction_entry).to) return area_from, area_to end function _get_area_from_to(reduction_entry::PNM.ThreeWindingTransformerWinding) tfw = PNM.get_transformer(reduction_entry) winding_int = PNM.get_winding_number(reduction_entry) if winding_int == 1 area_from = PSY.get_area(PSY.get_primary_star_arc(tfw).from) area_to = PSY.get_area(PSY.get_primary_star_arc(tfw).to) elseif winding_int == 2 area_from = PSY.get_area(PSY.get_secondary_star_arc(tfw).from) area_to = PSY.get_area(PSY.get_secondary_star_arc(tfw).to_index) elseif winding_int == 3 area_from = PSY.get_area(PSY.get_tertiary_star_arc(tfw).from) area_to = PSY.get_area(PSY.get_tertiary_star_arc(tfw).to) else @assert false "Winding number $winding_int is not valid for three-winding transformer" end return area_from, area_to end function _get_area_from_to(reduction_entry::PNM.BranchesParallel) return _get_area_from_to(first(reduction_entry)) end function _get_area_from_to(reduction_entry::PNM.BranchesSeries) area_froms = [_get_area_from_to(x)[1] for x in reduction_entry] area_tos = [_get_area_from_to(x)[2] for x in reduction_entry] all_areas = vcat(area_froms, area_tos) if length(unique(all_areas)) > 1 error( "Inter-area line found as part of a degree two chain reduction; this feature is not supported", ) end return first(all_areas), first(all_areas) end function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ModelConstructStage, device_model::DeviceModel{PSY.AreaInterchange, StaticBranch}, network_model::NetworkModel{T}, ) where {T <: PSI.AbstractPTDFModel} devices = get_available_components(device_model, sys) add_constraints!(container, FlowLimitConstraint, devices, device_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(network_model) add_constraints!( container, LineFlowBoundConstraint, devices, device_model, network_model, inter_area_branch_map, ) add_feedforward_constraints!(container, device_model, devices) return end function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ModelConstructStage, device_model::DeviceModel{PSY.AreaInterchange, StaticBranchUnbounded}, network_model::NetworkModel{AreaBalancePowerModel}, ) devices = get_available_components(device_model, sys) add_feedforward_constraints!(container, device_model, devices) return end #TODO Check if for SCUC AreaPTDF needs something else function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ModelConstructStage, device_model::DeviceModel{PSY.AreaInterchange, StaticBranchUnbounded}, network_model::NetworkModel{AreaPTDFPowerModel}, ) devices = get_available_components(device_model, sys) inter_area_branch_map = _get_branch_map(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 add_constraints!( container, LineFlowBoundConstraint, devices, device_model, network_model, inter_area_branch_map, ) add_feedforward_constraints!(container, device_model, devices) return end ================================================ FILE: src/devices_models/device_constructors/hvdcsystems_constructor.jl ================================================ function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ArgumentConstructStage, model::DeviceModel{PSY.InterconnectingConverter, LosslessConverter}, network_model::NetworkModel{<:PM.AbstractActivePowerModel}, ) devices = get_available_components( model, sys, ) add_variables!(container, ActivePowerVariable, devices, LosslessConverter()) add_to_expression!( container, ActivePowerBalance, ActivePowerVariable, devices, model, network_model, ) add_feedforward_arguments!(container, model, devices) return end function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ModelConstructStage, model::DeviceModel{PSY.InterconnectingConverter, LosslessConverter}, network_model::NetworkModel{<:PM.AbstractActivePowerModel}, ) devices = get_available_components( model, sys, ) add_feedforward_constraints!(container, model, devices) objective_function!(container, devices, model, get_network_formulation(network_model)) add_constraint_dual!(container, sys, model) return end function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ArgumentConstructStage, model::DeviceModel{PSY.InterconnectingConverter, QuadraticLossConverter}, network_model::NetworkModel{<:PM.AbstractActivePowerModel}, ) devices = get_available_components( model, sys, ) ##################### ##### Variables ##### ##################### # Add Power Variable add_variables!(container, ActivePowerVariable, devices, QuadraticLossConverter()) # p_c^{ac} add_variables!(container, ConverterDCPower, devices, QuadraticLossConverter()) # p_c # Add Current Variables: i, i+, i- add_variables!(container, ConverterCurrent, devices, QuadraticLossConverter()) # i add_variables!(container, SquaredConverterCurrent, devices, QuadraticLossConverter()) # i^sq use_linear_loss = PSI.get_attribute(model, "use_linear_loss") if use_linear_loss add_variables!( container, ConverterPositiveCurrent, devices, QuadraticLossConverter(), ) # i^+ add_variables!( container, ConverterNegativeCurrent, devices, QuadraticLossConverter(), ) # i^- add_variables!( container, ConverterCurrentDirection, devices, QuadraticLossConverter(), ) # ν end # Add Voltage Variables: v^sq add_variables!(container, SquaredDCVoltage, devices, QuadraticLossConverter()) # Add Bilinear Variables: γ, γ^{sq} add_variables!( container, AuxBilinearConverterVariable, devices, QuadraticLossConverter(), ) # γ add_variables!( container, AuxBilinearSquaredConverterVariable, devices, QuadraticLossConverter(), ) # γ^{sq} #### Add Interpolation Variables #### v_segments = PSI.get_attribute(model, "voltage_segments") i_segments = PSI.get_attribute(model, "current_segments") γ_segments = PSI.get_attribute(model, "bilinear_segments") vars_vector = [ # Voltage v # (InterpolationSquaredVoltageVariable, v_segments), # δ^v (InterpolationBinarySquaredVoltageVariable, v_segments), # z^v # Current i # (InterpolationSquaredCurrentVariable, i_segments), # δ^i (InterpolationBinarySquaredCurrentVariable, i_segments), # z^i # Bilinear γ # (InterpolationSquaredBilinearVariable, γ_segments), # δ^γ (InterpolationBinarySquaredBilinearVariable, γ_segments), # z^γ ] for (T, len_segments) in vars_vector add_sparse_pwl_interpolation_variables!( container, T(), devices, model, len_segments, ) end ##################### #### Expressions #### ##################### add_to_expression!( container, ActivePowerBalance, ActivePowerVariable, devices, model, network_model, ) add_to_expression!( container, DCCurrentBalance, ConverterCurrent, devices, model, network_model, ) add_feedforward_arguments!(container, model, devices) return end function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ModelConstructStage, model::DeviceModel{PSY.InterconnectingConverter, QuadraticLossConverter}, network_model::NetworkModel{<:PM.AbstractActivePowerModel}, ) devices = get_available_components( model, sys, ) add_constraints!( container, ConverterPowerCalculationConstraint, devices, model, network_model, ) add_constraints!( container, ConverterMcCormickEnvelopes, devices, model, network_model, ) add_constraints!( container, ConverterLossConstraint, devices, model, network_model, ) use_linear_loss = PSI.get_attribute(model, "use_linear_loss") if use_linear_loss add_constraints!( container, CurrentAbsoluteValueConstraint, devices, model, network_model, ) end add_constraints!( container, InterpolationVoltageConstraints, devices, model, network_model, ) add_constraints!( container, InterpolationCurrentConstraints, devices, model, network_model, ) add_constraints!( container, InterpolationBilinearConstraints, devices, model, network_model, ) add_feedforward_constraints!(container, model, devices) objective_function!(container, devices, model, get_network_formulation(network_model)) #add_constraint_dual!(container, sys, model) return end function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ArgumentConstructStage, model::DeviceModel{PSY.TModelHVDCLine, LosslessLine}, network_model::NetworkModel{<:PM.AbstractActivePowerModel}, ) devices = get_available_components( model, sys, ) add_variables!(container, FlowActivePowerVariable, devices, LosslessLine()) add_to_expression!( container, ActivePowerBalance, FlowActivePowerVariable, devices, model, network_model, ) add_feedforward_arguments!(container, model, devices) return end function construct_device!( ::OptimizationContainer, sys::PSY.System, ::ModelConstructStage, model::DeviceModel{PSY.TModelHVDCLine, LosslessLine}, ::NetworkModel{<:PM.AbstractActivePowerModel}, ) end function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ArgumentConstructStage, model::DeviceModel{PSY.TModelHVDCLine, DCLossyLine}, network_model::NetworkModel{<:PM.AbstractActivePowerModel}, ) devices = get_available_components( model, sys, ) add_variables!(container, DCLineCurrent, devices, DCLossyLine()) add_to_expression!( container, DCCurrentBalance, DCLineCurrent, devices, model, network_model, ) add_feedforward_arguments!(container, model, devices) return end function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ModelConstructStage, model::DeviceModel{PSY.TModelHVDCLine, DCLossyLine}, network_model::NetworkModel{<:PM.AbstractActivePowerModel}, ) devices = get_available_components( model, sys, ) add_constraints!( container, DCLineCurrentConstraint, devices, model, network_model, ) end ================================================ FILE: src/devices_models/device_constructors/load_constructor.jl ================================================ # AbstractPowerModel + ControllableLoad device model function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ArgumentConstructStage, model::DeviceModel{L, D}, network_model::NetworkModel{<:PM.AbstractPowerModel}, ) where { L <: PSY.ControllableLoad, D <: AbstractControllablePowerLoadFormulation, } devices = get_available_components(model, sys, ) add_variables!(container, ActivePowerVariable, devices, D()) add_variables!(container, ReactivePowerVariable, devices, D()) process_market_bid_parameters!(container, devices, model, false, true) # Add Variables to expressions add_to_expression!( container, ActivePowerBalance, ActivePowerVariable, devices, model, network_model, ) add_to_expression!( container, ReactivePowerBalance, ReactivePowerVariable, devices, model, network_model, ) if haskey(get_time_series_names(model), ActivePowerTimeSeriesParameter) add_parameters!(container, ActivePowerTimeSeriesParameter, devices, model) end add_cost_expressions!(container, devices, model) add_event_arguments!(container, devices, model, network_model) return end function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ModelConstructStage, model::DeviceModel{L, <:AbstractControllablePowerLoadFormulation}, network_model::NetworkModel{<:PM.AbstractPowerModel}, ) where {L <: PSY.ControllableLoad} devices = get_available_components(model, sys, ) add_constraints!( container, ActivePowerVariableLimitsConstraint, ActivePowerVariable, devices, model, network_model, ) add_constraints!( container, ReactivePowerVariableLimitsConstraint, ReactivePowerVariable, devices, model, network_model, ) add_feedforward_constraints!(container, model, devices) objective_function!(container, devices, model, get_network_formulation(network_model)) add_event_constraints!(container, devices, model, network_model) add_constraint_dual!(container, sys, model) return end # AbstractActivePowerModel + ControllableLoad device model function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ArgumentConstructStage, model::DeviceModel{L, D}, network_model::NetworkModel{<:PM.AbstractActivePowerModel}, ) where { L <: PSY.ControllableLoad, D <: AbstractControllablePowerLoadFormulation, } devices = get_available_components(model, sys, ) add_variables!(container, ActivePowerVariable, devices, D()) # Add Variables to expressions add_to_expression!( container, ActivePowerBalance, ActivePowerVariable, devices, model, network_model, ) if haskey(get_time_series_names(model), ActivePowerTimeSeriesParameter) add_parameters!(container, ActivePowerTimeSeriesParameter, devices, model) end process_market_bid_parameters!(container, devices, model, false, true) add_cost_expressions!(container, devices, model) add_event_arguments!(container, devices, model, network_model) return end function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ModelConstructStage, model::DeviceModel{L, <:AbstractControllablePowerLoadFormulation}, network_model::NetworkModel{<:PM.AbstractActivePowerModel}, ) where {L <: PSY.ControllableLoad} devices = get_available_components(model, sys, ) add_constraints!( container, ActivePowerVariableLimitsConstraint, ActivePowerVariable, devices, model, network_model, ) add_feedforward_constraints!(container, model, devices) objective_function!(container, devices, model, get_network_formulation(network_model)) add_event_constraints!(container, devices, model, network_model) add_constraint_dual!(container, sys, model) return end # AbstractPowerModel + PowerLoadInterruption device model function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ArgumentConstructStage, model::DeviceModel{L, PowerLoadInterruption}, network_model::NetworkModel{<:PM.AbstractPowerModel}, ) where {L <: PSY.ControllableLoad} devices = get_available_components(model, sys, ) add_variables!(container, ActivePowerVariable, devices, PowerLoadInterruption()) add_variables!(container, ReactivePowerVariable, devices, PowerLoadInterruption()) add_variables!(container, OnVariable, devices, PowerLoadInterruption()) # Add Variables to expressions add_to_expression!( container, ActivePowerBalance, ActivePowerVariable, devices, model, network_model, ) add_to_expression!( container, ReactivePowerBalance, ReactivePowerVariable, devices, model, network_model, ) if haskey(get_time_series_names(model), ActivePowerTimeSeriesParameter) add_parameters!(container, ActivePowerTimeSeriesParameter, devices, model) end process_market_bid_parameters!(container, devices, model, false, true) add_cost_expressions!(container, devices, model) add_event_arguments!(container, devices, model, network_model) return end function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ModelConstructStage, model::DeviceModel{L, PowerLoadInterruption}, network_model::NetworkModel{<:PM.AbstractPowerModel}, ) where {L <: PSY.ControllableLoad} devices = get_available_components(model, sys, ) add_constraints!( container, ActivePowerVariableLimitsConstraint, ActivePowerVariable, devices, model, network_model, ) add_constraints!( container, ActivePowerVariableLimitsConstraint, OnVariable, devices, model, network_model, ) add_constraints!( container, ReactivePowerVariableLimitsConstraint, ReactivePowerVariable, devices, model, network_model, ) add_feedforward_constraints!(container, model, devices) objective_function!(container, devices, model, get_network_formulation(network_model)) add_event_constraints!(container, devices, model, network_model) add_constraint_dual!(container, sys, model) return end # AbstractActivePowerModel + PowerLoadInterruption device model function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ArgumentConstructStage, model::DeviceModel{L, PowerLoadInterruption}, network_model::NetworkModel{<:PM.AbstractActivePowerModel}, ) where {L <: PSY.ControllableLoad} devices = get_available_components(model, sys, ) add_variables!(container, ActivePowerVariable, devices, PowerLoadInterruption()) add_variables!(container, OnVariable, devices, PowerLoadInterruption()) process_market_bid_parameters!(container, devices, model, false, true) # Add Variables to expressions add_to_expression!( container, ActivePowerBalance, ActivePowerVariable, devices, model, network_model, ) if haskey(get_time_series_names(model), ActivePowerTimeSeriesParameter) add_parameters!(container, ActivePowerTimeSeriesParameter, devices, model) end add_cost_expressions!(container, devices, model) add_event_arguments!(container, devices, model, network_model) return end function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ModelConstructStage, model::DeviceModel{L, PowerLoadInterruption}, network_model::NetworkModel{<:PM.AbstractActivePowerModel}, ) where {L <: PSY.ControllableLoad} devices = get_available_components(model, sys, ) add_constraints!( container, ActivePowerVariableLimitsConstraint, ActivePowerVariable, devices, model, network_model, ) add_constraints!( container, ActivePowerVariableLimitsConstraint, OnVariable, devices, model, network_model, ) add_feedforward_constraints!(container, model, devices) objective_function!(container, devices, model, get_network_formulation(network_model)) add_event_constraints!(container, devices, model, network_model) add_constraint_dual!(container, sys, model) return end # AbstractPowerModel + StaticPowerLoad device model function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ArgumentConstructStage, model::DeviceModel{L, StaticPowerLoad}, network_model::NetworkModel{<:PM.AbstractPowerModel}, ) where {L <: PSY.ElectricLoad} devices = get_available_components(model, sys, ) if haskey(get_time_series_names(model), ActivePowerTimeSeriesParameter) add_parameters!(container, ActivePowerTimeSeriesParameter, devices, model) end if haskey(get_time_series_names(model), ReactivePowerTimeSeriesParameter) add_parameters!(container, ReactivePowerTimeSeriesParameter, devices, model) end add_to_expression!( container, ActivePowerBalance, ActivePowerTimeSeriesParameter, devices, model, network_model, ) add_to_expression!( container, ReactivePowerBalance, ReactivePowerTimeSeriesParameter, devices, model, network_model, ) add_event_arguments!(container, devices, model, network_model) return end # AbstractActivePowerModel + StaticPowerLoad device model function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ArgumentConstructStage, model::DeviceModel{L, StaticPowerLoad}, network_model::NetworkModel{<:PM.AbstractActivePowerModel}, ) where {L <: PSY.ElectricLoad} devices = get_available_components(model, sys, ) if haskey(get_time_series_names(model), ActivePowerTimeSeriesParameter) add_parameters!(container, ActivePowerTimeSeriesParameter, devices, model) end add_to_expression!( container, ActivePowerBalance, ActivePowerTimeSeriesParameter, devices, model, network_model, ) add_event_arguments!(container, devices, model, network_model) return end function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ModelConstructStage, model::DeviceModel{<:PSY.ElectricLoad, StaticPowerLoad}, network_model::NetworkModel{<:PM.AbstractPowerModel}, ) # Static PowerLoad doesn't add any constraints to the model. This function covers # AbstractPowerModel and AbtractActivePowerModel return end # AbstractPowerModel + StaticLoad, but with non-StaticPowerLoad device models function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ArgumentConstructStage, model::DeviceModel{L, <:AbstractControllablePowerLoadFormulation}, network_model::NetworkModel{<:PM.AbstractPowerModel}, ) where {L <: PSY.StaticLoad} devices = get_available_components(model, sys, ) if haskey(get_time_series_names(model), ActivePowerTimeSeriesParameter) add_parameters!(container, ActivePowerTimeSeriesParameter, devices, model) end if haskey(get_time_series_names(model), ReactivePowerTimeSeriesParameter) add_parameters!(container, ReactivePowerTimeSeriesParameter, devices, model) end process_market_bid_parameters!(container, devices, model, false, true) add_to_expression!( container, ActivePowerBalance, ActivePowerTimeSeriesParameter, devices, model, network_model, ) add_to_expression!( container, ReactivePowerBalance, ReactivePowerTimeSeriesParameter, devices, model, network_model, ) add_event_arguments!(container, devices, model, network_model) return end # AbstractActivePowerModel + StaticLoad, but with non-StaticPowerLoad device models function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ArgumentConstructStage, model::DeviceModel{L, <:AbstractControllablePowerLoadFormulation}, network_model::NetworkModel{<:PM.AbstractActivePowerModel}, ) where {L <: PSY.StaticLoad} devices = get_available_components(model, sys, ) if haskey(get_time_series_names(model), ActivePowerTimeSeriesParameter) add_parameters!(container, ActivePowerTimeSeriesParameter, devices, model) end add_to_expression!( container, ActivePowerBalance, ActivePowerTimeSeriesParameter, devices, model, network_model, ) process_market_bid_parameters!(container, devices, model, false, true) add_event_arguments!(container, devices, model, network_model) return end function construct_device!( container::OptimizationContainer, sys::PSY.System, ccs::ModelConstructStage, model::DeviceModel{L, D}, network_model::NetworkModel{<:PM.AbstractPowerModel}, ) where { L <: PSY.StaticLoad, D <: AbstractControllablePowerLoadFormulation, } if D != StaticPowerLoad @warn( "The Formulation $(D) only applies to FormulationControllable Loads, \n Consider Changing the Device Formulation to StaticPowerLoad" ) end # Makes a new model with the correct formulation of the type. Needs to recover all the other fields # slacks, services and duals are not applicable to StaticPowerLoad so those are ignored new_model = DeviceModel( L, StaticPowerLoad; feedforwards = model.feedforwards, time_series_names = model.time_series_names, attributes = model.attributes, ) construct_device!(container, sys, ccs, new_model, network_model) return end function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ArgumentConstructStage, model::DeviceModel{PSY.ShiftablePowerLoad, PowerLoadShift}, network_model::NetworkModel{<:PM.AbstractPowerModel}, ) devices = get_available_components(model, sys) add_variables!(container, ShiftUpActivePowerVariable, devices, PowerLoadShift()) add_variables!(container, ShiftDownActivePowerVariable, devices, PowerLoadShift()) add_variables!(container, ReactivePowerVariable, devices, PowerLoadShift()) process_market_bid_parameters!(container, devices, model, false, true) if haskey(get_time_series_names(model), ActivePowerTimeSeriesParameter) add_parameters!(container, ActivePowerTimeSeriesParameter, devices, model) end if haskey(get_time_series_names(model), ShiftUpActivePowerTimeSeriesParameter) add_parameters!(container, ShiftUpActivePowerTimeSeriesParameter, devices, model) end if haskey(get_time_series_names(model), ShiftDownActivePowerTimeSeriesParameter) add_parameters!(container, ShiftDownActivePowerTimeSeriesParameter, devices, model) end # Add realized load expression add_expressions!(container, RealizedShiftedLoad, devices, model) # Add Parameters to expressions add_to_expression!( container, ActivePowerBalance, RealizedShiftedLoad, devices, model, network_model, ) add_to_expression!( container, ReactivePowerBalance, ReactivePowerVariable, devices, model, network_model, ) add_expressions!(container, ProductionCostExpression, devices, model) add_event_arguments!(container, devices, model, network_model) return end function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ModelConstructStage, model::DeviceModel{PSY.ShiftablePowerLoad, PowerLoadShift}, network_model::NetworkModel{<:PM.AbstractPowerModel}, ) devices = get_available_components(model, sys, ) add_constraints!( container, ShiftedActivePowerBalanceConstraint, devices, model, network_model, ) add_constraints!( container, RealizedShiftedLoadMinimumBoundConstraint, RealizedShiftedLoad, devices, model, network_model, ) add_constraints!( container, ShiftUpActivePowerVariableLimitsConstraint, ShiftUpActivePowerVariable, devices, model, network_model, ) add_constraints!( container, ShiftDownActivePowerVariableLimitsConstraint, ShiftDownActivePowerVariable, devices, model, network_model, ) add_constraints!( container, ReactivePowerVariableLimitsConstraint, ReactivePowerVariable, devices, model, network_model, ) add_constraints!( container, NonAnticipativityConstraint, devices, model, network_model, ) add_feedforward_constraints!(container, model, devices) objective_function!(container, devices, model, get_network_formulation(network_model)) add_event_constraints!(container, devices, model, network_model) add_constraint_dual!(container, sys, model) return end # AbstractActivePowerModel + PowerLoadShift device model function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ArgumentConstructStage, model::DeviceModel{PSY.ShiftablePowerLoad, PowerLoadShift}, network_model::NetworkModel{<:PM.AbstractActivePowerModel}, ) devices = get_available_components(model, sys) add_variables!(container, ShiftUpActivePowerVariable, devices, PowerLoadShift()) add_variables!(container, ShiftDownActivePowerVariable, devices, PowerLoadShift()) process_market_bid_parameters!(container, devices, model, false, true) if haskey(get_time_series_names(model), ActivePowerTimeSeriesParameter) add_parameters!(container, ActivePowerTimeSeriesParameter, devices, model) end if haskey(get_time_series_names(model), ShiftUpActivePowerTimeSeriesParameter) add_parameters!(container, ShiftUpActivePowerTimeSeriesParameter, devices, model) end if haskey(get_time_series_names(model), ShiftDownActivePowerTimeSeriesParameter) add_parameters!(container, ShiftDownActivePowerTimeSeriesParameter, devices, model) end # Add realized load expression add_expressions!(container, RealizedShiftedLoad, devices, model) # Add Parameters to expressions add_to_expression!( container, ActivePowerBalance, RealizedShiftedLoad, devices, model, network_model, ) add_expressions!(container, ProductionCostExpression, devices, model) add_event_arguments!(container, devices, model, network_model) return end function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ModelConstructStage, model::DeviceModel{PSY.ShiftablePowerLoad, PowerLoadShift}, network_model::NetworkModel{<:PM.AbstractActivePowerModel}, ) devices = get_available_components(model, sys, ) add_constraints!( container, ShiftedActivePowerBalanceConstraint, devices, model, network_model, ) add_constraints!( container, RealizedShiftedLoadMinimumBoundConstraint, RealizedShiftedLoad, devices, model, network_model, ) add_constraints!( container, ShiftUpActivePowerVariableLimitsConstraint, ShiftUpActivePowerVariable, devices, model, network_model, ) add_constraints!( container, ShiftDownActivePowerVariableLimitsConstraint, ShiftDownActivePowerVariable, devices, model, network_model, ) add_constraints!( container, NonAnticipativityConstraint, devices, model, network_model, ) add_feedforward_constraints!(container, model, devices) objective_function!(container, devices, model, get_network_formulation(network_model)) add_event_constraints!(container, devices, model, network_model) add_constraint_dual!(container, sys, model) return end ================================================ FILE: src/devices_models/device_constructors/reactivepowerdevice_constructor.jl ================================================ function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ArgumentConstructStage, model::DeviceModel{R, D}, network_model::NetworkModel{<:PM.AbstractPowerModel}, ) where { R <: PSY.SynchronousCondenser, D <: AbstractReactivePowerDeviceFormulation, } devices = get_available_components(model, sys) add_variables!(container, ReactivePowerVariable, devices, D()) add_to_expression!( container, ReactivePowerBalance, ReactivePowerVariable, devices, model, network_model, ) add_feedforward_arguments!(container, model, devices) end function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ModelConstructStage, model::DeviceModel{R, D}, network_model::NetworkModel{<:PM.AbstractPowerModel}, ) where { R <: PSY.SynchronousCondenser, D <: AbstractReactivePowerDeviceFormulation, } devices = get_available_components(model, sys) # No constraints # Add FFs add_feedforward_constraints!(container, model, devices) # No objective function return end function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ArgumentConstructStage, model::DeviceModel{R, D}, network_model::NetworkModel{<:PM.AbstractActivePowerModel}, ) where { R <: PSY.SynchronousCondenser, D <: AbstractReactivePowerDeviceFormulation, } # Do Nothing in Active Power Only Models return end ================================================ FILE: src/devices_models/device_constructors/regulationdevice_constructor.jl ================================================ function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ArgumentConstructStage, model::DeviceModel{PSY.RegulationDevice{T}, U}, network_model::NetworkModel{AreaBalancePowerModel}, ) where {T <: PSY.StaticInjection, U <: DeviceLimitedRegulation} devices = get_available_components(get_component_type(model), sys) add_parameters!(container, ActivePowerTimeSeriesParameter, devices, model) add_to_expression!( container, ActivePowerBalance, ActivePowerTimeSeriesParameter, devices, model, network_model, ) add_variables!(container, DeltaActivePowerUpVariable, devices, U()) add_variables!(container, DeltaActivePowerDownVariable, devices, U()) add_variables!(container, AdditionalDeltaActivePowerUpVariable, devices, U()) add_variables!(container, AdditionalDeltaActivePowerDownVariable, devices, U()) return end function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ModelConstructStage, model::DeviceModel{PSY.RegulationDevice{T}, DeviceLimitedRegulation}, network_model::NetworkModel{AreaBalancePowerModel}, ) where {T <: PSY.StaticInjection} devices = get_available_components(get_component_type(model), sys) add_constraints!( container, RegulationLimitsConstraint, DeltaActivePowerUpVariable, devices, model, network_model, ) add_constraints!(container, RampLimitConstraint, devices, model, network_model) add_constraints!( container, ParticipationAssignmentConstraint, devices, model, network_model, ) objective_function!(container, devices, model) return end function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ArgumentConstructStage, model::DeviceModel{PSY.RegulationDevice{T}, U}, network_model::NetworkModel{AreaBalancePowerModel}, ) where {T <: PSY.StaticInjection, U <: ReserveLimitedRegulation} devices = get_available_components(get_component_type(model), sys) add_parameters!(container, ActivePowerTimeSeriesParameter, devices, model) add_to_expression!( container, ActivePowerBalance, ActivePowerTimeSeriesParameter, devices, model, network_model, ) add_variables!(container, DeltaActivePowerUpVariable, devices, U()) add_variables!(container, DeltaActivePowerDownVariable, devices, U()) add_variables!(container, AdditionalDeltaActivePowerUpVariable, devices, U()) add_variables!(container, AdditionalDeltaActivePowerDownVariable, devices, U()) return end function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ModelConstructStage, model::DeviceModel{PSY.RegulationDevice{T}, ReserveLimitedRegulation}, network_model::NetworkModel{AreaBalancePowerModel}, ) where {T <: PSY.StaticInjection} devices = get_available_components(get_component_type(model), sys) add_constraints!( container, RegulationLimitsConstraint, DeltaActivePowerUpVariable, devices, model, network_model, ) add_constraints!( container, ParticipationAssignmentConstraint, devices, model, AreaBalancePowerModel, ) objective_function!(container, devices, model) return end function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ArgumentConstructStage, model::DeviceModel{PSY.RegulationDevice{<:PSY.StaticInjection}, FixedOutput}, network_model::NetworkModel{AreaBalancePowerModel}, ) devices = get_available_components(get_component_type(model), sys) add_parameters!(container, ActivePowerTimeSeriesParameter, devices, model) add_to_expression!( container, ActivePowerBalance, ActivePowerTimeSeriesParameter, devices, model, network_model, ) return end function construct_device!( ::OptimizationContainer, ::PSY.System, ::ModelConstructStage, ::DeviceModel{PSY.RegulationDevice{<:PSY.StaticInjection}, FixedOutput}, network_model::NetworkModel{AreaBalancePowerModel}, ) # There is no-op under FixedOutput formulation return end ================================================ FILE: src/devices_models/device_constructors/renewablegeneration_constructor.jl ================================================ function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ArgumentConstructStage, model::DeviceModel{R, D}, network_model::NetworkModel{<:PM.AbstractPowerModel}, ) where { R <: PSY.RenewableGen, D <: AbstractRenewableDispatchFormulation, } devices = get_available_components(model, sys) add_variables!(container, ActivePowerVariable, devices, D()) add_variables!(container, ReactivePowerVariable, devices, D()) if haskey(get_time_series_names(model), ActivePowerTimeSeriesParameter) add_parameters!(container, ActivePowerTimeSeriesParameter, devices, model) end if haskey(get_time_series_names(model), ReactivePowerTimeSeriesParameter) add_parameters!(container, ReactivePowerTimeSeriesParameter, devices, model) end process_market_bid_parameters!(container, devices, model) add_cost_expressions!(container, devices, model) # Expression add_to_expression!( container, ActivePowerBalance, ActivePowerVariable, devices, model, network_model, ) add_to_expression!( container, ReactivePowerBalance, ReactivePowerVariable, devices, model, network_model, ) if has_service_model(model) add_to_expression!( container, ActivePowerRangeExpressionLB, ActivePowerVariable, devices, model, network_model, ) add_to_expression!( container, ActivePowerRangeExpressionUB, ActivePowerVariable, devices, model, network_model, ) end add_feedforward_arguments!(container, model, devices) add_event_arguments!(container, devices, model, network_model) end function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ModelConstructStage, model::DeviceModel{R, <:AbstractRenewableDispatchFormulation}, network_model::NetworkModel{<:PM.AbstractPowerModel}, ) where {R <: PSY.RenewableGen} devices = get_available_components(model, sys) if has_service_model(model) add_constraints!( container, ActivePowerVariableLimitsConstraint, ActivePowerRangeExpressionLB, devices, model, network_model, ) add_constraints!( container, ActivePowerVariableLimitsConstraint, ActivePowerRangeExpressionUB, devices, model, network_model, ) else add_constraints!( container, ActivePowerVariableLimitsConstraint, ActivePowerVariable, devices, model, network_model, ) end add_constraints!( container, ReactivePowerVariableLimitsConstraint, ReactivePowerVariable, devices, model, network_model, ) add_feedforward_constraints!(container, model, devices) objective_function!(container, devices, model, get_network_formulation(network_model)) add_event_constraints!(container, devices, model, network_model) add_constraint_dual!(container, sys, model) return end function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ArgumentConstructStage, model::DeviceModel{R, D}, network_model::NetworkModel{<:PM.AbstractActivePowerModel}, ) where { R <: PSY.RenewableGen, D <: AbstractRenewableDispatchFormulation, } devices = get_available_components(model, sys) add_variables!(container, ActivePowerVariable, devices, D()) # this is always true!! see get_default_time_series_names in renewable_generation.jl # and line 62 of device_model.jl if haskey(get_time_series_names(model), ActivePowerTimeSeriesParameter) add_parameters!(container, ActivePowerTimeSeriesParameter, devices, model) end process_market_bid_parameters!(container, devices, model) add_cost_expressions!(container, devices, model) # Expression add_to_expression!( container, ActivePowerBalance, ActivePowerVariable, devices, model, network_model, ) if has_service_model(model) add_to_expression!( container, ActivePowerRangeExpressionLB, ActivePowerVariable, devices, model, network_model, ) add_to_expression!( container, ActivePowerRangeExpressionUB, ActivePowerVariable, devices, model, network_model, ) end add_feedforward_arguments!(container, model, devices) add_event_arguments!(container, devices, model, network_model) end function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ModelConstructStage, model::DeviceModel{R, <:AbstractRenewableDispatchFormulation}, network_model::NetworkModel{<:PM.AbstractActivePowerModel}, ) where {R <: PSY.RenewableGen} devices = get_available_components(model, sys) if has_service_model(model) add_constraints!( container, ActivePowerVariableLimitsConstraint, ActivePowerRangeExpressionLB, devices, model, network_model, ) add_constraints!( container, ActivePowerVariableLimitsConstraint, ActivePowerRangeExpressionUB, devices, model, network_model, ) else add_constraints!( container, ActivePowerVariableLimitsConstraint, ActivePowerVariable, devices, model, network_model, ) end add_feedforward_constraints!(container, model, devices) objective_function!(container, devices, model, get_network_formulation(network_model)) add_event_constraints!(container, devices, model, network_model) add_constraint_dual!(container, sys, model) return end function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ArgumentConstructStage, model::DeviceModel{R, FixedOutput}, network_model::NetworkModel{<:PM.AbstractPowerModel}, ) where {R <: PSY.RenewableGen} devices = get_available_components(model, sys) add_parameters!(container, ActivePowerTimeSeriesParameter, devices, model) add_parameters!(container, ReactivePowerTimeSeriesParameter, devices, model) process_market_bid_parameters!(container, devices, model) add_to_expression!( container, ActivePowerBalance, ActivePowerTimeSeriesParameter, devices, model, network_model, ) add_to_expression!( container, ReactivePowerBalance, ReactivePowerTimeSeriesParameter, devices, model, network_model, ) add_event_arguments!(container, devices, model, network_model) return end function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ArgumentConstructStage, model::DeviceModel{R, FixedOutput}, network_model::NetworkModel{<:PM.AbstractActivePowerModel}, ) where {R <: PSY.RenewableGen} devices = get_available_components(model, sys) add_parameters!(container, ActivePowerTimeSeriesParameter, devices, model) add_to_expression!( container, ActivePowerBalance, ActivePowerTimeSeriesParameter, devices, model, network_model, ) process_market_bid_parameters!(container, devices, model) add_event_arguments!(container, devices, model, network_model) return end function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ModelConstructStage, model::DeviceModel{<:PSY.RenewableGen, FixedOutput}, network_model::NetworkModel{<:PM.AbstractPowerModel}, ) # FixedOutput doesn't add any constraints to the model. This function covers # AbstractPowerModel and AbtractActivePowerModel return end ================================================ FILE: src/devices_models/device_constructors/source_constructor.jl ================================================ """ This function creates the arguments for the model for an import/export formulation for Source devices """ function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ArgumentConstructStage, model::DeviceModel{T, D}, network_model::NetworkModel{<:PM.AbstractPowerModel}, ) where { T <: PSY.Source, D <: ImportExportSourceModel, } devices = get_available_components(model, sys) add_variables!(container, ActivePowerInVariable, devices, D()) add_variables!(container, ActivePowerOutVariable, devices, D()) add_variables!(container, ReactivePowerVariable, devices, D()) add_expressions!(container, NetActivePower, devices, model) if haskey(get_time_series_names(model), ActivePowerOutTimeSeriesParameter) add_parameters!(container, ActivePowerOutTimeSeriesParameter, devices, model) end if haskey(get_time_series_names(model), ActivePowerInTimeSeriesParameter) add_parameters!(container, ActivePowerInTimeSeriesParameter, devices, model) end if get_attribute(model, "reservation") add_variables!(container, ReservationVariable, devices, D()) end process_import_export_parameters!(container, devices, model) add_to_expression!( container, ActivePowerBalance, ActivePowerInVariable, devices, model, network_model, ) add_to_expression!( container, ActivePowerBalance, ActivePowerOutVariable, devices, model, network_model, ) add_to_expression!( container, ReactivePowerBalance, ReactivePowerVariable, devices, model, network_model, ) add_cost_expressions!(container, devices, model) add_to_expression!( container, ActivePowerRangeExpressionLB, ActivePowerOutVariable, devices, model, network_model, ) add_to_expression!( container, ActivePowerRangeExpressionUB, ActivePowerOutVariable, devices, model, network_model, ) add_feedforward_arguments!(container, model, devices) return end """ This function creates the constraints for the model for an import/export formulation for Source devices """ function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ModelConstructStage, model::DeviceModel{T, D}, network_model::NetworkModel{<:PM.AbstractPowerModel}, ) where { T <: PSY.Source, D <: ImportExportSourceModel, } devices = get_available_components(model, sys) add_constraints!( container, ActivePowerVariableLimitsConstraint, ActivePowerRangeExpressionLB, devices, model, network_model, ) add_constraints!( container, ActivePowerVariableLimitsConstraint, ActivePowerRangeExpressionUB, devices, model, network_model, ) add_constraints!( container, InputActivePowerVariableLimitsConstraint, ActivePowerInVariable, devices, model, network_model, ) add_constraints!( container, ReactivePowerVariableLimitsConstraint, ReactivePowerVariable, devices, model, network_model, ) add_constraints!(container, ImportExportBudgetConstraint, devices, model, network_model) if haskey(get_time_series_names(model), ActivePowerOutTimeSeriesParameter) add_constraints!( container, ActivePowerOutVariableTimeSeriesLimitsConstraint, ActivePowerRangeExpressionUB, devices, model, network_model, ) end if haskey(get_time_series_names(model), ActivePowerInTimeSeriesParameter) add_constraints!( container, ActivePowerInVariableTimeSeriesLimitsConstraint, ActivePowerInVariable, devices, model, network_model, ) end add_feedforward_constraints!(container, model, devices) objective_function!(container, devices, model, get_network_formulation(network_model)) add_constraint_dual!(container, sys, model) return end """ This function creates the arguments for the model for an import/export formulation for Source devices """ function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ArgumentConstructStage, model::DeviceModel{T, D}, network_model::NetworkModel{<:PM.AbstractActivePowerModel}, ) where { T <: PSY.Source, D <: ImportExportSourceModel, } devices = get_available_components(model, sys) add_variables!(container, ActivePowerInVariable, devices, D()) add_variables!(container, ActivePowerOutVariable, devices, D()) add_expressions!(container, NetActivePower, devices, model) if haskey(get_time_series_names(model), ActivePowerOutTimeSeriesParameter) add_parameters!(container, ActivePowerOutTimeSeriesParameter, devices, model) end if haskey(get_time_series_names(model), ActivePowerInTimeSeriesParameter) add_parameters!(container, ActivePowerInTimeSeriesParameter, devices, model) end if get_attribute(model, "reservation") add_variables!(container, ReservationVariable, devices, D()) end process_import_export_parameters!(container, devices, model) add_to_expression!( container, ActivePowerBalance, ActivePowerInVariable, devices, model, network_model, ) add_to_expression!( container, ActivePowerBalance, ActivePowerOutVariable, devices, model, network_model, ) add_to_expression!( container, NetActivePower, ActivePowerInVariable(), devices, model, ) add_to_expression!( container, NetActivePower, ActivePowerOutVariable(), devices, model, ) add_cost_expressions!(container, devices, model) add_to_expression!( container, ActivePowerRangeExpressionLB, ActivePowerOutVariable, devices, model, network_model, ) add_to_expression!( container, ActivePowerRangeExpressionUB, ActivePowerOutVariable, devices, model, network_model, ) add_feedforward_arguments!(container, model, devices) return end """ This function creates the constraints for the model for an import/export formulation for Source devices """ function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ModelConstructStage, model::DeviceModel{T, D}, network_model::NetworkModel{<:PM.AbstractActivePowerModel}, ) where { T <: PSY.Source, D <: ImportExportSourceModel, } devices = get_available_components(model, sys) add_constraints!( container, ActivePowerVariableLimitsConstraint, ActivePowerRangeExpressionLB, devices, model, network_model, ) add_constraints!( container, ActivePowerVariableLimitsConstraint, ActivePowerRangeExpressionUB, devices, model, network_model, ) add_constraints!( container, InputActivePowerVariableLimitsConstraint, ActivePowerInVariable, devices, model, network_model, ) add_constraints!(container, ImportExportBudgetConstraint, devices, model, network_model) if haskey(get_time_series_names(model), ActivePowerOutTimeSeriesParameter) add_constraints!( container, ActivePowerOutVariableTimeSeriesLimitsConstraint, ActivePowerRangeExpressionUB, devices, model, network_model, ) end if haskey(get_time_series_names(model), ActivePowerInTimeSeriesParameter) add_constraints!( container, ActivePowerInVariableTimeSeriesLimitsConstraint, ActivePowerInVariable, devices, model, network_model, ) end add_feedforward_constraints!(container, model, devices) objective_function!(container, devices, model, get_network_formulation(network_model)) add_constraint_dual!(container, sys, model) return end """ This function creates the arguments for the model for a FixedOutput formulation for Source devices """ function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ArgumentConstructStage, model::DeviceModel{T, D}, network_model::NetworkModel{<:PM.AbstractActivePowerModel}, ) where { T <: PSY.Source, D <: FixedOutput, } devices = get_available_components(model, sys) add_parameters!(container, ActivePowerInTimeSeriesParameter, devices, model) add_parameters!(container, ActivePowerOutTimeSeriesParameter, devices, model) add_to_expression!( container, ActivePowerBalance, ActivePowerInTimeSeriesParameter, devices, model, network_model, ) add_to_expression!( container, ActivePowerBalance, ActivePowerOutTimeSeriesParameter, devices, model, network_model, ) add_event_arguments!(container, devices, model, network_model) return end """ This function creates the constraints for the model for a FixedOutput formulation for Source devices (no constraints added for FixedOutput) """ function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ModelConstructStage, model::DeviceModel{T, D}, network_model::NetworkModel{<:PM.AbstractActivePowerModel}, ) where { T <: PSY.Source, D <: FixedOutput, } # FixedOutput doesn't add any constraints to the model. return end ================================================ FILE: src/devices_models/device_constructors/thermalgeneration_constructor.jl ================================================ # TODO: Security constrained models implement the correct functions for the model function has_security_arguments(device_model::DeviceModel)::Bool return true end # TODO: Security constrained models implement the correct functions for the model function has_security_model(device_model::DeviceModel)::Bool return true end @inline function _handle_common_thermal_parameters!( container::OptimizationContainer, devices::IS.FlattenIteratorWrapper{T}, model::DeviceModel{T}, ) where {T <: PSY.ThermalGen} if haskey(get_time_series_names(model), FuelCostParameter) add_parameters!(container, FuelCostParameter, devices, model) end process_market_bid_parameters!(container, devices, model) end function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ArgumentConstructStage, device_model::DeviceModel{T, FixedOutput}, network_model::NetworkModel{<:PM.AbstractActivePowerModel}, ) where {T <: PSY.ThermalGen} devices = get_available_components(device_model, sys) add_parameters!(container, ActivePowerTimeSeriesParameter, devices, device_model) add_to_expression!( container, ActivePowerBalance, ActivePowerTimeSeriesParameter, devices, device_model, network_model, ) add_event_arguments!(container, devices, device_model, network_model) return end function construct_device!( ::OptimizationContainer, ::PSY.System, ::ModelConstructStage, ::DeviceModel{<:PSY.ThermalGen, FixedOutput}, network_model::NetworkModel{<:PM.AbstractPowerModel}, ) # FixedOutput doesn't add any constraints to the model. This function covers # AbstractPowerModel and AbtractActivePowerModel return end """ This function creates the arguments for the model for a full thermal dispatch formulation depending on combination of devices, device_formulation and system_formulation """ function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ArgumentConstructStage, device_model::DeviceModel{T, D}, network_model::NetworkModel{<:PM.AbstractPowerModel}, ) where { T <: PSY.ThermalGen, D <: AbstractStandardUnitCommitment, } devices = get_available_components(device_model, sys) add_variables!(container, ActivePowerVariable, devices, D()) add_variables!(container, ReactivePowerVariable, devices, D()) add_variables!(container, OnVariable, devices, D()) add_variables!(container, StartVariable, devices, D()) add_variables!(container, StopVariable, devices, D()) add_variables!(container, TimeDurationOn, devices, D()) add_variables!(container, TimeDurationOff, devices, D()) initial_conditions!(container, devices, D()) if haskey(get_time_series_names(device_model), ActivePowerTimeSeriesParameter) add_parameters!(container, ActivePowerTimeSeriesParameter, devices, device_model) end _handle_common_thermal_parameters!(container, devices, device_model) add_to_expression!( container, ActivePowerBalance, ActivePowerVariable, devices, device_model, network_model, ) add_to_expression!( container, ReactivePowerBalance, ReactivePowerVariable, devices, device_model, network_model, ) add_cost_expressions!(container, devices, device_model) add_to_expression!( container, ActivePowerRangeExpressionLB, ActivePowerVariable, devices, device_model, network_model, ) add_to_expression!( container, ActivePowerRangeExpressionUB, ActivePowerVariable, devices, device_model, network_model, ) add_to_expression!( container, FuelConsumptionExpression, ActivePowerVariable, devices, device_model, ) if get_use_slacks(device_model) add_variables!(container, RateofChangeConstraintSlackUp, devices, D()) add_variables!(container, RateofChangeConstraintSlackDown, devices, D()) end add_feedforward_arguments!(container, device_model, devices) add_event_arguments!(container, devices, device_model, network_model) return end """ This function creates the constraints for the model for a full thermal dispatch formulation depending on combination of devices, device_formulation and system_formulation """ function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ModelConstructStage, device_model::DeviceModel{T, U}, network_model::NetworkModel{<:PM.AbstractPowerModel}, ) where { T <: PSY.ThermalGen, U <: AbstractStandardUnitCommitment, } devices = get_available_components(device_model, sys) add_constraints!( container, ActivePowerVariableLimitsConstraint, ActivePowerRangeExpressionLB, devices, device_model, network_model, ) add_constraints!( container, ActivePowerVariableLimitsConstraint, ActivePowerRangeExpressionUB, devices, device_model, network_model, ) add_constraints!( container, ReactivePowerVariableLimitsConstraint, ReactivePowerVariable, devices, device_model, network_model, ) add_constraints!(container, CommitmentConstraint, devices, device_model, network_model) add_constraints!(container, RampConstraint, devices, device_model, network_model) add_constraints!(container, DurationConstraint, devices, device_model, network_model) if haskey(get_time_series_names(device_model), ActivePowerTimeSeriesParameter) add_constraints!( container, ActivePowerVariableTimeSeriesLimitsConstraint, ActivePowerRangeExpressionUB, devices, device_model, network_model, ) end add_feedforward_constraints!(container, device_model, devices) add_event_constraints!(container, devices, device_model, network_model) objective_function!( container, devices, device_model, get_network_formulation(network_model), ) if has_security_arguments(device_model) # TODO: SecurityConstrainedModels Implemenation of G-1 #add_constraints!( # container, # SecurityConstraint, # devices, # device_model, # network_model, #) end add_constraint_dual!(container, sys, device_model) return end """ This function creates the arguments model for a full thermal dispatch formulation depending on combination of devices, device_formulation and system_formulation """ function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ArgumentConstructStage, device_model::DeviceModel{T, D}, network_model::NetworkModel{<:PM.AbstractActivePowerModel}, ) where {T <: PSY.ThermalGen, D <: AbstractStandardUnitCommitment} devices = get_available_components(device_model, sys) add_variables!(container, ActivePowerVariable, devices, D()) add_variables!(container, OnVariable, devices, D()) add_variables!(container, StartVariable, devices, D()) add_variables!(container, StopVariable, devices, D()) add_variables!(container, TimeDurationOn, devices, D()) add_variables!(container, TimeDurationOff, devices, D()) initial_conditions!(container, devices, D()) if haskey(get_time_series_names(device_model), ActivePowerTimeSeriesParameter) add_parameters!(container, ActivePowerTimeSeriesParameter, devices, device_model) end _handle_common_thermal_parameters!(container, devices, device_model) add_to_expression!( container, ActivePowerBalance, ActivePowerVariable, devices, device_model, network_model, ) add_cost_expressions!(container, devices, device_model) add_to_expression!( container, ActivePowerRangeExpressionLB, ActivePowerVariable, devices, device_model, network_model, ) add_to_expression!( container, ActivePowerRangeExpressionUB, ActivePowerVariable, devices, device_model, network_model, ) add_to_expression!( container, FuelConsumptionExpression, ActivePowerVariable, devices, device_model, ) if get_use_slacks(device_model) add_variables!(container, RateofChangeConstraintSlackUp, devices, D()) add_variables!(container, RateofChangeConstraintSlackDown, devices, D()) end add_feedforward_arguments!(container, device_model, devices) add_event_arguments!(container, devices, device_model, network_model) return end """ This function creates the constraints for the model for a full thermal dispatch formulation depending on combination of devices, device_formulation and system_formulation """ function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ModelConstructStage, device_model::DeviceModel{T, <:AbstractStandardUnitCommitment}, network_model::NetworkModel{<:PM.AbstractActivePowerModel}, ) where {T <: PSY.ThermalGen} devices = get_available_components(device_model, sys) add_constraints!( container, ActivePowerVariableLimitsConstraint, ActivePowerRangeExpressionLB, devices, device_model, network_model, ) add_constraints!( container, ActivePowerVariableLimitsConstraint, ActivePowerRangeExpressionUB, devices, device_model, network_model, ) add_constraints!(container, CommitmentConstraint, devices, device_model, network_model) add_constraints!(container, RampConstraint, devices, device_model, network_model) add_constraints!(container, DurationConstraint, devices, device_model, network_model) if haskey(get_time_series_names(device_model), ActivePowerTimeSeriesParameter) add_constraints!( container, ActivePowerVariableTimeSeriesLimitsConstraint, ActivePowerRangeExpressionUB, devices, device_model, network_model, ) end add_feedforward_constraints!(container, device_model, devices) objective_function!( container, devices, device_model, get_network_formulation(network_model), ) add_event_constraints!(container, devices, device_model, network_model) add_constraint_dual!(container, sys, device_model) return end """ This function creates the model for a full thermal dispatch formulation depending on combination of devices, device_formulation and system_formulation """ function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ArgumentConstructStage, device_model::DeviceModel{T, ThermalBasicUnitCommitment}, network_model::NetworkModel{<:PM.AbstractPowerModel}, ) where {T <: PSY.ThermalGen} devices = get_available_components(device_model, sys) add_variables!(container, ActivePowerVariable, devices, ThermalBasicUnitCommitment()) add_variables!(container, ReactivePowerVariable, devices, ThermalBasicUnitCommitment()) add_variables!(container, OnVariable, devices, ThermalBasicUnitCommitment()) add_variables!(container, StartVariable, devices, ThermalBasicUnitCommitment()) add_variables!(container, StopVariable, devices, ThermalBasicUnitCommitment()) add_variables!(container, TimeDurationOn, devices, ThermalBasicUnitCommitment()) add_variables!(container, TimeDurationOff, devices, ThermalBasicUnitCommitment()) initial_conditions!(container, devices, ThermalBasicUnitCommitment()) if haskey(get_time_series_names(device_model), ActivePowerTimeSeriesParameter) add_parameters!(container, ActivePowerTimeSeriesParameter, devices, device_model) end _handle_common_thermal_parameters!(container, devices, device_model) add_to_expression!( container, ActivePowerBalance, ActivePowerVariable, devices, device_model, network_model, ) add_to_expression!( container, ReactivePowerBalance, ReactivePowerVariable, devices, device_model, network_model, ) add_cost_expressions!(container, devices, device_model) add_to_expression!( container, ActivePowerRangeExpressionLB, ActivePowerVariable, devices, device_model, network_model, ) add_to_expression!( container, ActivePowerRangeExpressionUB, ActivePowerVariable, devices, device_model, network_model, ) add_to_expression!( container, FuelConsumptionExpression, ActivePowerVariable, devices, device_model, ) if get_use_slacks(device_model) add_variables!( container, RateofChangeConstraintSlackUp, devices, ThermalBasicUnitCommitment(), ) add_variables!( container, RateofChangeConstraintSlackDown, devices, ThermalBasicUnitCommitment(), ) end add_feedforward_arguments!(container, device_model, devices) add_event_arguments!(container, devices, device_model, network_model) return end """ This function creates the model for a full thermal dispatch formulation depending on combination of devices, device_formulation and system_formulation """ function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ModelConstructStage, device_model::DeviceModel{T, ThermalBasicUnitCommitment}, network_model::NetworkModel{<:PM.AbstractPowerModel}, ) where {T <: PSY.ThermalGen} devices = get_available_components(device_model, sys) add_constraints!( container, ActivePowerVariableLimitsConstraint, ActivePowerRangeExpressionLB, devices, device_model, network_model, ) add_constraints!( container, ActivePowerVariableLimitsConstraint, ActivePowerRangeExpressionUB, devices, device_model, network_model, ) add_constraints!( container, ReactivePowerVariableLimitsConstraint, ReactivePowerVariable, devices, device_model, network_model, ) add_constraints!(container, CommitmentConstraint, devices, device_model, network_model) if haskey(get_time_series_names(device_model), ActivePowerTimeSeriesParameter) add_constraints!( container, ActivePowerVariableTimeSeriesLimitsConstraint, ActivePowerRangeExpressionUB, devices, device_model, network_model, ) end add_feedforward_constraints!(container, device_model, devices) objective_function!( container, devices, device_model, get_network_formulation(network_model), ) add_event_constraints!(container, devices, device_model, network_model) add_constraint_dual!(container, sys, device_model) return end """ This function creates the arguments for the model for a full thermal dispatch formulation depending on combination of devices, device_formulation and system_formulation """ function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ArgumentConstructStage, device_model::DeviceModel{T, ThermalBasicUnitCommitment}, network_model::NetworkModel{<:PM.AbstractActivePowerModel}, ) where {T <: PSY.ThermalGen} devices = get_available_components(device_model, sys) add_variables!(container, ActivePowerVariable, devices, ThermalBasicUnitCommitment()) add_variables!(container, OnVariable, devices, ThermalBasicUnitCommitment()) add_variables!(container, StartVariable, devices, ThermalBasicUnitCommitment()) add_variables!(container, StopVariable, devices, ThermalBasicUnitCommitment()) add_variables!(container, TimeDurationOn, devices, ThermalBasicUnitCommitment()) add_variables!(container, TimeDurationOff, devices, ThermalBasicUnitCommitment()) initial_conditions!(container, devices, ThermalBasicUnitCommitment()) if haskey(get_time_series_names(device_model), ActivePowerTimeSeriesParameter) add_parameters!(container, ActivePowerTimeSeriesParameter, devices, device_model) end _handle_common_thermal_parameters!(container, devices, device_model) add_to_expression!( container, ActivePowerBalance, ActivePowerVariable, devices, device_model, network_model, ) add_cost_expressions!(container, devices, device_model) add_to_expression!( container, ActivePowerRangeExpressionLB, ActivePowerVariable, devices, device_model, network_model, ) add_to_expression!( container, ActivePowerRangeExpressionUB, ActivePowerVariable, devices, device_model, network_model, ) add_to_expression!( container, FuelConsumptionExpression, ActivePowerVariable, devices, device_model, ) if get_use_slacks(device_model) add_variables!( container, RateofChangeConstraintSlackUp, devices, ThermalBasicUnitCommitment(), ) add_variables!( container, RateofChangeConstraintSlackDown, devices, ThermalBasicUnitCommitment(), ) end add_feedforward_arguments!(container, device_model, devices) add_event_arguments!(container, devices, device_model, network_model) return end """ This function creates the constraints for the model for a full thermal dispatch formulation depending on combination of devices, device_formulation and system_formulation """ function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ModelConstructStage, device_model::DeviceModel{T, ThermalBasicUnitCommitment}, network_model::NetworkModel{<:PM.AbstractActivePowerModel}, ) where {T <: PSY.ThermalGen} devices = get_available_components(device_model, sys) add_constraints!( container, ActivePowerVariableLimitsConstraint, ActivePowerRangeExpressionLB, devices, device_model, network_model, ) add_constraints!( container, ActivePowerVariableLimitsConstraint, ActivePowerRangeExpressionUB, devices, device_model, network_model, ) add_constraints!(container, CommitmentConstraint, devices, device_model, network_model) if haskey(get_time_series_names(device_model), ActivePowerTimeSeriesParameter) add_constraints!( container, ActivePowerVariableTimeSeriesLimitsConstraint, ActivePowerRangeExpressionUB, devices, device_model, network_model, ) end add_feedforward_constraints!(container, device_model, devices) objective_function!( container, devices, device_model, get_network_formulation(network_model), ) add_event_constraints!(container, devices, device_model, network_model) add_constraint_dual!(container, sys, device_model) return end """ This function creates the model for a full thermal dispatch formulation depending on combination of devices, device_formulation and system_formulation """ function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ArgumentConstructStage, device_model::DeviceModel{T, ThermalStandardDispatch}, network_model::NetworkModel{<:PM.AbstractPowerModel}, ) where {T <: PSY.ThermalGen} devices = get_available_components(device_model, sys) add_variables!(container, ActivePowerVariable, devices, ThermalStandardDispatch()) add_variables!(container, ReactivePowerVariable, devices, ThermalStandardDispatch()) initial_conditions!(container, devices, ThermalStandardDispatch()) _handle_common_thermal_parameters!(container, devices, device_model) add_to_expression!( container, ActivePowerBalance, ActivePowerVariable, devices, device_model, network_model, ) add_to_expression!( container, ReactivePowerBalance, ReactivePowerVariable, devices, device_model, network_model, ) add_cost_expressions!(container, devices, device_model) add_to_expression!( container, ActivePowerRangeExpressionLB, ActivePowerVariable, devices, device_model, network_model, ) add_to_expression!( container, ActivePowerRangeExpressionUB, ActivePowerVariable, devices, device_model, network_model, ) add_to_expression!( container, FuelConsumptionExpression, ActivePowerVariable, devices, device_model, ) if get_use_slacks(device_model) add_variables!( container, RateofChangeConstraintSlackUp, devices, ThermalStandardDispatch(), ) add_variables!( container, RateofChangeConstraintSlackDown, devices, ThermalStandardDispatch(), ) end add_feedforward_arguments!(container, device_model, devices) add_event_arguments!(container, devices, device_model, network_model) return end """ This function creates the model for a full thermal dispatch formulation depending on combination of devices, device_formulation and system_formulation """ function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ModelConstructStage, device_model::DeviceModel{T, ThermalStandardDispatch}, network_model::NetworkModel{<:PM.AbstractPowerModel}, ) where {T <: PSY.ThermalGen} devices = get_available_components(device_model, sys) add_constraints!( container, ActivePowerVariableLimitsConstraint, ActivePowerRangeExpressionLB, devices, device_model, network_model, ) add_constraints!( container, ActivePowerVariableLimitsConstraint, ActivePowerRangeExpressionUB, devices, device_model, network_model, ) add_constraints!( container, ReactivePowerVariableLimitsConstraint, ReactivePowerVariable, devices, device_model, network_model, ) add_constraints!(container, RampConstraint, devices, device_model, network_model) add_feedforward_constraints!(container, device_model, devices) objective_function!( container, devices, device_model, get_network_formulation(network_model), ) add_event_constraints!(container, devices, device_model, network_model) add_constraint_dual!(container, sys, device_model) return end """ This function creates the arguments for the model for a full thermal dispatch formulation depending on combination of devices, device_formulation and system_formulation """ function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ArgumentConstructStage, device_model::DeviceModel{T, ThermalStandardDispatch}, network_model::NetworkModel{<:PM.AbstractActivePowerModel}, ) where {T <: PSY.ThermalGen} devices = get_available_components(device_model, sys) add_variables!(container, ActivePowerVariable, devices, ThermalStandardDispatch()) initial_conditions!(container, devices, ThermalStandardDispatch()) _handle_common_thermal_parameters!(container, devices, device_model) add_to_expression!( container, ActivePowerBalance, ActivePowerVariable, devices, device_model, network_model, ) add_cost_expressions!(container, devices, device_model) add_to_expression!( container, ActivePowerRangeExpressionLB, ActivePowerVariable, devices, device_model, network_model, ) add_to_expression!( container, ActivePowerRangeExpressionUB, ActivePowerVariable, devices, device_model, network_model, ) add_to_expression!( container, FuelConsumptionExpression, ActivePowerVariable, devices, device_model, ) if get_use_slacks(device_model) add_variables!( container, RateofChangeConstraintSlackUp, devices, ThermalStandardDispatch(), ) add_variables!( container, RateofChangeConstraintSlackDown, devices, ThermalStandardDispatch(), ) end add_feedforward_arguments!(container, device_model, devices) add_event_arguments!(container, devices, device_model, network_model) return end """ This function creates the constraints for the model for a full thermal dispatch formulation depending on combination of devices, device_formulation and system_formulation """ function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ModelConstructStage, device_model::DeviceModel{T, ThermalStandardDispatch}, network_model::NetworkModel{<:PM.AbstractActivePowerModel}, ) where {T <: PSY.ThermalGen} devices = get_available_components(device_model, sys) add_constraints!( container, ActivePowerVariableLimitsConstraint, ActivePowerRangeExpressionLB, devices, device_model, network_model, ) add_constraints!( container, ActivePowerVariableLimitsConstraint, ActivePowerRangeExpressionUB, devices, device_model, network_model, ) add_constraints!(container, RampConstraint, devices, device_model, network_model) add_feedforward_constraints!(container, device_model, devices) objective_function!( container, devices, device_model, get_network_formulation(network_model), ) add_event_constraints!(container, devices, device_model, network_model) add_constraint_dual!(container, sys, device_model) return end function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ArgumentConstructStage, device_model::DeviceModel{T, D}, network_model::NetworkModel{<:PM.AbstractPowerModel}, ) where { T <: PSY.ThermalGen, D <: AbstractThermalDispatchFormulation, } devices = get_available_components(device_model, sys) add_variables!(container, ActivePowerVariable, devices, D()) add_variables!(container, ReactivePowerVariable, devices, D()) _handle_common_thermal_parameters!(container, devices, device_model) add_to_expression!( container, ActivePowerBalance, ActivePowerVariable, devices, device_model, network_model, ) add_to_expression!( container, ReactivePowerBalance, ReactivePowerVariable, devices, device_model, network_model, ) add_cost_expressions!(container, devices, device_model) add_to_expression!( container, ActivePowerRangeExpressionLB, ActivePowerVariable, devices, device_model, network_model, ) add_to_expression!( container, ActivePowerRangeExpressionUB, ActivePowerVariable, devices, device_model, network_model, ) add_to_expression!( container, FuelConsumptionExpression, ActivePowerVariable, devices, device_model, ) if get_use_slacks(device_model) add_variables!(container, RateofChangeConstraintSlackUp, devices, D()) add_variables!(container, RateofChangeConstraintSlackDown, devices, D()) end add_feedforward_arguments!(container, device_model, devices) add_event_arguments!(container, devices, device_model, network_model) return end function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ModelConstructStage, device_model::DeviceModel{T, <:AbstractThermalDispatchFormulation}, network_model::NetworkModel{<:PM.AbstractPowerModel}, ) where {T <: PSY.ThermalGen} devices = get_available_components(device_model, sys) add_constraints!( container, ActivePowerVariableLimitsConstraint, ActivePowerRangeExpressionLB, devices, device_model, network_model, ) add_constraints!( container, ActivePowerVariableLimitsConstraint, ActivePowerRangeExpressionUB, devices, device_model, network_model, ) add_constraints!( container, ReactivePowerVariableLimitsConstraint, ReactivePowerVariable, devices, device_model, network_model, ) add_feedforward_constraints!(container, device_model, devices) objective_function!( container, devices, device_model, get_network_formulation(network_model), ) add_event_constraints!(container, devices, device_model, network_model) add_constraint_dual!(container, sys, device_model) return end function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ArgumentConstructStage, device_model::DeviceModel{T, D}, network_model::NetworkModel{<:PM.AbstractActivePowerModel}, ) where { T <: PSY.ThermalGen, D <: AbstractThermalDispatchFormulation, } devices = get_available_components(device_model, sys) add_variables!(container, ActivePowerVariable, devices, D()) _handle_common_thermal_parameters!(container, devices, device_model) add_to_expression!( container, ActivePowerBalance, ActivePowerVariable, devices, device_model, network_model, ) add_cost_expressions!(container, devices, device_model) add_to_expression!( container, ActivePowerRangeExpressionLB, ActivePowerVariable, devices, device_model, network_model, ) add_to_expression!( container, ActivePowerRangeExpressionUB, ActivePowerVariable, devices, device_model, network_model, ) add_to_expression!( container, FuelConsumptionExpression, ActivePowerVariable, devices, device_model, ) if get_use_slacks(device_model) add_variables!(container, RateofChangeConstraintSlackUp, devices, D()) add_variables!(container, RateofChangeConstraintSlackDown, devices, D()) end add_feedforward_arguments!(container, device_model, devices) add_event_arguments!(container, devices, device_model, network_model) return end function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ModelConstructStage, device_model::DeviceModel{T, <:AbstractThermalDispatchFormulation}, network_model::NetworkModel{<:PM.AbstractActivePowerModel}, ) where {T <: PSY.ThermalGen} devices = get_available_components(device_model, sys) add_constraints!( container, ActivePowerVariableLimitsConstraint, ActivePowerRangeExpressionLB, devices, device_model, network_model, ) add_constraints!( container, ActivePowerVariableLimitsConstraint, ActivePowerRangeExpressionUB, devices, device_model, network_model, ) add_feedforward_constraints!(container, device_model, devices) add_event_constraints!(container, devices, device_model, network_model) objective_function!( container, devices, device_model, get_network_formulation(network_model), ) return end function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ArgumentConstructStage, device_model::DeviceModel{PSY.ThermalMultiStart, ThermalMultiStartUnitCommitment}, network_model::NetworkModel{<:PM.AbstractPowerModel}, ) devices = get_available_components(device_model, sys) add_variables!( container, PowerAboveMinimumVariable, devices, ThermalMultiStartUnitCommitment(), ) add_variables!( container, ReactivePowerVariable, devices, ThermalMultiStartUnitCommitment(), ) add_variables!(container, OnVariable, devices, ThermalMultiStartUnitCommitment()) add_variables!(container, StopVariable, devices, ThermalMultiStartUnitCommitment()) add_variables!(container, StartVariable, devices, ThermalMultiStartUnitCommitment()) add_variables!(container, ColdStartVariable, devices, ThermalMultiStartUnitCommitment()) add_variables!(container, WarmStartVariable, devices, ThermalMultiStartUnitCommitment()) add_variables!(container, HotStartVariable, devices, ThermalMultiStartUnitCommitment()) add_variables!(container, TimeDurationOn, devices, ThermalMultiStartUnitCommitment()) add_variables!(container, TimeDurationOff, devices, ThermalMultiStartUnitCommitment()) add_variables!(container, PowerOutput, devices, ThermalMultiStartUnitCommitment()) initial_conditions!(container, devices, ThermalMultiStartUnitCommitment()) if haskey(get_time_series_names(device_model), ActivePowerTimeSeriesParameter) add_parameters!(container, ActivePowerTimeSeriesParameter, devices, device_model) end _handle_common_thermal_parameters!(container, devices, device_model) add_to_expression!( container, ActivePowerBalance, PowerAboveMinimumVariable, devices, device_model, network_model, ) add_to_expression!( container, ActivePowerBalance, OnVariable, devices, device_model, network_model, ) add_to_expression!( container, ReactivePowerBalance, ReactivePowerVariable, devices, device_model, network_model, ) add_cost_expressions!(container, devices, device_model) add_to_expression!( container, ActivePowerRangeExpressionLB, PowerAboveMinimumVariable, devices, device_model, network_model, ) add_to_expression!( container, ActivePowerRangeExpressionUB, PowerAboveMinimumVariable, devices, device_model, network_model, ) add_to_expression!( container, FuelConsumptionExpression, PowerAboveMinimumVariable, devices, device_model, ) if get_use_slacks(device_model) add_variables!( container, RateofChangeConstraintSlackUp, devices, ThermalMultiStartUnitCommitment(), ) add_variables!( container, RateofChangeConstraintSlackDown, devices, ThermalMultiStartUnitCommitment(), ) end add_feedforward_arguments!(container, device_model, devices) add_event_arguments!(container, devices, device_model, network_model) return end function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ModelConstructStage, device_model::DeviceModel{PSY.ThermalMultiStart, ThermalMultiStartUnitCommitment}, network_model::NetworkModel{<:PM.AbstractPowerModel}, ) devices = get_available_components(device_model, sys) add_constraints!( container, ActivePowerVariableLimitsConstraint, ActivePowerRangeExpressionLB, devices, device_model, network_model, ) add_constraints!( container, ActivePowerVariableLimitsConstraint, ActivePowerRangeExpressionUB, devices, device_model, network_model, ) add_constraints!( container, ReactivePowerVariableLimitsConstraint, ReactivePowerVariable, devices, device_model, network_model, ) add_constraints!(container, CommitmentConstraint, devices, device_model, network_model) add_constraints!(container, RampConstraint, devices, device_model, network_model) add_constraints!(container, DurationConstraint, devices, device_model, network_model) add_constraints!( container, StartupTimeLimitTemperatureConstraint, devices, device_model, network_model, ) add_constraints!(container, StartTypeConstraint, devices, device_model, network_model) add_constraints!( container, StartupInitialConditionConstraint, devices, device_model, network_model, ) add_constraints!( container, ActiveRangeICConstraint, devices, device_model, network_model, ) if haskey(get_time_series_names(device_model), ActivePowerTimeSeriesParameter) add_constraints!( container, ActivePowerVariableTimeSeriesLimitsConstraint, ActivePowerRangeExpressionUB, devices, device_model, network_model, ) end add_feedforward_constraints!(container, device_model, devices) objective_function!( container, devices, device_model, get_network_formulation(network_model), ) add_event_constraints!(container, devices, device_model, network_model) add_constraint_dual!(container, sys, device_model) return end function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ArgumentConstructStage, device_model::DeviceModel{PSY.ThermalMultiStart, ThermalMultiStartUnitCommitment}, network_model::NetworkModel{<:PM.AbstractActivePowerModel}, ) devices = get_available_components(device_model, sys) add_variables!( container, PowerAboveMinimumVariable, devices, ThermalMultiStartUnitCommitment(), ) add_variables!(container, OnVariable, devices, ThermalMultiStartUnitCommitment()) add_variables!(container, StopVariable, devices, ThermalMultiStartUnitCommitment()) add_variables!(container, StartVariable, devices, ThermalMultiStartUnitCommitment()) add_variables!(container, ColdStartVariable, devices, ThermalMultiStartUnitCommitment()) add_variables!(container, WarmStartVariable, devices, ThermalMultiStartUnitCommitment()) add_variables!(container, HotStartVariable, devices, ThermalMultiStartUnitCommitment()) add_variables!(container, TimeDurationOn, devices, ThermalMultiStartUnitCommitment()) add_variables!(container, TimeDurationOff, devices, ThermalMultiStartUnitCommitment()) add_variables!(container, PowerOutput, devices, ThermalMultiStartUnitCommitment()) if haskey(get_time_series_names(device_model), ActivePowerTimeSeriesParameter) add_parameters!(container, ActivePowerTimeSeriesParameter, devices, device_model) end _handle_common_thermal_parameters!(container, devices, device_model) add_to_expression!( container, ActivePowerBalance, PowerAboveMinimumVariable, devices, device_model, network_model, ) add_to_expression!( container, ActivePowerBalance, OnVariable, devices, device_model, network_model, ) add_cost_expressions!(container, devices, device_model) add_to_expression!( container, ActivePowerRangeExpressionLB, PowerAboveMinimumVariable, devices, device_model, network_model, ) add_to_expression!( container, ActivePowerRangeExpressionUB, PowerAboveMinimumVariable, devices, device_model, network_model, ) add_to_expression!( container, FuelConsumptionExpression, PowerAboveMinimumVariable, devices, device_model, ) if get_use_slacks(device_model) add_variables!( container, RateofChangeConstraintSlackUp, devices, ThermalMultiStartUnitCommitment(), ) add_variables!( container, RateofChangeConstraintSlackDown, devices, ThermalMultiStartUnitCommitment(), ) end add_feedforward_arguments!(container, device_model, devices) add_event_arguments!(container, devices, device_model, network_model) return end function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ModelConstructStage, device_model::DeviceModel{PSY.ThermalMultiStart, ThermalMultiStartUnitCommitment}, network_model::NetworkModel{<:PM.AbstractActivePowerModel}, ) devices = get_available_components(device_model, sys) initial_conditions!(container, devices, ThermalMultiStartUnitCommitment()) add_constraints!( container, ActivePowerVariableLimitsConstraint, ActivePowerRangeExpressionLB, devices, device_model, network_model, ) add_constraints!( container, ActivePowerVariableLimitsConstraint, ActivePowerRangeExpressionUB, devices, device_model, network_model, ) add_constraints!(container, CommitmentConstraint, devices, device_model, network_model) add_constraints!(container, RampConstraint, devices, device_model, network_model) add_constraints!(container, DurationConstraint, devices, device_model, network_model) add_constraints!( container, StartupTimeLimitTemperatureConstraint, devices, device_model, network_model, ) add_constraints!(container, StartTypeConstraint, devices, device_model, network_model) add_constraints!( container, StartupInitialConditionConstraint, devices, device_model, network_model, ) add_constraints!( container, ActiveRangeICConstraint, devices, device_model, network_model, ) if haskey(get_time_series_names(device_model), ActivePowerTimeSeriesParameter) add_constraints!( container, ActivePowerVariableTimeSeriesLimitsConstraint, ActivePowerRangeExpressionUB, devices, device_model, network_model, ) end add_feedforward_constraints!(container, device_model, devices) objective_function!( container, devices, device_model, get_network_formulation(network_model), ) add_event_constraints!(container, devices, device_model, network_model) add_constraint_dual!(container, sys, device_model) return end function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ArgumentConstructStage, device_model::DeviceModel{T, ThermalCompactUnitCommitment}, network_model::NetworkModel{<:PM.AbstractPowerModel}, ) where {T <: PSY.ThermalGen} devices = get_available_components(device_model, sys) add_variables!( container, PowerAboveMinimumVariable, devices, ThermalCompactUnitCommitment(), ) add_variables!( container, ReactivePowerVariable, devices, ThermalCompactUnitCommitment(), ) add_variables!(container, OnVariable, devices, ThermalCompactUnitCommitment()) add_variables!(container, StartVariable, devices, ThermalCompactUnitCommitment()) add_variables!(container, StopVariable, devices, ThermalCompactUnitCommitment()) add_variables!(container, TimeDurationOn, devices, ThermalCompactUnitCommitment()) add_variables!(container, TimeDurationOff, devices, ThermalCompactUnitCommitment()) add_variables!(container, PowerOutput, devices, ThermalCompactUnitCommitment()) initial_conditions!(container, devices, ThermalCompactUnitCommitment()) if haskey(get_time_series_names(device_model), ActivePowerTimeSeriesParameter) add_parameters!(container, ActivePowerTimeSeriesParameter, devices, device_model) end _handle_common_thermal_parameters!(container, devices, device_model) add_to_expression!( container, ActivePowerBalance, PowerAboveMinimumVariable, devices, device_model, network_model, ) add_to_expression!( container, ActivePowerBalance, OnVariable, devices, device_model, network_model, ) add_cost_expressions!(container, devices, device_model) add_to_expression!( container, ActivePowerRangeExpressionLB, PowerAboveMinimumVariable, devices, device_model, network_model, ) add_to_expression!( container, ActivePowerRangeExpressionUB, PowerAboveMinimumVariable, devices, device_model, network_model, ) add_to_expression!( container, FuelConsumptionExpression, PowerAboveMinimumVariable, devices, device_model, ) if get_use_slacks(device_model) add_variables!( container, RateofChangeConstraintSlackUp, devices, ThermalCompactUnitCommitment(), ) add_variables!( container, RateofChangeConstraintSlackDown, devices, ThermalCompactUnitCommitment(), ) end add_feedforward_arguments!(container, device_model, devices) add_event_arguments!(container, devices, device_model, network_model) return end function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ModelConstructStage, device_model::DeviceModel{T, ThermalCompactUnitCommitment}, network_model::NetworkModel{<:PM.AbstractPowerModel}, ) where {T <: PSY.ThermalGen} devices = get_available_components(device_model, sys) add_constraints!( container, ActivePowerVariableLimitsConstraint, ActivePowerRangeExpressionLB, devices, device_model, network_model, ) add_constraints!( container, ActivePowerVariableLimitsConstraint, ActivePowerRangeExpressionUB, devices, device_model, network_model, ) add_constraints!( container, ReactivePowerVariableLimitsConstraint, ReactivePowerVariable, devices, device_model, network_model, ) add_constraints!(container, CommitmentConstraint, devices, device_model, network_model) add_constraints!(container, RampConstraint, devices, device_model, network_model) add_constraints!(container, DurationConstraint, devices, device_model, network_model) if haskey(get_time_series_names(device_model), ActivePowerTimeSeriesParameter) add_constraints!( container, ActivePowerVariableTimeSeriesLimitsConstraint, ActivePowerRangeExpressionUB, devices, device_model, network_model, ) end add_feedforward_constraints!(container, device_model, devices) objective_function!( container, devices, device_model, get_network_formulation(network_model), ) add_event_constraints!(container, devices, device_model, network_model) add_constraint_dual!(container, sys, device_model) return end function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ArgumentConstructStage, device_model::DeviceModel{T, ThermalCompactUnitCommitment}, network_model::NetworkModel{<:PM.AbstractActivePowerModel}, ) where {T <: PSY.ThermalGen} devices = get_available_components(device_model, sys) add_variables!( container, PowerAboveMinimumVariable, devices, ThermalCompactUnitCommitment(), ) add_variables!(container, OnVariable, devices, ThermalCompactUnitCommitment()) add_variables!(container, StartVariable, devices, ThermalCompactUnitCommitment()) add_variables!(container, StopVariable, devices, ThermalCompactUnitCommitment()) add_variables!(container, TimeDurationOn, devices, ThermalCompactUnitCommitment()) add_variables!(container, TimeDurationOff, devices, ThermalCompactUnitCommitment()) add_variables!(container, PowerOutput, devices, ThermalCompactUnitCommitment()) initial_conditions!(container, devices, ThermalCompactUnitCommitment()) if haskey(get_time_series_names(device_model), ActivePowerTimeSeriesParameter) add_parameters!(container, ActivePowerTimeSeriesParameter, devices, device_model) end _handle_common_thermal_parameters!(container, devices, device_model) add_to_expression!( container, ActivePowerBalance, PowerAboveMinimumVariable, devices, device_model, network_model, ) add_to_expression!( container, ActivePowerBalance, OnVariable, devices, device_model, network_model, ) add_cost_expressions!(container, devices, device_model) add_to_expression!( container, ActivePowerRangeExpressionLB, PowerAboveMinimumVariable, devices, device_model, network_model, ) add_to_expression!( container, ActivePowerRangeExpressionUB, PowerAboveMinimumVariable, devices, device_model, network_model, ) add_to_expression!( container, FuelConsumptionExpression, PowerAboveMinimumVariable, devices, device_model, ) if get_use_slacks(device_model) add_variables!( container, RateofChangeConstraintSlackUp, devices, ThermalCompactUnitCommitment(), ) add_variables!( container, RateofChangeConstraintSlackDown, devices, ThermalCompactUnitCommitment(), ) end add_feedforward_arguments!(container, device_model, devices) add_event_arguments!(container, devices, device_model, network_model) return end function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ModelConstructStage, device_model::DeviceModel{T, ThermalCompactUnitCommitment}, network_model::NetworkModel{<:PM.AbstractActivePowerModel}, ) where {T <: PSY.ThermalGen} devices = get_available_components(device_model, sys) add_constraints!( container, ActivePowerVariableLimitsConstraint, ActivePowerRangeExpressionLB, devices, device_model, network_model, ) add_constraints!( container, ActivePowerVariableLimitsConstraint, ActivePowerRangeExpressionUB, devices, device_model, network_model, ) add_constraints!(container, CommitmentConstraint, devices, device_model, network_model) add_constraints!(container, RampConstraint, devices, device_model, network_model) add_constraints!(container, DurationConstraint, devices, device_model, network_model) if haskey(get_time_series_names(device_model), ActivePowerTimeSeriesParameter) add_constraints!( container, ActivePowerVariableTimeSeriesLimitsConstraint, ActivePowerRangeExpressionUB, devices, device_model, network_model, ) end add_feedforward_constraints!(container, device_model, devices) objective_function!( container, devices, device_model, get_network_formulation(network_model), ) add_event_constraints!(container, devices, device_model, network_model) add_constraint_dual!(container, sys, device_model) return end function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ArgumentConstructStage, device_model::DeviceModel{T, ThermalBasicCompactUnitCommitment}, network_model::NetworkModel{<:PM.AbstractPowerModel}, ) where {T <: PSY.ThermalGen} devices = get_available_components(device_model, sys) add_variables!( container, PowerAboveMinimumVariable, devices, ThermalBasicCompactUnitCommitment(), ) add_variables!( container, ReactivePowerVariable, devices, ThermalBasicCompactUnitCommitment(), ) add_variables!(container, OnVariable, devices, ThermalBasicCompactUnitCommitment()) add_variables!(container, StartVariable, devices, ThermalBasicCompactUnitCommitment()) add_variables!(container, StopVariable, devices, ThermalBasicCompactUnitCommitment()) add_variables!(container, PowerOutput, devices, ThermalBasicCompactUnitCommitment()) add_variables!(container, TimeDurationOn, devices, ThermalBasicCompactUnitCommitment()) add_variables!(container, TimeDurationOff, devices, ThermalBasicCompactUnitCommitment()) initial_conditions!(container, devices, ThermalBasicCompactUnitCommitment()) if haskey(get_time_series_names(device_model), ActivePowerTimeSeriesParameter) add_parameters!(container, ActivePowerTimeSeriesParameter, devices, device_model) end _handle_common_thermal_parameters!(container, devices, device_model) add_to_expression!( container, ActivePowerBalance, PowerAboveMinimumVariable, devices, device_model, network_model, ) add_to_expression!( container, ActivePowerBalance, OnVariable, devices, device_model, network_model, ) add_cost_expressions!(container, devices, device_model) add_to_expression!( container, ActivePowerRangeExpressionLB, PowerAboveMinimumVariable, devices, device_model, network_model, ) add_to_expression!( container, ActivePowerRangeExpressionUB, PowerAboveMinimumVariable, devices, device_model, network_model, ) add_to_expression!( container, FuelConsumptionExpression, PowerAboveMinimumVariable, devices, device_model, ) if get_use_slacks(device_model) add_variables!( container, RateofChangeConstraintSlackUp, devices, ThermalBasicCompactUnitCommitment(), ) add_variables!( container, RateofChangeConstraintSlackDown, devices, ThermalBasicCompactUnitCommitment(), ) end add_feedforward_arguments!(container, device_model, devices) add_event_arguments!(container, devices, device_model, network_model) return end function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ModelConstructStage, device_model::DeviceModel{T, ThermalBasicCompactUnitCommitment}, network_model::NetworkModel{<:PM.AbstractPowerModel}, ) where {T <: PSY.ThermalGen} devices = get_available_components(device_model, sys) add_constraints!( container, ActivePowerVariableLimitsConstraint, ActivePowerRangeExpressionLB, devices, device_model, network_model, ) add_constraints!( container, ActivePowerVariableLimitsConstraint, ActivePowerRangeExpressionUB, devices, device_model, network_model, ) add_constraints!( container, ReactivePowerVariableLimitsConstraint, ReactivePowerVariable, devices, device_model, network_model, ) add_constraints!(container, CommitmentConstraint, devices, device_model, network_model) if haskey(get_time_series_names(device_model), ActivePowerTimeSeriesParameter) add_constraints!( container, ActivePowerVariableTimeSeriesLimitsConstraint, ActivePowerRangeExpressionUB, devices, device_model, network_model, ) end add_feedforward_constraints!(container, device_model, devices) objective_function!( container, devices, device_model, get_network_formulation(network_model), ) add_event_constraints!(container, devices, device_model, network_model) add_constraint_dual!(container, sys, device_model) return end function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ArgumentConstructStage, device_model::DeviceModel{T, ThermalBasicCompactUnitCommitment}, network_model::NetworkModel{<:PM.AbstractActivePowerModel}, ) where {T <: PSY.ThermalGen} devices = get_available_components(device_model, sys) add_variables!( container, PowerAboveMinimumVariable, devices, ThermalBasicCompactUnitCommitment(), ) add_variables!(container, OnVariable, devices, ThermalBasicCompactUnitCommitment()) add_variables!(container, StartVariable, devices, ThermalBasicCompactUnitCommitment()) add_variables!(container, StopVariable, devices, ThermalBasicCompactUnitCommitment()) add_variables!(container, PowerOutput, devices, ThermalBasicCompactUnitCommitment()) add_variables!(container, TimeDurationOn, devices, ThermalBasicCompactUnitCommitment()) add_variables!(container, TimeDurationOff, devices, ThermalBasicCompactUnitCommitment()) initial_conditions!(container, devices, ThermalBasicCompactUnitCommitment()) if haskey(get_time_series_names(device_model), ActivePowerTimeSeriesParameter) add_parameters!(container, ActivePowerTimeSeriesParameter, devices, device_model) end _handle_common_thermal_parameters!(container, devices, device_model) add_to_expression!( container, ActivePowerBalance, PowerAboveMinimumVariable, devices, device_model, network_model, ) add_to_expression!( container, ActivePowerBalance, OnVariable, devices, device_model, network_model, ) add_cost_expressions!(container, devices, device_model) add_to_expression!( container, ActivePowerRangeExpressionLB, PowerAboveMinimumVariable, devices, device_model, network_model, ) add_to_expression!( container, ActivePowerRangeExpressionUB, PowerAboveMinimumVariable, devices, device_model, network_model, ) add_to_expression!( container, FuelConsumptionExpression, PowerAboveMinimumVariable, devices, device_model, ) if get_use_slacks(device_model) add_variables!( container, RateofChangeConstraintSlackUp, devices, ThermalBasicCompactUnitCommitment(), ) add_variables!( container, RateofChangeConstraintSlackDown, devices, ThermalBasicCompactUnitCommitment(), ) end add_feedforward_arguments!(container, device_model, devices) add_event_arguments!(container, devices, device_model, network_model) return end function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ModelConstructStage, device_model::DeviceModel{T, ThermalBasicCompactUnitCommitment}, network_model::NetworkModel{<:PM.AbstractActivePowerModel}, ) where {T <: PSY.ThermalGen} devices = get_available_components(device_model, sys) add_constraints!( container, ActivePowerVariableLimitsConstraint, ActivePowerRangeExpressionLB, devices, device_model, network_model, ) add_constraints!( container, ActivePowerVariableLimitsConstraint, ActivePowerRangeExpressionUB, devices, device_model, network_model, ) add_constraints!(container, CommitmentConstraint, devices, device_model, network_model) if haskey(get_time_series_names(device_model), ActivePowerTimeSeriesParameter) add_constraints!( container, ActivePowerVariableTimeSeriesLimitsConstraint, ActivePowerRangeExpressionUB, devices, device_model, network_model, ) end add_feedforward_constraints!(container, device_model, devices) objective_function!( container, devices, device_model, get_network_formulation(network_model), ) add_event_constraints!(container, devices, device_model, network_model) add_constraint_dual!(container, sys, device_model) return end function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ArgumentConstructStage, device_model::DeviceModel{T, ThermalCompactDispatch}, network_model::NetworkModel{<:PM.AbstractPowerModel}, ) where {T <: PSY.ThermalGen} devices = get_available_components(device_model, sys) add_variables!(container, PowerAboveMinimumVariable, devices, ThermalCompactDispatch()) add_variables!(container, ReactivePowerVariable, devices, ThermalCompactDispatch()) add_variables!(container, PowerOutput, devices, ThermalCompactDispatch()) add_parameters!(container, OnStatusParameter, devices, device_model) _handle_common_thermal_parameters!(container, devices, device_model) add_feedforward_arguments!(container, device_model, devices) initial_conditions!(container, devices, ThermalCompactDispatch()) add_to_expression!( container, ActivePowerBalance, PowerAboveMinimumVariable, devices, device_model, network_model, ) add_cost_expressions!(container, devices, device_model) add_to_expression!( container, ReactivePowerBalance, ReactivePowerVariable, devices, device_model, network_model, ) add_to_expression!( container, ActivePowerBalance, OnStatusParameter, devices, device_model, network_model, ) add_to_expression!( container, ActivePowerRangeExpressionLB, PowerAboveMinimumVariable, devices, device_model, network_model, ) add_to_expression!( container, ActivePowerRangeExpressionUB, PowerAboveMinimumVariable, devices, device_model, network_model, ) add_to_expression!( container, FuelConsumptionExpression, PowerAboveMinimumVariable, devices, device_model, ) if get_use_slacks(device_model) add_variables!( container, RateofChangeConstraintSlackUp, devices, ThermalCompactDispatch(), ) add_variables!( container, RateofChangeConstraintSlackDown, devices, ThermalCompactDispatch(), ) end add_event_arguments!(container, devices, device_model, network_model) return end function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ModelConstructStage, device_model::DeviceModel{T, ThermalCompactDispatch}, network_model::NetworkModel{<:PM.AbstractPowerModel}, ) where {T <: PSY.ThermalGen} devices = get_available_components(device_model, sys) add_constraints!( container, ActivePowerVariableLimitsConstraint, ActivePowerRangeExpressionLB, devices, device_model, network_model, ) add_constraints!( container, ActivePowerVariableLimitsConstraint, ActivePowerRangeExpressionUB, devices, device_model, network_model, ) add_constraints!( container, ReactivePowerVariableLimitsConstraint, ReactivePowerVariable, devices, device_model, network_model, ) add_constraints!(container, RampConstraint, devices, device_model, network_model) add_feedforward_constraints!(container, device_model, devices) objective_function!( container, devices, device_model, get_network_formulation(network_model), ) add_event_constraints!(container, devices, device_model, network_model) add_constraint_dual!(container, sys, device_model) return end function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ArgumentConstructStage, device_model::DeviceModel{T, ThermalCompactDispatch}, network_model::NetworkModel{<:PM.AbstractActivePowerModel}, ) where {T <: PSY.ThermalGen} devices = get_available_components(device_model, sys) add_variables!(container, PowerAboveMinimumVariable, devices, ThermalCompactDispatch()) add_variables!(container, PowerOutput, devices, ThermalCompactDispatch()) add_parameters!(container, OnStatusParameter, devices, device_model) _handle_common_thermal_parameters!(container, devices, device_model) add_feedforward_arguments!(container, device_model, devices) add_to_expression!( container, ActivePowerBalance, PowerAboveMinimumVariable, devices, device_model, network_model, ) add_to_expression!( container, ActivePowerBalance, OnStatusParameter, devices, device_model, network_model, ) initial_conditions!(container, devices, ThermalCompactDispatch()) add_cost_expressions!(container, devices, device_model) add_to_expression!( container, ActivePowerRangeExpressionLB, PowerAboveMinimumVariable, devices, device_model, network_model, ) add_to_expression!( container, ActivePowerRangeExpressionUB, PowerAboveMinimumVariable, devices, device_model, network_model, ) add_to_expression!( container, FuelConsumptionExpression, PowerAboveMinimumVariable, devices, device_model, ) if get_use_slacks(device_model) add_variables!( container, RateofChangeConstraintSlackUp, devices, ThermalCompactDispatch(), ) add_variables!( container, RateofChangeConstraintSlackDown, devices, ThermalCompactDispatch(), ) end add_event_arguments!(container, devices, device_model, network_model) return end function construct_device!( container::OptimizationContainer, sys::PSY.System, ::ModelConstructStage, device_model::DeviceModel{T, ThermalCompactDispatch}, network_model::NetworkModel{<:PM.AbstractActivePowerModel}, ) where {T <: PSY.ThermalGen} devices = get_available_components(device_model, sys) add_constraints!( container, ActivePowerVariableLimitsConstraint, ActivePowerRangeExpressionLB, devices, device_model, network_model, ) add_constraints!( container, ActivePowerVariableLimitsConstraint, ActivePowerRangeExpressionUB, devices, device_model, network_model, ) add_constraints!(container, RampConstraint, devices, device_model, network_model) add_feedforward_constraints!(container, device_model, devices) objective_function!( container, devices, device_model, get_network_formulation(network_model), ) add_event_constraints!(container, devices, device_model, network_model) add_constraint_dual!(container, sys, device_model) return end ================================================ FILE: src/devices_models/devices/AC_branches.jl ================================================ # Note: Any future concrete formulation requires the definition of # construct_device!( # ::OptimizationContainer, # ::PSY.System, # ::DeviceModel{<:PSY.ACTransmission, MyNewFormulation}, # ::Union{Type{CopperPlatePowerModel}, Type{AreaBalancePowerModel}}, # ) = nothing # # Not implemented yet # struct TapControl <: AbstractBranchFormulation end #################################### Branch Variables ################################################## # Because of the way we integrate with PowerModels, most of the time PowerSimulations will create variables # for the branch flows either in AC or DC. #! format: off get_variable_binary(::FlowActivePowerVariable, ::Type{<:PSY.ACTransmission}, ::AbstractBranchFormulation,) = false get_variable_binary(::PhaseShifterAngle, ::Type{PSY.PhaseShiftingTransformer}, ::AbstractBranchFormulation,) = false get_parameter_multiplier(::FixValueParameter, ::PSY.ACTransmission, ::AbstractBranchFormulation) = 1.0 get_parameter_multiplier(::LowerBoundValueParameter, ::PSY.ACTransmission, ::AbstractBranchFormulation) = 1.0 get_parameter_multiplier(::UpperBoundValueParameter, ::PSY.ACTransmission, ::AbstractBranchFormulation) = 1.0 get_variable_multiplier(::PhaseShifterAngle, d::PSY.PhaseShiftingTransformer, ::PhaseAngleControl) = 1.0/PSY.get_x(d) get_multiplier_value(::AbstractDynamicBranchRatingTimeSeriesParameter, d::PSY.ACTransmission, ::StaticBranch) = PSY.get_rating(d) get_multiplier_value(::AbstractDynamicBranchRatingTimeSeriesParameter, d::PNM.BranchesParallel, ::StaticBranch) = PNM.get_equivalent_rating(d) get_initial_conditions_device_model(::OperationModel, ::DeviceModel{T, U}) where {T <: PSY.ACTransmission, U <: AbstractBranchFormulation} = DeviceModel(T, U) #### Properties of slack variables get_variable_binary(::FlowActivePowerSlackUpperBound, ::Type{<:PSY.ACTransmission}, ::AbstractBranchFormulation,) = false get_variable_binary(::FlowActivePowerSlackLowerBound, ::Type{<:PSY.ACTransmission}, ::AbstractBranchFormulation,) = false # These two methods are defined to avoid ambiguities get_variable_upper_bound(::FlowActivePowerSlackUpperBound, ::PSY.ACTransmission, ::AbstractBranchFormulation) = nothing get_variable_lower_bound(::FlowActivePowerSlackUpperBound, ::PSY.ACTransmission, ::AbstractBranchFormulation) = 0.0 get_variable_upper_bound(::FlowActivePowerSlackLowerBound, ::PSY.ACTransmission, ::AbstractBranchFormulation) = nothing get_variable_lower_bound(::FlowActivePowerSlackLowerBound, ::PSY.ACTransmission, ::AbstractBranchFormulation) = 0.0 get_variable_upper_bound(::FlowActivePowerVariable, ::PNM.BranchesSeries, ::AbstractBranchFormulation) = nothing get_variable_lower_bound(::FlowActivePowerVariable, ::PNM.BranchesSeries, ::AbstractBranchFormulation) = nothing get_variable_upper_bound(::FlowActivePowerVariable, ::PNM.BranchesParallel, ::AbstractBranchFormulation) = nothing get_variable_lower_bound(::FlowActivePowerVariable, ::PNM.BranchesParallel, ::AbstractBranchFormulation) = nothing get_variable_upper_bound(::FlowActivePowerVariable, ::PNM.ThreeWindingTransformerWinding, ::AbstractBranchFormulation) = nothing get_variable_lower_bound(::FlowActivePowerVariable, ::PNM.ThreeWindingTransformerWinding, ::AbstractBranchFormulation) = nothing #! format: on function get_default_time_series_names( ::Type{U}, ::Type{V}, ) where {U <: PSY.ACTransmission, V <: AbstractBranchFormulation} return Dict{Type{<:TimeSeriesParameter}, String}( DynamicBranchRatingTimeSeriesParameter => "dynamic_line_ratings", ) end function get_default_attributes( ::Type{U}, ::Type{V}, ) where {U <: PSY.ACTransmission, V <: AbstractBranchFormulation} return Dict{String, Any}() end #################################### Flow Variable Bounds ################################################## function add_variables!( container::OptimizationContainer, ::Type{T}, network_model::NetworkModel{<:AbstractPTDFModel}, devices::IS.FlattenIteratorWrapper{U}, formulation::AbstractBranchFormulation, ) where { T <: AbstractACActivePowerFlow, U <: PSY.ACTransmission} time_steps = get_time_steps(container) net_reduction_data = network_model.network_reduction branch_names = get_branch_argument_variable_axis(net_reduction_data, devices) reduced_branch_tracker = get_reduced_branch_tracker(network_model) all_branch_maps_by_type = PNM.get_all_branch_maps_by_type(net_reduction_data) variable_container = add_variable_container!( container, T(), U, branch_names, time_steps, ) for (name, (arc, reduction)) in PNM.get_name_to_arc_map(net_reduction_data, U) # TODO: entry is not type stable here, it can return any type ACTransmission. # It might have performance implications. Possibly separate this into other functions reduction_entry = all_branch_maps_by_type[reduction][U][arc] has_entry, tracker_container = search_for_reduced_branch_argument!( reduced_branch_tracker, arc, T, ) if has_entry @assert !isempty(tracker_container) name arc reduction end ub = get_variable_upper_bound(T(), reduction_entry, formulation) lb = get_variable_lower_bound(T(), reduction_entry, formulation) for t in time_steps if !has_entry tracker_container[t] = JuMP.@variable( get_jump_model(container), base_name = "$(T)_$(U)_$(reduction)_{$(name), $(t)}", ) ub !== nothing && JuMP.set_upper_bound(tracker_container[t], ub) lb !== nothing && JuMP.set_lower_bound(tracker_container[t], lb) end variable_container[name, t] = tracker_container[t] end end return end function add_variables!( container::OptimizationContainer, ::Type{T}, network_model::NetworkModel{<:PM.AbstractPowerModel}, devices::IS.FlattenIteratorWrapper{U}, formulation::AbstractBranchFormulation, ) where { T <: AbstractACActivePowerFlow, U <: PSY.ACTransmission} net_reduction_data = network_model.network_reduction time_steps = get_time_steps(container) branch_names = get_branch_argument_variable_axis(net_reduction_data, devices) reduced_branch_tracker = get_reduced_branch_tracker(network_model) all_branch_maps_by_type = PNM.get_all_branch_maps_by_type(net_reduction_data) variable_container = add_variable_container!( container, T(), U, branch_names, time_steps, ) for (name, (arc, reduction)) in PNM.get_name_to_arc_map(net_reduction_data, U) reduction_entry = all_branch_maps_by_type[reduction][U][arc] has_entry, tracker_container = search_for_reduced_branch_argument!( reduced_branch_tracker, arc, T, ) if has_entry @assert !isempty(tracker_container) name arc reduction end ub = get_variable_upper_bound(T(), reduction_entry, formulation) lb = get_variable_lower_bound(T(), reduction_entry, formulation) for t in time_steps if !has_entry tracker_container[t] = JuMP.@variable( get_jump_model(container), base_name = "$(T)_$(U)_$(reduction)_{$(name), $(t)}", ) ub !== nothing && JuMP.set_upper_bound(tracker_container[t], ub) lb !== nothing && JuMP.set_lower_bound(tracker_container[t], lb) end variable_container[name, t] = tracker_container[t] end end return end function add_variables!( ::OptimizationContainer, ::Type{T}, network_model::NetworkModel{<:AbstractPTDFModel}, devices::IS.FlattenIteratorWrapper{U}, formulation::StaticBranchUnbounded, ) where { T <: AbstractACActivePowerFlow, U <: PSY.ACTransmission} @debug "PTDF Branch Flows with StaticBranchUnbounded do not require flow variables $T. Flow values are given by PTDFBranchFlow expression." return end function add_variables!( container::OptimizationContainer, ::Type{S}, network_model::NetworkModel{CopperPlatePowerModel}, devices::IS.FlattenIteratorWrapper{T}, formulation::U, ) where { S <: AbstractACActivePowerFlow, T <: PSY.ACTransmission, U <: AbstractBranchFormulation, } @debug "AC Branches of type $(T) do not require flow variables $S in CopperPlatePowerModel." return end function _get_flow_variable_vector( container::OptimizationContainer, ::NetworkModel{<:PM.AbstractDCPModel}, ::Type{B}, ) where {B <: PSY.ACTransmission} return [get_variable(container, FlowActivePowerVariable(), B)] end function _get_flow_variable_vector( container::OptimizationContainer, ::NetworkModel{<:PM.AbstractPowerModel}, ::Type{B}, ) where {B <: PSY.ACTransmission} return [ get_variable(container, FlowActivePowerFromToVariable(), B), get_variable(container, FlowActivePowerToFromVariable(), B), ] end function branch_rate_bounds!( container::OptimizationContainer, ::DeviceModel{B, T}, network_model::NetworkModel{<:PM.AbstractPowerModel}, ) where {B <: PSY.ACTransmission, T <: AbstractBranchFormulation} time_steps = get_time_steps(container) net_reduction_data = get_network_reduction(network_model) all_branch_maps_by_type = net_reduction_data.all_branch_maps_by_type for var in _get_flow_variable_vector(container, network_model, B) for (name, (arc, reduction)) in PNM.get_name_to_arc_map(net_reduction_data, B) # TODO: entry is not type stable here, it can return any type ACTransmission. # It might have performance implications. Possibly separate this into other functions reduction_entry = all_branch_maps_by_type[reduction][B][arc] # Use the same limit values as FlowRateConstraint for consistency. limits = get_min_max_limits(reduction_entry, FlowRateConstraint, T) for t in time_steps @assert limits.min <= limits.max "Infeasible rate limits for branch $(name)" JuMP.set_upper_bound(var[name, t], limits.max) JuMP.set_lower_bound(var[name, t], limits.min) end end end return end ################################## PWL Loss Variables ################################## function _check_pwl_loss_model(devices) first_loss = PSY.get_loss(first(devices)) first_loss_type = typeof(first_loss) for d in devices loss = PSY.get_loss(d) if !isa(loss, first_loss_type) error( "Not all TwoTerminal HVDC lines have the same loss model data. Check that all loss models are LinearCurve or PiecewiseIncrementalCurve", ) end if isa(first_loss, PSY.PiecewiseIncrementalCurve) len_first_loss = length(PSY.get_slopes(first_loss)) len_loss = length(PSY.get_slopes(loss)) if len_first_loss != len_loss error( "Different length of PWL segments for TwoTerminal HVDC losses are not supported. Check that all HVDC data have the same amount of PWL segments.", ) end end end return end ################################## Rate Limits constraint_infos ############################ function get_rating(double_circuit::PNM.BranchesParallel) return sum([PSY.get_rating(circuit) for circuit in double_circuit]) end function get_rating(series_chain::PNM.BranchesSeries) return minimum([get_rating(segment) for segment in series_chain]) end function get_rating(device::T) where {T <: PSY.ACTransmission} return PSY.get_rating(device) end function get_rating( device::PNM.ThreeWindingTransformerWinding{T}, ) where {T <: PSY.ThreeWindingTransformer} return PNM.get_equivalent_rating(device) end """ Min and max limits for Abstract Branch Formulation """ function get_min_max_limits( double_circuit::PNM.BranchesParallel{<:PSY.ACTransmission}, constraint_type::Type{<:ConstraintType}, branch_formulation::Type{<:AbstractBranchFormulation}, ) # -> Union{Nothing, NamedTuple{(:min, :max), Tuple{Float64, Float64}}} min_max_by_circuit = [ get_min_max_limits(device, constraint_type, branch_formulation) for device in double_circuit ] min_by_circuit = [x.min for x in min_max_by_circuit] max_by_circuit = [x.max for x in min_max_by_circuit] # Limit by most restictive circuit: return (min = maximum(min_by_circuit), max = minimum(max_by_circuit)) end """ Min and max limits for Abstract Branch Formulation """ function get_min_max_limits( transformer_entry::PNM.ThreeWindingTransformerWinding, constraint_type::Type{<:ConstraintType}, branch_formulation::Type{<:AbstractBranchFormulation}, ) # -> Union{Nothing, NamedTuple{(:min, :max), Tuple{Float64, Float64}}} transformer = PNM.get_transformer(transformer_entry) winding_number = PNM.get_winding_number(transformer_entry) if winding_number == 1 limits = ( min = -1 * PSY.get_rating_primary(transformer), max = PSY.get_rating_primary(transformer), ) elseif winding_number == 2 limits = ( min = -1 * PSY.get_rating_secondary(transformer), max = PSY.get_rating_secondary(transformer), ) elseif winding_number == 3 limits = ( min = -1 * PSY.get_rating_tertiary(transformer), max = PSY.get_rating_tertiary(transformer), ) end return limits end """ Min and max limits for Abstract Branch Formulation """ function get_min_max_limits( series_chain::PNM.BranchesSeries, constraint_type::Type{<:ConstraintType}, branch_formulation::Type{<:AbstractBranchFormulation}, ) # -> Union{Nothing, NamedTuple{(:min, :max), Tuple{Float64, Float64}}} min_max_by_segment = [ get_min_max_limits(segment, constraint_type, branch_formulation) for segment in series_chain ] min_by_segment = [x.min for x in min_max_by_segment] max_by_segment = [x.max for x in min_max_by_segment] # Limit by most restictive segment: return (min = maximum(min_by_segment), max = minimum(max_by_segment)) end """ Min and max limits for Abstract Branch Formulation """ function get_min_max_limits( device::PSY.ACTransmission, ::Type{<:ConstraintType}, ::Type{<:AbstractBranchFormulation}, ) # -> Union{Nothing, NamedTuple{(:min, :max), Tuple{Float64, Float64}}} return ( min = -1 * PNM.get_equivalent_rating(device), max = PNM.get_equivalent_rating(device), ) end """ Min and max limits for Abstract Branch Formulation """ function get_min_max_limits( ::PSY.PhaseShiftingTransformer, ::Type{PhaseAngleControlLimit}, ::Type{PhaseAngleControl}, ) # -> Union{Nothing, NamedTuple{(:min, :max), Tuple{Float64, Float64}}} return (min = -π / 2, max = π / 2) end function _add_flow_rate_constraint!( container::OptimizationContainer, ::Type{T}, arc::Tuple{Int, Int}, use_slacks::Bool, con_lb::DenseAxisArray, con_ub::DenseAxisArray, var::DenseAxisArray, branch_maps_by_type::Dict, name::String, ) where {T <: PSY.ACTransmission} reduction_entry = branch_maps_by_type[arc] time_steps = get_time_steps(container) if use_slacks slack_ub = get_variable(container, FlowActivePowerSlackUpperBound(), T)[name, :] slack_lb = get_variable(container, FlowActivePowerSlackLowerBound(), T)[name, :] end limits = get_min_max_limits(reduction_entry, FlowRateConstraint, StaticBranch) for t in time_steps con_ub[name, t] = JuMP.@constraint( get_jump_model(container), var[name, t] - (use_slacks ? slack_ub[t] : 0.0) <= limits.max ) con_lb[name, t] = JuMP.@constraint( get_jump_model(container), var[name, t] + (use_slacks ? slack_lb[t] : 0.0) >= limits.min ) end return end """ Add branch rate limit constraints for ACBranch with AbstractActivePowerModel """ function add_constraints!( container::OptimizationContainer, cons_type::Type{FlowRateConstraint}, devices::IS.FlattenIteratorWrapper{T}, device_model::DeviceModel{T, U}, network_model::NetworkModel{V}, ) where { T <: PSY.ACTransmission, U <: AbstractBranchFormulation, V <: PM.AbstractActivePowerModel, } time_steps = get_time_steps(container) net_reduction_data = network_model.network_reduction reduced_branch_tracker = get_reduced_branch_tracker(network_model) branch_names = get_branch_argument_constraint_axis( net_reduction_data, reduced_branch_tracker, devices, cons_type, ) all_branch_maps_by_type = PNM.get_all_branch_maps_by_type(net_reduction_data) con_lb = add_constraints_container!( container, cons_type(), T, branch_names, time_steps; meta = "lb", ) con_ub = add_constraints_container!( container, cons_type(), T, branch_names, time_steps; meta = "ub", ) 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 (name, (arc, reduction)) in get_constraint_map_by_type(reduced_branch_tracker)[FlowRateConstraint][T] _add_flow_rate_constraint!( container, T, arc, use_slacks, con_lb, con_ub, array, all_branch_maps_by_type[reduction][T], name, ) end return end function _add_flow_rate_constraint_with_parameters!( container::OptimizationContainer, ::Type{T}, arc::Tuple{Int, Int}, use_slacks::Bool, con_lb::DenseAxisArray, con_ub::DenseAxisArray, var::DenseAxisArray, branch_maps_by_type::Dict, name::String, ts_name::String, ) where {T <: PSY.ACTransmission} time_steps = get_time_steps(container) if use_slacks slack_ub = get_variable(container, FlowActivePowerSlackUpperBound(), T)[name, :] slack_lb = get_variable(container, FlowActivePowerSlackLowerBound(), T)[name, :] end param_container = get_parameter(container, DynamicBranchRatingTimeSeriesParameter(), T) param = get_parameter_column_refs(param_container, name) mult = get_multiplier_array(param_container)[name, :] for t in time_steps @debug "Dynamic Branch Rating applied for branch $(name) at time step $(t)" con_ub[name, t] = JuMP.@constraint( get_jump_model(container), var[name, t] - (use_slacks ? slack_ub[t] : 0.0) <= param[t] * mult[t] ) con_lb[name, t] = JuMP.@constraint( get_jump_model(container), var[name, t] + (use_slacks ? slack_lb[t] : 0.0) >= -1.0 * param[t] * mult[t] ) end return end function add_constraints!( container::OptimizationContainer, cons_type::Type{FlowRateConstraint}, devices::IS.FlattenIteratorWrapper{T}, device_model::DeviceModel{T, U}, network_model::NetworkModel{V}, ) where { T <: PSY.ACTransmission, U <: AbstractBranchFormulation, V <: AbstractPTDFModel, } time_steps = get_time_steps(container) net_reduction_data = network_model.network_reduction reduced_branch_tracker = get_reduced_branch_tracker(network_model) branch_names = get_branch_argument_constraint_axis( net_reduction_data, reduced_branch_tracker, devices, cons_type, ) all_branch_maps_by_type = PNM.get_all_branch_maps_by_type(net_reduction_data) con_lb = add_constraints_container!( container, cons_type(), T, branch_names, time_steps; meta = "lb", ) con_ub = add_constraints_container!( container, cons_type(), T, branch_names, time_steps; meta = "ub", ) array = get_expression(container, PTDFBranchFlow(), 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 (name, (arc, reduction)) in get_constraint_map_by_type(reduced_branch_tracker)[FlowRateConstraint][T] # TODO: entry is not type stable here, it can return any type ACTransmission. # It might have performance implications. Possibly separate this into other functions reduction_entry = all_branch_maps_by_type[reduction][T][arc] limits = get_min_max_limits(reduction_entry, FlowRateConstraint, U) for t in time_steps con_ub[name, t] = JuMP.@constraint(get_jump_model(container), array[name, t] - (use_slacks ? slack_ub[name, t] : 0.0) <= limits.max) con_lb[name, t] = JuMP.@constraint(get_jump_model(container), array[name, t] + (use_slacks ? slack_lb[name, t] : 0.0) >= limits.min) end end return end function add_flow_rate_constraint_with_parameters!( container::OptimizationContainer, cons_type::Type{FlowRateConstraint}, devices::IS.FlattenIteratorWrapper{T}, device_model::DeviceModel{T, U}, network_model::NetworkModel{V}, ) where { T <: PSY.ACTransmission, U <: StaticBranch, V <: AbstractPTDFModel, } time_steps = get_time_steps(container) net_reduction_data = network_model.network_reduction reduced_branch_tracker = get_reduced_branch_tracker(network_model) branch_names = get_branch_argument_constraint_axis( net_reduction_data, reduced_branch_tracker, devices, cons_type, ) all_branch_maps_by_type = PNM.get_all_branch_maps_by_type(net_reduction_data) con_lb = add_constraints_container!( container, cons_type(), T, branch_names, time_steps; meta = "lb", ) con_ub = add_constraints_container!( container, cons_type(), T, branch_names, time_steps; meta = "ub", ) var_array = get_expression(container, PTDFBranchFlow(), T) ts_name = get_time_series_names(device_model)[DynamicBranchRatingTimeSeriesParameter] ts_type = get_default_time_series_type(container) use_slacks = get_use_slacks(device_model) for (name, (arc, reduction)) in get_constraint_map_by_type(reduced_branch_tracker)[FlowRateConstraint][T] if PNM.has_time_series( all_branch_maps_by_type[reduction][T][arc], ts_type, ts_name, ) _add_flow_rate_constraint_with_parameters!( container, T, arc, use_slacks, con_lb, con_ub, var_array, all_branch_maps_by_type[reduction][T], name, ts_name, ) else _add_flow_rate_constraint!( container, T, arc, use_slacks, con_lb, con_ub, var_array, all_branch_maps_by_type[reduction][T], name, ) end end return end """ Add rate limit from to constraints for ACBranch with AbstractPowerModel """ function add_constraints!( container::OptimizationContainer, cons_type::Type{FlowRateConstraintFromTo}, devices::IS.FlattenIteratorWrapper{B}, device_model::DeviceModel{B, <:AbstractBranchFormulation}, network_model::NetworkModel{T}, ) where {B <: PSY.ACTransmission, T <: PM.AbstractPowerModel} reduced_branch_tracker = get_reduced_branch_tracker(network_model) net_reduction_data = get_network_reduction(network_model) all_branch_maps_by_type = net_reduction_data.all_branch_maps_by_type device_names = get_branch_argument_constraint_axis( net_reduction_data, reduced_branch_tracker, devices, cons_type, ) time_steps = get_time_steps(container) var1 = get_variable(container, FlowActivePowerFromToVariable(), B) var2 = get_variable(container, FlowReactivePowerFromToVariable(), B) add_constraints_container!( container, cons_type(), B, device_names, time_steps, ) constraint = get_constraint(container, cons_type(), B) use_slacks = get_use_slacks(device_model) if use_slacks slack_ub = get_variable(container, FlowActivePowerSlackUpperBound(), B) end for (name, (arc, reduction)) in get_constraint_map_by_type(reduced_branch_tracker)[FlowRateConstraintFromTo][B] # TODO: entry is not type stable here, it can return any type ACTransmission. # It might have performance implications. Possibly separate this into other functions reduction_entry = all_branch_maps_by_type[reduction][B][arc] branch_rate = get_rating(reduction_entry) for t in time_steps constraint[name, t] = JuMP.@constraint( get_jump_model(container), var1[name, t]^2 + var2[name, t]^2 - (use_slacks ? slack_ub[name, t] : 0.0) <= branch_rate^2 ) end end return end """ Add rate limit to from constraints for ACBranch with AbstractPowerModel """ function add_constraints!( container::OptimizationContainer, cons_type::Type{FlowRateConstraintToFrom}, devices::IS.FlattenIteratorWrapper{B}, device_model::DeviceModel{B, <:AbstractBranchFormulation}, network_model::NetworkModel{T}, ) where {B <: PSY.ACTransmission, T <: PM.AbstractPowerModel} reduced_branch_tracker = get_reduced_branch_tracker(network_model) net_reduction_data = get_network_reduction(network_model) all_branch_maps_by_type = net_reduction_data.all_branch_maps_by_type time_steps = get_time_steps(container) device_names = get_branch_argument_constraint_axis( net_reduction_data, reduced_branch_tracker, devices, cons_type, ) var1 = get_variable(container, FlowActivePowerToFromVariable(), B) var2 = get_variable(container, FlowReactivePowerToFromVariable(), B) add_constraints_container!( container, cons_type(), B, device_names, time_steps, ) constraint = get_constraint(container, cons_type(), B) use_slacks = get_use_slacks(device_model) if use_slacks slack_ub = get_variable(container, FlowActivePowerSlackUpperBound(), B) end for (name, (arc, reduction)) in get_constraint_map_by_type(reduced_branch_tracker)[FlowRateConstraintToFrom][B] # TODO: entry is not type stable here, it can return any type ACTransmission. # It might have performance implications. Possibly separate this into other functions reduction_entry = all_branch_maps_by_type[reduction][B][arc] branch_rate = get_rating(reduction_entry) for t in time_steps constraint[name, t] = JuMP.@constraint( get_jump_model(container), var1[name, t]^2 + var2[name, t]^2 - (use_slacks ? slack_ub[name, t] : 0.0) <= branch_rate^2 ) end end return end function _make_flow_expressions!( name::String, time_steps::UnitRange{Int}, ptdf_col::Vector{Float64}, nodal_balance_expressions::Matrix{JuMP.AffExpr}, ) @debug "Making Flow Expression on thread $(Threads.threadid()) for branch $name" nz_idx = [i for i in eachindex(ptdf_col) if abs(ptdf_col[i]) > PTDF_ZERO_TOL] hint = length(nz_idx) expressions = Vector{JuMP.AffExpr}(undef, length(time_steps)) for t in time_steps acc = get_hinted_aff_expr(hint) @inbounds for i in nz_idx JuMP.add_to_expression!(acc, ptdf_col[i], nodal_balance_expressions[i, t]) end expressions[t] = acc end return name, expressions end function _make_flow_expressions!( name::String, time_steps::UnitRange{Int}, ptdf_col::SparseArrays.SparseVector{Float64, Int}, nodal_balance_expressions::Matrix{JuMP.AffExpr}, ) @debug "Making Flow Expression on thread $(Threads.threadid()) for branch $name" nz_idx = SparseArrays.nonzeroinds(ptdf_col) nz_val = SparseArrays.nonzeros(ptdf_col) hint = length(nz_idx) expressions = Vector{JuMP.AffExpr}(undef, length(time_steps)) for t in time_steps acc = get_hinted_aff_expr(hint) @inbounds for k in eachindex(nz_idx) JuMP.add_to_expression!( acc, nz_val[k], nodal_balance_expressions[nz_idx[k], t], ) end expressions[t] = acc end return name, expressions end function _add_expression_to_container!( branch_flow_expr::JuMPAffineExpressionDArrayStringInt, jump_model::JuMP.Model, time_steps::UnitRange{Int}, ptdf_col::AbstractVector{Float64}, nodal_balance_expressions::JuMPAffineExpressionDArrayIntInt, reduction_entry::T, branches::Vector{String}, ) where {T <: PSY.ACTransmission} name = PSY.get_name(reduction_entry) if name in branches branch_flow_expr[name, :] .= _make_flow_expressions!( name, time_steps, ptdf_col, nodal_balance_expressions.data, ) end return end function _add_expression_to_container!( branch_flow_expr::JuMPAffineExpressionDArrayStringInt, jump_model::JuMP.Model, time_steps::UnitRange{Int}, ptdf_col::AbstractVector{Float64}, nodal_balance_expressions::JuMPAffineExpressionDArrayIntInt, reduction_entry::Vector{Any}, branches::Vector{String}, ) names = _get_branch_names(reduction_entry) for name in names if name in branches branch_flow_expr[name, :] .= _make_flow_expressions!( name, time_steps, ptdf_col, nodal_balance_expressions.data, ) #Only one constraint added per arc; once it is found can return return end end end function _add_expression_to_container!( branch_flow_expr::JuMPAffineExpressionDArrayStringInt, jump_model::JuMP.Model, time_steps::UnitRange{Int}, ptdf_col::AbstractVector{Float64}, nodal_balance_expressions::JuMPAffineExpressionDArrayIntInt, reduction_entry::Set{PSY.ACTransmission}, branches::Vector{String}, ) names = _get_branch_names(reduction_entry) for name in names if name in branches branch_flow_expr[name, :] .= _make_flow_expressions!( name, time_steps, ptdf_col, nodal_balance_expressions.data, ) #Only one constraint added per arc; once it is found can return return end end end function add_expressions!( container::OptimizationContainer, ::Type{PTDFBranchFlow}, devices::IS.FlattenIteratorWrapper{B}, model::DeviceModel{B, <:AbstractBranchFormulation}, network_model::NetworkModel{<:AbstractPTDFModel}, ) where {B <: PSY.ACTransmission} time_steps = get_time_steps(container) ptdf = get_PTDF_matrix(network_model) net_reduction_data = network_model.network_reduction # This might need to be changed to something else branch_names = get_branch_argument_variable_axis(net_reduction_data, devices) # Needs to be a vector to use multi-threading name_to_arc_map = collect(PNM.get_name_to_arc_map(net_reduction_data, B)) nodal_balance_expressions = get_expression( container, ActivePowerBalance(), PSY.ACBus, ) branch_flow_expr = add_expression_container!(container, PTDFBranchFlow(), B, branch_names, time_steps, ) jump_model = get_jump_model(container) tasks = map(name_to_arc_map) do pair (name, (arc, _)) = pair ptdf_col = ptdf[arc, :] Threads.@spawn _make_flow_expressions!( 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, (arc, reduction)) in name_to_arc_map reduction_entry = all_branch_maps_by_type[reduction][B][arc] network_reduction_map = all_branch_maps_by_type[map] !haskey(network_reduction_map, branch_Type) && continue for (arc_tuple, reduction_entry) in network_reduction_map[branch_Type] ptdf_col = ptdf[arc_tuple, :] _add_expression_to_container!( branch_flow_expr, jump_model, time_steps, ptdf_col, nodal_balance_expressions, reduction_entry, name, ) end end =# return end """ Add network flow constraints for ACBranch and NetworkModel with <: AbstractPTDFModel """ function add_constraints!( container::OptimizationContainer, cons_type::Type{NetworkFlowConstraint}, devices::IS.FlattenIteratorWrapper{T}, device_model::DeviceModel{T, StaticBranchBounds}, network_model::NetworkModel{<:AbstractPTDFModel}, ) where {T <: PSY.ACTransmission} time_steps = get_time_steps(container) branch_flow_expr = get_expression(container, PTDFBranchFlow(), T) flow_variables = get_variable(container, FlowActivePowerVariable(), T) net_reduction_data = network_model.network_reduction reduced_branch_tracker = get_reduced_branch_tracker(network_model) branches = get_branch_argument_constraint_axis( net_reduction_data, reduced_branch_tracker, devices, cons_type, ) branch_flow = add_constraints_container!( container, NetworkFlowConstraint(), T, branches, time_steps, ) jump_model = get_jump_model(container) 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 name in branches for t in time_steps branch_flow[name, t] = JuMP.@constraint( jump_model, branch_flow_expr[name, t] - flow_variables[name, t] == (use_slacks ? slack_ub[name, t] - slack_lb[name, t] : 0.0) ) end end return end function add_constraints!( ::OptimizationContainer, cons_type::Type{NetworkFlowConstraint}, ::IS.FlattenIteratorWrapper{B}, ::DeviceModel{B, T}, ::NetworkModel{<:AbstractPTDFModel}, ) where {B <: PSY.ACTransmission, T <: Union{StaticBranchUnbounded, StaticBranch}} @debug "PTDF Branch Flows with $T do not require network flow constraints $cons_type. Flow values are given by PTDFBranchFlow." return end """ 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{<:AbstractPTDFModel}, ) where {T <: PSY.PhaseShiftingTransformer} ptdf = get_PTDF_matrix(network_model) branches = PSY.get_name.(devices) time_steps = get_time_steps(container) branch_flow = add_constraints_container!( container, NetworkFlowConstraint(), T, branches, time_steps, ) nodal_balance_expressions = get_expression(container, ActivePowerBalance(), PSY.ACBus) flow_variables = get_variable(container, FlowActivePowerVariable(), T) angle_variables = get_variable(container, PhaseShifterAngle(), T) jump_model = get_jump_model(container) for br in devices arc = PNM.get_arc_tuple(br) name = PSY.get_name(br) ptdf_col = ptdf[arc, :] inv_x = 1 / PSY.get_x(br) 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) ) + inv_x * angle_variables[name, t] - flow_variables[name, t] == 0.0 ) end end return end """ Min and max limits for monitored line """ function get_min_max_limits( device::PSY.MonitoredLine, ::Type{<:ConstraintType}, ::Type{T}, ) where {T <: AbstractBranchFormulation} if PSY.get_flow_limits(device).to_from != PSY.get_flow_limits(device).from_to @warn( "Flow limits in Line $(PSY.get_name(device)) aren't equal. The minimum will be used in formulation $(T)" ) end limit = min( PSY.get_rating(device), PSY.get_flow_limits(device).to_from, PSY.get_flow_limits(device).from_to, ) minmax = (min = -1 * limit, max = limit) return minmax end ############################## Flow Limits Constraints ##################################### """ Add branch flow constraints for monitored lines with DC Power Model """ function add_constraints!( container::OptimizationContainer, ::Type{FlowLimitConstraint}, devices::IS.FlattenIteratorWrapper{T}, model::DeviceModel{T, U}, ::NetworkModel{V}, ) where { T <: Union{PSY.PhaseShiftingTransformer, PSY.MonitoredLine}, U <: AbstractBranchFormulation, V <: PM.AbstractDCPModel, } add_range_constraints!( container, FlowLimitConstraint, FlowActivePowerVariable, devices, model, V, ) return end """ Don't add branch flow constraints for monitored lines if formulation is StaticBranchUnbounded """ function add_constraints!( ::OptimizationContainer, ::Type{FlowRateConstraintFromTo}, devices::IS.FlattenIteratorWrapper{T}, model::DeviceModel{T, U}, ::NetworkModel{V}, ) where { T <: PSY.MonitoredLine, U <: StaticBranchUnbounded, V <: PM.AbstractActivePowerModel, } return end """ Min and max limits for flow limit from-to constraint """ function get_min_max_limits( device::PSY.MonitoredLine, ::Type{FlowLimitFromToConstraint}, ::Type{<:AbstractBranchFormulation}, ) if PSY.get_flow_limits(device).to_from != PSY.get_flow_limits(device).from_to @warn( "Flow limits in Line $(PSY.get_name(device)) aren't equal. The minimum will be used in formulation $(T)" ) end return ( min = -1 * PSY.get_flow_limits(device).from_to, max = PSY.get_flow_limits(device).from_to, ) end """ Min and max limits for flow limit to-from constraint """ function get_min_max_limits( device::PSY.MonitoredLine, ::Type{FlowLimitToFromConstraint}, ::Type{<:AbstractBranchFormulation}, ) if PSY.get_flow_limits(device).to_from != PSY.get_flow_limits(device).from_to @warn( "Flow limits in Line $(PSY.get_name(device)) aren't equal. The minimum will be used in formulation $(T)" ) end return ( min = -1 * PSY.get_flow_limits(device).to_from, max = PSY.get_flow_limits(device).to_from, ) end """ Don't add branch flow constraints for monitored lines if formulation is StaticBranchUnbounded """ function add_constraints!( ::OptimizationContainer, ::Type{FlowLimitToFromConstraint}, devices::IS.FlattenIteratorWrapper{T}, model::DeviceModel{T, U}, ::NetworkModel{V}, ) where { T <: PSY.MonitoredLine, U <: StaticBranchUnbounded, V <: PM.AbstractActivePowerModel, } return end """ Add phase angle limits for phase shifters """ function add_constraints!( container::OptimizationContainer, ::Type{PhaseAngleControlLimit}, devices::IS.FlattenIteratorWrapper{T}, model::DeviceModel{T, PhaseAngleControl}, ::NetworkModel{U}, ) where {T <: PSY.PhaseShiftingTransformer, U <: PM.AbstractActivePowerModel} add_range_constraints!( container, PhaseAngleControlLimit, PhaseShifterAngle, devices, model, U, ) return end """ Add network flow constraints for PhaseShiftingTransformer and NetworkModel with PM.DCPPowerModel """ function add_constraints!( container::OptimizationContainer, ::Type{NetworkFlowConstraint}, devices::IS.FlattenIteratorWrapper{T}, model::DeviceModel{T, PhaseAngleControl}, ::NetworkModel{PM.DCPPowerModel}, ) where {T <: PSY.PhaseShiftingTransformer} time_steps = get_time_steps(container) flow_variables = get_variable(container, FlowActivePowerVariable(), T) ps_angle_variables = get_variable(container, PhaseShifterAngle(), T) bus_angle_variables = get_variable(container, VoltageAngle(), PSY.ACBus) jump_model = get_jump_model(container) branch_flow = add_constraints_container!( container, NetworkFlowConstraint(), T, axes(flow_variables)[1], time_steps, ) for br in devices name = PSY.get_name(br) inv_x = 1.0 / PSY.get_x(br) flow_variables_ = flow_variables[name, :] from_bus = PSY.get_name(PSY.get_from(PSY.get_arc(br))) to_bus = PSY.get_name(PSY.get_to(PSY.get_arc(br))) angle_variables_ = ps_angle_variables[name, :] bus_angle_from = bus_angle_variables[from_bus, :] bus_angle_to = bus_angle_variables[to_bus, :] @assert inv_x > 0.0 for t in time_steps branch_flow[name, t] = JuMP.@constraint( jump_model, flow_variables_[t] == inv_x * (bus_angle_from[t] - bus_angle_to[t] + angle_variables_[t]) ) end end return end function objective_function!( container::OptimizationContainer, ::IS.FlattenIteratorWrapper{T}, device_model::DeviceModel{T, <:AbstractBranchFormulation}, ::Type{<:PM.AbstractPowerModel}, ) where {T <: PSY.ACTransmission} if get_use_slacks(device_model) variable_up = get_variable(container, FlowActivePowerSlackUpperBound(), T) # Use device names because there might be a 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.ACTransmission} 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 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 ================================================ FILE: src/devices_models/devices/HVDCsystems.jl ================================================ #! format: off get_variable_binary(::ActivePowerVariable, ::Type{PSY.InterconnectingConverter}, ::AbstractConverterFormulation) = false get_variable_warm_start_value(::ActivePowerVariable, d::PSY.InterconnectingConverter, ::AbstractConverterFormulation) = PSY.get_active_power(d) get_variable_lower_bound(::ActivePowerVariable, d::PSY.InterconnectingConverter, ::AbstractConverterFormulation) = PSY.get_active_power_limits(d).min get_variable_upper_bound(::ActivePowerVariable, d::PSY.InterconnectingConverter, ::AbstractConverterFormulation) = PSY.get_active_power_limits(d).max get_variable_multiplier(_, ::Type{PSY.InterconnectingConverter}, ::AbstractConverterFormulation) = 1.0 function _get_flow_bounds(d::PSY.TModelHVDCLine) check_hvdc_line_limits_consistency(d) from_min = PSY.get_active_power_limits_from(d).min to_min = PSY.get_active_power_limits_to(d).min from_max = PSY.get_active_power_limits_from(d).max to_max = PSY.get_active_power_limits_to(d).max if from_min >= 0.0 && to_min >= 0.0 min_rate = min(from_min, to_min) elseif from_min <= 0.0 && to_min <= 0.0 min_rate = max(from_min, to_min) elseif from_min <= 0.0 && to_min >= 0.0 min_rate = from_min elseif to_min <= 0.0 && from_min >= 0.0 min_rate = to_min else @assert false end if from_max >= 0.0 && to_max >= 0.0 max_rate = min(from_max, to_max) elseif from_max <= 0.0 && to_max <= 0.0 max_rate = max(from_max, to_max) elseif from_max <= 0.0 && to_max >= 0.0 max_rate = from_max elseif from_max >= 0.0 && to_max <= 0.0 max_rate = to_max else @assert false end return min_rate, max_rate end get_variable_binary(::FlowActivePowerVariable, ::Type{PSY.TModelHVDCLine}, ::AbstractBranchFormulation) = false get_variable_binary(::DCLineCurrent, ::Type{PSY.TModelHVDCLine}, ::AbstractBranchFormulation) = false get_variable_warm_start_value(::FlowActivePowerVariable, d::PSY.TModelHVDCLine, ::AbstractBranchFormulation) = PSY.get_active_power_flow(d) get_variable_lower_bound(::FlowActivePowerVariable, d::PSY.TModelHVDCLine, ::AbstractBranchFormulation) = _get_flow_bounds(d)[1] get_variable_upper_bound(::FlowActivePowerVariable, d::PSY.TModelHVDCLine, ::AbstractBranchFormulation) = _get_flow_bounds(d)[2] get_parameter_multiplier(::FixValueParameter, ::PSY.DCBranch, ::AbstractBranchFormulation) = 1.0 get_parameter_multiplier(::LowerBoundValueParameter, ::PSY.DCBranch, ::AbstractBranchFormulation) = 1.0 get_parameter_multiplier(::UpperBoundValueParameter, ::PSY.DCBranch, ::AbstractBranchFormulation) = 1.0 # This is an approximation for DC lines since the actual current limit depends on the voltage, that is a variable in the optimization problem function get_variable_lower_bound(::DCLineCurrent, d::PSY.TModelHVDCLine, ::AbstractBranchFormulation) p_min_flow = _get_flow_bounds(d)[1] arc = PSY.get_arc(d) bus_from = arc.from bus_to = arc.to max_v = max(PSY.get_magnitude(bus_from), PSY.get_magnitude(bus_to)) return p_min_flow / max_v end # This is an approximation for DC lines since the actual current limit depends on the voltage, that is a variable in the optimization problem function get_variable_upper_bound(::DCLineCurrent, d::PSY.TModelHVDCLine, ::AbstractBranchFormulation) p_max_flow = _get_flow_bounds(d)[2] arc = PSY.get_arc(d) bus_from = arc.from bus_to = arc.to max_v = max(PSY.get_magnitude(bus_from), PSY.get_magnitude(bus_to)) return p_max_flow / max_v end get_variable_multiplier(_, ::Type{PSY.TModelHVDCLine}, ::AbstractBranchFormulation) = 1.0 requires_initialization(::AbstractConverterFormulation) = false requires_initialization(::LosslessLine) = false function get_initial_conditions_device_model( ::OperationModel, model::DeviceModel{PSY.InterconnectingConverter, <:AbstractConverterFormulation}, ) return model end function get_initial_conditions_device_model( ::OperationModel, model::DeviceModel{PSY.TModelHVDCLine, D}, ) where {D <: AbstractDCLineFormulation} return model end function get_default_time_series_names( ::Type{PSY.InterconnectingConverter}, ::Type{<:AbstractConverterFormulation}, ) return Dict{Type{<:TimeSeriesParameter}, String}() end function get_default_time_series_names( ::Type{PSY.TModelHVDCLine}, ::Type{<:AbstractBranchFormulation}, ) return Dict{Type{<:TimeSeriesParameter}, String}() end function get_default_attributes( ::Type{PSY.InterconnectingConverter}, ::Type{<:AbstractConverterFormulation}, ) return Dict{String, Any}() end function get_default_attributes( ::Type{PSY.TModelHVDCLine}, ::Type{<:AbstractBranchFormulation}, ) return Dict{String, Any}() end ############################################ ######## Quadratic Converter Model ######### ############################################ ## Binaries ### get_variable_binary(::ConverterDCPower, ::Type{PSY.InterconnectingConverter}, ::AbstractConverterFormulation) = false get_variable_binary(::ConverterPowerDirection, ::Type{PSY.InterconnectingConverter}, ::AbstractConverterFormulation) = true get_variable_binary(::ConverterCurrent, ::Type{PSY.InterconnectingConverter}, ::AbstractConverterFormulation) = false get_variable_binary(::ConverterPositiveCurrent, ::Type{PSY.InterconnectingConverter}, ::AbstractConverterFormulation) = false get_variable_binary(::ConverterNegativeCurrent, ::Type{PSY.InterconnectingConverter}, ::AbstractConverterFormulation) = false get_variable_binary(::ConverterCurrentDirection, ::Type{PSY.InterconnectingConverter}, ::AbstractConverterFormulation) = true get_variable_binary(::SquaredConverterCurrent, ::Type{PSY.InterconnectingConverter}, ::AbstractConverterFormulation) = false get_variable_binary(::SquaredDCVoltage, ::Type{PSY.InterconnectingConverter}, ::AbstractConverterFormulation) = false get_variable_binary(::AuxBilinearConverterVariable, ::Type{PSY.InterconnectingConverter}, ::AbstractConverterFormulation) = false get_variable_binary(::AuxBilinearSquaredConverterVariable, ::Type{PSY.InterconnectingConverter}, ::AbstractConverterFormulation) = false function get_variable_binary( ::W, ::Type{PSY.InterconnectingConverter}, ::AbstractConverterFormulation ) where W <: InterpolationVariableType return false end function get_variable_binary( ::W, ::Type{PSY.InterconnectingConverter}, ::AbstractConverterFormulation ) where W <: BinaryInterpolationVariableType return true end ### Warm Start ### get_variable_warm_start_value(::ConverterCurrent, d::PSY.InterconnectingConverter, ::AbstractConverterFormulation) = PSY.get_dc_current(d) ### Lower Bounds ### get_variable_lower_bound(::ConverterDCPower, d::PSY.InterconnectingConverter, ::AbstractConverterFormulation) = PSY.get_active_power_limits(d).min get_variable_lower_bound(::ConverterCurrent, d::PSY.InterconnectingConverter, ::AbstractConverterFormulation) = -PSY.get_max_dc_current(d) get_variable_lower_bound(::SquaredConverterCurrent, d::PSY.InterconnectingConverter, ::AbstractConverterFormulation) = 0.0 get_variable_lower_bound(::SquaredDCVoltage, d::PSY.InterconnectingConverter, ::AbstractConverterFormulation) = PSY.get_voltage_limits(d.dc_bus).min^2 get_variable_lower_bound(::InterpolationVariableType, d::PSY.InterconnectingConverter, ::AbstractConverterFormulation) = 0.0 get_variable_lower_bound(::ConverterPositiveCurrent, d::PSY.InterconnectingConverter,::AbstractConverterFormulation) = 0.0 get_variable_lower_bound(::ConverterNegativeCurrent, d::PSY.InterconnectingConverter,::AbstractConverterFormulation) = 0.0 ### Upper Bounds ### get_variable_upper_bound(::ConverterDCPower, d::PSY.InterconnectingConverter, ::AbstractConverterFormulation) = PSY.get_active_power_limits(d).max get_variable_upper_bound(::ConverterCurrent, d::PSY.InterconnectingConverter, ::AbstractConverterFormulation) = PSY.get_max_dc_current(d) get_variable_upper_bound(::SquaredConverterCurrent, d::PSY.InterconnectingConverter, ::AbstractConverterFormulation) = PSY.get_max_dc_current(d)^2 get_variable_upper_bound(::SquaredDCVoltage, d::PSY.InterconnectingConverter, ::AbstractConverterFormulation) = PSY.get_voltage_limits(d.dc_bus).max^2 get_variable_upper_bound(::InterpolationVariableType, d::PSY.InterconnectingConverter, ::AbstractConverterFormulation) = 1.0 get_variable_upper_bound(::ConverterPositiveCurrent, d::PSY.InterconnectingConverter,::AbstractConverterFormulation) = PSY.get_max_dc_current(d) get_variable_upper_bound(::ConverterNegativeCurrent, d::PSY.InterconnectingConverter,::AbstractConverterFormulation) = PSY.get_max_dc_current(d) function get_default_attributes( ::Type{PSY.InterconnectingConverter}, ::Type{QuadraticLossConverter}, ) return Dict{String, Any}( "voltage_segments" => 3, "current_segments" => 6, "bilinear_segments" => 10, "use_linear_loss" => true, ) end #! format: on ############################################ ############## Expressions ################# ############################################ function add_to_expression!( container::OptimizationContainer, ::Type{T}, ::Type{U}, devices::IS.FlattenIteratorWrapper{V}, ::DeviceModel{V, W}, network_model::NetworkModel{X}, ) where { T <: Union{ActivePowerBalance, DCCurrentBalance}, U <: Union{FlowActivePowerVariable, DCLineCurrent}, V <: PSY.TModelHVDCLine, W <: AbstractDCLineFormulation, X <: PM.AbstractPowerModel, } variable = get_variable(container, U(), V) expression = get_expression(container, T(), PSY.DCBus) for d in devices arc = PSY.get_arc(d) to_bus_number = PSY.get_number(PSY.get_to(arc)) from_bus_number = PSY.get_number(PSY.get_from(arc)) for t in get_time_steps(container) name = PSY.get_name(d) _add_to_jump_expression!( expression[to_bus_number, t], variable[name, t], 1.0, ) _add_to_jump_expression!( expression[from_bus_number, t], variable[name, t], -1.0, ) 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{X}, ) where { T <: ActivePowerBalance, U <: ActivePowerVariable, V <: PSY.InterconnectingConverter, W <: AbstractConverterFormulation, X <: AreaPTDFPowerModel, } _add_to_expression!( container, T, U, devices, device_model, network_model, ) return end function add_to_expression!( container::OptimizationContainer, ::Type{T}, ::Type{U}, devices::IS.FlattenIteratorWrapper{V}, device_model::DeviceModel{V, W}, network_model::NetworkModel{X}, ) where { T <: ActivePowerBalance, U <: ActivePowerVariable, V <: PSY.InterconnectingConverter, W <: AbstractConverterFormulation, X <: PM.AbstractPowerModel, } _add_to_expression!( container, T, U, devices, device_model, network_model, ) 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 <: ActivePowerVariable, V <: PSY.InterconnectingConverter, W <: AbstractConverterFormulation, X <: PM.AbstractPowerModel, } 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!( 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, } variable = get_variable(container, U(), V) expression_dc = get_expression(container, T(), PSY.DCBus) expression_ac = get_expression(container, T(), PSY.ACBus) area_expr = get_expression(container, T(), PSY.Area) network_reduction = get_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_number_ac = PNM.get_mapped_bus_number(network_reduction, device_bus) bus_number_dc = PSY.get_number(PSY.get_dc_bus(d)) 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!( expression_ac[bus_number_ac, t], variable[name, t], get_variable_multiplier(U(), V, W()), ) _add_to_jump_expression!( expression_dc[bus_number_dc, t], variable[name, t], -1.0, ) end end 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) sys_expr = get_expression(container, T(), PSY.System) for d in devices name = PSY.get_name(d) device_bus = PSY.get_bus(d) bus_number_ac = PSY.get_number(device_bus) ref_bus = get_reference_bus(network_model, device_bus) bus_number_dc = PSY.get_number(PSY.get_dc_bus(d)) for t in get_time_steps(container) _add_to_jump_expression!( sys_expr[ref_bus, t], variable[name, t], get_variable_multiplier(U(), V, W()), ) _add_to_jump_expression!( expression_ac[bus_number_ac, t], variable[name, t], get_variable_multiplier(U(), V, W()), ) _add_to_jump_expression!( expression_dc[bus_number_dc, t], variable[name, t], -1.0, ) end 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!( container::OptimizationContainer, ::Type{T}, ::Type{U}, devices::IS.FlattenIteratorWrapper{V}, model::DeviceModel{V, W}, network_model::NetworkModel{CopperPlatePowerModel}, ) where { T <: ActivePowerBalance, U <: ActivePowerVariable, V <: PSY.InterconnectingConverter, W <: AbstractConverterFormulation, } variable = get_variable(container, U(), V) expression_dc = get_expression(container, T(), PSY.DCBus) sys_expr = get_expression(container, T(), PSY.System) for d in devices name = PSY.get_name(d) device_bus = PSY.get_bus(d) ref_bus = get_reference_bus(network_model, device_bus) bus_number_dc = PSY.get_number(PSY.get_dc_bus(d)) for t in get_time_steps(container) _add_to_jump_expression!( sys_expr[ref_bus, t], variable[name, t], get_variable_multiplier(U(), V, W()), ) _add_to_jump_expression!( expression_dc[bus_number_dc, t], variable[name, t], -1.0, ) end end return end function add_to_expression!( container::OptimizationContainer, ::Type{T}, ::Type{U}, devices::IS.FlattenIteratorWrapper{V}, model::DeviceModel{V, W}, network_model::NetworkModel{CopperPlatePowerModel}, ) where { T <: ActivePowerBalance, U <: ActivePowerVariable, V <: PSY.InterconnectingConverter, W <: QuadraticLossConverter, } variable = get_variable(container, U(), V) sys_expr = get_expression(container, T(), PSY.System) for d in devices name = PSY.get_name(d) device_bus = PSY.get_bus(d) ref_bus = get_reference_bus(network_model, device_bus) for t in get_time_steps(container) _add_to_jump_expression!( sys_expr[ref_bus, 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}, model::DeviceModel{V, W}, network_model::NetworkModel{CopperPlatePowerModel}, ) where { T <: DCCurrentBalance, U <: ConverterCurrent, V <: PSY.InterconnectingConverter, W <: QuadraticLossConverter, } variable = get_variable(container, U(), V) expression_dc = get_expression(container, T(), PSY.DCBus) for d in devices name = PSY.get_name(d) bus_number_dc = PSY.get_number(PSY.get_dc_bus(d)) for t in get_time_steps(container) _add_to_jump_expression!( expression_dc[bus_number_dc, t], variable[name, t], -1.0, ) end end 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 <: ActivePowerVariable, V <: PSY.InterconnectingConverter, W <: AbstractConverterFormulation, X <: PTDFPowerModel, } variable = get_variable(container, U(), V) expression_dc = get_expression(container, T(), PSY.DCBus) expression_ac = get_expression(container, T(), PSY.ACBus) sys_expr = get_expression(container, T(), PSY.System) for d in devices name = PSY.get_name(d) device_bus = PSY.get_bus(d) bus_number_ac = PSY.get_number(device_bus) ref_bus = get_reference_bus(network_model, device_bus) bus_number_dc = PSY.get_number(PSY.get_dc_bus(d)) for t in get_time_steps(container) _add_to_jump_expression!( sys_expr[ref_bus, t], variable[name, t], get_variable_multiplier(U(), V, W()), ) _add_to_jump_expression!( expression_ac[bus_number_ac, t], variable[name, t], get_variable_multiplier(U(), V, W()), ) _add_to_jump_expression!( expression_dc[bus_number_dc, t], variable[name, t], -1.0, ) end end return end ############################################ ############## Constraints ################# ############################################ ############## HVDC Lines ################## function add_constraints!( container::OptimizationContainer, ::Type{DCLineCurrentConstraint}, devices::IS.FlattenIteratorWrapper{T}, model::DeviceModel{T, U}, network_model::NetworkModel{V}, ) where {T <: PSY.TModelHVDCLine, U <: DCLossyLine, V <: PM.AbstractPowerModel} variable = get_variable(container, DCLineCurrent(), T) dc_voltage = get_variable(container, DCVoltage(), PSY.DCBus) time_steps = get_time_steps(container) constraints = add_constraints_container!( container, DCLineCurrentConstraint(), T, PSY.get_name.(devices), time_steps, ) for d in devices arc = PSY.get_arc(d) from_bus_name = PSY.get_name(arc.from) to_bus_name = PSY.get_name(arc.to) name = PSY.get_name(d) r = PSY.get_r(d) if iszero(r) for t in time_steps constraints[name, t] = JuMP.@constraint( get_jump_model(container), dc_voltage[from_bus_name, t] == dc_voltage[to_bus_name, t] ) end else for t in get_time_steps(container) constraints[name, t] = JuMP.@constraint( get_jump_model(container), variable[name, t] == (dc_voltage[from_bus_name, t] - dc_voltage[to_bus_name, t]) / r ) end end end return end ############## Converters ################## function add_constraints!( container::OptimizationContainer, ::Type{ConverterPowerCalculationConstraint}, devices::IS.FlattenIteratorWrapper{U}, model::DeviceModel{U, V}, network_model::NetworkModel{X}, ) where { U <: PSY.InterconnectingConverter, V <: QuadraticLossConverter, X <: PM.AbstractActivePowerModel, } time_steps = get_time_steps(container) varcurrent = get_variable(container, ConverterCurrent(), U) var_dcvoltage = get_variable(container, DCVoltage(), PSY.DCBus) var_sq_current = get_variable(container, SquaredConverterCurrent(), U) var_sq_voltage = get_variable(container, SquaredDCVoltage(), U) var_bilinear = get_variable(container, AuxBilinearConverterVariable(), U) var_sq_bilinear = get_variable(container, AuxBilinearSquaredConverterVariable(), U) var_dc_power = get_variable(container, ConverterDCPower(), U) ipc_names = axes(varcurrent, 1) constraint = add_constraints_container!( container, ConverterPowerCalculationConstraint(), U, ipc_names, time_steps, ) constraint_aux = add_constraints_container!( container, ConverterPowerCalculationConstraint(), U, ipc_names, time_steps; meta = "aux", ) for device in devices name = PSY.get_name(device) dc_bus_name = PSY.get_name(PSY.get_dc_bus(device)) for t in time_steps # p_dc = v_dc * i_dc = 0.5 * (bilinear - v_dc^2 - i_dc^2) constraint[name, t] = JuMP.@constraint( get_jump_model(container), var_dc_power[name, t] == 0.5 * ( var_sq_bilinear[name, t] - var_sq_voltage[name, t] - var_sq_current[name, t] ) ) constraint_aux[name, t] = JuMP.@constraint( get_jump_model(container), var_bilinear[name, t] == var_dcvoltage[dc_bus_name, t] + varcurrent[name, t] ) end end return end function add_constraints!( container::OptimizationContainer, ::Type{ConverterMcCormickEnvelopes}, devices::IS.FlattenIteratorWrapper{U}, model::DeviceModel{U, V}, network_model::NetworkModel{X}, ) where { U <: PSY.InterconnectingConverter, V <: QuadraticLossConverter, X <: PM.AbstractActivePowerModel, } time_steps = get_time_steps(container) varcurrent = get_variable(container, ConverterCurrent(), U) var_dcvoltage = get_variable(container, DCVoltage(), PSY.DCBus) var_dc_power = get_variable(container, ConverterDCPower(), U) ipc_names = axes(varcurrent, 1) constraint1_under = add_constraints_container!( container, ConverterMcCormickEnvelopes(), U, ipc_names, time_steps; meta = "under_1", ) constraint2_under = add_constraints_container!( container, ConverterMcCormickEnvelopes(), U, ipc_names, time_steps; meta = "under_2", ) constraint1_over = add_constraints_container!( container, ConverterMcCormickEnvelopes(), U, ipc_names, time_steps; meta = "over_1", ) constraint2_over = add_constraints_container!( container, ConverterMcCormickEnvelopes(), U, ipc_names, time_steps; meta = "over_2", ) for device in devices name = PSY.get_name(device) dc_bus = PSY.get_dc_bus(device) dc_bus_name = PSY.get_name(dc_bus) V_min, V_max = PSY.get_voltage_limits(dc_bus) I_max = PSY.get_max_dc_current(device) I_min = -I_max for t in time_steps constraint1_under[name, t] = JuMP.@constraint( get_jump_model(container), var_dc_power[name, t] >= V_min * varcurrent[name, t] + var_dcvoltage[dc_bus_name, t] * I_min - I_min * V_min ) constraint2_under[name, t] = JuMP.@constraint( get_jump_model(container), var_dc_power[name, t] >= V_max * varcurrent[name, t] + var_dcvoltage[dc_bus_name, t] * I_max - I_max * V_max ) constraint1_over[name, t] = JuMP.@constraint( get_jump_model(container), var_dc_power[name, t] <= V_max * varcurrent[name, t] + var_dcvoltage[dc_bus_name, t] * I_min - I_min * V_max ) constraint2_over[name, t] = JuMP.@constraint( get_jump_model(container), var_dc_power[name, t] <= V_min * varcurrent[name, t] + var_dcvoltage[dc_bus_name, t] * I_max - I_max * V_min ) end end return end function add_constraints!( container::OptimizationContainer, ::Type{ConverterLossConstraint}, devices::IS.FlattenIteratorWrapper{U}, model::DeviceModel{U, V}, network_model::NetworkModel{X}, ) where { U <: PSY.InterconnectingConverter, V <: QuadraticLossConverter, X <: PM.AbstractActivePowerModel, } time_steps = get_time_steps(container) var_sq_current = get_variable(container, SquaredConverterCurrent(), U) var_ac_power = get_variable(container, ActivePowerVariable(), U) var_dc_power = get_variable(container, ConverterDCPower(), U) ipc_names = axes(var_sq_current, 1) constraint = add_constraints_container!( container, ConverterLossConstraint(), U, ipc_names, time_steps, ) use_linear_loss = PSI.get_attribute(model, "use_linear_loss") if use_linear_loss pos_current = get_variable(container, ConverterPositiveCurrent(), U) neg_current = get_variable(container, ConverterNegativeCurrent(), U) end for device in devices name = PSY.get_name(device) loss_function = PSY.get_loss_function(device) if isa(loss_function, PSY.QuadraticCurve) a = PSY.get_quadratic_term(loss_function) b = PSY.get_proportional_term(loss_function) c = PSY.get_constant_term(loss_function) else a = 0.0 b = PSY.get_proportional_term(loss_function) c = PSY.get_constant_term(loss_function) end for t in time_steps if use_linear_loss loss = a * var_sq_current[name, t] + b * (pos_current[name, t] + neg_current[name, t]) + c else loss = a * var_sq_current[name, t] + c end constraint[name, t] = JuMP.@constraint( get_jump_model(container), var_ac_power[name, t] == var_dc_power[name, t] - loss ) end end return end function add_constraints!( container::OptimizationContainer, ::Type{T}, devices::IS.FlattenIteratorWrapper{U}, ::DeviceModel{U, V}, ::NetworkModel{<:PM.AbstractPowerModel}, ) where { T <: CurrentAbsoluteValueConstraint, U <: PSY.InterconnectingConverter, V <: QuadraticLossConverter, } time_steps = get_time_steps(container) names = [PSY.get_name(d) for d in devices] JuMPmodel = get_jump_model(container) # current vars # current_var = get_variable(container, ConverterCurrent(), U) # From direction current_var_pos = get_variable(container, ConverterPositiveCurrent(), U) # From direction current_var_neg = get_variable(container, ConverterNegativeCurrent(), U) # From direction current_dir = get_variable(container, ConverterCurrentDirection(), U) constraint = add_constraints_container!( container, CurrentAbsoluteValueConstraint(), U, names, time_steps, ) constraint_pos_ub = add_constraints_container!( container, CurrentAbsoluteValueConstraint(), U, names, time_steps; meta = "pos_ub", ) constraint_neg_ub = add_constraints_container!( container, CurrentAbsoluteValueConstraint(), U, names, time_steps; meta = "neg_ub", ) for d in devices name = PSY.get_name(d) I_max = PSY.get_max_dc_current(d) for t in time_steps constraint[name, t] = JuMP.@constraint( JuMPmodel, current_var[name, t] == current_var_pos[name, t] - current_var_neg[name, t] ) constraint_pos_ub[name, t] = JuMP.@constraint( JuMPmodel, current_var_pos[name, t] <= I_max * current_dir[name, t] ) constraint_neg_ub[name, t] = JuMP.@constraint( JuMPmodel, current_var_neg[name, t] <= I_max * (1 - current_dir[name, t]) ) end end return end function add_constraints!( container::OptimizationContainer, ::Type{T}, devices::IS.FlattenIteratorWrapper{U}, model::DeviceModel{U, V}, ::NetworkModel{<:PM.AbstractPowerModel}, ) where { T <: InterpolationVoltageConstraints, U <: PSY.InterconnectingConverter, V <: QuadraticLossConverter, } dic_var_bkpts = Dict{String, Vector{Float64}}() dic_function_bkpts = Dict{String, Vector{Float64}}() num_segments = get_attribute(model, "voltage_segments") for d in devices name = PSY.get_name(d) vmin, vmax = PSY.get_voltage_limits(d.dc_bus) var_bkpts, function_bkpts = _get_breakpoints_for_pwl_function(vmin, vmax, x -> x^2; num_segments) dic_var_bkpts[name] = var_bkpts dic_function_bkpts[name] = function_bkpts end _add_generic_incremental_interpolation_constraint!( container, DCVoltage(), SquaredDCVoltage(), InterpolationSquaredVoltageVariable(), InterpolationBinarySquaredVoltageVariable(), InterpolationVoltageConstraints(), devices, dic_var_bkpts, dic_function_bkpts, ) return end function add_constraints!( container::OptimizationContainer, ::Type{T}, devices::IS.FlattenIteratorWrapper{U}, model::DeviceModel{U, V}, ::NetworkModel{<:PM.AbstractPowerModel}, ) where { T <: InterpolationCurrentConstraints, U <: PSY.InterconnectingConverter, V <: QuadraticLossConverter, } dic_var_bkpts = Dict{String, Vector{Float64}}() dic_function_bkpts = Dict{String, Vector{Float64}}() num_segments = get_attribute(model, "current_segments") for d in devices name = PSY.get_name(d) Imax = PSY.get_max_dc_current(d) Imin = -Imax var_bkpts, function_bkpts = _get_breakpoints_for_pwl_function(Imin, Imax, x -> x^2; num_segments) dic_var_bkpts[name] = var_bkpts dic_function_bkpts[name] = function_bkpts end _add_generic_incremental_interpolation_constraint!( container, ConverterCurrent(), SquaredConverterCurrent(), InterpolationSquaredCurrentVariable(), InterpolationBinarySquaredCurrentVariable(), InterpolationCurrentConstraints(), devices, dic_var_bkpts, dic_function_bkpts, ) return end function add_constraints!( container::OptimizationContainer, ::Type{T}, devices::IS.FlattenIteratorWrapper{U}, model::DeviceModel{U, V}, ::NetworkModel{<:PM.AbstractPowerModel}, ) where { T <: InterpolationBilinearConstraints, U <: PSY.InterconnectingConverter, V <: QuadraticLossConverter, } dic_var_bkpts = Dict{String, Vector{Float64}}() dic_function_bkpts = Dict{String, Vector{Float64}}() num_segments = get_attribute(model, "bilinear_segments") for d in devices name = PSY.get_name(d) vmin, vmax = PSY.get_voltage_limits(d.dc_bus) Imax = PSY.get_max_dc_current(d) Imin = -Imax γ_min = vmin * Imin γ_max = vmax * Imax var_bkpts, function_bkpts = _get_breakpoints_for_pwl_function(γ_min, γ_max, x -> x^2; num_segments) dic_var_bkpts[name] = var_bkpts dic_function_bkpts[name] = function_bkpts end _add_generic_incremental_interpolation_constraint!( container, AuxBilinearConverterVariable(), AuxBilinearSquaredConverterVariable(), InterpolationSquaredBilinearVariable(), InterpolationBinarySquaredBilinearVariable(), InterpolationBilinearConstraints(), devices, dic_var_bkpts, dic_function_bkpts, ) return end ############################################ ########### Objective Function ############# ############################################ function objective_function!( ::OptimizationContainer, ::IS.FlattenIteratorWrapper{PSY.InterconnectingConverter}, ::DeviceModel{PSY.InterconnectingConverter, D}, ::Type{<:PM.AbstractPowerModel}, ) where {D <: AbstractConverterFormulation} return end ================================================ FILE: src/devices_models/devices/TwoTerminalDC_branches.jl ================================================ #################################### Branch Variables ################################################## #! format: off get_variable_binary(::FlowActivePowerSlackUpperBound, ::Type{<:PSY.TwoTerminalHVDC}, ::AbstractTwoTerminalDCLineFormulation,) = false get_variable_binary(::FlowActivePowerSlackLowerBound, ::Type{<:PSY.TwoTerminalHVDC}, ::AbstractTwoTerminalDCLineFormulation,) = false get_variable_binary(::HVDCPiecewiseLossVariable, ::Type{<:PSY.TwoTerminalHVDC}, ::AbstractTwoTerminalDCLineFormulation,) = false get_variable_binary(::HVDCActivePowerReceivedFromVariable, ::Type{<:PSY.TwoTerminalHVDC}, ::AbstractTwoTerminalDCLineFormulation,) = false get_variable_binary(::HVDCActivePowerReceivedToVariable, ::Type{<:PSY.TwoTerminalHVDC}, ::AbstractTwoTerminalDCLineFormulation,) = false get_variable_binary(::HVDCPiecewiseBinaryLossVariable, ::Type{<:PSY.TwoTerminalHVDC}, ::AbstractTwoTerminalDCLineFormulation,) = true get_variable_binary(_, ::Type{<:PSY.TwoTerminalHVDC}, ::AbstractTwoTerminalDCLineFormulation) = false get_variable_binary(::FlowActivePowerVariable, ::Type{<:PSY.TwoTerminalHVDC}, ::AbstractTwoTerminalDCLineFormulation) = false get_variable_binary(::HVDCFlowDirectionVariable, ::Type{<:PSY.TwoTerminalHVDC}, ::AbstractTwoTerminalDCLineFormulation) = true get_variable_multiplier(::FlowActivePowerVariable, ::Type{<:PSY.TwoTerminalHVDC}, _) = NaN get_parameter_multiplier(::FixValueParameter, ::PSY.TwoTerminalHVDC, ::AbstractTwoTerminalDCLineFormulation) = 1.0 get_variable_multiplier(::FlowActivePowerFromToVariable, ::Type{<:PSY.TwoTerminalHVDC}, ::AbstractTwoTerminalDCLineFormulation) = -1.0 get_variable_multiplier(::FlowActivePowerToFromVariable, ::Type{<:PSY.TwoTerminalHVDC}, ::AbstractTwoTerminalDCLineFormulation) = -1.0 function get_variable_multiplier( ::HVDCLosses, d::PSY.TwoTerminalHVDC, ::HVDCTwoTerminalDispatch, ) loss = PSY.get_loss(d) if !isa(loss, PSY.LinearCurve) error( "HVDCTwoTerminalDispatch of branch $(PSY.get_name(d)) only accepts LinearCurve for loss models.", ) end l1 = PSY.get_proportional_term(loss) l0 = PSY.get_constant_term(loss) if l1 == 0.0 && l0 == 0.0 return 0.0 else return -1.0 end end get_variable_lower_bound(::FlowActivePowerVariable, d::PSY.TwoTerminalHVDC, ::HVDCTwoTerminalUnbounded) = nothing get_variable_upper_bound(::FlowActivePowerVariable, d::PSY.TwoTerminalHVDC, ::HVDCTwoTerminalUnbounded) = nothing get_variable_lower_bound(::FlowActivePowerVariable, d::PSY.TwoTerminalHVDC, ::AbstractTwoTerminalDCLineFormulation) = nothing get_variable_upper_bound(::FlowActivePowerVariable, d::PSY.TwoTerminalHVDC, ::AbstractTwoTerminalDCLineFormulation) = nothing get_variable_lower_bound(::HVDCLosses, d::PSY.TwoTerminalHVDC, ::HVDCTwoTerminalDispatch) = 0.0 get_variable_upper_bound(::FlowActivePowerFromToVariable, d::PSY.TwoTerminalHVDC, ::HVDCTwoTerminalDispatch) = PSY.get_active_power_limits_from(d).max get_variable_lower_bound(::FlowActivePowerFromToVariable, d::PSY.TwoTerminalHVDC, ::HVDCTwoTerminalDispatch) = PSY.get_active_power_limits_from(d).min get_variable_upper_bound(::FlowActivePowerToFromVariable, d::PSY.TwoTerminalHVDC, ::HVDCTwoTerminalDispatch) = PSY.get_active_power_limits_to(d).max get_variable_lower_bound(::FlowActivePowerToFromVariable, d::PSY.TwoTerminalHVDC, ::HVDCTwoTerminalDispatch) = PSY.get_active_power_limits_to(d).min get_variable_upper_bound(::HVDCActivePowerReceivedFromVariable, d::PSY.TwoTerminalHVDC, ::AbstractTwoTerminalDCLineFormulation) = PSY.get_active_power_limits_from(d).max get_variable_lower_bound(::HVDCActivePowerReceivedFromVariable, d::PSY.TwoTerminalHVDC, ::AbstractTwoTerminalDCLineFormulation) = PSY.get_active_power_limits_from(d).min get_variable_upper_bound(::HVDCActivePowerReceivedToVariable, d::PSY.TwoTerminalHVDC, ::AbstractTwoTerminalDCLineFormulation) = PSY.get_active_power_limits_to(d).max get_variable_lower_bound(::HVDCActivePowerReceivedToVariable, d::PSY.TwoTerminalHVDC, ::AbstractTwoTerminalDCLineFormulation) = PSY.get_active_power_limits_to(d).min function get_variable_upper_bound( ::HVDCLosses, d::PSY.TwoTerminalHVDC, ::HVDCTwoTerminalDispatch, ) loss = PSY.get_loss(d) if !isa(loss, PSY.LinearCurve) error( "HVDCTwoTerminalDispatch of branch $(PSY.get_name(d)) only accepts LinearCurve for loss models.", ) end l1 = PSY.get_proportional_term(loss) l0 = PSY.get_constant_term(loss) if l1 == 0.0 && l0 == 0.0 return 0.0 else return nothing end end get_variable_upper_bound(::HVDCPiecewiseLossVariable, d::PSY.TwoTerminalHVDC, ::Union{HVDCTwoTerminalDispatch, HVDCTwoTerminalPiecewiseLoss}) = 1.0 get_variable_lower_bound(::HVDCPiecewiseLossVariable, d::PSY.TwoTerminalHVDC, ::Union{HVDCTwoTerminalDispatch, HVDCTwoTerminalPiecewiseLoss}) = 0.0 #################################### LCC ################################################## get_variable_binary(::HVDCActivePowerReceivedFromVariable, ::Type{PSY.TwoTerminalLCCLine}, ::HVDCTwoTerminalLCC) = false get_variable_binary(::HVDCActivePowerReceivedToVariable, ::Type{PSY.TwoTerminalLCCLine}, ::HVDCTwoTerminalLCC) = false get_variable_binary(::HVDCReactivePowerReceivedFromVariable, ::Type{PSY.TwoTerminalLCCLine}, ::HVDCTwoTerminalLCC) = false get_variable_binary(::HVDCReactivePowerReceivedToVariable, ::Type{PSY.TwoTerminalLCCLine}, ::HVDCTwoTerminalLCC) = false get_variable_binary(::HVDCRectifierDelayAngleVariable, ::Type{PSY.TwoTerminalLCCLine}, ::HVDCTwoTerminalLCC) = false get_variable_binary(::HVDCInverterExtinctionAngleVariable, ::Type{PSY.TwoTerminalLCCLine}, ::HVDCTwoTerminalLCC) = false get_variable_binary(::HVDCRectifierPowerFactorAngleVariable, ::Type{PSY.TwoTerminalLCCLine}, ::HVDCTwoTerminalLCC) = false get_variable_binary(::HVDCInverterPowerFactorAngleVariable, ::Type{PSY.TwoTerminalLCCLine}, ::HVDCTwoTerminalLCC) = false get_variable_binary(::HVDCRectifierOverlapAngleVariable, ::Type{PSY.TwoTerminalLCCLine}, ::HVDCTwoTerminalLCC) = false get_variable_binary(::HVDCInverterOverlapAngleVariable, ::Type{PSY.TwoTerminalLCCLine}, ::HVDCTwoTerminalLCC) = false get_variable_binary(::HVDCRectifierDCVoltageVariable, ::Type{PSY.TwoTerminalLCCLine}, ::HVDCTwoTerminalLCC) = false get_variable_binary(::HVDCInverterDCVoltageVariable, ::Type{PSY.TwoTerminalLCCLine}, ::HVDCTwoTerminalLCC) = false get_variable_binary(::HVDCRectifierACCurrentVariable, ::Type{PSY.TwoTerminalLCCLine}, ::HVDCTwoTerminalLCC) = false get_variable_binary(::HVDCInverterACCurrentVariable, ::Type{PSY.TwoTerminalLCCLine}, ::HVDCTwoTerminalLCC) = false get_variable_binary(::DCLineCurrentFlowVariable, ::Type{PSY.TwoTerminalLCCLine}, ::HVDCTwoTerminalLCC) = false get_variable_binary(::HVDCRectifierTapSettingVariable, ::Type{PSY.TwoTerminalLCCLine}, ::HVDCTwoTerminalLCC) = false get_variable_binary(::HVDCInverterTapSettingVariable, ::Type{PSY.TwoTerminalLCCLine}, ::HVDCTwoTerminalLCC) = false get_variable_upper_bound(::HVDCRectifierDelayAngleVariable, d::PSY.TwoTerminalLCCLine, ::HVDCTwoTerminalLCC) = PSY.get_rectifier_delay_angle_limits(d).max get_variable_lower_bound(::HVDCRectifierDelayAngleVariable, d::PSY.TwoTerminalLCCLine, ::HVDCTwoTerminalLCC) = PSY.get_rectifier_delay_angle_limits(d).min get_variable_upper_bound(::HVDCInverterExtinctionAngleVariable, d::PSY.TwoTerminalLCCLine, ::HVDCTwoTerminalLCC) = PSY.get_inverter_extinction_angle_limits(d).max get_variable_lower_bound(::HVDCInverterExtinctionAngleVariable, d::PSY.TwoTerminalLCCLine, ::HVDCTwoTerminalLCC) = PSY.get_inverter_extinction_angle_limits(d).min get_variable_upper_bound(::HVDCRectifierTapSettingVariable, d::PSY.TwoTerminalLCCLine, ::HVDCTwoTerminalLCC) = PSY.get_rectifier_tap_limits(d).max get_variable_lower_bound(::HVDCRectifierTapSettingVariable, d::PSY.TwoTerminalLCCLine, ::HVDCTwoTerminalLCC) = PSY.get_rectifier_tap_limits(d).min get_variable_upper_bound(::HVDCInverterTapSettingVariable, d::PSY.TwoTerminalLCCLine, ::HVDCTwoTerminalLCC) = PSY.get_inverter_tap_limits(d).max get_variable_lower_bound(::HVDCInverterTapSettingVariable, d::PSY.TwoTerminalLCCLine, ::HVDCTwoTerminalLCC) = PSY.get_inverter_tap_limits(d).min #! format: on ########################################################## function get_default_time_series_names( ::Type{U}, ::Type{V}, ) where {U <: PSY.TwoTerminalHVDC, V <: AbstractTwoTerminalDCLineFormulation} return Dict{Type{<:TimeSeriesParameter}, String}() end function get_default_attributes( ::Type{U}, ::Type{V}, ) where {U <: PSY.TwoTerminalHVDC, V <: AbstractTwoTerminalDCLineFormulation} return Dict{String, Any}() end get_initial_conditions_device_model( ::OperationModel, ::DeviceModel{T, U}, ) where {T <: PSY.TwoTerminalHVDC, U <: AbstractTwoTerminalDCLineFormulation} = DeviceModel(T, U) ####################################### PWL Constraints ####################################################### function _get_range_segments(::PSY.TwoTerminalHVDC, loss::PSY.LinearCurve) return 1:4 end function _get_range_segments( ::PSY.TwoTerminalHVDC, loss::PSY.PiecewiseIncrementalCurve, ) loss_factors = PSY.get_slopes(loss) return 1:(2 * length(loss_factors) + 2) end function _add_dense_pwl_loss_variables!( container::OptimizationContainer, devices, model::DeviceModel{D, HVDCTwoTerminalPiecewiseLoss}, ) where {D <: PSY.TwoTerminalHVDC} # Check if type and length of PWL loss model are the same for all devices _check_pwl_loss_model(devices) # Create Variables time_steps = get_time_steps(container) settings = get_settings(container) formulation = HVDCTwoTerminalPiecewiseLoss() T = HVDCPiecewiseLossVariable binary = get_variable_binary(T(), D, formulation) first_loss = PSY.get_loss(first(devices)) if isa(first_loss, PSY.LinearCurve) len_segments = 4 # 2*1 + 2 elseif isa(first_loss, PSY.PiecewiseIncrementalCurve) len_segments = 2 * length(PSY.get_slopes(first_loss)) + 2 else error("Should not be here") end segments = ["pwl_$i" for i in 1:len_segments] T = HVDCPiecewiseLossVariable variable = add_variable_container!( container, T(), D, PSY.get_name.(devices), segments, time_steps, ) for t in time_steps, s in segments, d in devices name = PSY.get_name(d) variable[name, s, t] = JuMP.@variable( get_jump_model(container), base_name = "$(T)_$(D)_{$(name), $(s), $(t)}", binary = binary ) ub = get_variable_upper_bound(T(), d, formulation) ub !== nothing && JuMP.set_upper_bound(variable[name, s, t], ub) lb = get_variable_lower_bound(T(), d, formulation) lb !== nothing && JuMP.set_lower_bound(variable[name, s, t], lb) if get_warm_start(settings) init = get_variable_warm_start_value(T(), d, formulation) init !== nothing && JuMP.set_start_value(variable[name, s, t], init) end end end # Full Binary function _add_sparse_pwl_loss_variables!( container::OptimizationContainer, devices, ::DeviceModel{D, HVDCTwoTerminalPiecewiseLoss}, ) where {D <: PSY.TwoTerminalHVDC} # Check if type and length of PWL loss model are the same for all devices #_check_pwl_loss_model(devices) # Create Variables time_steps = get_time_steps(container) settings = get_settings(container) formulation = HVDCTwoTerminalPiecewiseLoss() T = HVDCPiecewiseLossVariable binary_T = get_variable_binary(T(), D, formulation) U = HVDCPiecewiseBinaryLossVariable binary_U = get_variable_binary(U(), D, formulation) first_loss = PSY.get_loss(first(devices)) if isa(first_loss, PSY.LinearCurve) len_segments = 3 # 2*1 + 1 elseif isa(first_loss, PSY.PiecewiseIncrementalCurve) len_segments = 2 * length(PSY.get_slopes(first_loss)) + 1 else error("Should not be here") end var_container = lazy_container_addition!(container, T(), D) var_container_binary = lazy_container_addition!(container, U(), D) for d in devices name = PSY.get_name(d) for t in time_steps pwlvars = Array{JuMP.VariableRef}(undef, len_segments) pwlvars_bin = Array{JuMP.VariableRef}(undef, len_segments) for i in 1:len_segments pwlvars[i] = var_container[(name, i, t)] = JuMP.@variable( get_jump_model(container), base_name = "$(T)_$(name)_{pwl_$(i), $(t)}", binary = binary_T ) ub = get_variable_upper_bound(T(), d, formulation) ub !== nothing && JuMP.set_upper_bound(var_container[name, i, t], ub) lb = get_variable_lower_bound(T(), d, formulation) lb !== nothing && JuMP.set_lower_bound(var_container[name, i, t], lb) pwlvars_bin[i] = var_container_binary[(name, i, t)] = JuMP.@variable( get_jump_model(container), base_name = "$(U)_$(name)_{pwl_$(i), $(t)}", binary = binary_U ) end end end end function _get_pwl_loss_params(d::PSY.TwoTerminalHVDC, loss::PSY.LinearCurve) from_to_loss_params = Vector{Float64}(undef, 4) to_from_loss_params = Vector{Float64}(undef, 4) loss_factor = PSY.get_proportional_term(loss) P_send0 = PSY.get_constant_term(loss) P_max_ft = PSY.get_active_power_limits_from(d).max P_max_tf = PSY.get_active_power_limits_to(d).max if P_max_ft != P_max_tf error( "HVDC Line $(PSY.get_name(d)) has non-symmetrical limits for from and to, that are not supported in the HVDCTwoTerminalPiecewiseLoss formulation", ) end P_sendS = P_max_ft ### Update Params Vectors ### from_to_loss_params[1] = -P_sendS - P_send0 from_to_loss_params[2] = -P_send0 from_to_loss_params[3] = 0.0 from_to_loss_params[4] = P_sendS * (1 - loss_factor) to_from_loss_params[1] = P_sendS * (1 - loss_factor) to_from_loss_params[2] = 0.0 to_from_loss_params[3] = -P_send0 to_from_loss_params[4] = -P_sendS - P_send0 return from_to_loss_params, to_from_loss_params end function _get_pwl_loss_params( d::PSY.TwoTerminalHVDC, loss::PSY.PiecewiseIncrementalCurve, ) p_breakpoints = PSY.get_x_coords(loss) loss_factors = PSY.get_slopes(loss) len_segments = length(loss_factors) len_variables = 2 * len_segments + 2 from_to_loss_params = Vector{Float64}(undef, len_variables) to_from_loss_params = similar(from_to_loss_params) P_max_ft = PSY.get_active_power_limits_from(d).max P_max_tf = PSY.get_active_power_limits_to(d).max if P_max_ft != P_max_tf error( "HVDC Line $(PSY.get_name(d)) has non-symmetrical limits for from and to, that are not supported in the HVDCTwoTerminalPiecewiseLoss formulation", ) end if P_max_ft != last(p_breakpoints) error( "Maximum power limit $P_max_ft of HVDC Line $(PSY.get_name(d)) has different value of last breakpoint from Loss data $(last(p_breakpoints)).", ) end ### Update Params Vectors ### ## Update from 1 to S for i in 1:len_segments from_to_loss_params[i] = -p_breakpoints[2 + len_segments - i] - p_breakpoints[1] # for i = 1: P_end, for i = len_segments: P_2 to_from_loss_params[i] = p_breakpoints[2 + len_segments - i] * (1 - loss_factors[len_segments + 1 - i]) end ## Update from S+1 and S+2 from_to_loss_params[len_segments + 1] = -p_breakpoints[1] # P_send0 from_to_loss_params[len_segments + 2] = 0.0 to_from_loss_params[len_segments + 1] = 0.0 to_from_loss_params[len_segments + 2] = -p_breakpoints[1] # P_send0 ## Update from S+3 to 2S+2 for i in 1:len_segments from_to_loss_params[2 + len_segments + i] = p_breakpoints[i + 1] * (1 - loss_factors[i]) to_from_loss_params[2 + len_segments + i] = -p_breakpoints[i + 1] - p_breakpoints[1] end return from_to_loss_params, to_from_loss_params end function add_variables!( container::OptimizationContainer, ::Type{FlowActivePowerVariable}, network_model::NetworkModel{CopperPlatePowerModel}, devices::IS.FlattenIteratorWrapper{T}, formulation::U, ) where {T <: PSY.TwoTerminalHVDC, U <: AbstractBranchFormulation} inter_network_branches = T[] for d in devices ref_bus_from = get_reference_bus(network_model, PSY.get_arc(d).from) ref_bus_to = get_reference_bus(network_model, PSY.get_arc(d).to) if ref_bus_from != ref_bus_to push!(inter_network_branches, d) else @warn( "HVDC Line $(PSY.get_name(d)) is in the same subnetwork, so the line will not be modeled." ) end end if !isempty(inter_network_branches) add_variables!(container, FlowActivePowerVariable, inter_network_branches, U()) end return end function add_constraints!( container::OptimizationContainer, ::Type{T}, devices::Union{Vector{U}, IS.FlattenIteratorWrapper{U}}, ::DeviceModel{U, HVDCTwoTerminalPiecewiseLoss}, ::NetworkModel{<:PM.AbstractPowerModel}, ) where {T <: HVDCFlowCalculationConstraint, U <: PSY.TwoTerminalHVDC} var_pwl = get_variable(container, HVDCPiecewiseLossVariable(), U) var_pwl_bin = get_variable(container, HVDCPiecewiseBinaryLossVariable(), U) names = PSY.get_name.(devices) time_steps = get_time_steps(container) flow_ft = get_variable(container, HVDCActivePowerReceivedFromVariable(), U) flow_tf = get_variable(container, HVDCActivePowerReceivedToVariable(), U) constraint_from_to = add_constraints_container!(container, T(), U, names, time_steps; meta = "ft") constraint_to_from = add_constraints_container!(container, T(), U, names, time_steps; meta = "tf") constraint_binary = add_constraints_container!(container, T(), U, names, time_steps; meta = "bin") for d in devices name = PSY.get_name(d) loss = PSY.get_loss(d) from_to_params, to_from_params = _get_pwl_loss_params(d, loss) range_segments = 1:(length(from_to_params) - 1) # 1:(2S+1) for t in time_steps ## Add Equality Constraints ## constraint_from_to[name, t] = JuMP.@constraint( get_jump_model(container), flow_ft[name, t] == sum( var_pwl_bin[name, ix, t] * from_to_params[ix] for ix in range_segments ) + sum( var_pwl[name, ix, t] * (from_to_params[ix + 1] - from_to_params[ix]) for ix in range_segments ) ) constraint_to_from[name, t] = JuMP.@constraint( get_jump_model(container), flow_tf[name, t] == sum( var_pwl_bin[name, ix, t] * to_from_params[ix] for ix in range_segments ) + sum( var_pwl[name, ix, t] * (to_from_params[ix + 1] - to_from_params[ix]) for ix in range_segments ) ) ## Add Binary Bound ### constraint_binary[name, t] = JuMP.@constraint( get_jump_model(container), sum(var_pwl_bin[name, ix, t] for ix in range_segments) == 1.0 ) ## Add Bounds for Continuous ## for ix in range_segments JuMP.@constraint( get_jump_model(container), var_pwl[name, ix, t] <= var_pwl_bin[name, ix, t] ) if ix == div(length(range_segments) + 1, 2) JuMP.fix(var_pwl[name, ix, t], 0.0; force = true) end end end end return end #################################### Rate Limits Constraints ################################################## function _get_flow_bounds(d::PSY.TwoTerminalHVDC) check_hvdc_line_limits_consistency(d) from_min = PSY.get_active_power_limits_from(d).min to_min = PSY.get_active_power_limits_to(d).min from_max = PSY.get_active_power_limits_from(d).max to_max = PSY.get_active_power_limits_to(d).max if from_min >= 0.0 && to_min >= 0.0 min_rate = min(from_min, to_min) elseif from_min <= 0.0 && to_min <= 0.0 min_rate = max(from_min, to_min) elseif from_min <= 0.0 && to_min >= 0.0 min_rate = from_min elseif to_min <= 0.0 && from_min >= 0.0 min_rate = to_min end if from_max >= 0.0 && to_max >= 0.0 max_rate = min(from_max, to_max) elseif from_max <= 0.0 && to_max <= 0.0 max_rate = max(from_max, to_max) elseif from_max <= 0.0 && to_max >= 0.0 max_rate = from_max elseif from_max >= 0.0 && to_max <= 0.0 max_rate = to_max end return min_rate, max_rate end add_constraints!( ::OptimizationContainer, ::Type{<:Union{FlowRateConstraintFromTo, FlowRateConstraintToFrom}}, ::IS.FlattenIteratorWrapper{T}, ::DeviceModel{T, HVDCTwoTerminalUnbounded}, ::NetworkModel{<:PM.AbstractPowerModel}, ) where {T <: PSY.TwoTerminalHVDC} = nothing add_constraints!( ::OptimizationContainer, ::Type{FlowRateConstraint}, ::IS.FlattenIteratorWrapper{T}, ::DeviceModel{T, HVDCTwoTerminalUnbounded}, ::NetworkModel{<:PM.AbstractPowerModel}, ) where {T <: PSY.TwoTerminalHVDC} = nothing function add_constraints!( container::OptimizationContainer, ::Type{T}, devices::Union{Vector{U}, IS.FlattenIteratorWrapper{U}}, ::DeviceModel{U, HVDCTwoTerminalLossless}, ::NetworkModel{<:PM.AbstractPowerModel}, ) where {T <: FlowRateConstraint, U <: PSY.TwoTerminalHVDC} time_steps = get_time_steps(container) names = PSY.get_name.(devices) var = get_variable(container, FlowActivePowerVariable(), U) constraint_ub = add_constraints_container!(container, T(), U, names, time_steps; meta = "ub") constraint_lb = add_constraints_container!(container, T(), U, names, time_steps; meta = "lb") for d in devices min_rate, max_rate = _get_flow_bounds(d) for t in time_steps constraint_ub[PSY.get_name(d), t] = JuMP.@constraint( get_jump_model(container), var[PSY.get_name(d), t] <= max_rate ) constraint_lb[PSY.get_name(d), t] = JuMP.@constraint( get_jump_model(container), min_rate <= var[PSY.get_name(d), t] ) end end return end function add_constraints!( container::OptimizationContainer, ::Type{T}, devices::Union{Vector{U}, IS.FlattenIteratorWrapper{U}}, ::DeviceModel{U, HVDCTwoTerminalLossless}, network_model::NetworkModel{CopperPlatePowerModel}, ) where {T <: FlowRateConstraint, U <: PSY.TwoTerminalHVDC} time_steps = get_time_steps(container) names = String[] modeled_devices = U[] for d in devices ref_bus_from = get_reference_bus(network_model, PSY.get_arc(d).from) ref_bus_to = get_reference_bus(network_model, PSY.get_arc(d).to) if ref_bus_from != ref_bus_to push!(names, PSY.get_name(d)) push!(modeled_devices, d) end end var = get_variable(container, FlowActivePowerVariable(), U) constraint_ub = add_constraints_container!(container, T(), U, names, time_steps; meta = "ub") constraint_lb = add_constraints_container!(container, T(), U, names, time_steps; meta = "lb") for d in modeled_devices min_rate, max_rate = _get_flow_bounds(d) for t in time_steps constraint_ub[PSY.get_name(d), t] = JuMP.@constraint( get_jump_model(container), var[PSY.get_name(d), t] <= max_rate ) constraint_lb[PSY.get_name(d), t] = JuMP.@constraint( get_jump_model(container), min_rate <= var[PSY.get_name(d), t] ) end end return end function _add_hvdc_flow_constraints!( container::OptimizationContainer, devices::Union{Vector{T}, IS.FlattenIteratorWrapper{T}}, constraint::FlowRateConstraintFromTo, ) where {T <: PSY.TwoTerminalHVDC} _add_hvdc_flow_constraints!( container, devices, FlowActivePowerFromToVariable(), constraint, ) end function _add_hvdc_flow_constraints!( container::OptimizationContainer, devices::Union{Vector{T}, IS.FlattenIteratorWrapper{T}}, constraint::FlowRateConstraintToFrom, ) where {T <: PSY.TwoTerminalHVDC} _add_hvdc_flow_constraints!( container, devices, FlowActivePowerToFromVariable(), constraint, ) end function _add_hvdc_flow_constraints!( container::OptimizationContainer, devices::Union{Vector{T}, IS.FlattenIteratorWrapper{T}}, var::Union{ FlowActivePowerFromToVariable, FlowActivePowerToFromVariable, HVDCActivePowerReceivedFromVariable, HVDCActivePowerReceivedToVariable, }, constraint::Union{FlowRateConstraintFromTo, FlowRateConstraintToFrom}, ) where {T <: PSY.TwoTerminalHVDC} time_steps = get_time_steps(container) names = PSY.get_name.(devices) variable = get_variable(container, var, T) constraint_ub = add_constraints_container!(container, constraint, T, names, time_steps; meta = "ub") constraint_lb = add_constraints_container!(container, constraint, T, names, time_steps; meta = "lb") for d in devices check_hvdc_line_limits_consistency(d) max_rate = get_variable_upper_bound(var, d, HVDCTwoTerminalDispatch()) min_rate = get_variable_lower_bound(var, d, HVDCTwoTerminalDispatch()) name = PSY.get_name(d) for t in time_steps constraint_ub[name, t] = JuMP.@constraint( get_jump_model(container), variable[name, t] <= max_rate ) constraint_lb[name, t] = JuMP.@constraint( get_jump_model(container), min_rate <= variable[name, t] ) end end return end function add_constraints!( container::OptimizationContainer, ::Type{T}, devices::IS.FlattenIteratorWrapper{U}, model::DeviceModel{U, HVDCTwoTerminalDispatch}, network_model::NetworkModel{CopperPlatePowerModel}, ) where { T <: Union{FlowRateConstraintFromTo, FlowRateConstraintToFrom}, U <: PSY.TwoTerminalHVDC, } inter_network_branches = U[] for d in devices ref_bus_from = get_reference_bus(network_model, PSY.get_arc(d).from) ref_bus_to = get_reference_bus(network_model, PSY.get_arc(d).to) if ref_bus_from != ref_bus_to push!(inter_network_branches, d) end end if !isempty(inter_network_branches) _add_hvdc_flow_constraints!(container, devices, T()) end return end function add_constraints!( container::OptimizationContainer, ::Type{T}, devices::IS.FlattenIteratorWrapper{U}, ::DeviceModel{U, HVDCTwoTerminalDispatch}, ::NetworkModel{<:PM.AbstractDCPModel}, ) where { T <: Union{FlowRateConstraintToFrom, FlowRateConstraintFromTo}, U <: PSY.TwoTerminalHVDC, } _add_hvdc_flow_constraints!(container, devices, T()) return end function add_constraints!( container::OptimizationContainer, ::Type{T}, devices::IS.FlattenIteratorWrapper{U}, ::DeviceModel{U, HVDCTwoTerminalDispatch}, ::NetworkModel{<:AbstractPTDFModel}, ) where { T <: Union{FlowRateConstraintToFrom, FlowRateConstraintFromTo}, U <: PSY.TwoTerminalHVDC, } _add_hvdc_flow_constraints!(container, devices, T()) return end function add_constraints!( container::OptimizationContainer, ::Type{T}, devices::IS.FlattenIteratorWrapper{U}, model::DeviceModel{U, V}, network_model::NetworkModel{CopperPlatePowerModel}, ) where { T <: Union{FlowRateConstraintFromTo, FlowRateConstraintToFrom}, U <: PSY.TwoTerminalHVDC, V <: HVDCTwoTerminalPiecewiseLoss, } inter_network_branches = U[] for d in devices ref_bus_from = get_reference_bus(network_model, PSY.get_arc(d).from) ref_bus_to = get_reference_bus(network_model, PSY.get_arc(d).to) if ref_bus_from != ref_bus_to push!(inter_network_branches, d) end end if !isempty(inter_network_branches) if T <: FlowRateConstraintFromTo _add_hvdc_flow_constraints!( container, devices, HVDCActivePowerReceivedFromVariable(), T(), ) else _add_hvdc_flow_constraints!( container, devices, HVDCActivePowerReceivedToVariable(), T(), ) end end return end function add_constraints!( container::OptimizationContainer, ::Type{T}, devices::IS.FlattenIteratorWrapper{U}, ::DeviceModel{U, V}, ::NetworkModel{<:AbstractPTDFModel}, ) where { T <: Union{FlowRateConstraintFromTo, FlowRateConstraintToFrom}, U <: PSY.TwoTerminalHVDC, V <: HVDCTwoTerminalPiecewiseLoss, } if T <: FlowRateConstraintFromTo _add_hvdc_flow_constraints!( container, devices, HVDCActivePowerReceivedFromVariable(), T(), ) else _add_hvdc_flow_constraints!( container, devices, HVDCActivePowerReceivedToVariable(), T(), ) end return end function add_constraints!( container::OptimizationContainer, ::Type{HVDCPowerBalance}, devices::IS.FlattenIteratorWrapper{T}, ::DeviceModel{T, <:AbstractTwoTerminalDCLineFormulation}, ::NetworkModel{<:PM.AbstractDCPModel}, ) where {T <: PSY.TwoTerminalHVDC} time_steps = get_time_steps(container) names = PSY.get_name.(devices) tf_var = get_variable(container, FlowActivePowerToFromVariable(), T) ft_var = get_variable(container, FlowActivePowerFromToVariable(), T) direction_var = get_variable(container, HVDCFlowDirectionVariable(), T) losses = get_variable(container, HVDCLosses(), T) constraint_ft_ub = add_constraints_container!( container, HVDCPowerBalance(), T, names, time_steps; meta = "ft_ub", ) constraint_tf_ub = add_constraints_container!( container, HVDCPowerBalance(), T, names, time_steps; meta = "tf_ub", ) constraint_ft_lb = add_constraints_container!( container, HVDCPowerBalance(), T, names, time_steps; meta = "tf_lb", ) constraint_tf_lb = add_constraints_container!( container, HVDCPowerBalance(), T, names, time_steps; meta = "ft_lb", ) constraint_loss = add_constraints_container!( container, HVDCPowerBalance(), T, names, time_steps; meta = "loss", ) constraint_loss_aux1 = add_constraints_container!( container, HVDCPowerBalance(), T, names, time_steps; meta = "loss_aux1", ) constraint_loss_aux2 = add_constraints_container!( container, HVDCPowerBalance(), T, names, time_steps; meta = "loss_aux2", ) constraint_loss_aux3 = add_constraints_container!( container, HVDCPowerBalance(), T, names, time_steps; meta = "loss_aux3", ) constraint_loss_aux4 = add_constraints_container!( container, HVDCPowerBalance(), T, names, time_steps; meta = "loss_aux4", ) for d in devices name = PSY.get_name(d) loss = PSY.get_loss(d) if !isa(loss, PSY.LinearCurve) error( "HVDCTwoTerminalDispatch of branch $(name) only accepts LinearCurve for loss models.", ) end l1 = PSY.get_proportional_term(loss) l0 = PSY.get_constant_term(loss) R_min_from, R_max_from = PSY.get_active_power_limits_from(d) R_min_to, R_max_to = PSY.get_active_power_limits_to(d) for t in get_time_steps(container) constraint_tf_ub[name, t] = JuMP.@constraint( get_jump_model(container), tf_var[name, t] <= R_max_to * direction_var[name, t] ) constraint_tf_lb[name, t] = JuMP.@constraint( get_jump_model(container), tf_var[name, t] >= R_min_to * (1 - direction_var[name, t]) ) constraint_ft_ub[name, t] = JuMP.@constraint( get_jump_model(container), ft_var[name, t] <= R_max_from * (1 - direction_var[name, t]) ) constraint_ft_lb[name, t] = JuMP.@constraint( get_jump_model(container), ft_var[name, t] >= R_min_from * direction_var[name, t] ) constraint_loss[name, t] = JuMP.@constraint( get_jump_model(container), tf_var[name, t] + ft_var[name, t] == losses[name, t] ) constraint_loss_aux1[name, t] = JuMP.@constraint( get_jump_model(container), losses[name, t] >= l0 + l1 * ft_var[name, t] ) constraint_loss_aux2[name, t] = JuMP.@constraint( get_jump_model(container), losses[name, t] >= l0 + l1 * tf_var[name, t] ) constraint_loss_aux3[name, t] = JuMP.@constraint( get_jump_model(container), losses[name, t] <= l0 + l1 * ft_var[name, t] + M_VALUE * direction_var[name, t] ) constraint_loss_aux4[name, t] = JuMP.@constraint( get_jump_model(container), losses[name, t] <= l0 + l1 * tf_var[name, t] + M_VALUE * (1 - direction_var[name, t]) ) end end return end function add_constraints!( container::OptimizationContainer, ::Type{HVDCRectifierDCLineVoltageConstraint}, devices::IS.FlattenIteratorWrapper{T}, ::DeviceModel{T, <:HVDCTwoTerminalLCC}, ::NetworkModel{PM.ACPPowerModel}, ) where {T <: PSY.TwoTerminalLCCLine} time_steps = get_time_steps(container) names = PSY.get_name.(devices) rect_dc_voltage_var = get_variable(container, HVDCRectifierDCVoltageVariable(), T) rect_ac_voltage_bus_var = get_variable(container, VoltageMagnitude(), PSY.ACBus) rect_delay_angle_var = get_variable(container, HVDCRectifierDelayAngleVariable(), T) rect_tap_setting_var = get_variable(container, HVDCRectifierTapSettingVariable(), T) dc_line_current_var = get_variable(container, DCLineCurrentFlowVariable(), T) constraint_rect_dc_volt = add_constraints_container!( container, HVDCRectifierDCLineVoltageConstraint(), T, names, time_steps; ) for d in devices name = PSY.get_name(d) rect_bridges = PSY.get_rectifier_bridges(d) dc_rect_com_reactance = PSY.get_rectifier_xc(d) rect_tap_ratio = PSY.get_rectifier_transformer_ratio(d) bus_from = PSY.get_arc(d).from bus_from_name = PSY.get_name(bus_from) for t in get_time_steps(container) constraint_rect_dc_volt[name, t] = JuMP.@constraint( get_jump_model(container), rect_dc_voltage_var[name, t] == (3 * rect_bridges / pi) * ( sqrt(2) * ( rect_tap_ratio * rect_ac_voltage_bus_var[bus_from_name, t] * cos(rect_delay_angle_var[name, t]) ) / rect_tap_setting_var[name, t] - dc_rect_com_reactance * dc_line_current_var[name, t] ) ) end end return end function add_constraints!( container::OptimizationContainer, ::Type{HVDCInverterDCLineVoltageConstraint}, devices::IS.FlattenIteratorWrapper{T}, ::DeviceModel{T, <:HVDCTwoTerminalLCC}, ::NetworkModel{PM.ACPPowerModel}, ) where {T <: PSY.TwoTerminalLCCLine} time_steps = get_time_steps(container) names = PSY.get_name.(devices) inv_dc_voltage_var = get_variable(container, HVDCInverterDCVoltageVariable(), T) inv_ac_voltage_bus_var = get_variable(container, VoltageMagnitude(), PSY.ACBus) inv_extinction_angle_var = get_variable(container, HVDCInverterExtinctionAngleVariable(), T) inv_tap_setting_var = get_variable(container, HVDCInverterTapSettingVariable(), T) dc_line_current_var = get_variable(container, DCLineCurrentFlowVariable(), T) constraint_inv_dc_volt = add_constraints_container!( container, HVDCInverterDCLineVoltageConstraint(), T, names, time_steps; ) for d in devices name = PSY.get_name(d) inv_bridges = PSY.get_inverter_bridges(d) dc_inv_com_reactance = PSY.get_inverter_xc(d) inv_tap_ratio = PSY.get_inverter_transformer_ratio(d) bus_to = PSY.get_arc(d).to bus_to_name = PSY.get_name(bus_to) for t in get_time_steps(container) constraint_inv_dc_volt[name, t] = JuMP.@constraint( get_jump_model(container), inv_dc_voltage_var[name, t] == (3 * inv_bridges / pi) * ( sqrt(2) * ( inv_tap_ratio * inv_ac_voltage_bus_var[bus_to_name, t] * cos(inv_extinction_angle_var[name, t]) ) / inv_tap_setting_var[name, t] - dc_inv_com_reactance * dc_line_current_var[name, t] ) ) end end return end function add_constraints!( container::OptimizationContainer, ::Type{HVDCRectifierOverlapAngleConstraint}, devices::IS.FlattenIteratorWrapper{T}, ::DeviceModel{T, <:HVDCTwoTerminalLCC}, ::NetworkModel{PM.ACPPowerModel}, ) where {T <: PSY.TwoTerminalLCCLine} time_steps = get_time_steps(container) names = PSY.get_name.(devices) rect_ac_voltage_bus_var = get_variable(container, VoltageMagnitude(), PSY.ACBus) rect_delay_angle_var = get_variable(container, HVDCRectifierDelayAngleVariable(), T) rect_overlap_angle_var = get_variable(container, HVDCRectifierOverlapAngleVariable(), T) rect_tap_setting_var = get_variable(container, HVDCRectifierTapSettingVariable(), T) dc_line_current_var = get_variable(container, DCLineCurrentFlowVariable(), T) constraint_rect_over_ang = add_constraints_container!( container, HVDCRectifierOverlapAngleConstraint(), T, names, time_steps; ) for d in devices name = PSY.get_name(d) dc_rect_com_reactance = PSY.get_rectifier_xc(d) rect_tap_ratio = PSY.get_rectifier_transformer_ratio(d) bus_from = PSY.get_arc(d).from bus_from_name = PSY.get_name(bus_from) for t in get_time_steps(container) constraint_rect_over_ang[name, t] = JuMP.@constraint( get_jump_model(container), rect_overlap_angle_var[name, t] == ( acos( cos(rect_delay_angle_var[name, t]) - ( ( sqrt(2) * dc_rect_com_reactance * dc_line_current_var[name, t] * rect_tap_setting_var[name, t] ) / ( rect_tap_ratio * rect_ac_voltage_bus_var[bus_from_name, t] ) ), ) - rect_delay_angle_var[name, t] ) ) end end return end function add_constraints!( container::OptimizationContainer, ::Type{HVDCInverterOverlapAngleConstraint}, devices::IS.FlattenIteratorWrapper{T}, ::DeviceModel{T, <:HVDCTwoTerminalLCC}, ::NetworkModel{PM.ACPPowerModel}, ) where {T <: PSY.TwoTerminalLCCLine} time_steps = get_time_steps(container) names = PSY.get_name.(devices) inv_ac_voltage_bus_var = get_variable(container, VoltageMagnitude(), PSY.ACBus) inv_extinction_angle_var = get_variable(container, HVDCInverterExtinctionAngleVariable(), T) inv_overlap_angle_var = get_variable(container, HVDCInverterOverlapAngleVariable(), T) inv_tap_setting_var = get_variable(container, HVDCInverterTapSettingVariable(), T) dc_line_current_var = get_variable(container, DCLineCurrentFlowVariable(), T) constraint_inv_over_ang = add_constraints_container!( container, HVDCInverterOverlapAngleConstraint(), T, names, time_steps; ) for d in devices name = PSY.get_name(d) dc_inv_com_reactance = PSY.get_inverter_xc(d) inv_tap_ratio = PSY.get_inverter_transformer_ratio(d) bus_to = PSY.get_arc(d).to bus_to_name = PSY.get_name(bus_to) for t in get_time_steps(container) constraint_inv_over_ang[name, t] = JuMP.@constraint( get_jump_model(container), inv_overlap_angle_var[name, t] == ( acos( cos(inv_extinction_angle_var[name, t]) - ( ( sqrt(2) * dc_inv_com_reactance * dc_line_current_var[name, t] * inv_tap_setting_var[name, t] ) / ( inv_tap_ratio * inv_ac_voltage_bus_var[bus_to_name, t] ) ), ) - inv_extinction_angle_var[name, t] ) ) end end return end function add_constraints!( container::OptimizationContainer, ::Type{HVDCRectifierPowerFactorAngleConstraint}, devices::IS.FlattenIteratorWrapper{T}, ::DeviceModel{T, <:HVDCTwoTerminalLCC}, ::NetworkModel{PM.ACPPowerModel}, ) where {T <: PSY.TwoTerminalLCCLine} time_steps = get_time_steps(container) names = PSY.get_name.(devices) rect_delay_angle_var = get_variable(container, HVDCRectifierDelayAngleVariable(), T) rect_overlap_angle_var = get_variable(container, HVDCRectifierOverlapAngleVariable(), T) rect_power_factor_var = get_variable(container, HVDCRectifierPowerFactorAngleVariable(), T) constraint_rect_power_factor_ang = add_constraints_container!( container, HVDCRectifierPowerFactorAngleConstraint(), T, names, time_steps; ) for d in devices name = PSY.get_name(d) for t in get_time_steps(container) constraint_rect_power_factor_ang[name, t] = JuMP.@constraint( get_jump_model(container), # Full equation not working with Ipopt # rect_power_factor_var[name, t] * # ( # cos(2 * rect_delay_angle_var[name, t]) - cos( # 2( # rect_overlap_angle_var[name, t] + # rect_delay_angle_var[name, t] # ), # ) # ) == atan( # ( # - 2 * rect_overlap_angle_var[name, t] + # - sin(2 * rect_delay_angle_var[name, t]) + sin( # 2 * ( # rect_overlap_angle_var[name, t] + # rect_delay_angle_var[name, t] # ), # ) # ) # ) # Approximation of rectifier power factor calculation rect_power_factor_var[name, t] == acos( 0.5 * cos(rect_delay_angle_var[name, t]) + 0.5 * cos( rect_delay_angle_var[name, t] + rect_overlap_angle_var[name, t], ), ) ) end end return end function add_constraints!( container::OptimizationContainer, ::Type{HVDCInverterPowerFactorAngleConstraint}, devices::IS.FlattenIteratorWrapper{T}, ::DeviceModel{T, <:HVDCTwoTerminalLCC}, ::NetworkModel{PM.ACPPowerModel}, ) where {T <: PSY.TwoTerminalLCCLine} time_steps = get_time_steps(container) names = PSY.get_name.(devices) inv_extinction_angle_var = get_variable(container, HVDCInverterExtinctionAngleVariable(), T) inv_overlap_angle_var = get_variable(container, HVDCInverterOverlapAngleVariable(), T) inv_power_factor_var = get_variable(container, HVDCInverterPowerFactorAngleVariable(), T) constraint_inv_power_factor_ang = add_constraints_container!( container, HVDCInverterPowerFactorAngleConstraint(), T, names, time_steps; ) for d in devices name = PSY.get_name(d) for t in get_time_steps(container) constraint_inv_power_factor_ang[name, t] = JuMP.@constraint( get_jump_model(container), # Full equation not working with Ipopt # inv_power_factor_var[name, t] * # ( # cos(2 * inv_extinction_angle_var[name, t]) - cos( # 2( # inv_overlap_angle_var[name, t] + # inv_extinction_angle_var[name, t] # ), # ) # ) == atan( # ( # - 2 * inv_overlap_angle_var[name, t] + # - sin(2 * inv_extinction_angle_var[name, t]) + sin( # 2 * ( # inv_overlap_angle_var[name, t] + # inv_extinction_angle_var[name, t] # ), # ) # ) # ) # Approximation of inverter power factor calculation inv_power_factor_var[name, t] == acos( 0.5 * cos(inv_extinction_angle_var[name, t]) + 0.5 * cos( inv_extinction_angle_var[name, t] + inv_overlap_angle_var[name, t], ), ) ) end end return end function add_constraints!( container::OptimizationContainer, ::Type{HVDCRectifierACCurrentFlowConstraint}, devices::IS.FlattenIteratorWrapper{T}, ::DeviceModel{T, <:HVDCTwoTerminalLCC}, ::NetworkModel{PM.ACPPowerModel}, ) where {T <: PSY.TwoTerminalLCCLine} time_steps = get_time_steps(container) names = PSY.get_name.(devices) rect_ac_current_var = get_variable(container, HVDCRectifierACCurrentVariable(), T) dc_line_current_var = get_variable(container, DCLineCurrentFlowVariable(), T) constraint_rect_ac_current = add_constraints_container!( container, HVDCRectifierACCurrentFlowConstraint(), T, names, time_steps; ) for d in devices name = PSY.get_name(d) rect_bridges = PSY.get_rectifier_bridges(d) for t in get_time_steps(container) constraint_rect_ac_current[name, t] = JuMP.@constraint( get_jump_model(container), rect_ac_current_var[name, t] == sqrt(6) * rect_bridges * dc_line_current_var[name, t] / pi ) end end return end function add_constraints!( container::OptimizationContainer, ::Type{HVDCInverterACCurrentFlowConstraint}, devices::IS.FlattenIteratorWrapper{T}, ::DeviceModel{T, <:HVDCTwoTerminalLCC}, ::NetworkModel{PM.ACPPowerModel}, ) where {T <: PSY.TwoTerminalLCCLine} time_steps = get_time_steps(container) names = PSY.get_name.(devices) inv_ac_current_var = get_variable(container, HVDCInverterACCurrentVariable(), T) dc_line_current_var = get_variable(container, DCLineCurrentFlowVariable(), T) constraint_inv_ac_current = add_constraints_container!( container, HVDCInverterACCurrentFlowConstraint(), T, names, time_steps; ) for d in devices name = PSY.get_name(d) inv_bridges = PSY.get_inverter_bridges(d) for t in get_time_steps(container) constraint_inv_ac_current[name, t] = JuMP.@constraint( get_jump_model(container), inv_ac_current_var[name, t] == sqrt(6) * inv_bridges * dc_line_current_var[name, t] / pi ) end end return end function add_constraints!( container::OptimizationContainer, ::Type{HVDCRectifierPowerCalculationConstraint}, devices::IS.FlattenIteratorWrapper{T}, ::DeviceModel{T, <:HVDCTwoTerminalLCC}, ::NetworkModel{PM.ACPPowerModel}, ) where {T <: PSY.TwoTerminalLCCLine} time_steps = get_time_steps(container) names = PSY.get_name.(devices) rect_ac_ppower_var = get_variable(container, HVDCActivePowerReceivedFromVariable(), T) rect_ac_qpower_var = get_variable(container, HVDCReactivePowerReceivedFromVariable(), T) rect_ac_current_var = get_variable(container, HVDCRectifierACCurrentVariable(), T) rect_ac_voltage_bus_var = get_variable(container, VoltageMagnitude(), PSY.ACBus) rect_power_factor_var = get_variable(container, HVDCRectifierPowerFactorAngleVariable(), T) rect_tap_setting_var = get_variable(container, HVDCRectifierTapSettingVariable(), T) constraint_ft_p = add_constraints_container!( container, HVDCRectifierPowerCalculationConstraint(), T, names, time_steps; meta = "active", ) constraint_ft_q = add_constraints_container!( container, HVDCRectifierPowerCalculationConstraint(), T, names, time_steps; meta = "reactive", ) for d in devices name = PSY.get_name(d) rect_tap_ratio = PSY.get_rectifier_transformer_ratio(d) bus_from = PSY.get_arc(d).from bus_from_name = PSY.get_name(bus_from) for t in get_time_steps(container) constraint_ft_p[name, t] = JuMP.@constraint( get_jump_model(container), rect_ac_ppower_var[name, t] == ( rect_tap_ratio * sqrt(3) * rect_ac_current_var[name, t] * rect_ac_voltage_bus_var[bus_from_name, t] * cos(rect_power_factor_var[name, t]) ) / rect_tap_setting_var[name, t], ) constraint_ft_q[name, t] = JuMP.@constraint( get_jump_model(container), rect_ac_qpower_var[name, t] == ( rect_tap_ratio * sqrt(3) * rect_ac_current_var[name, t] * rect_ac_voltage_bus_var[bus_from_name, t] * sin(rect_power_factor_var[name, t]) ) / rect_tap_setting_var[name, t], ) end end return end function add_constraints!( container::OptimizationContainer, ::Type{HVDCInverterPowerCalculationConstraint}, devices::IS.FlattenIteratorWrapper{T}, ::DeviceModel{T, <:HVDCTwoTerminalLCC}, ::NetworkModel{PM.ACPPowerModel}, ) where {T <: PSY.TwoTerminalLCCLine} time_steps = get_time_steps(container) names = PSY.get_name.(devices) inv_ac_ppower_var = get_variable(container, HVDCActivePowerReceivedToVariable(), T) inv_ac_qpower_var = get_variable(container, HVDCReactivePowerReceivedToVariable(), T) inv_ac_current_var = get_variable(container, HVDCInverterACCurrentVariable(), T) inv_ac_voltage_bus_var = get_variable(container, VoltageMagnitude(), PSY.ACBus) inv_power_factor_var = get_variable(container, HVDCInverterPowerFactorAngleVariable(), T) inv_tap_setting_var = get_variable(container, HVDCInverterTapSettingVariable(), T) constraint_ft_p = add_constraints_container!( container, HVDCInverterPowerCalculationConstraint(), T, names, time_steps; meta = "active", ) constraint_ft_q = add_constraints_container!( container, HVDCInverterPowerCalculationConstraint(), T, names, time_steps; meta = "reactive", ) for d in devices name = PSY.get_name(d) inv_tap_ratio = PSY.get_inverter_transformer_ratio(d) bus_to = PSY.get_arc(d).to bus_to_name = PSY.get_name(bus_to) for t in get_time_steps(container) constraint_ft_p[name, t] = JuMP.@constraint( get_jump_model(container), inv_ac_ppower_var[name, t] == ( inv_tap_ratio * sqrt(3) * inv_ac_current_var[name, t] * inv_ac_voltage_bus_var[bus_to_name, t] * cos(inv_power_factor_var[name, t]) ) / inv_tap_setting_var[name, t], ) constraint_ft_q[name, t] = JuMP.@constraint( get_jump_model(container), inv_ac_qpower_var[name, t] == ( inv_tap_ratio * sqrt(3) * inv_ac_current_var[name, t] * inv_ac_voltage_bus_var[bus_to_name, t] * sin(inv_power_factor_var[name, t]) ) / inv_tap_setting_var[name, t], ) end end return end function add_constraints!( container::OptimizationContainer, ::Type{HVDCTransmissionDCLineConstraint}, devices::IS.FlattenIteratorWrapper{T}, ::DeviceModel{T, <:HVDCTwoTerminalLCC}, ::NetworkModel{PM.ACPPowerModel}, ) where {T <: PSY.TwoTerminalLCCLine} time_steps = get_time_steps(container) names = PSY.get_name.(devices) rect_dc_voltage_var = get_variable(container, HVDCRectifierDCVoltageVariable(), T) inv_dc_voltage_var = get_variable(container, HVDCInverterDCVoltageVariable(), T) dc_line_current_var = get_variable(container, DCLineCurrentFlowVariable(), T) constraint_tl_c = add_constraints_container!( container, HVDCTransmissionDCLineConstraint(), T, names, time_steps; ) for d in devices name = PSY.get_name(d) dc_line_resistance = PSY.get_r(d) for t in get_time_steps(container) constraint_tl_c[name, t] = JuMP.@constraint( get_jump_model(container), inv_dc_voltage_var[name, t] == rect_dc_voltage_var[name, t] - dc_line_resistance * dc_line_current_var[name, t] ) end end return end ================================================ FILE: src/devices_models/devices/area_interchange.jl ================================================ #! 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) get_parameter_multiplier(::FixValueParameter, ::PSY.AreaInterchange, ::AbstractBranchFormulation) = 1.0 get_parameter_multiplier(::LowerBoundValueParameter, ::PSY.AreaInterchange, ::AbstractBranchFormulation) = 1.0 get_parameter_multiplier(::UpperBoundValueParameter, ::PSY.AreaInterchange, ::AbstractBranchFormulation) = 1.0 get_initial_conditions_device_model( ::OperationModel, model::DeviceModel{PSY.AreaInterchange, T}, ) where {T <: AbstractBranchFormulation} = DeviceModel(PSY.AreaInterchange, T) #! 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 <: PM.AbstractPowerModel} 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 function add_variables!( container::OptimizationContainer, ::Type{FlowActivePowerVariable}, model::NetworkModel{CopperPlatePowerModel}, devices::IS.FlattenIteratorWrapper{PSY.AreaInterchange}, formulation::AbstractBranchFormulation, ) @warn( "CopperPlatePowerModel ignores AreaInterchanges. Instead use AreaBalancePowerModel." ) 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 <: PM.AbstractActivePowerModel} time_steps = get_time_steps(container) device_names = PSY.get_name.(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{T}, inter_area_branch_map::Dict{ Tuple{String, String}, Dict{DataType, Vector{String}}, }, ) where {T <: AbstractPTDFModel} @assert !isempty(inter_area_branch_map) time_steps = get_time_steps(container) device_names_with_branches = Vector{String}() interchange_direction_branch_map = Dict{String, Dict{Float64, Dict{DataType, Vector{String}}}}() for area_interchange in devices inter_change_name = PSY.get_name(area_interchange) area_from_name = PSY.get_name(PSY.get_from_area(area_interchange)) area_to_name = PSY.get_name(PSY.get_to_area(area_interchange)) interchange_direction_branch_map[inter_change_name] = Dict{Float64, Dict{DataType, Vector{String}}}() if haskey(inter_area_branch_map, (area_from_name, area_to_name)) # 1 is the multiplier interchange_direction_branch_map[inter_change_name][1.0] = inter_area_branch_map[(area_from_name, area_to_name)] end if haskey(inter_area_branch_map, (area_to_name, area_from_name)) # -1 is the multiplier because the direction is reversed interchange_direction_branch_map[inter_change_name][-1.0] = inter_area_branch_map[(area_to_name, area_from_name)] end if isempty(interchange_direction_branch_map[inter_change_name]) @warn( "There are no branches modeled in Area InterChange $(summary(area_interchange)) \ LineFlowBoundConstraint not created" ) else push!(device_names_with_branches, inter_change_name) end end con_ub = add_constraints_container!( container, LineFlowBoundConstraint(), PSY.AreaInterchange, device_names_with_branches, time_steps; meta = "ub", ) con_lb = add_constraints_container!( container, LineFlowBoundConstraint(), PSY.AreaInterchange, device_names_with_branches, 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) (inter_change_name ∉ device_names_with_branches) && continue direction_branch_map = interchange_direction_branch_map[inter_change_name] for t in time_steps sum_of_flows = JuMP.AffExpr() for (mult, inter_area_branches) in direction_branch_map for (type, names) in inter_area_branches flow_expr = get_expression(container, PTDFBranchFlow(), type) for name in names JuMP.add_to_expression!(sum_of_flows, flow_expr[name, t], mult) end 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 ================================================ FILE: src/devices_models/devices/common/add_auxiliary_variable.jl ================================================ """ Add variables to the OptimizationContainer for any component. """ function add_variables!( container::OptimizationContainer, ::Type{T}, devices::Union{Vector{U}, IS.FlattenIteratorWrapper{U}}, formulation::Union{AbstractDeviceFormulation, AbstractServiceFormulation}, ) where {T <: AuxVariableType, U <: PSY.Component} add_variable!(container, T(), devices, formulation) return end @doc raw""" Default implementation of adding auxiliary variable to the model. """ function add_variable!( container::OptimizationContainer, var_type::AuxVariableType, devices::U, formulation, ) where {U <: Union{Vector{D}, IS.FlattenIteratorWrapper{D}}} where {D <: PSY.Component} @assert !isempty(devices) time_steps = get_time_steps(container) add_aux_variable_container!( container, var_type, D, PSY.get_name.(devices), time_steps, ) return end ================================================ FILE: src/devices_models/devices/common/add_constraint_dual.jl ================================================ function add_constraint_dual!( container::OptimizationContainer, sys::PSY.System, model::DeviceModel{T, D}, ) where {T <: PSY.Component, D <: AbstractDeviceFormulation} if !isempty(get_duals(model)) devices = get_available_components(model, sys) for constraint_type in get_duals(model) assign_dual_variable!(container, constraint_type, devices, D) end end return end function add_constraint_dual!( container::OptimizationContainer, sys::PSY.System, model::NetworkModel{T}, ) where {T <: PM.AbstractPowerModel} if !isempty(get_duals(model)) devices = get_available_components(model, PSY.ACBus, sys) for constraint_type in get_duals(model) assign_dual_variable!(container, constraint_type, devices, model) end end return end function add_constraint_dual!( container::OptimizationContainer, sys::PSY.System, model::NetworkModel{T}, ) 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) end end return end function add_constraint_dual!( container::OptimizationContainer, sys::PSY.System, model::ServiceModel{T, D}, ) where {T <: PSY.Service, D <: AbstractServiceFormulation} if !isempty(get_duals(model)) service = get_available_components(model, sys) for constraint_type in get_duals(model) assign_dual_variable!(container, constraint_type, service, D) end end return end function assign_dual_variable!( container::OptimizationContainer, constraint_type::Type{<:ConstraintType}, service::D, ::Type{<:AbstractServiceFormulation}, ) where {D <: PSY.Service} time_steps = get_time_steps(container) service_name = PSY.get_name(service) add_dual_container!( container, constraint_type, D, [service_name], time_steps; meta = service_name, ) return end function assign_dual_variable!( container::OptimizationContainer, constraint_type::Type{<:ConstraintType}, devices::U, ::Type{<:AbstractDeviceFormulation}, ) where {U <: Union{Vector{D}, IS.FlattenIteratorWrapper{D}}} where {D <: PSY.Device} @assert !isempty(devices) time_steps = get_time_steps(container) metas = _existing_constraint_metas(container, constraint_type, D) if isempty(metas) device_names = PSY.get_name.(devices) add_dual_container!(container, constraint_type, D, device_names, time_steps) else # Reuse the existing constraint container's row axis so the dual axis # matches the constraint exactly. Network reductions (radial / # degree-two) drop branches that pass the device-model filter, so the # constraint axis is a strict subset of PSY.get_name.(devices). Sizing # the dual from the device list would leave the dual broadcast in # process_duals incompatible with the constraint matrix. for meta in metas existing = get_constraint(container, ConstraintKey(constraint_type, D, meta)) row_axis = axes(existing)[1] add_dual_container!( container, constraint_type, D, row_axis, time_steps; meta = meta, ) end end return end function _existing_constraint_metas( container::OptimizationContainer, ::Type{T}, ::Type{D}, ) where {T <: ConstraintType, D} metas = String[] for key in get_constraint_keys(container) if IS.Optimization.get_entry_type(key) === T && IS.Optimization.get_component_type(key) === D push!(metas, key.meta) end end return metas end function assign_dual_variable!( container::OptimizationContainer, constraint_type::Type{<:ConstraintType}, devices::U, ::NetworkModel{<:PM.AbstractPowerModel}, ) where {U <: Union{Vector{D}, IS.FlattenIteratorWrapper{D}}} where {D <: PSY.ACBus} @assert !isempty(devices) time_steps = get_time_steps(container) add_dual_container!( container, constraint_type, D, PSY.get_name.(devices), time_steps, ) return end function assign_dual_variable!( container::OptimizationContainer, constraint_type::Type{CopperPlateBalanceConstraint}, ::U, network_model::NetworkModel{<:PM.AbstractPowerModel}, ) where {U <: PSY.System} time_steps = get_time_steps(container) ref_buses = get_reference_buses(network_model) add_dual_container!(container, constraint_type, U, ref_buses, time_steps) return end ================================================ FILE: src/devices_models/devices/common/add_pwl_methods.jl ================================================ """ _get_breakpoints_for_pwl_function(min_val, max_val, f; num_segments = DEFAULT_INTERPOLATION_LENGTH) Generate breakpoints for piecewise linear (PWL) approximation of a nonlinear function. This function creates equally-spaced breakpoints over the specified domain [min_val, max_val] and evaluates the given function at each breakpoint to construct a piecewise linear approximation. The breakpoints are used in optimization problems to linearize nonlinear constraints or objectives. # Arguments - `min_val::Float64`: Minimum value of the domain for the PWL approximation - `max_val::Float64`: Maximum value of the domain for the PWL approximation - `f`: Function to be approximated (must be callable with Float64 input) - `num_segments::Int`: Number of linear segments in the PWL approximation (default: DEFAULT_INTERPOLATION_LENGTH) # Returns - `Tuple{Vector{Float64}, Vector{Float64}}`: A tuple containing: - `x_bkpts`: Vector of x-coordinates (breakpoints) in the domain - `y_bkpts`: Vector of y-coordinates (function values at breakpoints) # Notes - The number of breakpoints is `num_segments + 1` - Breakpoints are equally spaced across the domain - The first breakpoint is always at `min_val` and the last at `max_val` """ function _get_breakpoints_for_pwl_function( min_val::Float64, max_val::Float64, f; num_segments = DEFAULT_INTERPOLATION_LENGTH, ) # Calculate total number of breakpoints (one more than segments) # num_segments is the number of linear segments in the PWL approximation # num_bkpts is the total number of breakpoints needed for the segments num_bkpts = num_segments + 1 # Calculate step size for equally-spaced breakpoints step = (max_val - min_val) / num_segments # Pre-allocate vectors for breakpoint coordinates x_bkpts = Vector{Float64}(undef, num_bkpts) # Domain values (x-coordinates) y_bkpts = Vector{Float64}(undef, num_bkpts) # Function values (y-coordinates) # Set the first breakpoint at the minimum domain value x_bkpts[1] = min_val y_bkpts[1] = f(min_val) # Generate remaining breakpoints by stepping through the domain for i in 1:num_segments x_val = min_val + step * i # Calculate x-coordinate of current breakpoint x_bkpts[i + 1] = x_val y_bkpts[i + 1] = f(x_val) # Evaluate function at current breakpoint end return x_bkpts, y_bkpts end """ add_sparse_pwl_interpolation_variables!(container, devices, ::T, model, num_segments = DEFAULT_INTERPOLATION_LENGTH) Add piecewise linear interpolation variables to an optimization container. This function creates the necessary variables for piecewise linear (PWL) approximation in optimization models. It adds either continuous interpolation variables (δ) or binary interpolation variables (z) depending on the variable type `T`. These variables are used in the incremental method for PWL approximation where: - **Interpolation variables (δ)**: Continuous variables ∈ [0,1] that represent weights for each segment - **Binary interpolation variables (z)**: Binary variables that enforce ordering constraints in incremental method The function creates a 3-dimensional variable structure indexed by (device_name, segment_index, time_step). For binary variables, the number of variables is one less than for continuous variables since they control transitions between segments. # Arguments - `container::OptimizationContainer`: The optimization container to add variables to - `devices`: Collection of devices for which to create PWL variables - `::T`: Type parameter specifying the variable type (InterpolationVariableType or BinaryInterpolationVariableType) - `model::DeviceModel{U, V}`: Device model containing formulation information for bounds - `num_segments::Int`: Number of linear segments in the PWL approximation (default: DEFAULT_INTERPOLATION_LENGTH) # Type Parameters - `T <: Union{InterpolationVariableType, BinaryInterpolationVariableType}`: Variable type to create - `U <: PSY.Component`: Component type for devices - `V <: AbstractDeviceFormulation`: Device formulation type for bounds # Notes - Binary variables have `num_segments - 1` variables (control transitions between segments) - Continuous variables have `num_segments` variables (one per segment) - Variable bounds are set based on the device formulation if available - Variables are created for all devices and time steps in the optimization horizon # See Also - `_add_generic_incremental_interpolation_constraint!`: Function that uses these variables in constraints """ function add_sparse_pwl_interpolation_variables!( container::OptimizationContainer, ::T, devices, model::DeviceModel{U, V}, num_segments = DEFAULT_INTERPOLATION_LENGTH, ) where { T <: Union{InterpolationVariableType, BinaryInterpolationVariableType}, U <: PSY.Component, V <: AbstractDeviceFormulation, } # TODO: Implement approach for deciding segment length # Extract time steps from the optimization container time_steps = get_time_steps(container) # Create variable container using lazy initialization var_container = lazy_container_addition!(container, T(), U) # Determine if this variable type should be binary based on type, component, and formulation binary_flag = get_variable_binary(T(), U, V()) # Calculate number of segments based on variable type: # - Binary variables: (num_segments - 1) to control transitions between segments # - Continuous variables: num_segments (one per segment) len_segs = binary_flag ? (num_segments - 1) : num_segments # Iterate over all devices to create PWL variables for d in devices name = PSY.get_name(d) # Create variables for each time step for t in time_steps # Pre-allocate array to store variable references for this device and time step pwlvars = Array{JuMP.VariableRef}(undef, len_segs) # Create individual PWL variables for each segment for i in 1:len_segs # Create JuMP variable with descriptive name and store in both arrays pwlvars[i] = var_container[(name, i, t)] = JuMP.@variable( get_jump_model(container), base_name = "$(T)_$(name)_{pwl_$(i), $(t)}", # Descriptive variable name binary = binary_flag # Set as binary if this is a binary variable type ) # Set upper bound if specified by the device formulation ub = get_variable_upper_bound(T(), d, V()) ub !== nothing && JuMP.set_upper_bound(var_container[name, i, t], ub) # Set lower bound if specified by the device formulation lb = get_variable_lower_bound(T(), d, V()) lb !== nothing && JuMP.set_lower_bound(var_container[name, i, t], lb) end end end return end """ _add_generic_incremental_interpolation_constraint!(container, ::R, ::S, ::T, ::U, ::V, devices, dic_var_bkpts, dic_function_bkpts; meta) Add incremental piecewise linear interpolation constraints to an optimization container. This function implements the incremental method for piecewise linear approximation in optimization models. It creates constraints that relate the original variable (x) to its piecewise linear approximation (y = f(x)) using interpolation variables (δ) and binary variables (z) to ensure proper ordering. The incremental method represents each segment of the PWL function as: - x = x₁ + Σᵢ δᵢ(xᵢ₊₁ - xᵢ) where δᵢ ∈ [0,1] - y = y₁ + Σᵢ δᵢ(yᵢ₊₁ - yᵢ) where yᵢ = f(xᵢ) Binary variables z ensure the incremental property: δᵢ₊₁ ≤ zᵢ ≤ δᵢ for adjacent segments. # Arguments - `container::OptimizationContainer`: The optimization container to add constraints to - `::R`: Type parameter for the original variable (x) - `::S`: Type parameter for the approximated variable (y = f(x)) - `::T`: Type parameter for the interpolation variables (δ) - `::U`: Type parameter for the binary interpolation variables (z) - `::V`: Type parameter for the constraint type - `devices::IS.FlattenIteratorWrapper{W}`: Collection of devices to apply constraints to - `dic_var_bkpts::Dict{String, Vector{Float64}}`: Breakpoints in the domain (x-coordinates) for each device - `dic_function_bkpts::Dict{String, Vector{Float64}}`: Function values at breakpoints (y-coordinates) for each device - `meta`: Metadata for constraint naming (default: empty) # Type Parameters - `R <: VariableType`: Original variable type - `S <: VariableType`: Approximated variable type - `T <: VariableType`: Interpolation variable type - `U <: VariableType`: Binary interpolation variable type - `V <: ConstraintType`: Constraint type - `W <: PSY.Component`: Component type for devices # Notes - Creates two types of constraints: variable interpolation and function interpolation - Adds ordering constraints for binary variables to ensure incremental property - All constraints are applied for each device and time step """ function _add_generic_incremental_interpolation_constraint!( container::OptimizationContainer, ::R, # original var : x ::S, # approximated var : y = f(x) ::T, # interpolation var : δ ::U, # binary interpolation var : z ::V, # constraint devices::IS.FlattenIteratorWrapper{W}, dic_var_bkpts::Dict{String, Vector{Float64}}, dic_function_bkpts::Dict{String, Vector{Float64}}; meta = IS.Optimization.CONTAINER_KEY_EMPTY_META, ) where { R <: VariableType, S <: VariableType, T <: VariableType, U <: VariableType, V <: ConstraintType, W <: PSY.Component, } # Extract time steps and device names for constraint indexing time_steps = get_time_steps(container) names = [PSY.get_name(d) for d in devices] JuMPmodel = get_jump_model(container) # Retrieve all required variables from the optimization container # Retrieve original variable for DCVoltage from the Bus x_var = if (R <: DCVoltage) get_variable(container, R(), PSY.DCBus) # Original variable (domain of function) else get_variable(container, R(), W) # Original variable (domain of function) end # Original variable (domain of function) y_var = get_variable(container, S(), W) # Approximated variable (range of function) δ_var = get_variable(container, T(), W) # Interpolation variables (weights for segments) z_var = get_variable(container, U(), W) # Binary variables (ordering constraints) # Create containers for the two main constraint types # Container for variable interpolation constraints: x = x₁ + Σᵢ δᵢ(xᵢ₊₁ - xᵢ) const_container_var = add_constraints_container!( container, V(), W, names, time_steps; meta = "$(meta)pwl_variable", ) # Container for function interpolation constraints: y = y₁ + Σᵢ δᵢ(yᵢ₊₁ - yᵢ) const_container_function = add_constraints_container!( container, V(), W, names, time_steps; meta = "$(meta)pwl_function", ) # Iterate over all devices to add constraints for each device and time step for d in devices name = PSY.get_name(d) bus_name = PSY.get_name(PSY.get_dc_bus(d)) # Get proper name for x variable (if is DCVoltage or not) x_name = (R <: DCVoltage) ? bus_name : name var_bkpts = dic_var_bkpts[name] # Breakpoints in domain (x-values) function_bkpts = dic_function_bkpts[name] # Function values at breakpoints (y-values) num_segments = length(var_bkpts) - 1 # Number of linear segments for t in time_steps # Variable interpolation constraint: x = x₁ + Σᵢ δᵢ(xᵢ₊₁ - xᵢ) # This ensures the original variable is expressed as a convex combination # of breakpoint intervals weighted by interpolation variables const_container_var[name, t] = JuMP.@constraint( JuMPmodel, x_var[x_name, t] == var_bkpts[1] + sum( δ_var[name, i, t] * (var_bkpts[i + 1] - var_bkpts[i]) for i in 1:num_segments ) ) # Function interpolation constraint: y = y₁ + Σᵢ δᵢ(yᵢ₊₁ - yᵢ) # This defines the piecewise linear approximation of the function const_container_function[name, t] = JuMP.@constraint( JuMPmodel, y_var[name, t] == function_bkpts[1] + sum( δ_var[name, i, t] * (function_bkpts[i + 1] - function_bkpts[i]) for i in 1:num_segments ) ) # Incremental ordering constraints using binary variables (SOS2) # These ensure that δᵢ₊₁ ≤ zᵢ ≤ δᵢ, which maintains the incremental property: # segments must be filled in order (δ₁ before δ₂, δ₂ before δ₃, etc.) for i in 1:(num_segments - 1) # z[i] must be >= δ[i+1]: can't activate later segment without current one JuMP.@constraint(JuMPmodel, z_var[name, i, t] >= δ_var[name, i + 1, t]) # z[i] must be <= δ[i]: can't be more activated than current segment JuMP.@constraint(JuMPmodel, z_var[name, i, t] <= δ_var[name, i, t]) end end end return end ================================================ FILE: src/devices_models/devices/common/add_to_expression.jl ================================================ _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 _get_variable_if_exists(::PSY.MarketBidCost) = nothing _get_variable_if_exists(cost::PSY.OperationalCost) = PSY.get_variable(cost) _is_fuel_curve(::Nothing) = false _is_fuel_curve(::PSY.CostCurve) = false _is_fuel_curve(::PSY.FuelCurve) = true _value_curve_is_quadratic(::PSY.LinearCurve) = false _value_curve_is_quadratic(::PSY.QuadraticCurve) = true _value_curve_is_quadratic(::PSY.PiecewisePointCurve) = false _value_curve_is_quadratic(::PSY.IncrementalCurve) = false _value_curve_is_quadratic(::PSY.AverageRateCurve) = false function add_expressions!( container::OptimizationContainer, ::Type{T}, devices::U, model::DeviceModel{D, W}, ) where { T <: ExpressionType, U <: Union{Vector{D}, IS.FlattenIteratorWrapper{D}}, W <: AbstractDeviceFormulation, } where {D <: PSY.Component} time_steps = get_time_steps(container) names = PSY.get_name.(devices) add_expression_container!(container, T(), D, names, time_steps) return end function add_expressions!( container::OptimizationContainer, ::Type{T}, devices::U, model::DeviceModel{D, W}, ) where { T <: FuelConsumptionExpression, U <: Union{Vector{D}, IS.FlattenIteratorWrapper{D}}, W <: AbstractDeviceFormulation, } where {D <: PSY.Component} time_steps = get_time_steps(container) names = String[] found_quad_fuel_functions = false for d in devices op_cost = PSY.get_operation_cost(d) fuel_curve = _get_variable_if_exists(op_cost) if _is_fuel_curve(fuel_curve) push!(names, PSY.get_name(d)) if !found_quad_fuel_functions found_quad_fuel_functions = _value_curve_is_quadratic(PSY.get_value_curve(fuel_curve)) end end end if !isempty(names) expr_type = found_quad_fuel_functions ? JuMP.QuadExpr : GAE add_expression_container!( container, T(), D, names, time_steps; expr_type = expr_type, ) end return end function add_expressions!( container::OptimizationContainer, ::Type{T}, devices::U, model::ServiceModel{V, W}, ) where { T <: ExpressionType, U <: Union{Vector{D}, IS.FlattenIteratorWrapper{D}}, V <: PSY.Reserve, W <: AbstractReservesFormulation, } where {D <: PSY.Component} time_steps = get_time_steps(container) @assert length(devices) == 1 add_expression_container!( container, T(), D, PSY.get_name.(devices), time_steps; meta = get_service_name(model), ) return end # Note: add_to_jump_expression! are used to control depending on the parameter type used # on the simulation. function _add_to_jump_expression!( expression::T, var::JuMP.VariableRef, multiplier::Float64, ) where {T <: JuMP.AbstractJuMPScalar} JuMP.add_to_expression!(expression, multiplier, var) return end function _add_to_jump_expression!( expression::T, value::Float64, ) where {T <: JuMP.AbstractJuMPScalar} JuMP.add_to_expression!(expression, value) return end function _add_to_jump_expression!( expression::T, var::JuMP.VariableRef, multiplier::Float64, constant::Float64, ) where {T <: JuMP.AbstractJuMPScalar} _add_to_jump_expression!(expression, constant) _add_to_jump_expression!(expression, var, multiplier) return end function _add_to_jump_expression!( expression::T, parameter::Float64, multiplier::Float64, ) where {T <: JuMP.AbstractJuMPScalar} _add_to_jump_expression!(expression, parameter * multiplier) return end """ Default implementation to add parameters to SystemBalanceExpressions """ function add_to_expression!( container::OptimizationContainer, ::Type{T}, ::Type{U}, devices::IS.FlattenIteratorWrapper{V}, model::DeviceModel{V, W}, network_model::NetworkModel{X}, ) where { T <: SystemBalanceExpressions, U <: TimeSeriesParameter, V <: PSY.Device, W <: AbstractDeviceFormulation, X <: PM.AbstractPowerModel, } param_container = get_parameter(container, U(), V) multiplier = get_multiplier_array(param_container) network_reduction = get_network_reduction(network_model) for d in devices, t in get_time_steps(container) bus_no = PNM.get_mapped_bus_number(network_reduction, PSY.get_bus(d)) name = PSY.get_name(d) _add_to_jump_expression!( get_expression(container, T(), PSY.ACBus)[bus_no, t], get_parameter_column_refs(param_container, name)[t], multiplier[name, t], ) end return end """ Generic electric load implementation to add parameters to SystemBalanceExpressions """ function add_to_expression!( container::OptimizationContainer, ::Type{T}, ::Type{U}, devices::IS.FlattenIteratorWrapper{V}, model::DeviceModel{V, W}, network_model::NetworkModel{X}, ) where { T <: SystemBalanceExpressions, U <: TimeSeriesParameter, V <: PSY.ElectricLoad, W <: AbstractLoadFormulation, X <: PM.AbstractPowerModel, } param_container = get_parameter(container, U(), V) multiplier = get_multiplier_array(param_container) network_reduction = get_network_reduction(network_model) ts_name = get_time_series_names(model)[U] ts_type = get_default_time_series_type(container) for d in devices bus_no = PNM.get_mapped_bus_number(network_reduction, PSY.get_bus(d)) name = PSY.get_name(d) has_ts = PSY.has_time_series(d, ts_type, ts_name) if !has_ts @warn "Device $(name) does not have time series of type $(ts_type) with name $(ts_name). Using default value of 1.0 for all time steps." end for t in get_time_steps(container) if has_ts param_value = get_parameter_column_refs(param_container, name)[t] mult = multiplier[name, t] else param_value = 1.0 mult = get_multiplier_value(U(), d, W()) end _add_to_jump_expression!( get_expression(container, T(), PSY.ACBus)[bus_no, t], param_value, mult, ) end end return end """ Motor load implementation to add constant power to ActivePowerBalance expression """ function add_to_expression!( container::OptimizationContainer, ::Type{T}, ::Type{U}, devices::IS.FlattenIteratorWrapper{V}, model::DeviceModel{V, W}, network_model::NetworkModel{X}, ) where { T <: ActivePowerBalance, U <: ActivePowerTimeSeriesParameter, V <: PSY.MotorLoad, W <: StaticPowerLoad, X <: PM.AbstractPowerModel, } network_reduction = get_network_reduction(network_model) for d in devices bus_no = PNM.get_mapped_bus_number(network_reduction, PSY.get_bus(d)) for t in get_time_steps(container) _add_to_jump_expression!( get_expression(container, T(), PSY.ACBus)[bus_no, t], PSY.get_active_power(d), -1.0, ) end end return end """ Motor load implementation to add constant power to ActivePowerBalance expression for AreaBalancePowerModel """ function add_to_expression!( container::OptimizationContainer, ::Type{T}, ::Type{U}, devices::IS.FlattenIteratorWrapper{V}, model::DeviceModel{V, W}, network_model::NetworkModel{AreaBalancePowerModel}, ) where { T <: ActivePowerBalance, U <: ActivePowerTimeSeriesParameter, V <: PSY.MotorLoad, W <: StaticPowerLoad, } network_reduction = get_network_reduction(network_model) for d in devices bus = PSY.get_bus(d) area_name = PSY.get_name(PSY.get_area(bus)) for t in get_time_steps(container) _add_to_jump_expression!( get_expression(container, T(), PSY.Area)[area_name, t], PSY.get_active_power(d), -1.0, ) end end return end """ Motor load implementation to add constant power to ActivePowerBalance expression """ function add_to_expression!( container::OptimizationContainer, ::Type{T}, ::Type{U}, devices::IS.FlattenIteratorWrapper{V}, model::DeviceModel{V, W}, network_model::NetworkModel{X}, ) where { T <: ReactivePowerBalance, U <: ReactivePowerTimeSeriesParameter, V <: PSY.MotorLoad, W <: StaticPowerLoad, X <: PM.ACPPowerModel, } network_reduction = get_network_reduction(network_model) for d in devices bus_no = PNM.get_mapped_bus_number(network_reduction, PSY.get_bus(d)) for t in get_time_steps(container) _add_to_jump_expression!( get_expression(container, T(), PSY.ACBus)[bus_no, t], PSY.get_reactive_power(d), -1.0, ) end end 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}, ::Type{U}, devices::IS.FlattenIteratorWrapper{V}, model::DeviceModel{V, W}, network_model::NetworkModel{AreaBalancePowerModel}, ) where { T <: SystemBalanceExpressions, U <: TimeSeriesParameter, V <: PSY.ElectricLoad, W <: AbstractLoadFormulation, } param_container = get_parameter(container, U(), V) multiplier = get_multiplier_array(param_container) ts_name = get_time_series_names(model)[U] ts_type = get_default_time_series_type(container) for d in devices bus = PSY.get_bus(d) area_name = PSY.get_name(PSY.get_area(bus)) name = PSY.get_name(d) has_ts = PSY.has_time_series(d, ts_type, ts_name) if !has_ts @warn "Device $(name) does not have time series of type $(ts_type) with name $(ts_name). Using default value of 1.0 for all time steps." end for t in get_time_steps(container) if has_ts param_value = get_parameter_column_refs(param_container, name)[t] mult = multiplier[name, t] else param_value = 1.0 mult = get_multiplier_value(U(), d, W()) end _add_to_jump_expression!( get_expression(container, T(), PSY.Area)[area_name, t], param_value, mult, ) end end 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 <: OnStatusParameter, V <: PSY.ThermalGen, W <: AbstractDeviceFormulation, X <: PM.AbstractPowerModel, } parameter = get_parameter_array(container, U(), V) network_reduction = get_network_reduction(network_model) for d in devices, t in get_time_steps(container) bus_no = PNM.get_mapped_bus_number(network_reduction, PSY.get_bus(d)) name = PSY.get_name(d) mult = get_expression_multiplier(U(), T(), d, W()) _add_to_jump_expression!( get_expression(container, T(), PSY.ACBus)[bus_no, t], parameter[name, t], mult, ) end return end """ Default implementation to add device variables to SystemBalanceExpressions """ function add_to_expression!( container::OptimizationContainer, ::Type{T}, ::Type{U}, devices::IS.FlattenIteratorWrapper{V}, ::DeviceModel{V, W}, network_model::NetworkModel{X}, ) where { T <: SystemBalanceExpressions, U <: VariableType, V <: PSY.StaticInjection, W <: AbstractDeviceFormulation, X <: PM.AbstractPowerModel, } variable = get_variable(container, U(), V) expression = get_expression(container, T(), PSY.ACBus) network_reduction = get_network_reduction(network_model) for d in devices name = PSY.get_name(d) bus_no = PNM.get_mapped_bus_number(network_reduction, PSY.get_bus(d)) for t in get_time_steps(container) _add_to_jump_expression!( expression[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}, ::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 """ 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 <: PSY.TwoTerminalHVDC, W <: HVDCTwoTerminalDispatch, X <: Union{PTDFPowerModel, CopperPlatePowerModel}, } variable = get_variable(container, U(), V) expression = get_expression(container, T(), PSY.System) for d in devices name = PSY.get_name(d) device_bus_from = PSY.get_arc(d).from device_bus_to = PSY.get_arc(d).to ref_bus_from = get_reference_bus(network_model, device_bus_from) ref_bus_to = get_reference_bus(network_model, device_bus_to) if ref_bus_from == ref_bus_to for t in get_time_steps(container) _add_to_jump_expression!( expression[ref_bus_from, t], variable[name, t], get_variable_multiplier(U(), d, W()), ) end end end 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 <: PSY.TwoTerminalHVDC, 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 """ function add_to_expression!( container::OptimizationContainer, ::Type{T}, ::Type{U}, devices::IS.FlattenIteratorWrapper{V}, ::DeviceModel{V, W}, network_model::NetworkModel{Union{PTDFPowerModel}}, ) where { T <: ActivePowerBalance, U <: FlowActivePowerToFromVariable, V <: PSY.TwoTerminalHVDC, W <: AbstractTwoTerminalDCLineFormulation, } var = get_variable(container, U(), V) nodal_expr = get_expression(container, T(), PSY.ACBus) sys_expr = get_expression(container, T(), PSY.System) network_reduction = get_network_reduction(network_model) for d in devices bus_no_to = PNM.get_mapped_bus_number(network_reduction, PSY.get_arc(d).to) ref_bus_from = get_reference_bus(network_model, PSY.get_arc(d).from) ref_bus_to = get_reference_bus(network_model, PSY.get_arc(d).to) for t in get_time_steps(container) flow_variable = var[PSY.get_name(d), t] _add_to_jump_expression!(nodal_expr[bus_no_to, t], flow_variable, -1.0) if ref_bus_from != ref_bus_to _add_to_jump_expression!(sys_expr[ref_bus_to, t], flow_variable, -1.0) end end end return end """ Default implementation to add branch variables to SystemBalanceExpressions """ 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 <: FlowActivePowerFromToVariable, V <: PSY.TwoTerminalHVDC, W <: AbstractTwoTerminalDCLineFormulation, X <: AbstractPTDFModel, } var = get_variable(container, U(), V) nodal_expr = get_expression(container, T(), PSY.ACBus) sys_expr = get_expression(container, T(), _system_expression_type(X)) network_reduction = get_network_reduction(network_model) for d in devices bus_no_from = PNM.get_mapped_bus_number(network_reduction, PSY.get_arc(d).from) ref_bus_to = get_reference_bus(network_model, PSY.get_arc(d).to) ref_bus_from = get_reference_bus(network_model, PSY.get_arc(d).from) for t in get_time_steps(container) flow_variable = var[PSY.get_name(d), t] _add_to_jump_expression!(nodal_expr[bus_no_from, t], flow_variable, -1.0) if ref_bus_from != ref_bus_to _add_to_jump_expression!(sys_expr[ref_bus_from, t], flow_variable, -1.0) end end end return end """ PWL implementation to add FromTo branch variables to SystemBalanceExpressions """ 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 <: HVDCActivePowerReceivedFromVariable, V <: PSY.TwoTerminalHVDC, W <: HVDCTwoTerminalPiecewiseLoss, X <: AbstractPTDFModel, } var = get_variable(container, U(), V) nodal_expr = get_expression(container, T(), PSY.ACBus) sys_expr = get_expression(container, T(), _system_expression_type(X)) network_reduction = get_network_reduction(network_model) for d in devices bus_no_from = PNM.get_mapped_bus_number(network_reduction, PSY.get_arc(d).from) ref_bus_to = get_reference_bus(network_model, PSY.get_arc(d).to) ref_bus_from = get_reference_bus(network_model, PSY.get_arc(d).from) for t in get_time_steps(container) flow_variable = var[PSY.get_name(d), t] _add_to_jump_expression!(nodal_expr[bus_no_from, t], flow_variable, 1.0) if ref_bus_from != ref_bus_to _add_to_jump_expression!(sys_expr[ref_bus_from, t], flow_variable, 1.0) end end end return end """ HVDC LCC implementation to add ActivePowerBalance expression for HVDCActivePowerReceivedFromVariable variable """ function add_to_expression!( container::OptimizationContainer, ::Type{T}, ::Type{U}, devices::IS.FlattenIteratorWrapper{V}, ::DeviceModel{V, W}, network_model::NetworkModel{X}, ) where { T <: ActivePowerBalance, # expression U <: HVDCActivePowerReceivedFromVariable, # variable V <: PSY.TwoTerminalHVDC, # power system type W <: HVDCTwoTerminalLCC, # formulation X <: ACPPowerModel, # network model } var = get_variable(container, U(), V) nodal_expr = get_expression(container, T(), PSY.ACBus) network_reduction = get_network_reduction(network_model) for d in devices bus_no_from = PNM.get_mapped_bus_number(network_reduction, PSY.get_arc(d).from) for t in get_time_steps(container) flow_variable = var[PSY.get_name(d), t] _add_to_jump_expression!(nodal_expr[bus_no_from, t], flow_variable, -1.0) end end return end """ HVDC LCC implementation to add ActivePowerBalance expression for HVDCActivePowerReceivedToVariable variable """ 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 <: HVDCActivePowerReceivedToVariable, V <: PSY.TwoTerminalHVDC, W <: HVDCTwoTerminalLCC, X <: ACPPowerModel, } var = get_variable(container, U(), V) nodal_expr = get_expression(container, T(), PSY.ACBus) network_reduction = get_network_reduction(network_model) for d in devices bus_no_to = PNM.get_mapped_bus_number(network_reduction, PSY.get_arc(d).to) for t in get_time_steps(container) flow_variable = var[PSY.get_name(d), t] _add_to_jump_expression!(nodal_expr[bus_no_to, t], flow_variable, 1.0) end end return end """ HVDC LCC implementation to add ReactivePowerBalance expression for HVDCReactivePowerReceivedFromVariable variable """ function add_to_expression!( container::OptimizationContainer, ::Type{T}, ::Type{U}, devices::IS.FlattenIteratorWrapper{V}, ::DeviceModel{V, W}, network_model::NetworkModel{X}, ) where { T <: ReactivePowerBalance, # expression U <: HVDCReactivePowerReceivedFromVariable, # variable V <: PSY.TwoTerminalHVDC, # power system type W <: HVDCTwoTerminalLCC, # formulation X <: ACPPowerModel, # network model } var = get_variable(container, U(), V) nodal_expr = get_expression(container, T(), PSY.ACBus) network_reduction = get_network_reduction(network_model) for d in devices bus_no_from = PNM.get_mapped_bus_number(network_reduction, PSY.get_arc(d).from) for t in get_time_steps(container) flow_variable = var[PSY.get_name(d), t] _add_to_jump_expression!(nodal_expr[bus_no_from, t], flow_variable, -1.0) end end return end """ HVDC LCC implementation to add ReactivePowerBalance expression for HVDCReactivePowerReceivedToVariable variable """ function add_to_expression!( container::OptimizationContainer, ::Type{T}, ::Type{U}, devices::IS.FlattenIteratorWrapper{V}, ::DeviceModel{V, W}, network_model::NetworkModel{X}, ) where { T <: ReactivePowerBalance, U <: HVDCReactivePowerReceivedToVariable, V <: PSY.TwoTerminalHVDC, W <: HVDCTwoTerminalLCC, X <: ACPPowerModel, } var = get_variable(container, U(), V) nodal_expr = get_expression(container, T(), PSY.ACBus) network_reduction = get_network_reduction(network_model) for d in devices bus_no_to = PNM.get_mapped_bus_number(network_reduction, PSY.get_arc(d).to) for t in get_time_steps(container) flow_variable = var[PSY.get_name(d), t] _add_to_jump_expression!(nodal_expr[bus_no_to, t], flow_variable, -1.0) end end return end """ PWL implementation to add FromTo branch variables to SystemBalanceExpressions """ 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 <: HVDCActivePowerReceivedToVariable, V <: PSY.TwoTerminalHVDC, W <: HVDCTwoTerminalPiecewiseLoss, X <: AbstractPTDFModel, } var = get_variable(container, U(), V) nodal_expr = get_expression(container, T(), PSY.ACBus) sys_expr = get_expression(container, T(), _system_expression_type(X)) network_reduction = get_network_reduction(network_model) for d in devices bus_no_to = PNM.get_mapped_bus_number(network_reduction, PSY.get_arc(d).to) ref_bus_to = get_reference_bus(network_model, PSY.get_arc(d).to) ref_bus_from = get_reference_bus(network_model, PSY.get_arc(d).from) for t in get_time_steps(container) flow_variable = var[PSY.get_name(d), t] _add_to_jump_expression!(nodal_expr[bus_no_to, t], flow_variable, 1.0) if ref_bus_from != ref_bus_to _add_to_jump_expression!(sys_expr[ref_bus_to, t], flow_variable, 1.0) end end end return end """ Default implementation to add branch variables to SystemBalanceExpressions """ 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 <: FlowActivePowerToFromVariable, V <: PSY.TwoTerminalHVDC, W <: AbstractTwoTerminalDCLineFormulation, X <: CopperPlatePowerModel, } if has_subnetworks(network_model) var = get_variable(container, U(), V) sys_expr = get_expression(container, T(), PSY.System) for d in devices ref_bus_from = get_reference_bus(network_model, PSY.get_arc(d).from) ref_bus_to = get_reference_bus(network_model, PSY.get_arc(d).to) for t in get_time_steps(container) flow_variable = var[PSY.get_name(d), t] if ref_bus_from != ref_bus_to _add_to_jump_expression!(sys_expr[ref_bus_to, t], flow_variable, 1.0) end end end end return end """ Default implementation to add branch variables to SystemBalanceExpressions """ 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 <: FlowActivePowerFromToVariable, V <: PSY.TwoTerminalHVDC, W <: AbstractTwoTerminalDCLineFormulation, X <: CopperPlatePowerModel, } if has_subnetworks(network_model) var = get_variable(container, U(), V) sys_expr = get_expression(container, T(), PSY.System) for d in devices ref_bus_from = get_reference_bus(network_model, PSY.get_arc(d).from) ref_bus_to = get_reference_bus(network_model, PSY.get_arc(d).to) for t in get_time_steps(container) flow_variable = var[PSY.get_name(d), t] if ref_bus_from != ref_bus_to _add_to_jump_expression!(sys_expr[ref_bus_to, t], flow_variable, -1.0) end end end end return end """ Default implementation to add branch variables to SystemBalanceExpressions """ 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 <: FlowActivePowerFromToVariable, V <: PSY.Branch, W <: AbstractDeviceFormulation, X <: PM.AbstractPowerModel, } variable = get_variable(container, U(), V) expression = get_expression(container, T(), PSY.ACBus) network_reduction = get_network_reduction(network_model) for d in devices name = PSY.get_name(d) bus_no_ = PSY.get_number(PSY.get_arc(d).from) bus_no = PNM.get_mapped_bus_number(network_reduction, bus_no_) for t in get_time_steps(container) _add_to_jump_expression!( expression[bus_no, t], variable[name, t], get_variable_multiplier(U(), V, W()), ) end end return end """ Default implementation to add branch variables to SystemBalanceExpressions """ 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 <: FlowActivePowerToFromVariable, V <: PSY.ACBranch, W <: AbstractDeviceFormulation, X <: PM.AbstractPowerModel, } variable = get_variable(container, U(), V) expression = get_expression(container, T(), PSY.ACBus) network_reduction = get_network_reduction(network_model) for d in devices name = PSY.get_name(d) bus_no_ = PSY.get_number(PSY.get_arc(d).to) bus_no = PNM.get_mapped_bus_number(network_reduction, bus_no_) for t in get_time_steps(container) _add_to_jump_expression!( expression[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}, ::DeviceModel{V, HVDCTwoTerminalDispatch}, network_model::NetworkModel{AreaBalancePowerModel}, ) where { T <: ActivePowerBalance, U <: FlowActivePowerToFromVariable, V <: PSY.TwoTerminalHVDC, } variable = get_variable(container, U(), V) expression = get_expression(container, T(), PSY.Area) for d in devices name = PSY.get_name(d) area_name = PSY.get_name(PSY.get_area(PSY.get_arc(d).to)) for t in get_time_steps(container) _add_to_jump_expression!( expression[area_name, t], variable[name, t], get_variable_multiplier(U(), V, HVDCTwoTerminalDispatch()), ) 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{AreaBalancePowerModel}, ) where { T <: SystemBalanceExpressions, U <: OnVariable, V <: PSY.ThermalGen, W <: AbstractCompactUnitCommitment, } _add_to_expression!( container, T, U, devices, device_model, network_model, ) return end function add_to_expression!( container::OptimizationContainer, ::Type{T}, ::Type{U}, devices::IS.FlattenIteratorWrapper{V}, device_model::DeviceModel{V, W}, network_model::NetworkModel{X}, ) where { T <: SystemBalanceExpressions, U <: OnVariable, V <: PSY.ThermalGen, W <: AbstractCompactUnitCommitment, X <: PM.AbstractPowerModel, } _add_to_expression!( container, T, U, devices, device_model, network_model, ) 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 <: SystemBalanceExpressions, U <: OnVariable, V <: PSY.ThermalGen, W <: AbstractCompactUnitCommitment, X <: PM.AbstractPowerModel, } variable = get_variable(container, U(), V) expression = get_expression(container, T(), PSY.ACBus) network_reduction = get_network_reduction(network_model) 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(network_reduction, bus_no_) for t in get_time_steps(container) if PSY.get_must_run(d) _add_to_jump_expression!( expression[bus_no, t], get_variable_multiplier(U(), d, W()), ) else _add_to_jump_expression!( expression[bus_no, t], variable[name, t], get_variable_multiplier(U(), d, W()), ) end end end 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.Area) for d in devices name = PSY.get_name(d) bus = PSY.get_bus(d) area_name = PSY.get_name(PSY.get_area(bus)) for t in get_time_steps(container) if PSY.get_must_run(d) _add_to_jump_expression!( expression[area_name, t], get_variable_multiplier(U(), d, W()), ) else _add_to_jump_expression!( expression[area_name, t], variable[name, t], get_variable_multiplier(U(), d, W()), ) end end end return end """ Default implementation to add parameters to Copperplate SystemBalanceExpressions """ function add_to_expression!( container::OptimizationContainer, ::Type{T}, ::Type{U}, devices::IS.FlattenIteratorWrapper{V}, device_model::DeviceModel{V, W}, network_model::NetworkModel{CopperPlatePowerModel}, ) where { T <: SystemBalanceExpressions, U <: TimeSeriesParameter, V <: PSY.StaticInjection, W <: AbstractDeviceFormulation, } param_container = get_parameter(container, U(), V) multiplier = get_multiplier_array(param_container) expression = get_expression(container, T(), PSY.System) for d in devices device_bus = PSY.get_bus(d) ref_bus = get_reference_bus(network_model, device_bus) name = PSY.get_name(d) for t in get_time_steps(container) _add_to_jump_expression!( expression[ref_bus, t], get_parameter_column_refs(param_container, name)[t], multiplier[name, t], ) end end end """ Electric Load implementation to add parameters to Copperplate SystemBalanceExpressions """ function add_to_expression!( container::OptimizationContainer, ::Type{T}, ::Type{U}, devices::IS.FlattenIteratorWrapper{V}, device_model::DeviceModel{V, W}, network_model::NetworkModel{CopperPlatePowerModel}, ) where { T <: SystemBalanceExpressions, U <: TimeSeriesParameter, V <: PSY.ElectricLoad, W <: AbstractLoadFormulation, } param_container = get_parameter(container, U(), V) multiplier = get_multiplier_array(param_container) expression = get_expression(container, T(), PSY.System) ts_name = get_time_series_names(device_model)[U] ts_type = get_default_time_series_type(container) for d in devices device_bus = PSY.get_bus(d) ref_bus = get_reference_bus(network_model, device_bus) name = PSY.get_name(d) has_ts = PSY.has_time_series(d, ts_type, ts_name) if !has_ts @warn "Device $(name) does not have time series of type $(ts_type) with name $(ts_name). Using default value of 1.0 for all time steps." end for t in get_time_steps(container) if has_ts param_value = get_parameter_column_refs(param_container, name)[t] mult = multiplier[name, t] else param_value = 1.0 mult = get_multiplier_value(U(), d, W()) end _add_to_jump_expression!( expression[ref_bus, t], param_value, mult, ) end end return end """ Motor load implementation to add parameters to SystemBalanceExpressions CopperPlate """ function add_to_expression!( container::OptimizationContainer, ::Type{T}, ::Type{U}, devices::IS.FlattenIteratorWrapper{V}, device_model::DeviceModel{V, W}, network_model::NetworkModel{CopperPlatePowerModel}, ) where { T <: ActivePowerBalance, U <: ActivePowerTimeSeriesParameter, V <: PSY.MotorLoad, W <: StaticPowerLoad, } expression = get_expression(container, T(), PSY.System) for d in devices device_bus = PSY.get_bus(d) ref_bus = get_reference_bus(network_model, device_bus) for t in get_time_steps(container) _add_to_jump_expression!( expression[ref_bus, t], PSY.get_active_power(d), -1.0, ) end end return end function add_to_expression!( container::OptimizationContainer, ::Type{T}, ::Type{U}, devices::IS.FlattenIteratorWrapper{V}, ::DeviceModel{V, W}, network_model::NetworkModel{CopperPlatePowerModel}, ) where { T <: ActivePowerBalance, U <: OnStatusParameter, V <: PSY.ThermalGen, W <: AbstractDeviceFormulation, } parameter = get_parameter_array(container, U(), V) expression = get_expression(container, T(), PSY.System) for d in devices name = PSY.get_name(d) device_bus = PSY.get_bus(d) ref_bus = get_reference_bus(network_model, device_bus) for t in get_time_steps(container) mult = get_expression_multiplier(U(), T(), d, W()) _add_to_jump_expression!(expression[ref_bus, t], parameter[name, t], mult) end end return end """ Default implementation to add variables to SystemBalanceExpressions """ function add_to_expression!( container::OptimizationContainer, ::Type{T}, ::Type{U}, devices::IS.FlattenIteratorWrapper{V}, device_model::DeviceModel{V, W}, network_model::NetworkModel{CopperPlatePowerModel}, ) where { T <: ActivePowerBalance, U <: VariableType, V <: PSY.StaticInjection, W <: AbstractDeviceFormulation, } variable = get_variable(container, U(), V) expression = get_expression(container, T(), PSY.System) for d in devices device_bus = PSY.get_bus(d) ref_bus = get_reference_bus(network_model, device_bus) name = PSY.get_name(d) for t in get_time_steps(container) _add_to_jump_expression!( expression[ref_bus, 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{CopperPlatePowerModel}, ) where { T <: ActivePowerBalance, U <: OnVariable, V <: PSY.ThermalGen, W <: AbstractCompactUnitCommitment, } variable = get_variable(container, U(), V) expression = get_expression(container, T(), PSY.System) for d in devices name = PSY.get_name(d) device_bus = PSY.get_bus(d) ref_bus = get_reference_bus(network_model, device_bus) for t in get_time_steps(container) _add_to_jump_expression!( expression[ref_bus, t], variable[name, t], get_variable_multiplier(U(), d, W()), ) end end return end """ Default implementation to add parameters to PTDF SystemBalanceExpressions """ function add_to_expression!( container::OptimizationContainer, ::Type{T}, ::Type{U}, devices::IS.FlattenIteratorWrapper{V}, device_model::DeviceModel{V, W}, network_model::NetworkModel{X}, ) where { T <: SystemBalanceExpressions, U <: TimeSeriesParameter, V <: PSY.StaticInjection, W <: AbstractDeviceFormulation, X <: AbstractPTDFModel, } param_container = get_parameter(container, U(), V) multiplier = get_multiplier_array(param_container) sys_expr = get_expression(container, T(), _system_expression_type(X)) nodal_expr = get_expression(container, T(), PSY.ACBus) network_reduction = get_network_reduction(network_model) for d in devices name = PSY.get_name(d) device_bus = PSY.get_bus(d) bus_no = PNM.get_mapped_bus_number(network_reduction, PSY.get_number(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_index, t], param[t], multiplier[name, t]) _add_to_jump_expression!(nodal_expr[bus_no, t], param[t], multiplier[name, t]) end end return end """ Electric Load implementation to add parameters to PTDF SystemBalanceExpressions """ function add_to_expression!( container::OptimizationContainer, ::Type{T}, ::Type{U}, devices::IS.FlattenIteratorWrapper{V}, device_model::DeviceModel{V, W}, network_model::NetworkModel{X}, ) where { T <: SystemBalanceExpressions, U <: TimeSeriesParameter, V <: PSY.ElectricLoad, W <: AbstractLoadFormulation, X <: AbstractPTDFModel, } param_container = get_parameter(container, U(), V) multiplier = get_multiplier_array(param_container) sys_expr = get_expression(container, T(), _system_expression_type(X)) nodal_expr = get_expression(container, T(), PSY.ACBus) network_reduction = get_network_reduction(network_model) ts_name = get_time_series_names(device_model)[U] ts_type = get_default_time_series_type(container) for d in devices name = PSY.get_name(d) has_ts = PSY.has_time_series(d, ts_type, ts_name) if !has_ts @warn "Device $(name) does not have time series of type $(ts_type) with name $(ts_name). Using default value of 1.0 for all time steps." end device_bus = PSY.get_bus(d) bus_no_ = PSY.get_number(device_bus) bus_no = PNM.get_mapped_bus_number(network_reduction, bus_no_) ref_index = _ref_index(network_model, device_bus) for t in get_time_steps(container) if has_ts param = get_parameter_column_refs(param_container, name)[t] mult = multiplier[name, t] else param = 1.0 mult = get_multiplier_value(U(), d, W()) end _add_to_jump_expression!(sys_expr[ref_index, t], param, mult) _add_to_jump_expression!(nodal_expr[bus_no, t], param, mult) end end 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 <: OnStatusParameter, V <: PSY.ThermalGen, W <: AbstractDeviceFormulation, 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) network_reduction = get_network_reduction(network_model) 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(network_reduction, bus_no_) mult = get_expression_multiplier(U(), T(), d, W()) device_bus = PSY.get_bus(d) 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 """ Default implementation to add variables to SystemBalanceExpressions """ function add_to_expression!( container::OptimizationContainer, ::Type{T}, ::Type{U}, devices::IS.FlattenIteratorWrapper{V}, device_model::DeviceModel{V, W}, network_model::NetworkModel{X}, ) 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) nodal_expr = get_expression(container, T(), PSY.ACBus) network_reduction = get_network_reduction(network_model) for d in devices name = PSY.get_name(d) device_bus = PSY.get_bus(d) bus_no = PNM.get_mapped_bus_number(network_reduction, device_bus) ref_index = _ref_index(network_model, device_bus) for t in get_time_steps(container) _add_to_jump_expression!( sys_expr[ref_index, 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 """ Motor Load implementation to add constant motor power to PTDF SystemBalanceExpressions """ function add_to_expression!( container::OptimizationContainer, ::Type{T}, ::Type{U}, devices::IS.FlattenIteratorWrapper{V}, device_model::DeviceModel{V, W}, network_model::NetworkModel{X}, ) where { T <: ActivePowerBalance, U <: ActivePowerTimeSeriesParameter, V <: PSY.MotorLoad, W <: StaticPowerLoad, X <: AbstractPTDFModel, } sys_expr = get_expression(container, T(), PSY.System) nodal_expr = get_expression(container, T(), PSY.ACBus) network_reduction = get_network_reduction(network_model) for d in devices device_bus = PSY.get_bus(d) bus_no = PNM.get_mapped_bus_number(network_reduction, device_bus) ref_index = _ref_index(network_model, device_bus) for t in get_time_steps(container) _add_to_jump_expression!( sys_expr[ref_index, t], PSY.get_active_power(d), -1.0, ) _add_to_jump_expression!( nodal_expr[bus_no, t], PSY.get_active_power(d), -1.0, ) 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{X}, ) where { T <: ActivePowerBalance, U <: ActivePowerVariable, V <: PSY.StaticInjection, W <: AbstractDeviceFormulation, X <: AreaPTDFPowerModel, } variable = get_variable(container, U(), V) area_expr = get_expression(container, T(), PSY.Area) nodal_expr = get_expression(container, T(), PSY.ACBus) network_reduction = get_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(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 # The on variables are included in the system balance expressions becuase they # are multiplied by the Pmin and the active power is not the total active power # but the power above minimum. function add_to_expression!( container::OptimizationContainer, ::Type{T}, ::Type{U}, devices::IS.FlattenIteratorWrapper{V}, device_model::DeviceModel{V, W}, network_model::NetworkModel{X}, ) where { T <: ActivePowerBalance, U <: OnVariable, V <: PSY.ThermalGen, W <: AbstractCompactUnitCommitment, X <: PTDFPowerModel, } variable = get_variable(container, U(), V) sys_expr = get_expression(container, T(), _system_expression_type(PTDFPowerModel)) nodal_expr = get_expression(container, T(), PSY.ACBus) network_reduction = get_network_reduction(network_model) for d in devices name = PSY.get_name(d) bus_no = PNM.get_mapped_bus_number(network_reduction, 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_index, t], variable[name, t], get_variable_multiplier(U(), d, W()), ) _add_to_jump_expression!( nodal_expr[bus_no, t], variable[name, t], get_variable_multiplier(U(), d, W()), ) end end return end """ Implementation of add_to_expression! for lossless branch/network models """ 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 <: FlowActivePowerVariable, V <: PSY.ACBranch, W <: AbstractBranchFormulation, X <: PM.AbstractActivePowerModel, } var = get_variable(container, U(), V) expression = get_expression(container, T(), PSY.ACBus) network_reduction = get_network_reduction(network_model) for d in devices bus_no_from = PNM.get_mapped_bus_number(network_reduction, PSY.get_arc(d).from) bus_no_to = PNM.get_mapped_bus_number(network_reduction, PSY.get_arc(d).to) for t in get_time_steps(container) flow_variable = var[PSY.get_name(d), t] _add_to_jump_expression!( expression[bus_no_from, t], flow_variable, -1.0, ) _add_to_jump_expression!( expression[bus_no_to, t], flow_variable, 1.0, ) end end return end function add_to_expression!( container::OptimizationContainer, ::Type{ActivePowerBalance}, ::Type{FlowActivePowerVariable}, devices::IS.FlattenIteratorWrapper{PSY.AreaInterchange}, ::DeviceModel{PSY.AreaInterchange, W}, network_model::NetworkModel{U}, ) where { W <: AbstractBranchFormulation, U <: Union{AreaBalancePowerModel, AreaPTDFPowerModel}, } flow_variable = get_variable(container, FlowActivePowerVariable(), PSY.AreaInterchange) expression = get_expression(container, ActivePowerBalance(), 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 function add_to_expression!( container::OptimizationContainer, ::Type{ActivePowerBalance}, ::Type{FlowActivePowerVariable}, devices::IS.FlattenIteratorWrapper{PSY.AreaInterchange}, ::DeviceModel{PSY.AreaInterchange, W}, network_model::NetworkModel{U}, ) where { W <: AbstractBranchFormulation, U <: PM.AbstractActivePowerModel, } @debug "AreaInterchanges do not contribute to ActivePowerBalance expressions in non-area models." return end """ Implementation of add_to_expression! for lossless branch/network models """ 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 <: FlowActivePowerVariable, V <: PSY.TwoTerminalHVDC, W <: AbstractBranchFormulation, X <: PTDFPowerModel, } var = get_variable(container, U(), V) nodal_expr = get_expression(container, T(), PSY.ACBus) sys_expr = get_expression(container, T(), PSY.System) network_reduction = get_network_reduction(network_model) for d in devices bus_no_from = PNM.get_mapped_bus_number(network_reduction, PSY.get_arc(d).from) bus_no_to = PNM.get_mapped_bus_number(network_reduction, PSY.get_arc(d).to) ref_bus_from = get_reference_bus(network_model, PSY.get_arc(d).from) ref_bus_to = get_reference_bus(network_model, PSY.get_arc(d).to) for t in get_time_steps(container) flow_variable = var[PSY.get_name(d), t] _add_to_jump_expression!(nodal_expr[bus_no_from, t], flow_variable, -1.0) _add_to_jump_expression!(nodal_expr[bus_no_to, t], flow_variable, 1.0) if ref_bus_from != ref_bus_to _add_to_jump_expression!(sys_expr[ref_bus_from, t], flow_variable, -1.0) _add_to_jump_expression!(sys_expr[ref_bus_to, t], flow_variable, 1.0) end end end return end """ Implementation of add_to_expression! for lossless branch/network models """ function add_to_expression!( container::OptimizationContainer, ::Type{T}, ::Type{U}, devices::IS.FlattenIteratorWrapper{V}, ::DeviceModel{V, W}, network_model::NetworkModel{CopperPlatePowerModel}, ) where { T <: ActivePowerBalance, U <: FlowActivePowerVariable, V <: PSY.ACBranch, W <: AbstractBranchFormulation, } inter_network_branches = V[] for d in devices ref_bus_from = get_reference_bus(network_model, PSY.get_arc(d).from) ref_bus_to = get_reference_bus(network_model, PSY.get_arc(d).to) if ref_bus_from != ref_bus_to push!(inter_network_branches, d) end end if !isempty(inter_network_branches) var = get_variable(container, U(), V) sys_expr = get_expression(container, T(), PSY.System) for d in devices ref_bus_from = get_reference_bus(network_model, PSY.get_arc(d).from) ref_bus_to = get_reference_bus(network_model, PSY.get_arc(d).to) if ref_bus_from == ref_bus_to continue end for t in get_time_steps(container) flow_variable = var[PSY.get_name(d), t] _add_to_jump_expression!(sys_expr[ref_bus_from, t], flow_variable, -1.0) _add_to_jump_expression!(sys_expr[ref_bus_to, t], flow_variable, 1.0) end end end return end """ Implementation of add_to_expression! for lossless branch/network models """ function add_to_expression!( container::OptimizationContainer, ::Type{T}, ::Type{U}, devices::IS.FlattenIteratorWrapper{PSY.PhaseShiftingTransformer}, ::DeviceModel{PSY.PhaseShiftingTransformer, V}, 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) network_reduction = get_network_reduction(network_model) for d in devices bus_no_from = PNM.get_mapped_bus_number(network_reduction, PSY.get_arc(d).from) bus_no_to = PNM.get_mapped_bus_number(network_reduction, PSY.get_arc(d).to) for t in get_time_steps(container) flow_variable = var[PSY.get_name(d), t] _add_to_jump_expression!( expression[bus_no_from, t], flow_variable, -get_variable_multiplier(U(), d, V()), ) _add_to_jump_expression!( expression[bus_no_to, t], flow_variable, get_variable_multiplier(U(), d, V()), ) end end return end function add_to_expression!( container::OptimizationContainer, ::Type{T}, ::Type{U}, devices::IS.FlattenIteratorWrapper{V}, model::DeviceModel{V, W}, network_model::NetworkModel{X}, ) where { T <: Union{ActivePowerRangeExpressionUB, ActivePowerRangeExpressionLB}, U <: VariableType, V <: PSY.Device, W <: AbstractDeviceFormulation, X <: PM.AbstractPowerModel, } variable = get_variable(container, U(), V) if !has_container_key(container, T, V) add_expressions!(container, T, devices, model) end expression = get_expression(container, T(), V) for d in devices, t in get_time_steps(container) name = PSY.get_name(d) _add_to_jump_expression!(expression[name, t], variable[name, t], 1.0) end return end function add_to_expression!( container::OptimizationContainer, ::Type{T}, ::Type{U}, devices::Union{Vector{V}, IS.FlattenIteratorWrapper{V}}, model::ServiceModel{X, W}, ) where { T <: ActivePowerRangeExpressionUB, U <: VariableType, V <: PSY.Component, X <: PSY.Reserve{PSY.ReserveUp}, W <: AbstractReservesFormulation, } service_name = get_service_name(model) variable = get_variable(container, U(), X, service_name) if !has_container_key(container, T, V) add_expressions!(container, T, devices, model) end expression = get_expression(container, T(), V) for d in devices, t in get_time_steps(container) name = PSY.get_name(d) _add_to_jump_expression!(expression[name, t], variable[name, t], 1.0) end return end function add_to_expression!( container::OptimizationContainer, ::Type{InterfaceTotalFlow}, ::Type{T}, service::PSY.TransmissionInterface, 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) for t in get_time_steps(container) _add_to_jump_expression!( expression[service_name, t], variable[t], get_variable_multiplier(T(), service, U()), ) end return end function _handle_nodal_or_zonal_interfaces( br_type::Type{V}, net_reduction_data::PNM.NetworkReductionData, direction_map::Dict{String, Int}, contributing_devices::Vector{V}, variable::JuMPVariableArray, expression::DenseAxisArray, # There is no good type for a DenseAxisArray slice ) where {V <: PSY.ACTransmission} all_branch_maps_by_type = net_reduction_data.all_branch_maps_by_type for (name, (arc, reduction)) in PNM.get_name_to_arc_map(net_reduction_data, br_type) reduction_entry = all_branch_maps_by_type[reduction][br_type][arc] if _reduced_entry_in_interface(reduction_entry, contributing_devices) if isempty(direction_map) direction = 1.0 else direction = _get_direction( arc, reduction_entry, direction_map, net_reduction_data, ) end for t in axes(variable, 2) _add_to_jump_expression!( expression[t], variable[name, t], Float64(direction), ) end end end return end function _handle_nodal_or_zonal_interfaces( ::Type{PSY.AreaInterchange}, net_reduction_data::PNM.NetworkReductionData, direction_map::Dict{String, Int}, contributing_devices::Vector{PSY.AreaInterchange}, variable::JuMPVariableArray, expression::DenseAxisArray, # There is no good type for a DenseAxisArray slice ) for device in contributing_devices name = PSY.get_name(device) if isempty(direction_map) direction = 1.0 else direction = direction_map[name] end for t in axes(variable, 2) _add_to_jump_expression!( expression[t], variable[name, t], Float64(direction), ) end end return end function add_to_expression!( container::OptimizationContainer, ::Type{InterfaceTotalFlow}, ::Type{FlowActivePowerVariable}, service::PSY.TransmissionInterface, model::ServiceModel{PSY.TransmissionInterface, V}, network_model::NetworkModel{<:PM.AbstractActivePowerModel}, ) where {V <: Union{ConstantMaxInterfaceFlow, VariableMaxInterfaceFlow}} net_reduction_data = get_network_reduction(network_model) expression = get_expression(container, InterfaceTotalFlow(), PSY.TransmissionInterface) service_name = get_service_name(model) direction_map = PSY.get_direction_mapping(service) contributing_devices_map = get_contributing_devices_map(model) for (br_type, contributing_devices) in contributing_devices_map variable = get_variable(container, FlowActivePowerVariable(), br_type) _handle_nodal_or_zonal_interfaces( br_type, net_reduction_data, direction_map, contributing_devices, variable, expression[service_name, :], ) end return end function _is_interchanges_interfaces( contributing_devices_map::Dict{Type{<:PSY.Component}, Vector{<:PSY.Component}}, ) if PSY.AreaInterchange ∈ keys(contributing_devices_map) @assert length(keys(contributing_devices_map)) == 1 return true end return false end function add_to_expression!( container::OptimizationContainer, ::Type{InterfaceTotalFlow}, ::Type{FlowActivePowerVariable}, service::PSY.TransmissionInterface, model::ServiceModel{PSY.TransmissionInterface, V}, network_model::NetworkModel{AreaPTDFPowerModel}, ) where {V <: Union{ConstantMaxInterfaceFlow, VariableMaxInterfaceFlow}} net_reduction_data = get_network_reduction(network_model) expression = get_expression(container, InterfaceTotalFlow(), PSY.TransmissionInterface) service_name = get_service_name(model) direction_map = PSY.get_direction_mapping(service) contributing_devices_map = get_contributing_devices_map(model) # Ignore interfaces over lines for AreaPTDFModel if !_is_interchanges_interfaces(contributing_devices_map) return end variable = get_variable(container, FlowActivePowerVariable(), PSY.AreaInterchange) _handle_nodal_or_zonal_interfaces( PSY.AreaInterchange, net_reduction_data, direction_map, contributing_devices_map[PSY.AreaInterchange], variable, expression[service_name, :], ) return end function add_to_expression!( container::OptimizationContainer, ::Type{InterfaceTotalFlow}, ::Type{PTDFBranchFlow}, service::PSY.TransmissionInterface, model::ServiceModel{PSY.TransmissionInterface, V}, network_model::NetworkModel{<:AbstractPTDFModel}, ) where {V <: Union{ConstantMaxInterfaceFlow, VariableMaxInterfaceFlow}} net_reduction_data = get_network_reduction(network_model) expression = get_expression(container, InterfaceTotalFlow(), PSY.TransmissionInterface) service_name = get_service_name(model) direction_map = PSY.get_direction_mapping(service) contributing_devices_map = get_contributing_devices_map(model) # Interfaces over interchanges if _is_interchanges_interfaces(contributing_devices_map) return end for (br_type, contributing_devices) in contributing_devices_map flow_expression = get_expression(container, PTDFBranchFlow(), br_type) all_branch_maps_by_type = net_reduction_data.all_branch_maps_by_type for (name, (arc, reduction)) in PNM.get_name_to_arc_map(net_reduction_data, br_type) reduction_entry = all_branch_maps_by_type[reduction][br_type][arc] if _reduced_entry_in_interface(reduction_entry, contributing_devices) if isempty(direction_map) direction = 1.0 else direction = _get_direction( arc, reduction_entry, direction_map, net_reduction_data, ) end for t in axes(flow_expression, 2) JuMP.add_to_expression!( expression[service_name, t], flow_expression[name, t], Float64(direction), ) end end end end return end function _get_direction( ::Tuple{Int, Int}, reduction_entry::PSY.ACTransmission, direction_map::Dict{String, Int}, ::PNM.NetworkReductionData, ) name = PSY.get_name(reduction_entry) if !haskey(direction_map, name) @warn "Direction not found for $(summary(reduction_entry)). Will use the default from -> to direction" return 1.0 else return direction_map[name] end end function _get_direction( arc_tuple::Tuple{Int, Int}, reduction_entry::PNM.BranchesParallel, direction_map::Dict{String, Int}, net_reduction_data::PNM.NetworkReductionData, ) # Loops through parallel branches twice, but there are relatively few parallel branches per reduction entry: directions = [ _get_direction(arc_tuple, x, direction_map, net_reduction_data) for x in reduction_entry ] if allequal(directions) return first(directions) end throw( ArgumentError( "The interface direction mapping contains a double circuit with opposite directions. Modify the data to have consistent directions for double circuits.", ), ) end function _get_direction( arc_tuple::Tuple{Int, Int}, reduction_entry::PNM.BranchesSeries, direction_map::Dict{String, Int}, net_reduction_data::PNM.NetworkReductionData, ) # direction of segments from the user provided mapping: mapping_directions = [ _get_direction(arc_tuple, x, direction_map, net_reduction_data) for x in reduction_entry ] # direction of segments relative to the reduced degree two chain: _, segment_orientations = PNM._get_chain_data(arc_tuple, reduction_entry, net_reduction_data) segment_directions = [x == :FromTo ? 1.0 : -1.0 for x in segment_orientations] net_directions = mapping_directions .* segment_directions if allequal(net_directions) return first(net_directions) else throw( ArgumentError( "The interface direction mapping for degree two chain with arc $(arc_tuple) is inconsistent. Check the mapping entries and the orientation of the segment arcs within the chain.", ), ) end end # These checks can be moved to happen at the service template check level function _reduced_entry_in_interface( reduction_entry::PSY.ACTransmission, contributing_devices::Vector{<:PSY.ACTransmission}, ) reduction_entry_name = PSY.get_name(reduction_entry) # This is compared by name given that the reduction data uses copies of the devices # so, simple comparisons will not work for device in contributing_devices device_name = PSY.get_name(device) if reduction_entry_name == device_name return true end end return false end function _reduced_entry_in_interface( reduction_entry::PNM.BranchesParallel, contributing_devices::Vector{<:PSY.ACTransmission}, ) in_interface = [ _reduced_entry_in_interface(x, contributing_devices) for x in reduction_entry ] if !allequal(in_interface) branch_names = PSY.get_name.(reduction_entry) throw( ArgumentError( "An interface is specified with only part of a double-circuit that has been reduced. Branches: $(branch_names[in_interface]) are in the interface and branches: $(branch_names[.!in_interface]) are not. Modify the data to include all of or none of the parallel segements.", ), ) end return first(in_interface) end function _reduced_entry_in_interface( reduction_entry::PNM.BranchesSeries, contributing_devices::Vector{<:PSY.ACTransmission}, ) in_interface = [ _reduced_entry_in_interface(x, contributing_devices) for x in reduction_entry ] if !allequal(in_interface) throw( ArgumentError( "An interface is specified with only portion of a degree two chain reduction that has been reduced. Modify the data to include all segments of the reduced chain", ), ) end return first(in_interface) end function add_to_expression!( container::OptimizationContainer, ::Type{T}, ::Type{U}, devices::Union{Vector{V}, IS.FlattenIteratorWrapper{V}}, model::ServiceModel{X, W}, ) where { T <: ActivePowerRangeExpressionLB, U <: VariableType, V <: PSY.Component, X <: PSY.Reserve{PSY.ReserveDown}, W <: AbstractReservesFormulation, } service_name = get_service_name(model) variable = get_variable(container, U(), X, service_name) if !has_container_key(container, T, V) add_expressions!(container, T, devices, model) end expression = get_expression(container, T(), V) for d in devices, t in get_time_steps(container) name = PSY.get_name(d) _add_to_jump_expression!(expression[name, t], variable[name, t], -1.0) end return end function add_to_expression!( container::OptimizationContainer, ::Type{T}, ::U, devices::IS.FlattenIteratorWrapper{V}, model::DeviceModel{V, W}, ) where { T <: Union{ActivePowerRangeExpressionUB, ActivePowerRangeExpressionLB}, U <: OnStatusParameter, V <: PSY.Device, W <: AbstractDeviceFormulation, } parameter_array = get_parameter_array(container, U(), V) if !has_container_key(container, T, V) add_expressions!(container, T, devices, model) end expression = get_expression(container, T(), V) 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, mult, ) end end return end function add_to_expression!( container::OptimizationContainer, ::Type{T}, ::U, devices::IS.FlattenIteratorWrapper{V}, model::DeviceModel{V, W}, ) where { T <: Union{ActivePowerRangeExpressionUB, ActivePowerRangeExpressionLB}, U <: OnStatusParameter, V <: PSY.ThermalGen, W <: AbstractThermalDispatchFormulation, } parameter_array = get_parameter_array(container, U(), V) if !has_container_key(container, T, V) add_expressions!(container, T, devices, model) end expression = get_expression(container, T(), V) for d in devices if PSY.get_must_run(d) continue end 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) end end return end function add_to_expression!( container::OptimizationContainer, ::Type{U}, model::ServiceModel{V, W}, devices_template::Dict{Symbol, DeviceModel}, ) where {U <: VariableType, V <: PSY.Reserve, W <: AbstractReservesFormulation} contributing_devices_map = get_contributing_devices_map(model) for (device_type, devices) in contributing_devices_map device_model = get(devices_template, Symbol(device_type), nothing) device_model === nothing && continue expression_type = get_expression_type_for_reserve(U(), device_type, V) add_to_expression!(container, expression_type, U, devices, model) end return end function add_to_expression!( container::OptimizationContainer, ::Type{T}, ::Type{U}, ::PSY.System, network_model::NetworkModel{W}, ) where { T <: ActivePowerBalance, U <: Union{SystemBalanceSlackUp, SystemBalanceSlackDown}, W <: Union{CopperPlatePowerModel, PTDFPowerModel}, } variable = get_variable(container, U(), 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!( expression[n, t], variable[n, t], get_variable_multiplier(U(), PSY.System, W()), ) end return end function add_to_expression!( container::OptimizationContainer, ::Type{T}, ::Type{U}, sys::PSY.System, network_model::NetworkModel{V}, ) where { T <: ActivePowerBalance, U <: Union{SystemBalanceSlackUp, SystemBalanceSlackDown}, V <: AreaPTDFPowerModel, } 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}, ::Type{U}, sys::PSY.System, ::NetworkModel{AreaBalancePowerModel}, ) where { T <: ActivePowerBalance, U <: Union{SystemBalanceSlackUp, SystemBalanceSlackDown}, } variable = get_variable(container, U(), PSY.Area) expression = get_expression(container, T(), PSY.Area) @assert_op length(axes(variable, 1)) == length(axes(expression, 1)) for t in get_time_steps(container), n in axes(expression, 1) _add_to_jump_expression!( expression[n, t], variable[n, t], get_variable_multiplier(U(), PSY.Area, AreaBalancePowerModel), ) end return end function add_to_expression!( container::OptimizationContainer, ::Type{T}, ::Type{U}, sys::PSY.System, ::NetworkModel{W}, ) where { T <: ActivePowerBalance, U <: Union{SystemBalanceSlackUp, SystemBalanceSlackDown}, W <: PM.AbstractActivePowerModel, } variable = get_variable(container, U(), PSY.ACBus) expression = get_expression(container, T(), PSY.ACBus) @assert_op length(axes(variable, 1)) == length(axes(expression, 1)) # We uses axis here to avoid double addition of the slacks to the aggregated buses for t in get_time_steps(container), n in axes(expression, 1) _add_to_jump_expression!( expression[n, t], variable[n, t], get_variable_multiplier(U(), PSY.ACBus, W), ) end return end function add_to_expression!( container::OptimizationContainer, ::Type{T}, ::Type{U}, sys::PSY.System, ::NetworkModel{W}, ) where { T <: ActivePowerBalance, U <: Union{SystemBalanceSlackUp, SystemBalanceSlackDown}, W <: PM.AbstractPowerModel, } variable = get_variable(container, U(), PSY.ACBus, "P") expression = get_expression(container, T(), PSY.ACBus) # We uses axis here to avoid double addition of the slacks to the aggregated buses for t in get_time_steps(container), n in axes(expression, 1) _add_to_jump_expression!( expression[n, t], variable[n, t], get_variable_multiplier(U(), PSY.ACBus, W), ) end return end function add_to_expression!( container::OptimizationContainer, ::Type{T}, ::Type{U}, sys::PSY.System, ::NetworkModel{W}, ) where { T <: ReactivePowerBalance, U <: Union{SystemBalanceSlackUp, SystemBalanceSlackDown}, W <: PM.AbstractPowerModel, } variable = get_variable(container, U(), PSY.ACBus, "Q") expression = get_expression(container, T(), PSY.ACBus) # We uses axis here to avoid double addition of the slacks to the aggregated buses for t in get_time_steps(container), n in axes(expression, 1) _add_to_jump_expression!( expression[n, t], variable[n, t], get_variable_multiplier(U(), PSY.ACBus, W), ) end return end function add_to_expression!( container::OptimizationContainer, ::Type{S}, cost_expression::JuMPOrFloat, component::T, time_period::Int, ) where {S <: Union{CostExpressions, FuelConsumptionExpression}, T <: PSY.Component} if has_container_key(container, S, T) device_cost_expression = get_expression(container, S(), T) component_name = PSY.get_name(component) JuMP.add_to_expression!( device_cost_expression[component_name, time_period], cost_expression, ) end return end """ Specialized `add_to_expression!` for `ConstituentCostExpression` subtypes. In addition to adding to the constituent expression, this method automatically propagates the cost to `ProductionCostExpression`, so callers do not need to add to both. """ function add_to_expression!( container::OptimizationContainer, ::Type{S}, cost_expression::JuMPOrFloat, component::T, time_period::Int, ) where {S <: ConstituentCostExpression, T <: PSY.Component} if has_container_key(container, S, T) device_cost_expression = get_expression(container, S(), T) component_name = PSY.get_name(component) JuMP.add_to_expression!( device_cost_expression[component_name, time_period], cost_expression, ) end if has_container_key(container, ProductionCostExpression, T) prod_cost_expression = get_expression(container, ProductionCostExpression(), T) JuMP.add_to_expression!( prod_cost_expression[PSY.get_name(component), time_period], cost_expression, ) end 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 # Disambiguate: ConstituentCostExpression ∩ ReserveDemandCurve — no ProductionCostExpression # propagation since reserves don't have a ProductionCostExpression container. function add_to_expression!( container::OptimizationContainer, ::Type{S}, cost_expression::JuMP.AbstractJuMPScalar, component::T, time_period::Int, ) where {S <: ConstituentCostExpression, 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 # Method-level dispatch on the value-curve type carried by the FuelCurve. One method per # concrete ValueCurve subtype defined by PowerSystems; non-Linear/Quadratic curves are # explicit no-ops, mirroring the silent skip of the original `if/elseif` chain. function _add_fuel_consumption_term!( expression, variable, name::String, var_cost::PSY.FuelCurve, value_curve::PSY.LinearCurve, base_power::Float64, device_base_power::Float64, dt::Float64, time_steps, ) power_units = PSY.get_power_units(var_cost) proportional_term = PSY.get_proportional_term(value_curve) prop_term_per_unit = get_proportional_cost_per_system_unit( proportional_term, power_units, base_power, device_base_power, ) for t in time_steps JuMP.add_to_expression!( expression[name, t], prop_term_per_unit * dt, variable[name, t], ) end return end function _add_fuel_consumption_term!( expression, variable, name::String, var_cost::PSY.FuelCurve, value_curve::PSY.QuadraticCurve, base_power::Float64, device_base_power::Float64, dt::Float64, time_steps, ) power_units = PSY.get_power_units(var_cost) proportional_term = PSY.get_proportional_term(value_curve) quadratic_term = PSY.get_quadratic_term(value_curve) prop_term_per_unit = get_proportional_cost_per_system_unit( proportional_term, power_units, base_power, device_base_power, ) quad_term_per_unit = get_quadratic_cost_per_system_unit( quadratic_term, power_units, base_power, device_base_power, ) for t in time_steps fuel_expr = ( variable[name, t] .^ 2 * quad_term_per_unit + variable[name, t] * prop_term_per_unit ) * dt JuMP.add_to_expression!( expression[name, t], fuel_expr, ) end return end _add_fuel_consumption_term!(_, _, _::String, _::PSY.FuelCurve, ::PSY.PiecewisePointCurve, _::Float64, _::Float64, _::Float64, _) = nothing _add_fuel_consumption_term!(_, _, _::String, _::PSY.FuelCurve, ::PSY.IncrementalCurve, _::Float64, _::Float64, _::Float64, _) = nothing _add_fuel_consumption_term!(_, _, _::String, _::PSY.FuelCurve, ::PSY.AverageRateCurve, _::Float64, _::Float64, _::Float64, _) = nothing function add_to_expression!( container::OptimizationContainer, ::Type{T}, ::Type{U}, devices::IS.FlattenIteratorWrapper{V}, model::DeviceModel{V, W}, ) where { T <: FuelConsumptionExpression, U <: ActivePowerVariable, V <: PSY.ThermalGen, W <: AbstractDeviceFormulation, } variable = get_variable(container, U(), V) time_steps = get_time_steps(container) base_power = get_base_power(container) resolution = get_resolution(container) dt = Dates.value(resolution) / MILLISECONDS_IN_HOUR for d in devices op_cost = PSY.get_operation_cost(d) var_cost = _get_variable_if_exists(op_cost) _is_fuel_curve(var_cost) || continue expression = get_expression(container, T(), V) name = PSY.get_name(d) device_base_power = PSY.get_base_power(d) value_curve = PSY.get_value_curve(var_cost) _add_fuel_consumption_term!( expression, variable, name, var_cost, value_curve, base_power, device_base_power, dt, time_steps, ) end return end function _add_compact_fuel_consumption_term!( container::OptimizationContainer, ::Type{W}, expression, variable, d::V, var_cost::PSY.FuelCurve, value_curve::PSY.LinearCurve, base_power::Float64, device_base_power::Float64, dt::Float64, time_steps, ) where {V <: PSY.ThermalGen, W <: AbstractDeviceFormulation} name = PSY.get_name(d) P_min = PSY.get_active_power_limits(d).min power_units = PSY.get_power_units(var_cost) proportional_term = PSY.get_proportional_term(value_curve) prop_term_per_unit = get_proportional_cost_per_system_unit( proportional_term, power_units, base_power, device_base_power, ) for t in time_steps sos_status = _get_sos_value(container, W, d) if sos_status == SOSStatusVariable.NO_VARIABLE JuMP.add_to_expression!( expression[name, t], P_min * prop_term_per_unit * dt, ) elseif sos_status == SOSStatusVariable.PARAMETER param = get_default_on_parameter(d) bin = get_parameter(container, param, V).parameter_array[name, t] JuMP.add_to_expression!( expression[name, t], P_min * prop_term_per_unit * dt, bin, ) elseif sos_status == SOSStatusVariable.VARIABLE var = get_default_on_variable(d) bin = get_variable(container, var, V)[name, t] JuMP.add_to_expression!( expression[name, t], P_min * prop_term_per_unit * dt, bin, ) else @assert false end JuMP.add_to_expression!( expression[name, t], prop_term_per_unit * dt, variable[name, t], ) end return end function _add_compact_fuel_consumption_term!( ::OptimizationContainer, ::Type{W}, _, _, ::PSY.ThermalGen, ::PSY.FuelCurve, ::PSY.QuadraticCurve, ::Float64, ::Float64, ::Float64, _, ) where {W <: AbstractDeviceFormulation} error("Quadratic Curves are not accepted with Compact Formulation: $W") end _add_compact_fuel_consumption_term!(::OptimizationContainer, ::Type{<:AbstractDeviceFormulation}, _, _, ::PSY.ThermalGen, ::PSY.FuelCurve, ::PSY.PiecewisePointCurve, ::Float64, ::Float64, ::Float64, _) = nothing _add_compact_fuel_consumption_term!(::OptimizationContainer, ::Type{<:AbstractDeviceFormulation}, _, _, ::PSY.ThermalGen, ::PSY.FuelCurve, ::PSY.IncrementalCurve, ::Float64, ::Float64, ::Float64, _) = nothing _add_compact_fuel_consumption_term!(::OptimizationContainer, ::Type{<:AbstractDeviceFormulation}, _, _, ::PSY.ThermalGen, ::PSY.FuelCurve, ::PSY.AverageRateCurve, ::Float64, ::Float64, ::Float64, _) = nothing function add_to_expression!( container::OptimizationContainer, ::Type{T}, ::Type{U}, devices::IS.FlattenIteratorWrapper{V}, model::DeviceModel{V, W}, ) where { T <: FuelConsumptionExpression, U <: PowerAboveMinimumVariable, V <: PSY.ThermalGen, W <: AbstractDeviceFormulation, } variable = get_variable(container, U(), V) time_steps = get_time_steps(container) base_power = get_base_power(container) resolution = get_resolution(container) dt = Dates.value(resolution) / MILLISECONDS_IN_HOUR for d in devices op_cost = PSY.get_operation_cost(d) var_cost = _get_variable_if_exists(op_cost) _is_fuel_curve(var_cost) || continue expression = get_expression(container, T(), V) name = PSY.get_name(d) device_base_power = PSY.get_base_power(d) value_curve = PSY.get_value_curve(var_cost) _add_compact_fuel_consumption_term!( container, W, expression, variable, d, var_cost, value_curve, base_power, device_base_power, dt, time_steps, ) end return end function add_to_expression!( container::OptimizationContainer, ::Type{T}, ::U, devices::IS.FlattenIteratorWrapper{V}, model::DeviceModel{V, W}, ) where { T <: NetActivePower, U <: Union{ActivePowerInVariable, ActivePowerOutVariable}, V <: PSY.Source, W <: AbstractSourceFormulation, } expression = get_expression(container, T(), V) variable = get_variable(container, U(), V) mult = get_variable_multiplier(U(), V, W()) for d in devices name = PSY.get_name(d) for t in get_time_steps(container) JuMP.add_to_expression!( expression[name, t], variable[name, t] * mult, ) end end return end ################################## ##### Cost Expression Setup ###### ################################## """ Adds all cost expression containers appropriate for the given device type and formulation in a single pass through the device list. The default method adds only `ProductionCostExpression`. The `PSY.ThermalGen` overload adds its full set of `ConstituentCostExpression` subtypes (Fuel, StartUp, ShutDown, Fixed, VOM), which automatically propagate into `ProductionCostExpression` via the `ConstituentCostExpression` dispatch on `add_to_expression!`. The `PSY.RenewableGen` overload adds `FixedCostExpression` and `VOMCostExpression` (which propagate the same way) plus `CurtailmentCostExpression` (a direct `CostExpressions` subtype, recorded as a per-device reporting expression and not propagated to `ProductionCostExpression`). """ function add_cost_expressions!( container::OptimizationContainer, devices::U, model::DeviceModel{D, W}, ) where {D <: PSY.Component, U, W <: AbstractDeviceFormulation} time_steps = get_time_steps(container) names = PSY.get_name.(devices) add_expression_container!(container, ProductionCostExpression(), D, names, time_steps) return end function add_cost_expressions!( container::OptimizationContainer, devices::U, model::DeviceModel{D, W}, ) where {D <: PSY.ThermalGen, U, W <: AbstractThermalFormulation} time_steps = get_time_steps(container) n_devices = length(devices) all_names = Vector{String}(undef, n_devices) fuel_names = sizehint!(String[], n_devices) has_quad_fuel = false for (i, d) in enumerate(devices) name = PSY.get_name(d) all_names[i] = name fuel_curve = _get_variable_if_exists(PSY.get_operation_cost(d)) if _is_fuel_curve(fuel_curve) push!(fuel_names, name) if !has_quad_fuel has_quad_fuel = _value_curve_is_quadratic(PSY.get_value_curve(fuel_curve)) end end end if !isempty(fuel_names) fuel_expr_type = has_quad_fuel ? JuMP.QuadExpr : GAE add_expression_container!( container, FuelConsumptionExpression(), D, fuel_names, time_steps; expr_type = fuel_expr_type, ) end add_expression_container!( container, ProductionCostExpression(), D, all_names, time_steps, ) add_expression_container!(container, FuelCostExpression(), D, all_names, time_steps) add_expression_container!(container, StartUpCostExpression(), D, all_names, time_steps) add_expression_container!(container, ShutDownCostExpression(), D, all_names, time_steps) add_expression_container!(container, FixedCostExpression(), D, all_names, time_steps) add_expression_container!(container, VOMCostExpression(), D, all_names, time_steps) return end function add_cost_expressions!( container::OptimizationContainer, devices::U, model::DeviceModel{D, W}, ) where {D <: PSY.RenewableGen, U, W <: AbstractRenewableDispatchFormulation} time_steps = get_time_steps(container) names = PSY.get_name.(devices) add_expression_container!(container, ProductionCostExpression(), D, names, time_steps) add_expression_container!(container, FixedCostExpression(), D, names, time_steps) add_expression_container!(container, CurtailmentCostExpression(), D, names, time_steps) add_expression_container!(container, VOMCostExpression(), D, names, time_steps) return end #= function add_to_expression!( container::OptimizationContainer, ::Type{T}, ::Type{U}, areas::IS.FlattenIteratorWrapper{V}, model::ServiceModel{PSY.AGC, W}, ) where { T <: Union{EmergencyUp, EmergencyDown}, U <: Union{AdditionalDeltaActivePowerUpVariable, AdditionalDeltaActivePowerDownVariable}, V <: PSY.Area, W <: AbstractServiceFormulation, } names = PSY.get_name.(areas) time_steps = get_time_steps(container) if !has_container_key(container, T, V) expression = add_expression_container!(container, T(), V, names, time_steps) end expression = get_expression(container, T(), V) variable = get_variable(container, U(), V) for n in names, t in time_steps _add_to_jump_expression!(expression[n, t], variable[n, t], 1.0) end return end function add_to_expression!( container::OptimizationContainer, ::Type{T}, ::Type{U}, services::IS.FlattenIteratorWrapper{V}, model::ServiceModel{V, W}, ) where { T <: RawACE, U <: SteadyStateFrequencyDeviation, V <: PSY.AGC, W <: AbstractServiceFormulation, } names = PSY.get_name.(services) time_steps = get_time_steps(container) if !has_container_key(container, T, V) expression = add_expression_container!(container, T(), PSY.AGC, names, time_steps) end expression = get_expression(container, T(), PSY.AGC) variable = get_variable(container, U(), PSY.AGC) for s in services, t in time_steps name = PSY.get_name(s) _add_to_jump_expression!( expression[name, t], variable[t], get_variable_multiplier(U(), s, W()), ) end return end =# ================================================ FILE: src/devices_models/devices/common/add_variable.jl ================================================ """ Add variables to the OptimizationContainer for any component. """ function add_variables!( container::OptimizationContainer, ::Type{T}, devices::Union{Vector{U}, IS.FlattenIteratorWrapper{U}}, formulation::Union{AbstractServiceFormulation, AbstractDeviceFormulation}, ) where {T <: VariableType, U <: PSY.Component} add_variable!(container, T(), devices, formulation) return end #= # TODO: Contingency. Is this needed? """ Add Contingency-related variables to the OptimizationContainer for any component. """ function add_variables!( container::OptimizationContainer, ::Type{T}, devices::Union{Vector{U}, IS.FlattenIteratorWrapper{U}}, devices_outages::Union{Vector{U}, IS.FlattenIteratorWrapper{U}}, formulation::Union{AbstractServiceFormulation, AbstractDeviceFormulation}, ) where {T <: VariableType, U <: PSY.Component} add_variable!(container, T(), devices, devices_outages, formulation) return end =# """ Add variables to the OptimizationContainer for a service. """ function add_variables!( container::OptimizationContainer, ::Type{T}, service::U, contributing_devices::Union{Vector{V}, IS.FlattenIteratorWrapper{V}}, formulation::AbstractReservesFormulation, ) where {T <: VariableType, U <: PSY.AbstractReserve, V <: PSY.Component} # PERF: compilation hotspot. Switch to TSC. add_service_variable!(container, T(), service, contributing_devices, formulation) return end @doc raw""" Adds a variable to the optimization model and to the affine expressions contained in the optimization_container model according to the specified sign. Based on the inputs, the variable can be specified as binary. # Bounds ``` lb_value_function <= varstart[name, t] <= ub_value_function ``` If binary = true: ``` varstart[name, t] in {0,1} ``` # LaTeX `` lb \ge x^{device}_t \le ub \forall t `` `` x^{device}_t \in {0,1} \forall t iff \text{binary = true}`` # Arguments * container::OptimizationContainer : the optimization_container model built in PowerSimulations * devices : Vector or Iterator with the devices * var_key::VariableKey : Base Name for the variable * binary::Bool : Select if the variable is binary * expression_name::Symbol : Expression_name name stored in container.expressions to add the variable * sign::Float64 : sign of the addition of the variable to the expression_name. Default Value is 1.0 # Accepted Keyword Arguments * ub_value : Provides the function over device to obtain the value for a upper_bound * lb_value : Provides the function over device to obtain the value for a lower_bound. If the variable is meant to be positive define lb = x -> 0.0 * initial_value : Provides the function over device to obtain the warm start value """ function add_variable!( container::OptimizationContainer, variable_type::T, devices::U, formulation, ) where { T <: VariableType, U <: Union{Vector{D}, IS.FlattenIteratorWrapper{D}}, } where {D <: PSY.Component} @assert !isempty(devices) time_steps = get_time_steps(container) settings = get_settings(container) binary = get_variable_binary(variable_type, D, formulation) variable = add_variable_container!( container, variable_type, D, PSY.get_name.(devices), time_steps, ) for t in time_steps, d in devices name = PSY.get_name(d) variable[name, t] = JuMP.@variable( get_jump_model(container), base_name = "$(T)_$(D)_{$(name), $(t)}", binary = binary ) ub = get_variable_upper_bound(variable_type, d, formulation) ub !== nothing && JuMP.set_upper_bound(variable[name, t], ub) lb = get_variable_lower_bound(variable_type, d, formulation) lb !== nothing && JuMP.set_lower_bound(variable[name, t], lb) if get_warm_start(settings) init = get_variable_warm_start_value(variable_type, d, formulation) init !== nothing && JuMP.set_start_value(variable[name, t], init) end end return end function add_service_variable!( container::OptimizationContainer, variable_type::T, service::U, contributing_devices::V, formulation::AbstractServiceFormulation, ) where { T <: VariableType, U <: PSY.Service, V <: Union{Vector{D}, IS.FlattenIteratorWrapper{D}}, } where {D <: PSY.Component} @assert !isempty(contributing_devices) time_steps = get_time_steps(container) binary = get_variable_binary(variable_type, U, formulation) variable = add_variable_container!( container, variable_type, U, PSY.get_name(service), [PSY.get_name(d) for d in contributing_devices], time_steps, ) for t in time_steps, d in contributing_devices name = PSY.get_name(d) variable[name, t] = JuMP.@variable( get_jump_model(container), base_name = "$(T)_$(U)_$(PSY.get_name(service))_{$(name), $(t)}", binary = binary ) ub = get_variable_upper_bound(variable_type, service, d, formulation) ub !== nothing && JuMP.set_upper_bound(variable[name, t], ub) lb = get_variable_lower_bound(variable_type, service, d, formulation) lb !== nothing && !binary && JuMP.set_lower_bound(variable[name, t], lb) init = get_variable_warm_start_value(variable_type, d, formulation) init !== nothing && JuMP.set_start_value(variable[name, t], init) end return end ================================================ FILE: src/devices_models/devices/common/duration_constraints.jl ================================================ @doc raw""" This formulation of the duration constraints adds over the start times looking backwards. # LaTeX * Minimum up-time constraint: If ``t \leq d_{min}^{up} - d_{init}^{up}`` and ``d_{init}^{up} > 0`` `` 1 + \sum_{i=t-d_{min}^{up} + 1}^t x_i^{start} - x_t^{on} \leq 0 `` for i in the set of time steps. Otherwise: `` \sum_{i=t-d_{min}^{up} + 1}^t x_i^{start} - x_t^{on} \leq 0 `` for i in the set of time steps. * Minimum down-time constraint: If ``t \leq d_{min}^{down} - d_{init}^{down}`` and ``d_{init}^{down} > 0`` `` 1 + \sum_{i=t-d_{min}^{down} + 1}^t x_i^{stop} + x_t^{on} \leq 1 `` for i in the set of time steps. Otherwise: `` \sum_{i=t-d_{min}^{down} + 1}^t x_i^{stop} + x_t^{on} \leq 1 `` for i in the set of time steps. # Arguments * container::OptimizationContainer : the optimization_container model built in PowerSimulations * duration_data::Vector{UpDown} : gives how many time steps variable needs to be up or down * initial_duration::Matrix{InitialCondition} : gives initial conditions for up (column 1) and down (column 2) * cons_name::Symbol : name of the constraint * var_keys::Tuple{VariableKey, VariableKey, VariableKey}) : names of the variables - : var_keys[1] : varon - : var_keys[2] : varstart - : var_keys[3] : varstop """ function device_duration_retrospective!( container::OptimizationContainer, duration_data::Vector{UpDown}, initial_duration::Matrix{InitialCondition}, cons_type::ConstraintType, var_types::Tuple{VariableType, VariableType, VariableType}, ::Type{T}, ) where {T <: PSY.Component} time_steps = get_time_steps(container) varon = get_variable(container, var_types[1], T) varstart = get_variable(container, var_types[2], T) varstop = get_variable(container, var_types[3], T) device_name_sets = [ get_component_name(ic) for ic in initial_duration[:, 1] if !isnothing(get_value(ic)) ] con_up = add_constraints_container!( container, cons_type, T, device_name_sets, time_steps; meta = "up", ) con_down = add_constraints_container!( container, cons_type, T, device_name_sets, time_steps; meta = "dn", ) for t in time_steps for (ix, ic) in enumerate(initial_duration[:, 1]) isnothing(get_value(ic)) && continue name = get_component_name(ic) # Minimum Up-time Constraint lhs_on = JuMP.GenericAffExpr{Float64, JuMP.VariableRef}(0) for i in UnitRange{Int}(Int(t - duration_data[ix].up + 1), t) if i in time_steps JuMP.add_to_expression!(lhs_on, varstart[name, i]) end end if t <= max(0, duration_data[ix].up - get_value(ic)) && get_value(ic) > 0 JuMP.add_to_expression!(lhs_on, 1) end con_up[name, t] = JuMP.@constraint(get_jump_model(container), lhs_on - varon[name, t] <= 0.0) end for (ix, ic) in enumerate(initial_duration[:, 2]) isnothing(get_value(ic)) && continue name = get_component_name(ic) # Minimum Down-time Constraint lhs_off = JuMP.GenericAffExpr{Float64, JuMP.VariableRef}(0) for i in UnitRange{Int}(Int(t - duration_data[ix].down + 1), t) if i in time_steps JuMP.add_to_expression!(lhs_off, varstop[name, i]) end end if t <= max(0, duration_data[ix].down - get_value(ic)) && get_value(ic) > 0 JuMP.add_to_expression!(lhs_off, 1) end con_down[name, t] = JuMP.@constraint(get_jump_model(container), lhs_off + varon[name, t] <= 1.0) end end return end @doc raw""" This formulation of the duration constraints looks ahead in the time frame of the model. # LaTeX * Minimum up-time constraint: If ``t \leq d_{min}^{up}`` `` d_{min}^{down}x_t^{stop} - \sum_{i=t-d_{min}^{up} + 1}^t x_i^{on} - x_{init}^{up} \leq 0 `` for i in the set of time steps. Otherwise: `` d_{min}^{down}x_t^{stop} - \sum_{i=t-d_{min}^{up} + 1}^t x_i^{on} \leq 0 `` for i in the set of time steps. * Minimum down-time constraint: If ``t \leq d_{min}^{down}`` `` d_{min}^{up}x_t^{start} - \sum_{i=t-d_{min^{down} + 1}^t (1 - x_i^{on}) - x_{init}^{down} \leq 0 `` for i in the set of time steps. Otherwise: `` d_{min}^{up}x_t^{start} - \sum_{i=t-d_{min^{down} + 1}^t (1 - x_i^{on}) \leq 0 `` for i in the set of time steps. # Arguments * container::OptimizationContainer : the optimization_container model built in PowerSimulations * duration_data::Vector{UpDown} : gives how many time steps variable needs to be up or down * initial_duration::Matrix{InitialCondition} : gives initial conditions for up (column 1) and down (column 2) * cons_name::Symbol : name of the constraint * var_keys::Tuple{VariableKey, VariableKey, VariableKey}) : names of the variables - : var_keys[1] : varon - : var_keys[2] : varstart - : var_keys[3] : varstop """ function device_duration_look_ahead!( container::OptimizationContainer, duration_data::Vector{UpDown}, initial_duration::Matrix{InitialCondition}, cons_type_up::ConstraintType, cons_type_down::ConstraintType, var_types::Tuple{VariableType, VariableType, VariableType}, ::Type{T}, ) where {T <: PSY.Component} time_steps = get_time_steps(container) varon = get_variable(container, var_types[1], T) varstart = get_variable(container, var_types[2], T) varstop = get_variable(container, var_types[3], T) device_name_sets = [get_component_name(ic) for ic in initial_duration[:, 1]] con_up = add_constraints_container!(container, cons_type_up, device_name_sets, time_steps) con_down = add_constraints_container!(container, cons_type_down, device_name_sets, time_steps) for t in time_steps for (ix, ic) in enumerate(initial_duration[:, 1]) isnothing(get_value(ic)) && continue name = get_component_name(ic) # Minimum Up-time Constraint lhs_on = JuMP.GenericAffExpr{Float64, JuMP.VariableRef}(0) for i in UnitRange{Int}(Int(t - duration_data[ix].up + 1), t) if i in time_steps JuMP.add_to_expression!(lhs_on, varon[name, i]) end end if t <= duration_data[ix].up JuMP.add_to_expression!(lhs_on, get_value(ic)) end con_up[name, t] = JuMP.@constraint( get_jump_model(container), varstop[name, t] * duration_data[ix].up - lhs_on <= 0.0 ) end for (ix, ic) in enumerate(initial_duration[:, 2]) isnothing(get_value(ic)) && continue name = get_component_name(ic) # Minimum Down-time Constraint lhs_off = JuMP.GenericAffExpr{Float64, JuMP.VariableRef}(0) for i in UnitRange{Int}(Int(t - duration_data[ix].down + 1), t) if i in time_steps JuMP.add_to_expression!(lhs_off, 1) JuMP.add_to_expression!(lhs_off, -1, varon[name, i]) end end if t <= duration_data[ix].down JuMP.add_to_expression!(lhs_off, get_value(ic)) end con_down[name, t] = JuMP.@constraint( get_jump_model(container), varstart[name, t] * duration_data[ix].down - lhs_off <= 0.0 ) end end return end @doc raw""" This formulation of the duration constraints considers parameters. # LaTeX * Minimum up-time constraint: If ``t \leq d_{min}^{up}`` `` d_{min}^{down}x_t^{stop} - \sum_{i=t-d_{min}^{up} + 1}^t x_i^{on} - x_{init}^{up} \leq 0 `` for i in the set of time steps. Otherwise: `` \sum_{i=t-d_{min}^{up} + 1}^t x_i^{start} - x_t^{on} \leq 0 `` for i in the set of time steps. * Minimum down-time constraint: If ``t \leq d_{min}^{down}`` `` d_{min}^{up}x_t^{start} - \sum_{i=t-d_{min^{down} + 1}^t (1 - x_i^{on}) - x_{init}^{down} \leq 0 `` for i in the set of time steps. Otherwise: `` \sum_{i=t-d_{min}^{down} + 1}^t x_i^{stop} + x_t^{on} \leq 1 `` for i in the set of time steps. # Arguments * container::OptimizationContainer : the optimization_container model built in PowerSimulations * duration_data::Vector{UpDown} : gives how many time steps variable needs to be up or down * initial_duration_on::Vector{InitialCondition} : gives initial number of time steps variable is up * initial_duration_off::Vector{InitialCondition} : gives initial number of time steps variable is down * cons_name::Symbol : name of the constraint * var_keys::Tuple{VariableKey, VariableKey, VariableKey}) : names of the variables - : var_keys[1] : varon - : var_keys[2] : varstart - : var_keys[3] : varstop """ function device_duration_parameters!( container::OptimizationContainer, duration_data::Vector{UpDown}, initial_duration::Matrix{InitialCondition}, cons_type::ConstraintType, var_types::Tuple{VariableType, VariableType, VariableType}, ::Type{T}, ) where {T <: PSY.Component} time_steps = get_time_steps(container) varon = get_variable(container, var_types[1], T) varstart = get_variable(container, var_types[2], T) varstop = get_variable(container, var_types[3], T) device_name_sets = [get_component_name(ic) for ic in initial_duration[:, 1]] con_up = add_constraints_container!( container, cons_type, T, device_name_sets, time_steps; meta = "up", ) con_down = add_constraints_container!( container, cons_type, T, device_name_sets, time_steps; meta = "dn", ) for t in time_steps for (ix, ic) in enumerate(initial_duration[:, 1]) isnothing(get_value(ic)) && continue name = get_component_name(ic) # Minimum Up-time Constraint lhs_on = JuMP.GenericAffExpr{Float64, JuMP.VariableRef}(0) for i in UnitRange{Int}(Int(t - duration_data[ix].up + 1), t) if t <= duration_data[ix].up if in(i, time_steps) JuMP.add_to_expression!(lhs_on, varon[name, i]) end else JuMP.add_to_expression!(lhs_on, varstart[name, i]) end end if t <= duration_data[ix].up JuMP.add_to_expression!(lhs_on, get_value(ic)) con_up[name, t] = JuMP.@constraint( get_jump_model(container), varstop[name, t] * duration_data[ix].up - lhs_on <= 0.0 ) else con_up[name, t] = JuMP.@constraint( get_jump_model(container), lhs_on - varon[name, t] <= 0.0 ) end end for (ix, ic) in enumerate(initial_duration[:, 2]) isnothing(get_value(ic)) && continue name = get_component_name(ic) # Minimum Down-time Constraint lhs_off = JuMP.GenericAffExpr{Float64, JuMP.VariableRef}(0) for i in UnitRange{Int}(Int(t - duration_data[ix].down + 1), t) if t <= duration_data[ix].down if in(i, time_steps) JuMP.add_to_expression!(lhs_off, 1) JuMP.add_to_expression!(lhs_off, -1, varon[name, i]) end else JuMP.add_to_expression!(lhs_off, varstop[name, i]) end end if t <= duration_data[ix].down JuMP.add_to_expression!(lhs_off, get_value(ic)) con_down[name, t] = JuMP.@constraint( get_jump_model(container), varstart[name, t] * duration_data[ix].down - lhs_off <= 0.0 ) else con_down[name, t] = JuMP.@constraint( get_jump_model(container), lhs_off + varon[name, t] <= 1.0 ) end end end return end @doc raw""" This formulation of the duration constraints adds over the start times looking backwards. # LaTeX * Minimum up-time constraint: `` \sum_{i=t-min(d_{min}^{up}, T)+ 1}^t x_i^{start} - x_t^{on} \leq 0 `` for i in the set of time steps. * Minimum down-time constraint: `` \sum_{i=t-min(d_{min}^{down}, T) + 1}^t x_i^{stop} + x_t^{on} \leq 1 `` for i in the set of time steps. # Arguments * container::OptimizationContainer : the optimization_container model built in PowerSimulations * duration_data::Vector{UpDown} : gives how many time steps variable needs to be up or down * initial_duration::Matrix{InitialCondition} : gives initial conditions for up (column 1) and down (column 2) * cons_name::Symbol : name of the constraint * var_keys::Tuple{VariableKey, VariableKey, VariableKey}) : names of the variables - : var_keys[1] : varon - : var_keys[2] : varstart - : var_keys[3] : varstop """ function device_duration_compact_retrospective!( container::OptimizationContainer, duration_data::Vector{UpDown}, initial_duration::Matrix{InitialCondition}, cons_type::ConstraintType, var_types::Tuple{VariableType, VariableType, VariableType}, ::Type{T}, ) where {T <: PSY.Component} time_steps = get_time_steps(container) varon = get_variable(container, var_types[1], T) varstart = get_variable(container, var_types[2], T) varstop = get_variable(container, var_types[3], T) device_name_sets = [get_component_name(ic) for ic in initial_duration[:, 1]] con_up = add_constraints_container!( container, cons_type, T, device_name_sets, time_steps; meta = "up", sparse = true, ) con_down = add_constraints_container!( container, cons_type, T, device_name_sets, time_steps; meta = "dn", sparse = true, ) total_time_steps = length(time_steps) for t in time_steps for (ix, ic) in enumerate(initial_duration[:, 1]) isnothing(get_value(ic)) && continue name = get_component_name(ic) # Minimum Up-time Constraint lhs_on = JuMP.GenericAffExpr{Float64, JuMP.VariableRef}(0) if t in UnitRange{Int}( Int(min(duration_data[ix].up, total_time_steps)), total_time_steps, ) for i in UnitRange{Int}(Int(t - duration_data[ix].up + 1), t) if i in time_steps JuMP.add_to_expression!(lhs_on, varstart[name, i]) end end elseif t <= max(0, duration_data[ix].up - get_value(ic)) && get_value(ic) > 0 JuMP.add_to_expression!(lhs_on, 1) else continue end con_up[name, t] = JuMP.@constraint(get_jump_model(container), lhs_on - varon[name, t] <= 0.0) end for (ix, ic) in enumerate(initial_duration[:, 2]) isnothing(get_value(ic)) && continue name = get_component_name(ic) # Minimum Down-time Constraint lhs_off = JuMP.GenericAffExpr{Float64, JuMP.VariableRef}(0) if t in UnitRange{Int}( Int(min(duration_data[ix].down, total_time_steps)), total_time_steps, ) for i in UnitRange{Int}(Int(t - duration_data[ix].down + 1), t) if i in time_steps JuMP.add_to_expression!(lhs_off, varstop[name, i]) end end elseif t <= max(0, duration_data[ix].down - get_value(ic)) && get_value(ic) > 0 JuMP.add_to_expression!(lhs_off, 1) else continue end con_down[name, t] = JuMP.@constraint(get_jump_model(container), lhs_off + varon[name, t] <= 1.0) end end for c in [con_up, con_down] # Workaround to remove invalid key combinations filter!(x -> x.second !== nothing, c.data) end return end ================================================ FILE: src/devices_models/devices/common/get_time_series.jl ================================================ function _get_time_series( container::OptimizationContainer, component::PSY.Component, attributes::TimeSeriesAttributes{T}; interval::Dates.Millisecond = UNSET_INTERVAL, ) where {T <: PSY.TimeSeriesData} return get_time_series_initial_values!( container, T, component, get_time_series_name(attributes); interval = interval, ) end function get_time_series( container::OptimizationContainer, component::T, parameter::TimeSeriesParameter, meta = ISOPT.CONTAINER_KEY_EMPTY_META; interval::Dates.Millisecond = UNSET_INTERVAL, ) where {T <: PSY.Component} parameter_container = get_parameter(container, parameter, T, meta) return _get_time_series( container, component, parameter_container.attributes; interval = interval, ) end # This is just for temporary compatibility with current code. Needs to be eliminated once the time series # refactor is done. function get_time_series( container::OptimizationContainer, component::PSY.Component, forecast_name::String; interval::Dates.Millisecond = UNSET_INTERVAL, ) ts_type = get_default_time_series_type(container) return _get_time_series( container, component, TimeSeriesAttributes(ts_type, forecast_name); interval = interval, ) end ================================================ FILE: src/devices_models/devices/common/objective_function/common.jl ================================================ ################################## ######## Helper Functions ######## ################################## get_output_offer_curves(cost::PSY.ImportExportCost, args...; kwargs...) = PSY.get_import_offer_curves(cost, args...; kwargs...) get_output_offer_curves(cost::PSY.MarketBidCost, args...; kwargs...) = PSY.get_incremental_offer_curves(cost, args...; kwargs...) get_input_offer_curves(cost::PSY.ImportExportCost, args...; kwargs...) = PSY.get_export_offer_curves(cost, args...; kwargs...) get_input_offer_curves(cost::PSY.MarketBidCost, args...; kwargs...) = PSY.get_decremental_offer_curves(cost, args...; kwargs...) # TODO deduplicate, these signatures are getting out of hand get_output_offer_curves( component::PSY.Component, cost::PSY.ImportExportCost, args...; kwargs..., ) = PSY.get_import_offer_curves(component, cost, args...; kwargs...) get_output_offer_curves( component::PSY.Component, cost::PSY.MarketBidCost, args...; kwargs..., ) = PSY.get_incremental_offer_curves(component, cost, args...; kwargs...) get_input_offer_curves( component::PSY.Component, cost::PSY.ImportExportCost, args...; kwargs..., ) = PSY.get_export_offer_curves(component, cost, args...; kwargs...) get_input_offer_curves( component::PSY.Component, cost::PSY.MarketBidCost, args...; kwargs..., ) = PSY.get_decremental_offer_curves(component, cost, args...; kwargs...) """ Either looks up a value in the component using `getter_func` or fetches the value from the parameter `U()`, depending on whether we are in the time-variant case or not """ function _lookup_maybe_time_variant_param( ::OptimizationContainer, component::T, ::Int, ::Val{false}, # not time variant getter_func::F, ::U, ) where {T <: PSY.Component, F <: Function, U <: ParameterType} return getter_func(component) end function _lookup_maybe_time_variant_param( container::OptimizationContainer, component::T, time_period::Int, ::Val{true}, # yes time variant ::F, ::U, ) where {T <: PSY.Component, F <: Function, U <: ParameterType} # PERF this is modeled on the old get_fuel_cost_value function, but is it really # performant to be fetching the whole array and multiplier array anew for every time step? parameter_array = get_parameter_array(container, U(), T) parameter_multiplier = get_parameter_multiplier_array(container, U(), T) name = PSY.get_name(component) return parameter_array[name, time_period] .* parameter_multiplier[name, time_period] end ################################## #### 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()) _add_vom_cost_to_objective!(container, U(), d, op_cost_data, V()) end return end ################################## #### Curtailment Cost ############ ################################## function add_curtailment_cost!( container::OptimizationContainer, ::U, devices::IS.FlattenIteratorWrapper{T}, ::V, ) where {T <: PSY.RenewableGen, U <: VariableType, V <: AbstractDeviceFormulation} for d in devices op_cost_data = PSY.get_operation_cost(d) !hasproperty(op_cost_data, :curtailment_cost) && continue cost_function = PSY.get_curtailment_cost(op_cost_data) isnothing(cost_function) && continue _add_curtailment_cost!(container, U(), d, cost_function, V()) end return end ################################## #### Start/Stop Variable Cost #### ################################## get_shutdown_cost_value( container::OptimizationContainer, component::PSY.Component, time_period::Int, is_time_variant_::Bool, ) = _lookup_maybe_time_variant_param( container, component, time_period, Val(is_time_variant_), PSY.get_shut_down ∘ PSY.get_operation_cost, ShutdownCostParameter(), ) 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 PSY.get_must_run(d) && continue add_as_time_variant = is_time_variant(PSY.get_shut_down(PSY.get_operation_cost(d))) for t in get_time_steps(container) my_cost_term = get_shutdown_cost_value( container, d, t, add_as_time_variant, ) iszero(my_cost_term) && continue exp = _add_proportional_term_maybe_variant!( Val(add_as_time_variant), container, U(), d, my_cost_term * multiplier, t) add_to_expression!(container, ShutDownCostExpression, exp, d, 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} # NOTE: anything time-varying should implement its own method. 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, FixedCostExpression, exp, d, t) end end return end ################################## ########## VOM Cost ############## ################################## function _add_vom_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()) power_units = PSY.get_power_units(variable_cost_data) vom_cost = PSY.get_vom_cost(variable_cost_data) multiplier = 1.0 # VOM Cost is always positive cost_term = PSY.get_proportional_term(vom_cost) iszero(cost_term) && return base_power = get_base_power(container) device_base_power = PSY.get_base_power(component) cost_term_normalized = get_proportional_cost_per_system_unit( cost_term, power_units, base_power, device_base_power, ) resolution = get_resolution(container) dt = Dates.value(resolution) / MILLISECONDS_IN_HOUR for t in get_time_steps(container) exp = _add_proportional_term!( container, T(), component, cost_term_normalized * multiplier * dt, t, ) add_to_expression!(container, VOMCostExpression, exp, component, t) end return end ################################## ######## OnVariable Cost ######### ################################## function add_proportional_cost!( container::OptimizationContainer, ::U, devices::IS.FlattenIteratorWrapper{T}, ::V, ) where {T <: PSY.ThermalGen, U <: OnVariable, V <: AbstractThermalUnitCommitment} multiplier = objective_function_multiplier(U(), V()) for d in devices op_cost_data = PSY.get_operation_cost(d) for t in get_time_steps(container) cost_term = proportional_cost(container, op_cost_data, U(), d, V(), t) add_as_time_variant = is_time_variant_term(container, op_cost_data, U(), d, V(), t) iszero(cost_term) && continue cost_term *= multiplier exp = if PSY.get_must_run(d) cost_term # note we do not add this to the objective function else _add_proportional_term_maybe_variant!( Val(add_as_time_variant), container, U(), d, cost_term, t) end add_to_expression!(container, FixedCostExpression, exp, d, t) end end return end # code repetition: same as above, just change types and remove must_run check. function add_proportional_cost!( container::OptimizationContainer, ::U, devices::IS.FlattenIteratorWrapper{T}, ::PowerLoadInterruption, ) where {T <: PSY.ControllableLoad, U <: OnVariable} multiplier = objective_function_multiplier(U(), PowerLoadInterruption()) for d in devices op_cost_data = PSY.get_operation_cost(d) for t in get_time_steps(container) cost_term = proportional_cost( container, op_cost_data, U(), d, PowerLoadInterruption(), t, ) add_as_time_variant = is_time_variant_term( container, op_cost_data, U(), d, PowerLoadInterruption(), t, ) iszero(cost_term) && continue cost_term *= multiplier exp = _add_proportional_term_maybe_variant!( Val(add_as_time_variant), container, U(), d, cost_term, t) add_to_expression!(container, FixedCostExpression, 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 get_startup_cost_value( container::OptimizationContainer, ::T, component::V, ::U, time_period::Int, is_time_variant_::Bool, ) where {T <: VariableType, V <: PSY.Component, U <: AbstractDeviceFormulation} raw_startup_cost = _lookup_maybe_time_variant_param( container, component, time_period, Val(is_time_variant_), PSY.get_start_up ∘ PSY.get_operation_cost, StartupCostParameter(), ) return start_up_cost(raw_startup_cost, component, T(), U()) 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} multiplier = objective_function_multiplier(T(), U()) PSY.get_must_run(component) && return add_as_time_variant = is_time_variant(PSY.get_start_up(op_cost)) for t in get_time_steps(container) my_cost_term = get_startup_cost_value( container, T(), component, U(), t, add_as_time_variant, ) iszero(my_cost_term) && continue exp = _add_proportional_term_maybe_variant!( Val(add_as_time_variant), container, T(), component, my_cost_term * multiplier, t) add_to_expression!(container, StartUpCostExpression, exp, component, 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_helper( 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 return lin_cost end # Invariant function _add_proportional_term!( container::OptimizationContainer, ::T, component::U, linear_term::Float64, time_period::Int, ) where {T <: VariableType, U <: PSY.Component} lin_cost = _add_proportional_term_helper( container, T(), component, linear_term, time_period) add_to_objective_invariant_expression!(container, lin_cost) return lin_cost end # Variant function _add_proportional_term_variant!( container::OptimizationContainer, ::T, component::U, linear_term::Float64, time_period::Int, ) where {T <: VariableType, U <: PSY.Component} lin_cost = _add_proportional_term_helper( container, T(), component, linear_term, time_period) add_to_objective_variant_expression!(container, lin_cost) return lin_cost end # Maybe variant _add_proportional_term_maybe_variant!( ::Val{false}, container::OptimizationContainer, ::T, component::U, linear_term::Float64, time_period::Int, ) where {T <: VariableType, U <: PSY.Component} = _add_proportional_term!(container, T(), component, linear_term, time_period) _add_proportional_term_maybe_variant!( ::Val{true}, container::OptimizationContainer, ::T, component::U, linear_term::Float64, time_period::Int, ) where {T <: VariableType, U <: PSY.Component} = _add_proportional_term_variant!(container, T(), component, linear_term, time_period) 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 ##################### ################################################## get_fuel_cost_value( container::OptimizationContainer, component::PSY.Component, time_period::Int, is_time_variant_::Bool, ) = _lookup_maybe_time_variant_param( container, component, time_period, Val(is_time_variant_), PSY.get_fuel_cost, FuelCostParameter(), ) function _add_time_varying_fuel_variable_cost!( container::OptimizationContainer, ::T, component::V, fuel_cost::IS.TimeSeriesKey, ) where {T <: VariableType, V <: PSY.Component} parameter = get_parameter_array(container, FuelCostParameter(), V) multiplier = get_parameter_multiplier_array(container, FuelCostParameter(), V) expression = get_expression(container, FuelConsumptionExpression(), V) name = PSY.get_name(component) for t in get_time_steps(container) cost_expr = expression[name, t] * parameter[name, t] * multiplier[name, t] add_to_expression!( container, FuelCostExpression, cost_expr, component, t, ) add_to_objective_variant_expression!(container, cost_expr) end return end # Used for dispatch (on/off decision) for devices where operation_cost::Union{MarketBidCost, FooCost} # currently: ThermalGen, ControllableLoad subtypes. function _onvar_cost(::PSY.CostCurve{PSY.PiecewisePointCurve}) # OnVariableCost is included in the Point itself for PiecewisePointCurve return 0.0 end function _onvar_cost( cost_function::Union{PSY.CostCurve{PSY.LinearCurve}, PSY.CostCurve{PSY.QuadraticCurve}}, ) 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 _onvar_cost(::PSY.CostCurve{PSY.PiecewiseIncrementalCurve}) # Input at min is used to transform to InputOutputCurve return 0.0 end function _onvar_cost(::PSY.CostCurve{PSY.PiecewiseAverageCurve}) # Input at min is used to transform to InputOutputCurve return 0.0 end function _onvar_cost( ::OptimizationContainer, cost_function::PSY.CostCurve{T}, ::PSY.Component, ::Int, ) where {T <: IS.ValueCurve} return _onvar_cost(cost_function) end ================================================ FILE: src/devices_models/devices/common/objective_function/import_export.jl ================================================ _include_min_gen_power_in_constraint( ::PSY.Source, ::ActivePowerOutVariable, ::AbstractDeviceFormulation, ) = false _include_min_gen_power_in_constraint( ::PSY.Source, ::ActivePowerInVariable, ::AbstractDeviceFormulation, ) = false function _add_variable_cost_to_objective!( container::OptimizationContainer, ::T, component::PSY.Source, cost_function::PSY.ImportExportCost, ::U, ) where { T <: ActivePowerOutVariable, U <: AbstractSourceFormulation, } component_name = PSY.get_name(component) @debug "Import Export Cost" _group = PSI.LOG_GROUP_COST_FUNCTIONS component_name import_cost_curves = PSY.get_import_offer_curves(cost_function) if !isnothing(import_cost_curves) add_pwl_term!( false, container, component, cost_function, T(), U(), ) end return end function _add_variable_cost_to_objective!( container::OptimizationContainer, ::T, component::PSY.Source, cost_function::PSY.ImportExportCost, ::U, ) where { T <: ActivePowerInVariable, U <: AbstractSourceFormulation, } component_name = PSY.get_name(component) @debug "Import Export Cost" _group = PSI.LOG_GROUP_COST_FUNCTIONS component_name export_cost_curves = PSY.get_export_offer_curves(cost_function) if !isnothing(export_cost_curves) add_pwl_term!( true, container, component, cost_function, T(), U(), ) end return end # _process_occ_parameters_helper and the helpers it depends on (even purely IEC ones) are in objective_function/market_bid.jl function process_import_export_parameters!( container::OptimizationContainer, devices_in, model::DeviceModel, ) devices = filter(_has_import_export_cost, collect(devices_in)) for param in ( IncrementalPiecewiseLinearSlopeParameter(), IncrementalPiecewiseLinearBreakpointParameter(), DecrementalPiecewiseLinearSlopeParameter(), DecrementalPiecewiseLinearBreakpointParameter(), ) # Validate and add the parameters _process_occ_parameters_helper(param, container, model, devices) end end ================================================ FILE: src/devices_models/devices/common/objective_function/linear_curve.jl ================================================ # 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, FuelCostExpression, 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::V, ::Float64, # already normalized in MMBTU/p.u. fuel_cost::IS.TimeSeriesKey, ) where {T <: VariableType, V <: PSY.Component} _add_time_varying_fuel_variable_cost!(container, T(), component, fuel_cost) 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.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 function _add_curtailment_cost!( container::OptimizationContainer, ::T, component::PSY.RenewableDispatch, 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) iszero(proportional_term) && return proportional_term_per_unit = get_proportional_cost_per_system_unit( proportional_term, power_units, base_power, device_base_power, ) resolution = get_resolution(container) dt = Dates.value(resolution) / MILLISECONDS_IN_HOUR name = PSY.get_name(component) dispatch_vars = get_variable(container, T(), PSY.RenewableDispatch) for t in get_time_steps(container) breakpoints, _ = _get_pwl_data(false, container, component, t) offer_max = breakpoints[end] dispatch = dispatch_vars[name, t] curtailment_cost = proportional_term_per_unit * dt * (offer_max - dispatch) add_to_expression!( container, CurtailmentCostExpression, curtailment_cost, component, t, ) end return end function _add_curtailment_cost!( container::OptimizationContainer, ::T, component::PSY.RenewableGen, 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) iszero(proportional_term) && return proportional_term_per_unit = get_proportional_cost_per_system_unit( proportional_term, power_units, base_power, device_base_power, ) resolution = get_resolution(container) dt = Dates.value(resolution) / MILLISECONDS_IN_HOUR name = PSY.get_name(component) dispatch_vars = get_variable(container, T(), PSY.RenewableGen) for t in get_time_steps(container) breakpoints, _ = _get_pwl_data(false, container, component, t) offer_max = breakpoints[end] dispatch = dispatch_vars[name, t] curtailment_cost = proportional_term_per_unit * dt * (offer_max - dispatch) add_to_expression!( container, CurtailmentCostExpression, curtailment_cost, component, t, ) end return end ================================================ FILE: src/devices_models/devices/common/objective_function/market_bid.jl ================================================ ################################################## ################ PWL Parameters ################# ################################################## # Helper functions to manage incremental (false) vs. decremental (true) cases get_initial_input_maybe_decremental(::Val{true}, device::PSY.StaticInjection) = PSY.get_decremental_initial_input(PSY.get_operation_cost(device)) get_initial_input_maybe_decremental(::Val{false}, device::PSY.StaticInjection) = PSY.get_incremental_initial_input(PSY.get_operation_cost(device)) get_offer_curves_maybe_decremental(::Val{true}, device::PSY.StaticInjection) = get_input_offer_curves(PSY.get_operation_cost(device)) get_offer_curves_maybe_decremental(::Val{false}, device::PSY.StaticInjection) = get_output_offer_curves(PSY.get_operation_cost(device)) # Dictionaries to handle more incremental (false) vs. decremental (true) cases const _SLOPE_PARAMS::Dict{Bool, Type{<:AbstractPiecewiseLinearSlopeParameter}} = Dict( false => IncrementalPiecewiseLinearSlopeParameter, true => DecrementalPiecewiseLinearSlopeParameter) const _BREAKPOINT_PARAMS::Dict{Bool, Type{<:AbstractPiecewiseLinearBreakpointParameter}} = Dict( false => IncrementalPiecewiseLinearBreakpointParameter, true => DecrementalPiecewiseLinearBreakpointParameter) const _PIECEWISE_BLOCK_VARS::Dict{Bool, Type{<:AbstractPiecewiseLinearBlockOffer}} = Dict( false => PiecewiseLinearBlockIncrementalOffer, true => PiecewiseLinearBlockDecrementalOffer) const _PIECEWISE_BLOCK_CONSTRAINTS::Dict{ Bool, Type{<:AbstractPiecewiseLinearBlockOfferConstraint}, } = Dict( false => PiecewiseLinearBlockIncrementalOfferConstraint, true => PiecewiseLinearBlockDecrementalOfferConstraint) # Determines whether we care about various types of costs, given the formulation # NOTE: currently works based on what has already been added to the container; # alternatively we could dispatch on the formulation directly _consider_parameter( ::StartupCostParameter, container::OptimizationContainer, ::DeviceModel{T, D}, ) where {T, D} = any(has_container_key.([container], [StartVariable, MULTI_START_VARIABLES...], [T])) _consider_parameter( ::ShutdownCostParameter, container::OptimizationContainer, ::DeviceModel{T, D}, ) where {T, D} = has_container_key(container, StopVariable, T) # FIXME storage doesn't currently have an OnVariable. should it have one? _consider_parameter( ::AbstractCostAtMinParameter, container::OptimizationContainer, ::DeviceModel{T, D}, ) where {T, D} = has_container_key(container, OnVariable, T) # For slopes and breakpoints, the relevant variables won't have been created yet, so we'll # just check all components for the presence of the relevant time series _consider_parameter( ::AbstractPiecewiseLinearSlopeParameter, ::OptimizationContainer, ::DeviceModel{T, D}, ) where {T, D} = true _consider_parameter( ::AbstractPiecewiseLinearBreakpointParameter, ::OptimizationContainer, ::DeviceModel{T, D}, ) where {T, D} = true _has_market_bid_cost(device::PSY.StaticInjection) = PSY.get_operation_cost(device) isa PSY.MarketBidCost _has_market_bid_cost(::PSY.RenewableNonDispatch) = false _has_market_bid_cost(::PSY.PowerLoad) = false # PowerLoads don't even have operation cost. _has_market_bid_cost(device::PSY.ControllableLoad) = PSY.get_operation_cost(device) isa PSY.MarketBidCost _has_import_export_cost(device::PSY.Source) = PSY.get_operation_cost(device) isa PSY.ImportExportCost _has_import_export_cost(::PSY.StaticInjection) = false _has_offer_curve_cost(device::PSY.Component) = _has_market_bid_cost(device) || _has_import_export_cost(device) _has_parameter_time_series(::StartupCostParameter, device::PSY.StaticInjection) = is_time_variant(PSY.get_start_up(PSY.get_operation_cost(device))) _has_parameter_time_series(::ShutdownCostParameter, device::PSY.StaticInjection) = is_time_variant(PSY.get_shut_down(PSY.get_operation_cost(device))) _has_parameter_time_series( ::T, device::PSY.StaticInjection, ) where {T <: AbstractCostAtMinParameter} = _has_offer_curve_cost(device) && is_time_variant(_get_parameter_field(T(), PSY.get_operation_cost(device))) _has_parameter_time_series( ::T, device::PSY.StaticInjection, ) where {T <: AbstractPiecewiseLinearSlopeParameter} = _has_offer_curve_cost(device) && is_time_variant(_get_parameter_field(T(), PSY.get_operation_cost(device))) _has_parameter_time_series( ::T, device::PSY.StaticInjection, ) where {T <: AbstractPiecewiseLinearBreakpointParameter} = _has_offer_curve_cost(device) && is_time_variant(_get_parameter_field(T(), PSY.get_operation_cost(device))) function validate_initial_input_time_series(device::PSY.StaticInjection, decremental::Bool) initial_input = get_initial_input_maybe_decremental(Val(decremental), device) initial_is_ts = is_time_variant(initial_input) variable_is_ts = is_time_variant( get_offer_curves_maybe_decremental(Val(decremental), device)) label = decremental ? "decremental" : "incremental" (initial_is_ts && !variable_is_ts) && @warn "In `MarketBidCost` for $(get_name(device)), found time series for `$(label)_initial_input` but non-time-series `$(label)_offer_curves`; will ignore `initial_input` of `$(label)_offer_curves" (variable_is_ts && !initial_is_ts) && throw( ArgumentError( "In `MarketBidCost` for $(get_name(device)), if providing time series for `$(label)_offer_curves`, must also provide time series for `$(label)_initial_input`", ), ) if !variable_is_ts && !initial_is_ts _validate_eltype( Union{Float64, Nothing}, device, initial_input, " initial_input", ) else _validate_eltype( Float64, device, initial_input, " initial_input", ) end end function validate_occ_breakpoints_slopes(device::PSY.StaticInjection, decremental::Bool) offer_curves = get_offer_curves_maybe_decremental(Val(decremental), device) device_name = get_name(device) is_ts = is_time_variant(offer_curves) expected_type = if is_ts IS.PiecewiseStepData else PSY.CostCurve{PSY.PiecewiseIncrementalCurve} end p1 = nothing apply_maybe_across_time_series(device, offer_curves) do x curve_type = decremental ? "decremental" : "incremental" _validate_eltype(expected_type, device, x, " $curve_type offer curves") if decremental PSY.is_concave(x) || throw( ArgumentError( "Decremental $(nameof(typeof(PSY.get_operation_cost(device)))) for component $(device_name) is non-concave", ), ) else PSY.is_convex(x) || throw( ArgumentError( "Incremental $(nameof(typeof(PSY.get_operation_cost(device)))) for component $(device_name) is non-convex", ), ) end # Different specific validations for MBC versus IEC p1 = _validate_occ_subtype( PSY.get_operation_cost(device), decremental, is_ts, x, device_name, p1, ) end end function _validate_occ_subtype( ::PSY.MarketBidCost, decremental, is_ts, curve::PSY.PiecewiseStepData, device_name::String, p1::Union{Nothing, Float64}, ) @assert is_ts my_p1 = first(PSY.get_x_coords(curve)) if isnothing(p1) p1 = my_p1 elseif !isapprox(p1, my_p1) throw( ArgumentError( "Inconsistent minimum breakpoint values in time series MarketBidCost for $(device_name) offer curves. For time-variable MarketBidCost, all first x-coordinates must be equal across the entire time series.", ), ) end return p1 end _validate_occ_subtype( ::PSY.MarketBidCost, decremental, is_ts, ::PSY.CostCurve, args..., ) = @assert !is_ts function _validate_occ_subtype( cost::PSY.ImportExportCost, decremental, is_ts, curve::PSY.CostCurve, args..., ) # In the non-time-variable case, a VOM cost and initial input are represented; these must be zero @assert !is_ts !iszero(PSY.get_vom_cost(curve)) && throw( ArgumentError( "For ImportExportCost, VOM cost must be zero.", ), ) vc = PSY.get_value_curve(curve) !iszero(PSY.get_initial_input(curve)) && throw( ArgumentError( "For ImportExportCost, initial input must be zero.", ), ) _validate_occ_subtype(cost, decremental, true, PSY.get_function_data(vc)) # also do the FunctionData validations end function _validate_occ_subtype( ::PSY.ImportExportCost, decremental, is_ts, curve::PSY.PiecewiseStepData, args..., ) # In the time-variable case, VOM cost and initial input cannot be represented, so they cannot be nonzero @assert is_ts if !iszero(first(PSY.get_x_coords(curve))) throw( ArgumentError( "For ImportExportCost, the first breakpoint must be zero.", ), ) end end # Warn if hot/warm/cold startup costs are given for non-`ThermalMultiStart` function validate_occ_component( ::StartupCostParameter, device::PSY.ThermalMultiStart, ) startup = PSY.get_start_up(PSY.get_operation_cost(device)) _validate_eltype( Union{Float64, NTuple{3, Float64}, StartUpStages}, device, startup, " startup cost", ) end function validate_occ_component(::StartupCostParameter, device::PSY.StaticInjection) startup = PSY.get_start_up(PSY.get_operation_cost(device)) contains_multistart = false apply_maybe_across_time_series(device, startup) do x if x isa Float64 return elseif x isa Union{NTuple{3, Float64}, StartUpStages} contains_multistart = true else location = is_time_variant(startup) ? " in time series $(get_name(startup))" : "" throw( ArgumentError( "Expected Float64 or NTuple{3, Float64} or StartUpStages startup cost but got $(typeof(x))$location for $(get_name(device))", ), ) end end if contains_multistart location = is_time_variant(startup) ? " in time series $(get_name(startup))" : "" @warn "Multi-start costs detected$location for non-multi-start unit $(get_name(device)), will take the maximum" end return end # Validate eltype of shutdown costs function validate_occ_component(::ShutdownCostParameter, device::PSY.StaticInjection) shutdown = PSY.get_shut_down(PSY.get_operation_cost(device)) _validate_eltype(Float64, device, shutdown, " for shutdown cost") end # Renewable-specific validations that warn when costs are nonzero. # There warnings are captured by the with_logger, though, so we don't actually see them. function validate_occ_component( ::StartupCostParameter, device::Union{PSY.RenewableDispatch, PSY.Storage}, ) startup = PSY.get_start_up(PSY.get_operation_cost(device)) apply_maybe_across_time_series(device, startup) do x if x != PSY.single_start_up_to_stages(0.0) #println( @warn "Nonzero startup cost detected for renewable generation or storage device $(get_name(device))." # ) end end end function validate_occ_component( ::ShutdownCostParameter, device::Union{PSY.RenewableDispatch, PSY.Storage}, ) shutdown = PSY.get_shut_down(PSY.get_operation_cost(device)) apply_maybe_across_time_series(device, shutdown) do x if x != 0.0 #println( @warn "Nonzero shutdown cost detected for renewable generation or storage device $(get_name(device))." #) end end end function validate_occ_component( ::IncrementalCostAtMinParameter, device::Union{PSY.RenewableDispatch, PSY.Storage}, ) no_load_cost = PSY.get_no_load_cost(PSY.get_operation_cost(device)) if !isnothing(no_load_cost) apply_maybe_across_time_series(device, no_load_cost) do x if x != 0.0 #println( @warn "Nonzero no-load cost detected for renewable generation or storage device $(get_name(device))." #) end end end end function validate_occ_component( ::DecrementalCostAtMinParameter, device::PSY.Storage, ) no_load_cost = PSY.get_no_load_cost(PSY.get_operation_cost(device)) if !isnothing(no_load_cost) apply_maybe_across_time_series(device, no_load_cost) do x if x != 0.0 #println( @warn "Nonzero no-load cost detected for storage device $(get_name(device))." #) end end end end # Validate that initial input ts always appears if variable ts appears, warn if initial input ts appears without variable ts validate_occ_component( ::IncrementalCostAtMinParameter, device::PSY.StaticInjection, ) = validate_initial_input_time_series(device, false) validate_occ_component( ::DecrementalCostAtMinParameter, device::PSY.StaticInjection, ) = validate_initial_input_time_series(device, true) # Validate convexity/concavity of cost curves as appropriate, verify P1 = min gen power validate_occ_component( ::IncrementalPiecewiseLinearBreakpointParameter, device::PSY.StaticInjection, ) = validate_occ_breakpoints_slopes(device, false) validate_occ_component( ::DecrementalPiecewiseLinearBreakpointParameter, device::PSY.StaticInjection, ) = validate_occ_breakpoints_slopes(device, true) # Slope and breakpoint validations are done together, nothing to do here validate_occ_component( ::AbstractPiecewiseLinearSlopeParameter, device::PSY.StaticInjection, ) = nothing # Validates and adds parameters for a given OfferCurveCost-related ParameterType # PERF: could switch to a TSC here. function _process_occ_parameters_helper( ::P, container::OptimizationContainer, model, devices, ) where {P <: ParameterType} param_instance = P() for device in devices validate_occ_component(param_instance, device) end if _consider_parameter(param_instance, container, model) ts_devices = filter(device -> _has_parameter_time_series(param_instance, device), devices) (length(ts_devices) > 0) && add_parameters!(container, P, ts_devices, model) end end "Validate MarketBidCosts and add the appropriate parameters" function process_market_bid_parameters!( container::OptimizationContainer, devices_in, model::DeviceModel, incremental::Bool = true, decremental::Bool = false, ) devices = filter(_has_market_bid_cost, collect(devices_in)) # https://github.com/Sienna-Platform/InfrastructureSystems.jl/issues/460 isempty(devices) && return # Validate and add the parameters: for param in ( StartupCostParameter(), ShutdownCostParameter(), ) _process_occ_parameters_helper(param, container, model, devices) end if incremental for param in ( IncrementalCostAtMinParameter(), IncrementalPiecewiseLinearSlopeParameter(), IncrementalPiecewiseLinearBreakpointParameter(), ) _process_occ_parameters_helper(param, container, model, devices) end end if decremental for param in ( DecrementalCostAtMinParameter(), DecrementalPiecewiseLinearSlopeParameter(), DecrementalPiecewiseLinearBreakpointParameter(), ) _process_occ_parameters_helper(param, container, model, devices) end end end ################################################## ################# PWL Variables ################## ################################################## # For Market Bid function _add_pwl_variables!( container::OptimizationContainer, ::Type{T}, component_name::String, time_period::Int, n_tranches::Int, ::Type{U}, ) where { T <: PSY.Component, U <: AbstractPiecewiseLinearBlockOffer, } var_container = lazy_container_addition!(container, U(), T) # length(PiecewiseStepData) gets number of segments, here we want number of points pwlvars = Array{JuMP.VariableRef}(undef, n_tranches) for i in 1:n_tranches pwlvars[i] = var_container[(component_name, i, time_period)] = JuMP.@variable( get_jump_model(container), base_name = "$(nameof(U))_$(component_name)_{pwl_$(i), $time_period}", lower_bound = 0.0, ) end return pwlvars end ################################################## ################# PWL Constraints ################ ################################################## # without this, you get "variable OnVariable__RenewableDispatch is not stored" # TODO: really this falls along the divide of # commitment (OnVariable + ActivePower) vs dispatch (ActivePower only) _include_min_gen_power_in_constraint( ::PSY.RenewableDispatch, ::ActivePowerVariable, ::AbstractDeviceFormulation, ) = false _include_min_gen_power_in_constraint( ::PSY.Generator, ::ActivePowerVariable, ::AbstractDeviceFormulation, ) = true _include_min_gen_power_in_constraint( ::PSY.ControllableLoad, ::ActivePowerVariable, ::PowerLoadInterruption, ) = true _include_min_gen_power_in_constraint( ::PSY.ControllableLoad, ::ActivePowerVariable, ::PowerLoadDispatch, ) = false _include_min_gen_power_in_constraint( ::Any, ::PowerAboveMinimumVariable, ::AbstractDeviceFormulation, ) = false # add the minimum generation power to the PWL constraint, as a constant. Returns true for # formulations where there's nonzero minimum power (first breakpoint), but no OnVariable. # TODO: cleaner way? e.g. can we just do this whenever there's no OnVariable? _include_constant_min_gen_power_in_constraint( ::PSY.ControllableLoad, ::ActivePowerVariable, ::PowerLoadDispatch, ) = true _include_constant_min_gen_power_in_constraint( ::PSY.ControllableLoad, ::ActivePowerVariable, ::PowerLoadInterruption, ) = false _include_constant_min_gen_power_in_constraint( ::PSY.RenewableGen, ::ActivePowerVariable, ::AbstractRenewableDispatchFormulation, ) = true _include_constant_min_gen_power_in_constraint( ::Any, ::VariableType, ::AbstractDeviceFormulation, ) = false """ 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, ::D, break_points::Vector{<:JuMPOrFloat}, period::Int, ::Type{V}, ::Type{W}, ) where {T <: PSY.Component, U <: VariableType, D <: AbstractDeviceFormulation, V <: AbstractPiecewiseLinearBlockOffer, W <: AbstractPiecewiseLinearBlockOfferConstraint} variables = get_variable(container, U(), T) const_container = lazy_container_addition!( container, W(), T, axes(variables)..., ) len_cost_data = length(break_points) - 1 jump_model = get_jump_model(container) pwl_vars = get_variable(container, V(), T) name = PSY.get_name(component) sum_pwl_vars = sum(pwl_vars[name, ix, period] for ix in 1:len_cost_data) # As detailed in https://github.com/Sienna-Platform/PowerSimulations.jl/issues/1318, # time-variable P1 is problematic, so for now we require P1 to be constant. Thus we can # just look up what it is currently fixed to and use that here without worrying about # updating. if _include_constant_min_gen_power_in_constraint(component, U(), D()) # TODO this seems kind of redundant with the sum_pwl_vars += jump_fixed_value(first(break_points))::Float64 elseif _include_min_gen_power_in_constraint(component, U(), D()) on_vars = get_variable(container, OnVariable(), T) p1::Float64 = jump_fixed_value(first(break_points)) sum_pwl_vars += p1 * on_vars[name, period] end const_container[name, period] = JuMP.@constraint( jump_model, variables[name, period] == sum_pwl_vars ) 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, PiecewiseLinearBlockIncrementalOfferConstraint(), T, axes(variables)...; meta = name, ) len_cost_data = length(break_points) - 1 jump_model = get_jump_model(container) pwl_vars = get_variable(container, PiecewiseLinearBlockIncrementalOffer(), 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 ################# ################################################## get_offer_curves_for_var(var, comp::PSY.Component) = get_offer_curves_for_var(var, get_operation_cost(comp)) get_offer_curves_for_var(::PiecewiseLinearBlockIncrementalOffer, cost::PSY.MarketBidCost) = get_offer_curves_maybe_decremental(Val(false), cost) get_offer_curves_for_var(::PiecewiseLinearBlockDecrementalOffer, cost::PSY.MarketBidCost) = get_offer_curves_maybe_decremental(Val(true), cost) get_multiplier_for_var(::PiecewiseLinearBlockIncrementalOffer) = OBJECTIVE_FUNCTION_POSITIVE get_multiplier_for_var(::PiecewiseLinearBlockDecrementalOffer) = OBJECTIVE_FUNCTION_NEGATIVE function _get_pwl_cost_expression( container::OptimizationContainer, component::T, time_period::Int, slopes_normalized::Vector{Float64}, ::U, ::V, ::W, ) where { T <: PSY.Component, U <: VariableType, V <: AbstractDeviceFormulation, W <: AbstractPiecewiseLinearBlockOffer, } resolution = get_resolution(container) dt = Dates.value(resolution) / MILLISECONDS_IN_HOUR multiplier = get_multiplier_for_var(W()) * dt name = PSY.get_name(component) pwl_var_container = get_variable(container, W(), T) gen_cost = JuMP.AffExpr(0.0) for (i, cost) in enumerate(slopes_normalized) JuMP.add_to_expression!( gen_cost, (cost * multiplier), pwl_var_container[(name, i, time_period)], ) end return gen_cost end """ Get cost expression for StepwiseCostReserve """ function _get_pwl_cost_expression( container::OptimizationContainer, component::T, time_period::Int, slopes_normalized::Vector{Float64}, multiplier::Float64, ) where {T <: PSY.ReserveDemandCurve} name = PSY.get_name(component) pwl_var_container = get_variable(container, PiecewiseLinearBlockIncrementalOffer(), T) ordc_cost = JuMP.AffExpr(0.0) for (i, slope) in enumerate(slopes_normalized) JuMP.add_to_expression!( ordc_cost, slope * multiplier, pwl_var_container[(name, i, time_period)], ) end return ordc_cost end ############################################### ######## MarketBidCost: Fixed Curves ########## ############################################### # Serves a similar role as _lookup_maybe_time_variant_param, but needs extra logic function _get_pwl_data( is_decremental::Bool, container::OptimizationContainer, component::T, time::Int, ) where {T <: PSY.Component} cost_data = get_offer_curves_maybe_decremental(Val(is_decremental), component) if is_time_variant(cost_data) name = PSY.get_name(component) SlopeParam = _SLOPE_PARAMS[is_decremental] slope_param_arr = get_parameter_array(container, SlopeParam(), T) slope_param_mult = get_parameter_multiplier_array(container, SlopeParam(), T) @assert size(slope_param_arr) == size(slope_param_mult) # multiplier arrays should be 3D too slope_cost_component = slope_param_arr[name, :, time] .* slope_param_mult[name, :, time] slope_cost_component = slope_cost_component.data BreakpointParam = _BREAKPOINT_PARAMS[is_decremental] breakpoint_param_container = get_parameter(container, BreakpointParam(), T) breakpoint_param_arr = get_parameter_column_refs(breakpoint_param_container, name) # performs component -> time series many-to-one mapping breakpoint_param_mult = get_multiplier_array(breakpoint_param_container) @assert size(breakpoint_param_arr) == size(breakpoint_param_mult[name, :, :]) breakpoint_cost_component = breakpoint_param_arr[:, time] .* breakpoint_param_mult[name, :, time] breakpoint_cost_component = breakpoint_cost_component.data @assert_op length(slope_cost_component) == length(breakpoint_cost_component) - 1 # PSY's cost_function_timeseries.jl says this will always be natural units unit_system = PSY.UnitSystem.NATURAL_UNITS else cost_component = PSY.get_function_data(PSY.get_value_curve(cost_data)) breakpoint_cost_component = PSY.get_x_coords(cost_component) slope_cost_component = PSY.get_y_coords(cost_component) unit_system = PSY.get_power_units(cost_data) end breakpoints, slopes = get_piecewise_curve_per_system_unit( breakpoint_cost_component, slope_cost_component, unit_system, get_base_power(container), PSY.get_base_power(component), ) return breakpoints, slopes end """ Add PWL cost terms for data coming from the MarketBidCost with a fixed incremental offer curve """ function add_pwl_term!( is_decremental::Bool, container::OptimizationContainer, component::T, ::PSY.OfferCurveCost, ::U, ::V, ) where {T <: PSY.Component, U <: VariableType, V <: AbstractDeviceFormulation} name = PSY.get_name(component) W = _PIECEWISE_BLOCK_VARS[is_decremental] X = _PIECEWISE_BLOCK_CONSTRAINTS[is_decremental] name = PSY.get_name(component) time_steps = get_time_steps(container) for t in time_steps breakpoints, slopes = _get_pwl_data(is_decremental, container, component, t) _add_pwl_variables!(container, T, name, t, length(slopes), W) _add_pwl_constraint!( container, component, U(), V(), breakpoints, t, W, X, ) pwl_cost = _get_pwl_cost_expression( container, component, t, slopes, U(), V(), W(), ) add_to_expression!( container, ProductionCostExpression, pwl_cost, component, t, ) if is_time_variant( get_offer_curves_maybe_decremental(Val(is_decremental), component), ) add_to_objective_variant_expression!(container, pwl_cost) else add_to_objective_invariant_expression!(container, pwl_cost) end end end ################################################## ########## PWL for StepwiseCostReserve ########## ################################################## # Not touching this in PR #1303, TODO figure it out later -GKS 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_curve_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, length(IS.get_y_coords(data)), PiecewiseLinearBlockIncrementalOffer, ) _add_pwl_constraint!(container, component, U(), break_points, sos_val, t) pwl_cost = _get_pwl_cost_expression( container, component, t, IS.get_y_coords(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.OfferCurveCost, ::U, ) where {T <: VariableType, U <: AbstractDeviceFormulation} component_name = PSY.get_name(component) @debug "Market Bid" _group = LOG_GROUP_COST_FUNCTIONS component_name if !isnothing(get_input_offer_curves(cost_function)) error("Component $(component_name) is not allowed to participate as a demand.") end add_pwl_term!( false, # I suspect this is a problem. could very well be decremental, storage container, component, cost_function, T(), U(), ) return end function _add_variable_cost_to_objective!( container::OptimizationContainer, ::T, component::PSY.Component, cost_function::PSY.OfferCurveCost, ::U, ) where {T <: VariableType, U <: AbstractControllablePowerLoadFormulation} component_name = PSY.get_name(component) @debug "Market Bid" _group = LOG_GROUP_COST_FUNCTIONS component_name if !(isnothing(get_output_offer_curves(cost_function))) error("Component $(component_name) is not allowed to participate as a supply.") end add_pwl_term!( true, container, component, cost_function, T(), U(), ) 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 # "copy-paste and change incremental to decremental" here. Refactor? function _add_vom_cost_to_objective!( container::OptimizationContainer, ::T, component::PSY.Component, op_cost::PSY.OfferCurveCost, ::U, ) where {T <: VariableType, U <: AbstractDeviceFormulation} incremental_cost_curves = get_output_offer_curves(op_cost) if is_time_variant(incremental_cost_curves) # TODO this might imply a change to the MBC struct? @warn "Incremental curves are time variant, there is no VOM cost source. Skipping VOM cost." return end _add_vom_cost_to_objective_helper!( container, T(), component, op_cost, incremental_cost_curves, U(), ) return end function _add_vom_cost_to_objective!( container::OptimizationContainer, ::T, component::PSY.Component, op_cost::PSY.OfferCurveCost, ::U, ) where {T <: VariableType, U <: AbstractControllablePowerLoadFormulation} decremental_cost_curves = get_input_offer_curves(op_cost) if is_time_variant(decremental_cost_curves) # TODO this might imply a change to the MBC struct? @warn "Decremental curves are time variant, there is no VOM cost source. Skipping VOM cost." return end _add_vom_cost_to_objective_helper!( container, T(), component, op_cost, decremental_cost_curves, U(), ) return end function _add_vom_cost_to_objective_helper!( container::OptimizationContainer, ::T, component::PSY.Component, ::PSY.OfferCurveCost, cost_data::PSY.CostCurve{PSY.PiecewiseIncrementalCurve}, ::U, ) where {T <: VariableType, U <: AbstractDeviceFormulation} power_units = PSY.get_power_units(cost_data) vom_cost = PSY.get_vom_cost(cost_data) multiplier = 1.0 # VOM Cost is always positive cost_term = PSY.get_proportional_term(vom_cost) iszero(cost_term) && return base_power = get_base_power(container) device_base_power = PSY.get_base_power(component) cost_term_normalized = get_proportional_cost_per_system_unit(cost_term, power_units, base_power, device_base_power) resolution = get_resolution(container) dt = Dates.value(resolution) / MILLISECONDS_IN_HOUR for t in get_time_steps(container) exp = _add_proportional_term!( container, T(), component, cost_term_normalized * multiplier * dt, t) add_to_expression!(container, VOMCostExpression, exp, component, t) end return end ================================================ FILE: src/devices_models/devices/common/objective_function/piecewise_linear.jl ================================================ ################################################## ################# 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 ################ ################################################## function _determine_bin_lhs( container::OptimizationContainer, sos_status::SOSStatusVariable, component::T, period::Int) where {T <: PSY.Component} name = PSY.get_name(component) if sos_status == SOSStatusVariable.NO_VARIABLE return 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) return 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) return 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 end function _get_bin_lhs( container::OptimizationContainer, sos_status::SOSStatusVariable, component::T, period::Int) where {T <: PSY.Component} return _determine_bin_lhs(container, sos_status, component, period) end function _get_bin_lhs( container::OptimizationContainer, sos_status::SOSStatusVariable, component::PSY.ThermalGen, period::Int) if PSY.get_must_run(component) return 1.0 else return _determine_bin_lhs(container, sos_status, component, period) end end """ 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) ) bin = _get_bin_lhs(container, sos_status, component, period) 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) y_coords_cost_data = PSY.get_y_coords(cost_data) for (i, cost) in enumerate(y_coords_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, ) # Multiplier is not necessary here. There is no negative cost for fuel curves. resolution = get_resolution(container) dt = Dates.value(resolution) / MILLISECONDS_IN_HOUR fuel_consumption_expression = _get_pwl_cost_expression( container, component, time_period, cost_data_normalized, dt, ) return fuel_consumption_expression 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 x_coords = PSY.get_x_coords(data) if x_coords[1] != 0.0 y_coords = PSY.get_y_coords(data) x_first = round(x_coords[1]; digits = 3) y_first = round(y_coords[1]; digits = 3) slope_first = round(slopes[1]; digits = 3) guess_y_zero = y_coords[1] - slopes[1] * x_coords[1] @warn( "PWL has no 0.0 intercept for generator $(name). First point is given at (x = $(x_first), y = $(y_first)). Adding a first intercept at (x = 0.0, y = $(round(guess_y_zero, digits = 3)) to have equal initial slope $(slope_first)" ) if guess_y_zero < 0.0 error( "Added zero intercept has negative cost for generator $(name). Consider using other formulation or improve data.", ) end # adds a first intercept a x = 0.0 and y above the intercept of the first tuple to make convex equivalent (avoid floating point issues of almost equal slopes) intercept_point = (x = 0.0, y = guess_y_zero + COST_EPSILON) data = PSY.PiecewiseLinearData(vcat(intercept_point, PSY.get_points(data))) @assert PSY.is_convex(data) 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) temp_cost_function = create_temporary_cost_function_in_system_per_unit(cost_function, data) for t in time_steps _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, temp_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::PSY.CostCurve{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, FuelCostExpression, pwl_cost_expressions[t], component, t, ) add_to_objective_invariant_expression!(container, pwl_cost_expressions[t]) 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_function::PSY.CostCurve{PSY.PiecewisePointCurve}: container for piecewise linear cost """ function _add_variable_cost_to_objective!( container::OptimizationContainer, ::T, component::PSY.Component, cost_function::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_fuel_consumption_expressions = _add_pwl_term!(container, component, cost_function, T(), U()) is_time_variant_ = is_time_variant(PSY.get_fuel_cost(cost_function)) for t in get_time_steps(container) fuel_cost_value = get_fuel_cost_value( container, component, t, is_time_variant_, ) pwl_cost_expression = pwl_fuel_consumption_expressions[t] * fuel_cost_value add_to_expression!( container, FuelCostExpression, pwl_cost_expression, component, t, ) add_to_expression!( container, FuelConsumptionExpression, pwl_fuel_consumption_expressions[t], component, t, ) if is_time_variant_ add_to_objective_variant_expression!(container, pwl_cost_expression) else add_to_objective_invariant_expression!(container, pwl_cost_expression) end 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 ================================================ FILE: src/devices_models/devices/common/objective_function/quadratic_curve.jl ================================================ # 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, FuelCostExpression, cost_term, component, time_period, ) return end # Dispatch for vector proportional/quadratic terms function _add_quadraticcurve_variable_cost!( container::OptimizationContainer, ::T, ::U, component::PSY.Component, proportional_term_per_unit::Vector{Float64}, quadratic_term_per_unit::Vector{Float64}, ) where {T <: VariableType, U <: AbstractDeviceFormulation} lb, ub = get_min_max_limits(component, ActivePowerVariableLimitsConstraint, U) for t in get_time_steps(container) _check_quadratic_monotonicity( PSY.get_name(component), quadratic_term_per_unit[t], proportional_term_per_unit[t], lb, ub, ) _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, ::U, component::PSY.Component, proportional_term_per_unit::Float64, quadratic_term_per_unit::Float64, ) where {T <: VariableType, U <: AbstractDeviceFormulation} lb, ub = get_min_max_limits(component, ActivePowerVariableLimitsConstraint, U) _check_quadratic_monotonicity(PSY.get_name(component), quadratic_term_per_unit, proportional_term_per_unit, lb, ub, ) 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 function _check_quadratic_monotonicity( name::String, quad_term::Float64, linear_term::Float64, lb::Float64, ub::Float64, ) fp_lb = 2 * quad_term * lb + linear_term fp_ub = 2 * quad_term * ub + linear_term if fp_lb < 0 || fp_ub < 0 @warn "Cost function for component $name is not monotonically increasing in the range [$lb, $ub]. \ This can lead to unexpected results" 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(), U(), 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 compatible with Compact formulations", ), ) return end function _add_fuel_quadratic_variable_cost!( container::OptimizationContainer, ::T, ::U, component::PSY.Component, proportional_fuel_curve::Float64, quadratic_fuel_curve::Float64, fuel_cost::Float64, ) where {T <: VariableType, U <: AbstractDeviceFormulation} _add_quadraticcurve_variable_cost!( container, T(), U(), component, proportional_fuel_curve * fuel_cost, quadratic_fuel_curve * fuel_cost, ) end function _add_fuel_quadratic_variable_cost!( container::OptimizationContainer, ::T, ::AbstractDeviceFormulation, component::PSY.Component, proportional_fuel_curve::Float64, quadratic_fuel_curve::Float64, fuel_cost::IS.TimeSeriesKey, ) where {T <: VariableType} _add_time_varying_fuel_variable_cost!(container, T(), component, fuel_cost) 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(), U(), component, multiplier * proportional_term_per_unit, multiplier * quadratic_term_per_unit, fuel_cost, ) return end ================================================ FILE: src/devices_models/devices/common/range_constraint.jl ================================================ ######## CONSTRAINTS ############ _add_lb(::RangeConstraintLBExpressions) = true _add_ub(::RangeConstraintLBExpressions) = false _add_lb(::RangeConstraintUBExpressions) = false _add_ub(::RangeConstraintUBExpressions) = true _add_lb(::ExpressionType) = true _add_ub(::ExpressionType) = false # Generic fallback functions function get_startup_shutdown( device, ::Type{<:VariableType}, ::Type{<:AbstractDeviceFormulation}, ) # -> Union{Nothing, NamedTuple{(:startup, :shutdown), Tuple{Float64, Float64}}} nothing end @doc raw""" Constructs min/max range constraint from device variable. If min and max within an epsilon width: ``` variable[name, t] == limits.max ``` Otherwise: ``` limits.min <= variable[name, t] <= limits.max ``` where limits in constraint_infos. # LaTeX `` x = limits^{max}, \text{ for } |limits^{max} - limits^{min}| < \varepsilon `` `` limits^{min} \leq x \leq limits^{max}, \text{ otherwise } `` """ function add_range_constraints!( container::OptimizationContainer, ::Type{T}, ::Type{U}, devices::IS.FlattenIteratorWrapper{V}, model::DeviceModel{V, W}, ::Type{X}, ) where { T <: ConstraintType, U <: VariableType, V <: PSY.Component, W <: AbstractDeviceFormulation, X <: PM.AbstractPowerModel, } array = get_variable(container, U(), V) _add_lower_bound_range_constraints_impl!(container, T, array, devices, model) _add_upper_bound_range_constraints_impl!(container, T, array, devices, model) return end function add_range_constraints!( container::OptimizationContainer, ::Type{T}, ::Type{U}, devices::IS.FlattenIteratorWrapper{V}, model::DeviceModel{V, W}, ::Type{X}, ) where { T <: ConstraintType, U <: RangeConstraintLBExpressions, V <: PSY.Component, W <: AbstractDeviceFormulation, X <: PM.AbstractPowerModel, } array = get_expression(container, U(), V) _add_lower_bound_range_constraints_impl!(container, T, array, devices, model) return end function add_range_constraints!( container::OptimizationContainer, ::Type{T}, ::Type{U}, devices::IS.FlattenIteratorWrapper{V}, model::DeviceModel{V, W}, ::Type{X}, ) where { T <: ConstraintType, U <: RangeConstraintUBExpressions, V <: PSY.Component, W <: AbstractDeviceFormulation, X <: PM.AbstractPowerModel, } array = get_expression(container, U(), V) _add_upper_bound_range_constraints_impl!(container, T, array, devices, model) return end function _add_lower_bound_range_constraints_impl!( container::OptimizationContainer, ::Type{T}, array, devices::IS.FlattenIteratorWrapper{V}, model::DeviceModel{V, W}, ) where {T <: ConstraintType, V <: PSY.Component, W <: AbstractDeviceFormulation} time_steps = get_time_steps(container) device_names = PSY.get_name.(devices) con_lb = add_constraints_container!(container, T(), V, device_names, time_steps; meta = "lb") for device in devices, t in time_steps ci_name = PSY.get_name(device) limits = get_min_max_limits(device, T, W) # depends on constraint type and formulation type con_lb[ci_name, t] = JuMP.@constraint(get_jump_model(container), array[ci_name, t] >= limits.min) end return end function _add_upper_bound_range_constraints_impl!( container::OptimizationContainer, ::Type{T}, array, devices::IS.FlattenIteratorWrapper{V}, model::DeviceModel{V, W}, ) where {T <: ConstraintType, V <: PSY.Component, W <: AbstractDeviceFormulation} time_steps = get_time_steps(container) device_names = PSY.get_name.(devices) con_ub = add_constraints_container!(container, T(), V, device_names, time_steps; meta = "ub") for device in devices, t in time_steps ci_name = PSY.get_name(device) limits = get_min_max_limits(device, T, W) # depends on constraint type and formulation type con_ub[ci_name, t] = JuMP.@constraint(get_jump_model(container), array[ci_name, t] <= limits.max) end return end @doc raw""" Constructs min/max range constraint from device variable and on/off decision variable. If device min = 0: ``` varcts[name, t] <= limits.max*varbin[name, t]) ``` ``` varcts[name, t] >= 0.0 ``` Otherwise: ``` varcts[name, t] <= limits.max*varbin[name, t] ``` ``` varcts[name, t] >= limits.min*varbin[name, t] ``` where limits in constraint_infos. # LaTeX `` 0 \leq x^{cts} \leq limits^{max} x^{bin}, \text{ for } limits^{min} = 0 `` `` limits^{min} x^{bin} \leq x^{cts} \leq limits^{max} x^{bin}, \text{ otherwise } `` """ function add_semicontinuous_range_constraints!( container::OptimizationContainer, ::Type{T}, ::Type{U}, devices::IS.FlattenIteratorWrapper{V}, model::DeviceModel{V, W}, ::Type{X}, ) where { T <: ConstraintType, U <: VariableType, V <: PSY.Component, W <: AbstractDeviceFormulation, X <: PM.AbstractPowerModel, } array = get_variable(container, U(), V) _add_semicontinuous_lower_bound_range_constraints_impl!( container, T, array, devices, model, ) _add_semicontinuous_upper_bound_range_constraints_impl!( container, T, array, devices, model, ) return end function add_semicontinuous_range_constraints!( container::OptimizationContainer, ::Type{T}, ::Type{U}, devices::IS.FlattenIteratorWrapper{V}, model::DeviceModel{V, W}, ::Type{X}, ) where { T <: ConstraintType, U <: RangeConstraintLBExpressions, V <: PSY.Component, W <: AbstractDeviceFormulation, X <: PM.AbstractPowerModel, } array = get_expression(container, U(), V) _add_semicontinuous_lower_bound_range_constraints_impl!( container, T, array, devices, model, ) return end function add_semicontinuous_range_constraints!( container::OptimizationContainer, ::Type{T}, ::Type{U}, devices::IS.FlattenIteratorWrapper{V}, model::DeviceModel{V, W}, ::Type{X}, ) where { T <: ConstraintType, U <: RangeConstraintUBExpressions, V <: PSY.Component, W <: AbstractDeviceFormulation, X <: PM.AbstractPowerModel, } array = get_expression(container, U(), V) _add_semicontinuous_upper_bound_range_constraints_impl!( container, T, array, devices, model, ) return end function _add_semicontinuous_lower_bound_range_constraints_impl!( container::OptimizationContainer, ::Type{T}, array, devices::IS.FlattenIteratorWrapper{V}, ::DeviceModel{V, W}, ) where {T <: ConstraintType, V <: PSY.Component, W <: AbstractDeviceFormulation} time_steps = get_time_steps(container) names = PSY.get_name.(devices) con_lb = add_constraints_container!(container, T(), V, names, time_steps; meta = "lb") varbin = get_variable(container, OnVariable(), V) for device in devices ci_name = PSY.get_name(device) limits = get_min_max_limits(device, T, W) # depends on constraint type and formulation type for t in time_steps con_lb[ci_name, t] = JuMP.@constraint( get_jump_model(container), array[ci_name, t] >= limits.min * varbin[ci_name, t] ) end end return end function _add_semicontinuous_lower_bound_range_constraints_impl!( container::OptimizationContainer, ::Type{T}, array, devices::IS.FlattenIteratorWrapper{V}, ::DeviceModel{V, W}, ) where {T <: ConstraintType, V <: PSY.ThermalGen, W <: AbstractDeviceFormulation} time_steps = get_time_steps(container) names = PSY.get_name.(devices) con_lb = add_constraints_container!(container, T(), V, names, time_steps; meta = "lb") varbin = get_variable(container, OnVariable(), V) for device in devices ci_name = PSY.get_name(device) limits = get_min_max_limits(device, T, W) # depends on constraint type and formulation type if PSY.get_must_run(device) for t in time_steps con_lb[ci_name, t] = JuMP.@constraint( get_jump_model(container), array[ci_name, t] >= limits.min ) end else for t in time_steps con_lb[ci_name, t] = JuMP.@constraint( get_jump_model(container), array[ci_name, t] >= limits.min * varbin[ci_name, t] ) end end end return end function _add_semicontinuous_upper_bound_range_constraints_impl!( container::OptimizationContainer, ::Type{T}, array, devices::IS.FlattenIteratorWrapper{V}, model::DeviceModel{V, W}, ) where {T <: ConstraintType, V <: PSY.ThermalGen, W <: AbstractDeviceFormulation} time_steps = get_time_steps(container) names = PSY.get_name.(devices) con_ub = add_constraints_container!(container, T(), V, names, time_steps; meta = "ub") varbin = get_variable(container, OnVariable(), V) for device in devices ci_name = PSY.get_name(device) limits = get_min_max_limits(device, T, W) # depends on constraint type and formulation type if PSY.get_must_run(device) for t in time_steps con_ub[ci_name, t] = JuMP.@constraint( get_jump_model(container), array[ci_name, t] <= limits.max ) end else for t in time_steps con_ub[ci_name, t] = JuMP.@constraint( get_jump_model(container), array[ci_name, t] <= limits.max * varbin[ci_name, t] ) end end end return end function _add_semicontinuous_upper_bound_range_constraints_impl!( container::OptimizationContainer, ::Type{T}, array, devices::IS.FlattenIteratorWrapper{V}, model::DeviceModel{V, W}, ) where {T <: ConstraintType, V <: PSY.Component, W <: AbstractDeviceFormulation} time_steps = get_time_steps(container) names = PSY.get_name.(devices) con_ub = add_constraints_container!(container, T(), V, names, time_steps; meta = "ub") varbin = get_variable(container, OnVariable(), V) for device in devices, t in time_steps ci_name = PSY.get_name(device) limits = get_min_max_limits(device, T, W) # depends on constraint type and formulation type con_ub[ci_name, t] = JuMP.@constraint( get_jump_model(container), array[ci_name, t] <= limits.max * varbin[ci_name, t] ) end return end @doc raw""" Constructs min/max range constraint from device variable and reservation decision variable. ``` varcts[name, t] <= limits.max * (1 - varbin[name, t]) ``` ``` varcts[name, t] >= limits.min * (1 - varbin[name, t]) ``` where limits in constraint_infos. # LaTeX `` 0 \leq x^{cts} \leq limits^{max} (1 - x^{bin}), \text{ for } limits^{min} = 0 `` `` limits^{min} (1 - x^{bin}) \leq x^{cts} \leq limits^{max} (1 - x^{bin}), \text{ otherwise } `` """ function add_reserve_range_constraints!( container::OptimizationContainer, ::Type{T}, ::Type{U}, devices::IS.FlattenIteratorWrapper{V}, model::DeviceModel{V, W}, ::Type{X}, ) where { T <: InputActivePowerVariableLimitsConstraint, U <: VariableType, V <: PSY.Component, W <: AbstractDeviceFormulation, X <: PM.AbstractPowerModel, } array = get_variable(container, U(), V) _add_reserve_upper_bound_range_constraints!(container, T, array, devices, model) _add_reserve_lower_bound_range_constraints!(container, T, array, devices, model) return end function add_reserve_range_constraints!( container::OptimizationContainer, ::Type{T}, ::Type{U}, devices::IS.FlattenIteratorWrapper{V}, model::DeviceModel{V, W}, ::Type{X}, ) where { T <: InputActivePowerVariableLimitsConstraint, U <: ExpressionType, V <: PSY.Component, W <: AbstractDeviceFormulation, X <: PM.AbstractPowerModel, } array = get_expression(container, U(), W) _add_ub(U()) && _add_reserve_upper_bound_range_constraints!(container, T, array, devices, model) _add_lb(U()) && _add_reserve_lower_bound_range_constraints!(container, T, array, devices, model) return end function _add_reserve_lower_bound_range_constraints!( container::OptimizationContainer, ::Type{T}, array, devices::IS.FlattenIteratorWrapper{V}, ::DeviceModel{V, W}, ) where { T <: InputActivePowerVariableLimitsConstraint, V <: PSY.Component, W <: AbstractDeviceFormulation, } time_steps = get_time_steps(container) names = PSY.get_name.(devices) binary_variables = [ReservationVariable()] IS.@assert_op length(binary_variables) == 1 varbin = get_variable(container, only(binary_variables), V) names = [PSY.get_name(x) for x in devices] # MOI has a semicontinous set, but after some tests is not clear most MILP solvers support it. # In the future this can be updated con_lb = add_constraints_container!(container, T(), V, names, time_steps; meta = "lb") for device in devices, t in time_steps ci_name = PSY.get_name(device) limits = get_min_max_limits(device, T, W) con_lb[ci_name, t] = JuMP.@constraint( get_jump_model(container), array[ci_name, t] >= limits.min * (1 - varbin[ci_name, t]) ) end return end function _add_reserve_upper_bound_range_constraints!( container::OptimizationContainer, ::Type{T}, array, devices::IS.FlattenIteratorWrapper{V}, model::DeviceModel{V, W}, ) where { T <: InputActivePowerVariableLimitsConstraint, V <: PSY.Component, W <: AbstractDeviceFormulation, } time_steps = get_time_steps(container) names = PSY.get_name.(devices) binary_variables = [ReservationVariable()] IS.@assert_op length(binary_variables) == 1 varbin = get_variable(container, only(binary_variables), V) names = [PSY.get_name(x) for x in devices] # MOI has a semicontinous set, but after some tests is not clear most MILP solvers support it. # In the future this can be updated con_ub = add_constraints_container!(container, T(), V, names, time_steps; meta = "ub") for device in devices, t in time_steps ci_name = PSY.get_name(device) limits = get_min_max_limits(device, T, W) con_ub[ci_name, t] = JuMP.@constraint( get_jump_model(container), array[ci_name, t] <= limits.max * (1 - varbin[ci_name, t]) ) end return end @doc raw""" Constructs min/max range constraint from device variable and reservation decision variable. ``` varcts[name, t] <= limits.max * varbin[name, t] ``` ``` varcts[name, t] >= limits.min * varbin[name, t] ``` where limits in constraint_infos. # LaTeX `` limits^{min} x^{bin} \leq x^{cts} \leq limits^{max} x^{bin},`` """ function add_reserve_range_constraints!( container::OptimizationContainer, ::Type{T}, ::Type{U}, devices::IS.FlattenIteratorWrapper{W}, model::DeviceModel{W, X}, ::Type{Y}, ) where { T <: Union{ ReactivePowerVariableLimitsConstraint, ActivePowerVariableLimitsConstraint, OutputActivePowerVariableLimitsConstraint, }, U <: VariableType, W <: PSY.Component, X <: AbstractDeviceFormulation, Y <: PM.AbstractPowerModel, } array = get_variable(container, U(), W) _add_reserve_upper_bound_range_constraints!(container, T, array, devices, model) _add_reserve_lower_bound_range_constraints!(container, T, array, devices, model) return end @doc raw""" Constructs min/max range constraint from device variable and reservation decision variable. ``` varcts[name, t] <= limits.max * varbin[name, t] ``` ``` varcts[name, t] >= limits.min * varbin[name, t] ``` where limits in constraint_infos. # LaTeX `` limits^{min} x^{bin} \leq x^{cts} \leq limits^{max} x^{bin},`` """ function add_reserve_range_constraints!( container::OptimizationContainer, ::Type{T}, ::Type{U}, devices::IS.FlattenIteratorWrapper{W}, model::DeviceModel{W, X}, ::Type{Y}, ) where { T <: Union{ ReactivePowerVariableLimitsConstraint, ActivePowerVariableLimitsConstraint, OutputActivePowerVariableLimitsConstraint, }, U <: ExpressionType, W <: PSY.Component, X <: AbstractDeviceFormulation, Y <: PM.AbstractPowerModel, } array = get_expression(container, U(), W) _add_ub(U()) && _add_reserve_upper_bound_range_constraints!(container, T, array, devices, model) _add_lb(U()) && _add_reserve_lower_bound_range_constraints!(container, T, array, devices, model) return end function _add_reserve_lower_bound_range_constraints!( container::OptimizationContainer, ::Type{T}, array, devices::IS.FlattenIteratorWrapper{W}, ::DeviceModel{W, X}, ) where { T <: Union{ ReactivePowerVariableLimitsConstraint, ActivePowerVariableLimitsConstraint, OutputActivePowerVariableLimitsConstraint, }, W <: PSY.Component, X <: AbstractDeviceFormulation, } time_steps = get_time_steps(container) names = PSY.get_name.(devices) binary_variables = [ReservationVariable()] con_lb = add_constraints_container!(container, T(), W, names, time_steps; meta = "lb") @assert length(binary_variables) == 1 "Expected $(binary_variables) for $U $V $T $W to be length 1" varbin = get_variable(container, only(binary_variables), W) for device in devices, t in time_steps ci_name = PSY.get_name(device) limits = get_min_max_limits(device, T, X) # depends on constraint type and formulation type con_lb[ci_name, t] = JuMP.@constraint( get_jump_model(container), array[ci_name, t] >= limits.min * varbin[ci_name, t] ) end return end function _add_reserve_upper_bound_range_constraints!( container::OptimizationContainer, ::Type{T}, array, devices::IS.FlattenIteratorWrapper{W}, ::DeviceModel{W, X}, ) where { T <: Union{ ReactivePowerVariableLimitsConstraint, ActivePowerVariableLimitsConstraint, OutputActivePowerVariableLimitsConstraint, }, W <: PSY.Component, X <: AbstractDeviceFormulation, } time_steps = get_time_steps(container) names = PSY.get_name.(devices) binary_variables = [ReservationVariable()] con_ub = add_constraints_container!(container, T(), W, names, time_steps; meta = "ub") @assert length(binary_variables) == 1 "Expected $(binary_variables) for $U $V $T $W to be length 1" varbin = get_variable(container, only(binary_variables), W) for device in devices, t in time_steps ci_name = PSY.get_name(device) limits = get_min_max_limits(device, T, X) # depends on constraint type and formulation type con_ub[ci_name, t] = JuMP.@constraint( get_jump_model(container), array[ci_name, t] <= limits.max * varbin[ci_name, t] ) end return end function add_parameterized_lower_bound_range_constraints( container::OptimizationContainer, ::Type{T}, ::Type{U}, ::Type{P}, devices::IS.FlattenIteratorWrapper{V}, model::DeviceModel{V, W}, ::Type{X}, ) where { T <: ConstraintType, U <: ExpressionType, P <: ParameterType, V <: PSY.Component, W <: AbstractDeviceFormulation, X <: PM.AbstractPowerModel, } array = get_expression(container, U(), V) _add_parameterized_lower_bound_range_constraints_impl!( container, T, array, P, devices, model, ) return end function add_parameterized_lower_bound_range_constraints( container::OptimizationContainer, ::Type{T}, ::Type{U}, ::Type{P}, devices::IS.FlattenIteratorWrapper{V}, model::DeviceModel{V, W}, ::Type{X}, ) where { T <: ConstraintType, U <: VariableType, P <: ParameterType, V <: PSY.Component, W <: AbstractDeviceFormulation, X <: PM.AbstractPowerModel, } array = get_variable(container, U(), V) _add_parameterized_lower_bound_range_constraints_impl!( container, T, array, P, devices, model, ) return end # This function is re-used in SemiContinuousFeedforward function lower_bound_range_with_parameter!( container::OptimizationContainer, constraint_container::JuMPConstraintArray, lhs_array, ::Type{P}, devices::IS.FlattenIteratorWrapper{V}, model::DeviceModel{V, W}, ) where {P <: ParameterType, V <: PSY.Component, W <: AbstractDeviceFormulation} param_array = get_parameter_array(container, P(), V) param_multiplier = get_parameter_multiplier_array(container, P(), V) jump_model = get_jump_model(container) time_steps = axes(constraint_container)[2] for device in devices, t in time_steps name = PSY.get_name(device) constraint_container[name, t] = JuMP.@constraint( jump_model, lhs_array[name, t] >= param_multiplier[name, t] * param_array[name, t] ) end return end function lower_bound_range_with_parameter!( container::OptimizationContainer, constraint_container::JuMPConstraintArray, lhs_array, ::Type{P}, devices::IS.FlattenIteratorWrapper{V}, model::DeviceModel{V, W}, ) where {P <: TimeSeriesParameter, V <: PSY.Component, W <: AbstractDeviceFormulation} param_container = get_parameter(container, U(), V) mult = get_multiplier_array(param_container) jump_model = get_jump_model(container) time_steps = axes(constraint_container)[2] ts_name = get_time_series_names(model)[P] ts_type = get_default_time_series_type(container) for device in devices if !(PSY.has_time_series(device, ts_type, ts_name)) continue end name = PSY.get_name(device) param = get_parameter_column_refs(param_container, name) for t in time_steps constraint_container[name, t] = JuMP.@constraint(jump_model, lhs_array[name, t] >= mult[name, t] * param[t]) end end return end function _add_parameterized_lower_bound_range_constraints_impl!( container::OptimizationContainer, ::Type{T}, array, ::Type{U}, devices::IS.FlattenIteratorWrapper{V}, model::DeviceModel{V, W}, ) where { T <: ConstraintType, U <: ParameterType, V <: PSY.Component, W <: AbstractDeviceFormulation, } time_steps = get_time_steps(container) ts_name = get_time_series_names(model)[U] ts_type = get_default_time_series_type(container) names = [PSY.get_name(d) for d in devices if PSY.has_time_series(d, ts_type, ts_name)] if isempty(names) @debug "There are no $V devices with time series data" return end constraint = add_constraints_container!(container, T(), V, names, time_steps; meta = "lb") parameter = get_parameter_array(container, U(), V) multiplier = get_parameter_multiplier_array(container, U(), V) jump_model = get_jump_model(container) lower_bound_range_with_parameter!( jump_model, constraint, array, multiplier, parameter, devices, ) return end function add_parameterized_upper_bound_range_constraints( container::OptimizationContainer, ::Type{T}, ::Type{U}, ::Type{P}, devices::Union{Vector{V}, IS.FlattenIteratorWrapper{V}}, model::DeviceModel{V, W}, ::Type{X}, ) where { T <: ConstraintType, U <: ExpressionType, P <: ParameterType, V <: PSY.Component, W <: AbstractDeviceFormulation, X <: PM.AbstractPowerModel, } array = get_expression(container, U(), V) _add_parameterized_upper_bound_range_constraints_impl!( container, T, array, P(), devices, model, ) return end function add_parameterized_upper_bound_range_constraints( container::OptimizationContainer, ::Type{T}, ::Type{U}, ::Type{P}, devices::Union{Vector{V}, IS.FlattenIteratorWrapper{V}}, model::DeviceModel{V, W}, ::Type{X}, ) where { T <: ConstraintType, U <: VariableType, P <: ParameterType, V <: PSY.Component, W <: AbstractDeviceFormulation, X <: PM.AbstractPowerModel, } array = get_variable(container, U(), V) _add_parameterized_upper_bound_range_constraints_impl!( container, T, array, P(), devices, model, ) return end # This function is re-used in SemiContinuousFeedforward function upper_bound_range_with_parameter!( container::OptimizationContainer, constraint_container::JuMPConstraintArray, lhs_array, param::P, devices::Union{Vector{V}, IS.FlattenIteratorWrapper{V}}, ::DeviceModel{V, W}, ) where {P <: AvailableStatusParameter, V <: PSY.Component, W <: AbstractDeviceFormulation} param_array = get_parameter_array(container, param, V) param_multiplier = get_parameter_multiplier_array(container, P(), V) jump_model = get_jump_model(container) time_steps = axes(constraint_container)[2] for device in devices, t in time_steps ub = PSY.get_max_active_power(device) name = PSY.get_name(device) constraint_container[name, t] = JuMP.@constraint( jump_model, lhs_array[name, t] <= ub * param_array[name, t] ) end return end # This function is re-used in SemiContinuousFeedforward function upper_bound_range_with_parameter!( container::OptimizationContainer, constraint_container::JuMPConstraintArray, lhs_array, param::P, devices::Union{Vector{V}, IS.FlattenIteratorWrapper{V}}, ::DeviceModel{V, W}, ) where {P <: ParameterType, V <: PSY.Component, W <: AbstractDeviceFormulation} param_array = get_parameter_array(container, param, V) param_multiplier = get_parameter_multiplier_array(container, P(), V) jump_model = get_jump_model(container) time_steps = axes(constraint_container)[2] for device in devices, t in time_steps name = PSY.get_name(device) constraint_container[name, t] = JuMP.@constraint( jump_model, lhs_array[name, t] <= param_multiplier[name, t] * param_array[name, t] ) end return end function upper_bound_range_with_parameter!( container::OptimizationContainer, constraint_container::JuMPConstraintArray, lhs_array, param::P, devices::IS.FlattenIteratorWrapper{V}, model::DeviceModel{V, W}, ) where {P <: TimeSeriesParameter, V <: PSY.Component, W <: AbstractDeviceFormulation} param_container = get_parameter(container, param, V) mult = get_multiplier_array(param_container) jump_model = get_jump_model(container) time_steps = axes(constraint_container)[2] ts_name = get_time_series_names(model)[P] ts_type = get_default_time_series_type(container) for device in devices name = PSY.get_name(device) if !(PSY.has_time_series(device, ts_type, ts_name)) continue end param = get_parameter_column_refs(param_container, name) for t in time_steps constraint_container[name, t] = JuMP.@constraint(jump_model, lhs_array[name, t] <= mult[name, t] * param[t]) end end return end function _add_parameterized_upper_bound_range_constraints_impl!( container::OptimizationContainer, ::Type{T}, array, param::P, devices::Union{Vector{V}, IS.FlattenIteratorWrapper{V}}, model::DeviceModel{V, W}, ) where { T <: ConstraintType, P <: TimeSeriesParameter, V <: PSY.Component, W <: AbstractDeviceFormulation, } time_steps = get_time_steps(container) ts_name = get_time_series_names(model)[P] ts_type = get_default_time_series_type(container) # PERF: compilation hotspot. Switch to TSC. names = [PSY.get_name(d) for d in devices if PSY.has_time_series(d, ts_type, ts_name)] if isempty(names) @debug "There are no $V devices with time series data $ts_type, $ts_name" return end constraint = add_constraints_container!(container, T(), V, names, time_steps; meta = "ub") upper_bound_range_with_parameter!(container, constraint, array, param, devices, model) return end function _add_parameterized_upper_bound_range_constraints_impl!( container::OptimizationContainer, ::Type{T}, array, param::P, devices::Union{Vector{V}, IS.FlattenIteratorWrapper{V}}, model::DeviceModel{V, W}, ) where { T <: ConstraintType, P <: ParameterType, V <: PSY.Component, W <: AbstractDeviceFormulation, } time_steps = get_time_steps(container) names = PSY.get_name.(devices) constraint = add_constraints_container!(container, T(), V, names, time_steps; meta = "ub") upper_bound_range_with_parameter!(container, constraint, array, param, devices, model) return end ================================================ FILE: src/devices_models/devices/common/rateofchange_constraints.jl ================================================ function _get_minutes_per_period(container::OptimizationContainer) resolution = get_resolution(container) if resolution > Dates.Minute(1) minutes_per_period = Dates.value(Dates.Minute(resolution)) else @warn("Not all formulations support under 1-minute resolutions. Exercise caution.") minutes_per_period = Dates.value(Dates.Second(resolution)) / 60 end return minutes_per_period end function _get_ramp_constraint_devices( container::OptimizationContainer, devices::Union{Vector{U}, IS.FlattenIteratorWrapper{U}}, ) where {U <: PSY.Component} minutes_per_period = _get_minutes_per_period(container) filtered_device = Vector{U}() for d in devices ramp_limits = PSY.get_ramp_limits(d) if ramp_limits !== nothing p_lims = PSY.get_active_power_limits(d) max_rate = abs(p_lims.min - p_lims.max) / minutes_per_period if (ramp_limits.up >= max_rate) & (ramp_limits.down >= max_rate) @debug "Generator has a nonbinding ramp limits. Constraints Skipped" PSY.get_name( d, ) continue else push!(filtered_device, d) end end end return filtered_device end function _get_ramp_slack_vars( container::OptimizationContainer, model::DeviceModel{V, W}, name::String, t::Int, ) where {V <: PSY.Component, W <: AbstractDeviceFormulation} if get_use_slacks(model) slack_ub = get_variable(container, RateofChangeConstraintSlackUp(), V) slack_lb = get_variable(container, RateofChangeConstraintSlackDown(), V) sl_ub = slack_ub[name, t] sl_lb = slack_lb[name, t] else sl_ub = 0.0 sl_lb = 0.0 end return sl_ub, sl_lb end @doc raw""" Constructs allowed rate-of-change constraints from variables, initial condtions, and rate data. If t = 1: ``` variable[name, 1] - initial_conditions[ix].value <= rate_data[1][ix].up ``` ``` initial_conditions[ix].value - variable[name, 1] <= rate_data[1][ix].down ``` If t > 1: ``` variable[name, t] - variable[name, t-1] <= rate_data[1][ix].up ``` ``` variable[name, t-1] - variable[name, t] <= rate_data[1][ix].down ``` # LaTeX `` r^{down} \leq x_1 - x_{init} \leq r^{up}, \text{ for } t = 1 `` `` r^{down} \leq x_t - x_{t-1} \leq r^{up}, \forall t \geq 2 `` """ function add_linear_ramp_constraints!( container::OptimizationContainer, T::Type{<:ConstraintType}, U::Type{S}, devices::IS.FlattenIteratorWrapper{V}, model::DeviceModel{V, W}, ::Type{<:PM.AbstractPowerModel}, ) where { S <: Union{PowerAboveMinimumVariable, ActivePowerVariable}, V <: PSY.Component, W <: AbstractDeviceFormulation, } time_steps = get_time_steps(container) variable = get_variable(container, U(), 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) expr_dn = get_expression(container, ActivePowerRangeExpressionLB(), V) expr_up = get_expression(container, ActivePowerRangeExpressionUB(), V) device_name_set = PSY.get_name.(ramp_devices) con_up = add_constraints_container!( container, T(), V, device_name_set, time_steps; meta = "up", ) con_down = add_constraints_container!( container, T(), V, device_name_set, 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 ∉ device_name_set && continue ramp_limits = PSY.get_ramp_limits(get_component(ic)) ic_power = get_value(ic) @debug "add rate_of_change_constraint" name ic_power sl_ub, sl_lb = _get_ramp_slack_vars(container, model, name, 1) con_up[name, 1] = JuMP.@constraint( get_jump_model(container), expr_up[name, 1] - ic_power - sl_ub <= ramp_limits.up * minutes_per_period ) con_down[name, 1] = JuMP.@constraint( get_jump_model(container), ic_power - expr_dn[name, 1] + sl_lb >= -1 * ramp_limits.down * minutes_per_period ) for t in time_steps[2:end] sl_ub, sl_lb = _get_ramp_slack_vars(container, model, name, t) con_up[name, t] = JuMP.@constraint( get_jump_model(container), expr_up[name, t] - variable[name, t - 1] - sl_ub <= ramp_limits.up * minutes_per_period ) con_down[name, t] = JuMP.@constraint( get_jump_model(container), variable[name, t - 1] - expr_dn[name, t] + sl_lb >= -1 * ramp_limits.down * minutes_per_period ) end end return end # Helper function containing the shared ramp constraint logic function _add_linear_ramp_constraints_impl!( container::OptimizationContainer, T::Type{<:ConstraintType}, U::Type{<:VariableType}, devices::IS.FlattenIteratorWrapper{V}, model::DeviceModel{V, W}, ) where {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) 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) device_name_set = PSY.get_name.(ramp_devices) con_up = add_constraints_container!( container, T(), V, device_name_set, time_steps; meta = "up", ) con_down = add_constraints_container!( container, T(), V, device_name_set, 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 ∉ device_name_set && continue 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 sl_ub, sl_lb = _get_ramp_slack_vars(container, model, name, 1) con_up[name, 1] = JuMP.@constraint( get_jump_model(container), variable[name, 1] - ic_power - sl_ub <= ramp_limits.up * minutes_per_period ) con_down[name, 1] = JuMP.@constraint( get_jump_model(container), ic_power - variable[name, 1] - sl_lb <= ramp_limits.down * minutes_per_period ) for t in time_steps[2:end] sl_ub, sl_lb = _get_ramp_slack_vars(container, model, name, t) con_up[name, t] = JuMP.@constraint( get_jump_model(container), variable[name, t] - variable[name, t - 1] - sl_ub <= ramp_limits.up * minutes_per_period ) con_down[name, t] = JuMP.@constraint( get_jump_model(container), variable[name, t - 1] - variable[name, t] - sl_lb <= ramp_limits.down * minutes_per_period ) end end return end function add_linear_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} return _add_linear_ramp_constraints_impl!(container, T, U, devices, model) end function add_linear_ramp_constraints!( container::OptimizationContainer, T::Type{<:ConstraintType}, U::Type{ActivePowerVariable}, devices::IS.FlattenIteratorWrapper{V}, model::DeviceModel{V, W}, X::Type{<:PM.AbstractPowerModel}, ) where {V <: PSY.ThermalGen, W <: AbstractThermalDispatchFormulation} # Fallback to generic implementation if OnStatusParameter is not present if !has_container_key(container, OnStatusParameter, V) return _add_linear_ramp_constraints_impl!(container, T, U, devices, model) end time_steps = get_time_steps(container) variable = get_variable(container, U(), 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) # Commitment path from UC as a PARAMETER (fixed 0/1) on_param = get_parameter(container, OnStatusParameter(), V) on_status = on_param.parameter_array # on_status[name, t] ∈ {0,1} (fixed) 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") jump_model = get_jump_model(container) for dev in ramp_devices name = PSY.get_name(dev) ramp_limits = PSY.get_ramp_limits(dev) power_limits = PSY.get_active_power_limits(dev) # --- t = 1: Use ic_power to determine starting ramp condition ic_idx = findfirst(ic -> get_component_name(ic) == name, initial_conditions_power) ic_power = get_value(initial_conditions_power[ic_idx]) ycur = on_status[name, 1] sl_ub, sl_lb = _get_ramp_slack_vars(container, model, name, 1) # Ramp UP from IC con_up[name, 1] = JuMP.@constraint(jump_model, variable[name, 1] - ic_power - sl_ub <= ramp_limits.up * minutes_per_period + power_limits.max * (1 - ycur) ) # Ramp DOWN from IC con_down[name, 1] = JuMP.@constraint(jump_model, ic_power - variable[name, 1] - sl_lb <= ramp_limits.down * minutes_per_period + power_limits.max * (1 - ycur) ) # --- t ≥ 2: gate by previous status y_{t-1} for t in time_steps[2:end] yprev = on_status[name, t - 1] # 0/1 fixed from UC ycur = on_status[name, t] # 0/1 fixed from UC sl_ub, sl_lb = _get_ramp_slack_vars(container, model, name, t) # Ramp UP when already ON previously con_up[name, t] = JuMP.@constraint(jump_model, variable[name, t] - variable[name, t - 1] - sl_ub <= ramp_limits.up * minutes_per_period + power_limits.max * (2 - yprev - ycur) ) # Ramp DOWN when already ON previously con_down[name, t] = JuMP.@constraint(jump_model, variable[name, t - 1] - variable[name, t] - sl_lb <= ramp_limits.down * minutes_per_period + power_limits.max * (2 - yprev - ycur) ) end end return end @doc raw""" Constructs allowed rate-of-change constraints from variables, initial condtions, start/stop status, and rate data # Equations If t = 1: ``` variable[name, 1] - initial_conditions[ix].value <= rate_data[1][ix].up + rate_data[2][ix].max*varstart[name, 1] ``` ``` initial_conditions[ix].value - variable[name, 1] <= rate_data[1][ix].down + rate_data[2][ix].min*varstop[name, 1] ``` If t > 1: ``` variable[name, t] - variable[name, t-1] <= rate_data[1][ix].up + rate_data[2][ix].max*varstart[name, t] ``` ``` variable[name, t-1] - variable[name, t] <= rate_data[1][ix].down + rate_data[2][ix].min*varstop[name, t] ``` # LaTeX `` r^{down} + r^{min} x^{stop}_1 \leq x_1 - x_{init} \leq r^{up} + r^{max} x^{start}_1, \text{ for } t = 1 `` `` r^{down} + r^{min} x^{stop}_t \leq x_t - x_{t-1} \leq r^{up} + r^{max} x^{start}_t, \forall t \geq 2 `` """ function add_semicontinuous_ramp_constraints!( container::OptimizationContainer, T::Type{<:ConstraintType}, U::Type{S}, devices::IS.FlattenIteratorWrapper{V}, model::DeviceModel{V, W}, ::Type{<:PM.AbstractPowerModel}, ) where { S <: Union{PowerAboveMinimumVariable, ActivePowerVariable}, V <: PSY.Component, W <: AbstractDeviceFormulation, } 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) expr_dn = get_expression(container, ActivePowerRangeExpressionLB(), V) expr_up = get_expression(container, ActivePowerRangeExpressionUB(), V) device_name_set = PSY.get_name.(ramp_devices) con_up = add_constraints_container!( container, T(), V, device_name_set, time_steps; meta = "up", ) con_down = add_constraints_container!( container, T(), V, device_name_set, time_steps; meta = "dn", ) for ic in initial_conditions_power component = get_component(ic) name = get_component_name(ic) # This is to filter out devices that dont need a ramping constraint name ∉ device_name_set && 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 if hasmethod(PSY.get_must_run, Tuple{V}) must_run = PSY.get_must_run(component) else must_run = false end if must_run rhs_up = ramp_limits.up * minutes_per_period rhd_dn = ramp_limits.down * minutes_per_period else rhs_up = ramp_limits.up * minutes_per_period + power_limits.min * varstart[name, 1] rhd_dn = ramp_limits.down * minutes_per_period + power_limits.min * varstop[name, 1] end sl_ub, sl_lb = _get_ramp_slack_vars(container, model, name, 1) con_up[name, 1] = JuMP.@constraint( get_jump_model(container), expr_up[name, 1] - ic_power - sl_ub <= if must_run ramp_limits.up * minutes_per_period else ramp_limits.up * minutes_per_period + power_limits.min * varstart[name, 1] end ) con_down[name, 1] = JuMP.@constraint( get_jump_model(container), ic_power - expr_dn[name, 1] - sl_lb <= if must_run ramp_limits.down * minutes_per_period else ramp_limits.down * minutes_per_period + power_limits.min * varstop[name, 1] end ) for t in time_steps[2:end] sl_ub, sl_lb = _get_ramp_slack_vars(container, model, name, t) con_up[name, t] = JuMP.@constraint( get_jump_model(container), expr_up[name, t] - variable[name, t - 1] - sl_ub <= if must_run ramp_limits.up * minutes_per_period else ramp_limits.up * minutes_per_period + power_limits.min * varstart[name, t] end ) con_down[name, t] = JuMP.@constraint( get_jump_model(container), variable[name, t - 1] - expr_dn[name, t] - sl_lb <= if must_run ramp_limits.down * minutes_per_period else ramp_limits.down * minutes_per_period + power_limits.min * varstop[name, t] end ) end end return end ================================================ FILE: src/devices_models/devices/common/set_expression.jl ================================================ """ Replaces an expression value in the expression container if the key exists """ function set_expression!( container::OptimizationContainer, ::Type{S}, cost_expression::JuMP.AbstractJuMPScalar, component::T, time_period::Int, ) where {S <: CostExpressions, T <: PSY.Component} if has_container_key(container, S, T) device_cost_expression = get_expression(container, S(), T) component_name = PSY.get_name(component) device_cost_expression[component_name, time_period] = cost_expression end return end ================================================ FILE: src/devices_models/devices/default_interface_methods.jl ================================================ ########################### Interfaces ######################################################## get_variable_key(variabletype, d) = error("Not Implemented") get_variable_binary(pv, t::Type{<:PSY.Component}, _) = error("`get_variable_binary` must be implemented for $pv and $t") get_variable_warm_start_value(_, ::PSY.Component, __) = nothing get_variable_lower_bound(_, ::PSY.Component, __) = nothing get_variable_upper_bound(_, ::PSY.Component, __) = nothing #! format: off get_multiplier_value(::StartupCostParameter, d::PSY.Device, ::AbstractDeviceFormulation) = 1.0 get_multiplier_value(::ShutdownCostParameter, d::PSY.Device, ::AbstractDeviceFormulation) = 1.0 get_multiplier_value(::AbstractCostAtMinParameter, d::PSY.Device, ::AbstractDeviceFormulation) = 1.0 get_multiplier_value(::AbstractPiecewiseLinearSlopeParameter, d::PSY.Device, ::AbstractDeviceFormulation) = 1.0 get_multiplier_value(::AbstractPiecewiseLinearBreakpointParameter, d::PSY.Device, ::AbstractDeviceFormulation) = 1.0 #! format: on get_multiplier_value(x, y::PSY.Component, z) = error( "Unable to get multiplier for parameter $x, device $(IS.summary(y)), formulation $z", ) get_expression_type_for_reserve(_, y::Type{<:PSY.Component}, z) = error("`get_expression_type_for_reserve` must be implemented for $y and $z") get_initial_conditions_device_model( ::OperationModel, ::DeviceModel{T, D}, ) where {T <: PSY.Device, D <: AbstractDeviceFormulation} = error("`get_initial_conditions_device_model` must be implemented for $T and $D") requires_initialization(::AbstractDeviceFormulation) = false does_subcomponent_exist(T::PSY.Component, S::Type{<:PSY.Component}) = error("`does_subcomponent_exist` must be implemented for $T and subcomponent type $S") _get_initial_condition_type( X::Type{<:ConstraintType}, Y::Type{<:PSY.Component}, Z::Type{<:AbstractDeviceFormulation}, ) = error("`_get_initial_condition_type` must be implemented for $X , $Y and $Z") get_initial_conditions_device_model( ::OperationModel, model::DeviceModel{T, FixedOutput}, ) where {T <: PSY.Device} = model get_default_on_variable(component::T) where {T <: PSY.Component} = OnVariable() get_default_on_parameter(component::T) where {T <: PSY.Component} = OnStatusParameter() ================================================ FILE: src/devices_models/devices/electric_loads.jl ================================================ #! format: off ########################### ElectricLoad #################################### get_variable_multiplier(_, ::Type{<:PSY.ElectricLoad}, ::AbstractLoadFormulation) = -1.0 ########################### ActivePowerVariable, ElectricLoad #################################### get_variable_binary(::ActivePowerVariable, ::Type{<:PSY.ElectricLoad}, ::AbstractLoadFormulation) = false get_variable_lower_bound(::ActivePowerVariable, d::PSY.ElectricLoad, ::AbstractLoadFormulation) = 0.0 get_variable_upper_bound(::ActivePowerVariable, d::PSY.ElectricLoad, ::AbstractLoadFormulation) = PSY.get_max_active_power(d) ########################### ReactivePowerVariable, ElectricLoad #################################### get_variable_binary(::ReactivePowerVariable, ::Type{<:PSY.ElectricLoad}, ::AbstractLoadFormulation) = false get_variable_lower_bound(::ReactivePowerVariable, d::PSY.ElectricLoad, ::AbstractLoadFormulation) = 0.0 get_variable_upper_bound(::ReactivePowerVariable, d::PSY.ElectricLoad, ::AbstractLoadFormulation) = PSY.get_max_reactive_power(d) ########################### ReactivePowerVariable, ElectricLoad #################################### get_variable_binary(::OnVariable, ::Type{<:PSY.ElectricLoad}, ::AbstractLoadFormulation) = true get_multiplier_value(::TimeSeriesParameter, d::PSY.ElectricLoad, ::StaticPowerLoad) = -1*PSY.get_max_active_power(d) get_multiplier_value(::ReactivePowerTimeSeriesParameter, d::PSY.ElectricLoad, ::StaticPowerLoad) = -1*PSY.get_max_reactive_power(d) get_multiplier_value(::TimeSeriesParameter, d::PSY.ElectricLoad, ::AbstractControllablePowerLoadFormulation) = PSY.get_max_active_power(d) ########################### ShiftablePowerLoad ##################################### get_variable_binary(::ShiftUpActivePowerVariable, ::Type{<:PSY.ElectricLoad}, ::PowerLoadShift) = false get_variable_lower_bound(::ShiftUpActivePowerVariable, d::PSY.ElectricLoad, ::PowerLoadShift) = 0.0 get_variable_upper_bound(::ShiftUpActivePowerVariable, d::PSY.ElectricLoad, ::PowerLoadShift) = nothing # Unbounded above by default, but can be limited by time series parameters get_variable_binary(::ShiftDownActivePowerVariable, ::Type{<:PSY.ElectricLoad}, ::PowerLoadShift) = false get_variable_lower_bound(::ShiftDownActivePowerVariable, d::PSY.ElectricLoad, ::PowerLoadShift) = 0.0 get_variable_upper_bound(::ShiftDownActivePowerVariable, d::PSY.ElectricLoad, ::PowerLoadShift) = nothing # Unbounded above by default, but can be limited by time series parameters ###################################################### # To avoid ambiguity with default_interface_methods.jl: get_multiplier_value(::AbstractPiecewiseLinearBreakpointParameter, ::PSY.ElectricLoad, ::StaticPowerLoad) = 1.0 get_multiplier_value(::AbstractPiecewiseLinearBreakpointParameter, ::PSY.ElectricLoad, ::AbstractControllablePowerLoadFormulation) = 1.0 ########################Objective Function################################################## proportional_cost(cost::Nothing, ::OnVariable, ::PSY.ElectricLoad, ::AbstractControllablePowerLoadFormulation)=1.0 proportional_cost(cost::PSY.OperationalCost, ::OnVariable, ::PSY.ElectricLoad, ::AbstractControllablePowerLoadFormulation)=PSY.get_fixed(cost) objective_function_multiplier(::VariableType, ::AbstractControllablePowerLoadFormulation)=OBJECTIVE_FUNCTION_NEGATIVE objective_function_multiplier(::ShiftUpActivePowerVariable, ::AbstractControllablePowerLoadFormulation)=OBJECTIVE_FUNCTION_NEGATIVE objective_function_multiplier(::ShiftDownActivePowerVariable, ::PowerLoadShift)=OBJECTIVE_FUNCTION_POSITIVE variable_cost(::Nothing, ::PSY.ElectricLoad, ::ActivePowerVariable, ::AbstractControllablePowerLoadFormulation)=1.0 variable_cost(cost::PSY.OperationalCost, ::ActivePowerVariable, ::PSY.ElectricLoad, ::AbstractControllablePowerLoadFormulation)=PSY.get_variable(cost) variable_cost(cost::PSY.OperationalCost, ::ShiftUpActivePowerVariable, ::PSY.ElectricLoad, ::AbstractControllablePowerLoadFormulation)=PSY.get_variable(cost) variable_cost(cost::PSY.OperationalCost, ::ShiftDownActivePowerVariable, ::PSY.ElectricLoad, ::AbstractControllablePowerLoadFormulation)=PSY.get_variable(cost) #! format: on function get_default_time_series_names( ::Type{<:PSY.ElectricLoad}, ::Type{<:Union{FixedOutput, AbstractLoadFormulation}}, ) return Dict{Type{<:TimeSeriesParameter}, String}( ActivePowerTimeSeriesParameter => "max_active_power", ReactivePowerTimeSeriesParameter => "max_active_power", ) end function get_default_attributes( ::Type{U}, ::Type{V}, ) where {U <: PSY.ElectricLoad, V <: Union{FixedOutput, AbstractLoadFormulation}} return Dict{String, Any}() end get_initial_conditions_device_model( ::OperationModel, ::DeviceModel{T, <:AbstractLoadFormulation}, ) where {T <: PSY.ElectricLoad} = DeviceModel(T, StaticPowerLoad) function get_default_time_series_names( ::Type{<:PSY.MotorLoad}, ::Type{<:Union{FixedOutput, AbstractLoadFormulation}}, ) return Dict{Type{<:TimeSeriesParameter}, String}() end function get_default_time_series_names( ::Type{<:PSY.ShiftablePowerLoad}, ::Type{PowerLoadShift}, ) return Dict{Type{<:TimeSeriesParameter}, String}( ActivePowerTimeSeriesParameter => "max_active_power", ReactivePowerTimeSeriesParameter => "max_active_power", ShiftUpActivePowerTimeSeriesParameter => "shift_up_max_active_power", ShiftDownActivePowerTimeSeriesParameter => "shift_down_max_active_power", ) end ####################### Expressions ######################### function add_expressions!( container::OptimizationContainer, ::Type{T}, devices::U, model::DeviceModel{D, W}, ) where { T <: RealizedShiftedLoad, U <: Union{Vector{D}, IS.FlattenIteratorWrapper{D}}, W <: PowerLoadShift, } where {D <: PSY.ShiftablePowerLoad} time_steps = get_time_steps(container) names = PSY.get_name.(devices) expression = add_expression_container!(container, T(), D, names, time_steps) shift_up = get_variable(container, ShiftUpActivePowerVariable(), D) shift_down = get_variable(container, ShiftDownActivePowerVariable(), D) param_container = get_parameter(container, ActivePowerTimeSeriesParameter(), D) multiplier = get_multiplier_array(param_container) for t in time_steps, d in devices name = PSY.get_name(d) expression[name, t] = get_parameter_column_refs(param_container, name)[t] * multiplier[name, t] + shift_up[name, t] - shift_down[name, t] 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{CopperPlatePowerModel}, ) where { T <: ActivePowerBalance, U <: RealizedShiftedLoad, V <: PSY.StaticInjection, W <: AbstractDeviceFormulation, } realized_load = get_expression(container, U(), V) expression = get_expression(container, T(), PSY.System) for d in devices device_bus = PSY.get_bus(d) ref_bus = get_reference_bus(network_model, device_bus) name = PSY.get_name(d) for t in get_time_steps(container) JuMP.add_to_expression!( expression[ref_bus, t], -1.0, # Realized load enter negative to the balance realized_load[name, t], ) end end return end """ Electric Load implementation to add parameters to PTDF ActivePowerBalance expressions """ function add_to_expression!( container::OptimizationContainer, ::Type{T}, ::Type{U}, devices::IS.FlattenIteratorWrapper{V}, device_model::DeviceModel{V, W}, network_model::NetworkModel{X}, ) where { T <: ActivePowerBalance, U <: RealizedShiftedLoad, V <: PSY.ShiftablePowerLoad, W <: PowerLoadShift, X <: AbstractPTDFModel, } realized_load = get_expression(container, U(), V) sys_expr = get_expression(container, T(), _system_expression_type(X)) nodal_expr = get_expression(container, T(), PSY.ACBus) network_reduction = get_network_reduction(network_model) for d in devices name = PSY.get_name(d) device_bus = PSY.get_bus(d) bus_no_ = PSY.get_number(device_bus) bus_no = PNM.get_mapped_bus_number(network_reduction, bus_no_) ref_index = _ref_index(network_model, device_bus) for t in get_time_steps(container) JuMP.add_to_expression!( sys_expr[ref_index, t], -1.0, # Realized load enter negative to the balance realized_load[name, t], ) JuMP.add_to_expression!( nodal_expr[bus_no, t], -1.0, # Realized load enter negative to the balance realized_load[name, t], ) end end return end ####################################### Reactive Power Constraints ######################### """ Reactive Power Constraints on Controllable Loads Assume Constant power_factor """ function add_constraints!( container::OptimizationContainer, T::Type{<:ReactivePowerVariableLimitsConstraint}, U::Type{<:ReactivePowerVariable}, devices::IS.FlattenIteratorWrapper{V}, ::DeviceModel{V, W}, network_model::NetworkModel{X}, ) where { V <: PSY.ElectricLoad, W <: AbstractControllablePowerLoadFormulation, X <: PM.AbstractPowerModel, } time_steps = get_time_steps(container) constraint = add_constraints_container!( container, T(), V, PSY.get_name.(devices), time_steps, ) jump_model = get_jump_model(container) for t in time_steps, d in devices name = PSY.get_name(d) pf = sin(atan((PSY.get_max_reactive_power(d) / PSY.get_max_active_power(d)))) reactive = get_variable(container, U(), V)[name, t] real = get_variable(container, ActivePowerVariable(), V)[name, t] constraint[name, t] = JuMP.@constraint(jump_model, reactive == real * pf) end end function add_constraints!( container::OptimizationContainer, ::Type{ActivePowerVariableLimitsConstraint}, U::Type{<:VariableType}, devices::IS.FlattenIteratorWrapper{V}, model::DeviceModel{V, W}, ::NetworkModel{X}, ) where {V <: PSY.ControllableLoad, W <: PowerLoadDispatch, X <: PM.AbstractPowerModel} add_parameterized_upper_bound_range_constraints( container, ActivePowerVariableTimeSeriesLimitsConstraint, U, ActivePowerTimeSeriesParameter, devices, model, X, ) return end function add_constraints!( container::OptimizationContainer, T::Type{ActivePowerVariableLimitsConstraint}, U::Type{<:VariableType}, devices::IS.FlattenIteratorWrapper{V}, model::DeviceModel{V, W}, ::NetworkModel{X}, ) where {V <: PSY.ControllableLoad, W <: PowerLoadInterruption, X <: PM.AbstractPowerModel} add_parameterized_upper_bound_range_constraints( container, ActivePowerVariableTimeSeriesLimitsConstraint, U, ActivePowerTimeSeriesParameter, devices, model, X, ) return end function add_constraints!( container::OptimizationContainer, T::Type{ActivePowerVariableLimitsConstraint}, U::Type{OnVariable}, devices::IS.FlattenIteratorWrapper{V}, model::DeviceModel{V, W}, ::NetworkModel{X}, ) where {V <: PSY.ControllableLoad, W <: PowerLoadInterruption, X <: PM.AbstractPowerModel} time_steps = get_time_steps(container) constraint = add_constraints_container!( container, T(), V, PSY.get_name.(devices), time_steps; meta = "binary", ) on_variable = get_variable(container, U(), V) power = get_variable(container, ActivePowerVariable(), V) jump_model = get_jump_model(container) for t in time_steps, d in devices name = PSY.get_name(d) pmax = PSY.get_max_active_power(d) constraint[name, t] = JuMP.@constraint(jump_model, power[name, t] <= on_variable[name, t] * pmax) end return end function add_constraints!( container::OptimizationContainer, T::Type{ShiftedActivePowerBalanceConstraint}, devices::IS.FlattenIteratorWrapper{V}, model::DeviceModel{V, W}, ::NetworkModel{X}, ) where {V <: PSY.ShiftablePowerLoad, W <: PowerLoadShift, X <: PM.AbstractPowerModel} time_steps = get_time_steps(container) constraint = add_constraints_container!( container, T(), V, PSY.get_name.(devices), ) up_variable = get_variable(container, ShiftUpActivePowerVariable(), V) down_variable = get_variable(container, ShiftDownActivePowerVariable(), V) jump_model = get_jump_model(container) for d in devices name = PSY.get_name(d) constraint[name] = JuMP.@constraint( jump_model, sum(up_variable[name, t] - down_variable[name, t] for t in time_steps) == 0.0 ) end additional_balance_interval = PSI.get_attribute(model, "additional_balance_interval") if !isnothing(additional_balance_interval) constraint_aux = add_constraints_container!( container, T(), V, PSY.get_name.(devices); meta = "additional", ) resolution = PSI.get_resolution(container) interval_length = Dates.Millisecond(additional_balance_interval).value ÷ Dates.Millisecond(resolution).value for d in devices name = PSY.get_name(d) constraint_aux[name] = JuMP.@constraint( container.JuMPmodel, sum( up_variable[name, t] - down_variable[name, t] for t in 1:interval_length ) == 0.0 ) end end return end function add_constraints!( container::OptimizationContainer, T::Type{RealizedShiftedLoadMinimumBoundConstraint}, U::Type{<:ExpressionType}, devices::IS.FlattenIteratorWrapper{V}, ::DeviceModel{V, W}, ::NetworkModel{X}, ) where {V <: PSY.ShiftablePowerLoad, W <: PowerLoadShift, X <: PM.AbstractPowerModel} time_steps = get_time_steps(container) constraint = add_constraints_container!( container, T(), V, PSY.get_name.(devices), time_steps, ) realized_load = get_expression(container, U(), V) jump_model = get_jump_model(container) for d in devices, t in time_steps name = PSY.get_name(d) constraint[name, t] = JuMP.@constraint(jump_model, realized_load[name, t] >= 0.0) end return end function add_constraints!( container::OptimizationContainer, T::Type{NonAnticipativityConstraint}, devices::IS.FlattenIteratorWrapper{V}, ::DeviceModel{V, W}, ::NetworkModel{X}, ) where {V <: PSY.ShiftablePowerLoad, W <: PowerLoadShift, X <: PM.AbstractPowerModel} time_steps = get_time_steps(container) constraint = add_constraints_container!( container, T(), V, PSY.get_name.(devices), time_steps, ) up_variable = get_variable(container, ShiftUpActivePowerVariable(), V) down_variable = get_variable(container, ShiftDownActivePowerVariable(), V) jump_model = get_jump_model(container) for d in devices name = PSY.get_name(d) for t in time_steps constraint[name, t] = JuMP.@constraint( jump_model, sum(down_variable[name, τ] - up_variable[name, τ] for τ in 1:t) >= 0.0 ) end end return end function add_constraints!( container::OptimizationContainer, ::Type{ShiftUpActivePowerVariableLimitsConstraint}, U::Type{<:VariableType}, devices::IS.FlattenIteratorWrapper{V}, model::DeviceModel{V, W}, ::NetworkModel{X}, ) where {V <: PSY.ShiftablePowerLoad, W <: PowerLoadShift, X <: PM.AbstractPowerModel} add_parameterized_upper_bound_range_constraints( container, ShiftUpActivePowerVariableLimitsConstraint, U, ShiftUpActivePowerTimeSeriesParameter, devices, model, X, ) return end function add_constraints!( container::OptimizationContainer, ::Type{ShiftDownActivePowerVariableLimitsConstraint}, U::Type{<:VariableType}, devices::IS.FlattenIteratorWrapper{V}, model::DeviceModel{V, W}, ::NetworkModel{X}, ) where {V <: PSY.ShiftablePowerLoad, W <: PowerLoadShift, X <: PM.AbstractPowerModel} add_parameterized_upper_bound_range_constraints( container, ShiftDownActivePowerVariableLimitsConstraint, U, ShiftDownActivePowerTimeSeriesParameter, devices, model, X, ) return end ############################## FormulationControllable Load Cost ########################### function objective_function!( container::OptimizationContainer, devices::IS.FlattenIteratorWrapper{T}, ::DeviceModel{T, U}, ::Type{<:PM.AbstractPowerModel}, ) where {T <: PSY.ControllableLoad, U <: PowerLoadDispatch} add_variable_cost!(container, ActivePowerVariable(), devices, U()) return end function objective_function!( container::OptimizationContainer, devices::IS.FlattenIteratorWrapper{T}, ::DeviceModel{T, U}, ::Type{<:PM.AbstractPowerModel}, ) where {T <: PSY.ControllableLoad, U <: PowerLoadInterruption} add_variable_cost!(container, ActivePowerVariable(), devices, U()) add_proportional_cost!(container, OnVariable(), devices, U()) return end # code repetition: basically copy-paste from thermal_generation.jl, just change types # and incremental to decremental. function proportional_cost( container::OptimizationContainer, cost::PSY.LoadCost, S::OnVariable, T::PSY.ControllableLoad, U::PowerLoadInterruption, t::Int, ) return onvar_cost(container, cost, S, T, U, t) + PSY.get_constant_term(PSY.get_vom_cost(PSY.get_variable(cost))) + PSY.get_fixed(cost) end function onvar_cost( container::OptimizationContainer, cost::PSY.LoadCost, ::OnVariable, d::PSY.ControllableLoad, ::PowerLoadInterruption, t::Int, ) return _onvar_cost(container, PSY.get_variable(cost), d, t) end is_time_variant_term( ::OptimizationContainer, ::PSY.LoadCost, ::OnVariable, ::PSY.ControllableLoad, ::AbstractLoadFormulation, ::Int, ) = false is_time_variant_term( ::OptimizationContainer, cost::PSY.MarketBidCost, ::OnVariable, ::PSY.ControllableLoad, ::PowerLoadInterruption, ::Int, ) = is_time_variant(PSY.get_decremental_initial_input(cost)) proportional_cost( container::OptimizationContainer, cost::PSY.MarketBidCost, ::OnVariable, comp::PSY.ControllableLoad, ::PowerLoadInterruption, t::Int, ) = _lookup_maybe_time_variant_param(container, comp, t, Val(is_time_variant(PSY.get_decremental_initial_input(cost))), PSY.get_initial_input ∘ PSY.get_decremental_offer_curves ∘ PSY.get_operation_cost, DecrementalCostAtMinParameter()) ########## PowerLoadShift Formulation Costs ############### function objective_function!( container::OptimizationContainer, devices::IS.FlattenIteratorWrapper{T}, ::DeviceModel{T, U}, ::Type{<:PM.AbstractPowerModel}, ) where {T <: PSY.ShiftablePowerLoad, U <: PowerLoadShift} add_variable_cost!(container, ShiftUpActivePowerVariable(), devices, U()) add_variable_cost!(container, ShiftDownActivePowerVariable(), devices, U()) return end ### Special Method to skip VOM cost on ShiftUpActivePowerVariable ### function add_variable_cost!( container::OptimizationContainer, ::U, devices::IS.FlattenIteratorWrapper{T}, ::V, ) where {T <: PSY.ShiftablePowerLoad, U <: ShiftUpActivePowerVariable, V <: PowerLoadShift} 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 ================================================ FILE: src/devices_models/devices/reactivepower_device.jl ================================================ #! format: off requires_initialization(::AbstractReactivePowerDeviceFormulation) = false get_variable_multiplier(_, ::Type{<:PSY.SynchronousCondenser}, ::AbstractReactivePowerDeviceFormulation) = 1.0 ############## ReactivePowerVariable, SynchronousCondensers #################### get_variable_binary(::ReactivePowerVariable, ::Type{PSY.SynchronousCondenser}, ::AbstractReactivePowerDeviceFormulation) = false get_variable_warm_start_value(::ReactivePowerVariable, d::PSY.SynchronousCondenser, ::AbstractReactivePowerDeviceFormulation) = PSY.get_reactive_power(d) get_variable_lower_bound(::ReactivePowerVariable, d::PSY.SynchronousCondenser, ::AbstractReactivePowerDeviceFormulation) = isnothing(PSY.get_reactive_power_limits(d)) ? nothing : PSY.get_reactive_power_limits(d).min get_variable_upper_bound(::ReactivePowerVariable, d::PSY.SynchronousCondenser, ::AbstractReactivePowerDeviceFormulation) = isnothing(PSY.get_reactive_power_limits(d)) ? nothing : PSY.get_reactive_power_limits(d).max #! format: on function get_initial_conditions_device_model( model::OperationModel, ::DeviceModel{T, D}, ) where {T <: PSY.SynchronousCondenser, D <: AbstractReactivePowerDeviceFormulation} return DeviceModel(T, SynchronousCondenserBasicDispatch) end function get_default_attributes( ::Type{U}, ::Type{V}, ) where {U <: PSY.SynchronousCondenser, V <: AbstractReactivePowerDeviceFormulation} return Dict{String, Any}() end function get_default_time_series_names( ::Type{<:PSY.SynchronousCondenser}, ::Type{<:AbstractReactivePowerDeviceFormulation}, ) return Dict{Type{<:TimeSeriesParameter}, String}() end ================================================ FILE: src/devices_models/devices/regulation_device.jl ================================================ #! format: off get_variable_multiplier(_, ::Type{PSY.RegulationDevice{PSY.ThermalStandard}}, ::DeviceLimitedRegulation) = NaN ############################ DeltaActivePowerUpVariable, RegulationDevice ########################### get_variable_binary(::DeltaActivePowerUpVariable, ::Type{<:PSY.RegulationDevice}, ::AbstractRegulationFormulation) = false get_variable_lower_bound(::DeltaActivePowerUpVariable, ::PSY.RegulationDevice, ::AbstractRegulationFormulation) = 0.0 ############################ DeltaActivePowerDownVariable, RegulationDevice ########################### get_variable_binary(::DeltaActivePowerDownVariable, ::Type{<:PSY.RegulationDevice}, ::AbstractRegulationFormulation) = false get_variable_lower_bound(::DeltaActivePowerDownVariable, ::PSY.RegulationDevice, ::AbstractRegulationFormulation) = 0.0 ############################ AdditionalDeltaActivePowerUpVariable, RegulationDevice ########################### get_variable_binary(::AdditionalDeltaActivePowerUpVariable, ::Type{<:PSY.RegulationDevice}, ::AbstractRegulationFormulation) = false get_variable_lower_bound(::AdditionalDeltaActivePowerUpVariable, ::PSY.RegulationDevice, ::AbstractRegulationFormulation) = 0.0 ############################ AdditionalDeltaActivePowerDownVariable, RegulationDevice ########################### get_variable_binary(::AdditionalDeltaActivePowerDownVariable, ::Type{<:PSY.RegulationDevice}, ::AbstractRegulationFormulation) = false get_variable_lower_bound(::AdditionalDeltaActivePowerDownVariable, ::PSY.RegulationDevice, ::AbstractRegulationFormulation) = 0.0 get_multiplier_value(::ActivePowerTimeSeriesParameter, d::PSY.RegulationDevice, _) = PSY.get_max_active_power(d) ########################Objective Function################################################## proportional_cost(::PSY.OperationalCost, ::AdditionalDeltaActivePowerUpVariable, d::PSY.RegulationDevice, ::AbstractRegulationFormulation) = isapprox(PSY.get_participation_factor(d).up, 0.0; atol=1e-2) ? SERVICES_SLACK_COST : 1 / PSY.get_participation_factor(d).up proportional_cost(::PSY.OperationalCost, ::AdditionalDeltaActivePowerDownVariable, d::PSY.RegulationDevice, ::AbstractRegulationFormulation) = isapprox(PSY.get_participation_factor(d).dn, 0.0; atol=1e-2) ? SERVICES_SLACK_COST : 1 / PSY.get_participation_factor(d).dn objective_function_multiplier(::VariableType, ::AbstractRegulationFormulation)=OBJECTIVE_FUNCTION_POSITIVE #! format: on function get_default_time_series_names( ::Type{<:PSY.RegulationDevice{T}}, ::Type{<:AbstractRegulationFormulation}, ) where {T <: PSY.StaticInjection} return Dict{Type{<:TimeSeriesParameter}, String}( ActivePowerTimeSeriesParameter => "max_active_power", ) end function get_default_attributes( ::Type{<:PSY.RegulationDevice{T}}, ::Type{<:AbstractRegulationFormulation}, ) where {T <: PSY.StaticInjection} return Dict{String, Any}() end function add_constraints!( container::OptimizationContainer, ::Type{S}, ::Type{DeltaActivePowerUpVariable}, devices::IS.FlattenIteratorWrapper{T}, ::DeviceModel{T, DeviceLimitedRegulation}, ::NetworkModel{AreaBalancePowerModel}, ) where { S <: RegulationLimitsConstraint, T <: PSY.RegulationDevice{U}, } where {U <: PSY.StaticInjection} var_up = get_variable(container, DeltaActivePowerUpVariable(), T) var_dn = get_variable(container, DeltaActivePowerDownVariable(), T) base_points_param = get_parameter(container, ActivePowerTimeSeriesParameter(), T) multiplier = get_multiplier_array(base_points_param) names = [PSY.get_name(g) for g in devices] time_steps = get_time_steps(container) container_up = add_constraints_container!(container, S(), U, names, time_steps; meta = "up") container_dn = add_constraints_container!(container, S(), U, names, time_steps; meta = "dn") for d in devices name = PSY.get_name(d) limits = PSY.get_active_power_limits(d) param = get_parameter_column_refs(base_points_param, name) for t in time_steps container_up[name, t] = JuMP.@constraint( container.JuMPmodel, var_up[name, t] <= limits.max - param[t] * multiplier[name, t] ) container_dn[name, t] = JuMP.@constraint( container.JuMPmodel, var_dn[name, t] <= param[t] * multiplier[name, t] - limits.min ) end end return end function add_constraints!( container::OptimizationContainer, ::Type{S}, ::Type{DeltaActivePowerUpVariable}, devices::IS.FlattenIteratorWrapper{T}, ::DeviceModel{T, ReserveLimitedRegulation}, ::NetworkModel{AreaBalancePowerModel}, ) where { S <: RegulationLimitsConstraint, T <: PSY.RegulationDevice{U}, } where {U <: PSY.StaticInjection} var_up = get_variable(container, DeltaActivePowerUpVariable(), T) var_dn = get_variable(container, DeltaActivePowerDownVariable(), T) names = [PSY.get_name(g) for g in devices] time_steps = get_time_steps(container) container_up = add_constraints_container!(container, S(), U, names, time_steps; meta = "up") container_dn = add_constraints_container!(container, S(), U, names, time_steps; meta = "dn") for d in devices name = PSY.get_name(d) limit_up = PSY.get_reserve_limit_up(d) limit_dn = PSY.get_reserve_limit_dn(d) for t in time_steps container_up[name, t] = JuMP.@constraint(container.JuMPmodel, var_up[name, t] <= limit_up) container_dn[name, t] = JuMP.@constraint(container.JuMPmodel, var_dn[name, t] <= limit_dn) end end return end function add_constraints!( container::OptimizationContainer, ::Type{S}, devices::IS.FlattenIteratorWrapper{T}, ::DeviceModel{T, DeviceLimitedRegulation}, ::NetworkModel{AreaBalancePowerModel}, ) where { S <: RampLimitConstraint, T <: PSY.RegulationDevice{U}, } where {U <: PSY.StaticInjection} R_up = get_variable(container, DeltaActivePowerUpVariable(), T) R_dn = get_variable(container, DeltaActivePowerDownVariable(), T) resolution = Dates.value(Dates.Minute(get_resolution(container))) names = [PSY.get_name(g) for g in devices] time_steps = get_time_steps(container) container_up = add_constraints_container!(container, S(), U, names, time_steps; meta = "up") container_dn = add_constraints_container!(container, S(), U, names, time_steps; meta = "dn") for d in devices ramp_limits = PSY.get_ramp_limits(d) ramp_limits === nothing && continue scaling_factor = resolution * SECONDS_IN_MINUTE name = PSY.get_name(d) for t in time_steps container_up[name, t] = JuMP.@constraint( container.JuMPmodel, R_up[name, t] <= ramp_limits.up * scaling_factor ) container_dn[name, t] = JuMP.@constraint( container.JuMPmodel, R_dn[name, t] <= ramp_limits.down * scaling_factor ) end end return end function add_constraints!( container::OptimizationContainer, ::Type{S}, devices::IS.FlattenIteratorWrapper{T}, ::DeviceModel{T, <:AbstractRegulationFormulation}, ::NetworkModel{AreaBalancePowerModel}, ) where { S <: ParticipationAssignmentConstraint, T <: PSY.RegulationDevice{U}, } where {U <: PSY.StaticInjection} time_steps = get_time_steps(container) R_up = get_variable(container, DeltaActivePowerUpVariable(), T) R_dn = get_variable(container, DeltaActivePowerDownVariable(), T) R_up_emergency = get_variable(container, AdditionalDeltaActivePowerUpVariable(), T) R_dn_emergency = get_variable(container, AdditionalDeltaActivePowerUpVariable(), T) area_reserve_up = get_variable(container, DeltaActivePowerUpVariable(), PSY.AGC) area_reserve_dn = get_variable(container, DeltaActivePowerDownVariable(), PSY.AGC) component_names = PSY.get_name.(devices) participation_assignment_up = add_constraints_container!( container, S(), T, component_names, time_steps; meta = "up", ) participation_assignment_dn = add_constraints_container!( container, S(), T, component_names, time_steps; meta = "dn", ) expr_up = get_expression(container, EmergencyUp(), PSY.Area) expr_dn = get_expression(container, EmergencyDown(), PSY.Area) for d in devices name = PSY.get_name(d) services = PSY.get_services(d) if length(services) > 1 device_agc = (a for a in PSY.get_services(d) if isa(a, PSY.AGC)) agc_name = PSY.get_name.(device_agc)[1] area_name = PSY.get_name.(PSY.get_area.(device_agc))[1] else device_agc = first(services) agc_name = PSY.get_name(device_agc) area_name = PSY.get_name(PSY.get_area(device_agc)) end p_factor = PSY.get_participation_factor(d) for t in time_steps participation_assignment_up[name, t] = JuMP.@constraint( container.JuMPmodel, R_up[name, t] == (p_factor.up * area_reserve_up[agc_name, t]) + R_up_emergency[name, t] ) participation_assignment_dn[name, t] = JuMP.@constraint( container.JuMPmodel, R_dn[name, t] == (p_factor.dn * area_reserve_dn[agc_name, t]) + R_dn_emergency[name, t] ) JuMP.add_to_expression!(expr_up[area_name, t], -1.0, R_up_emergency[name, t]) JuMP.add_to_expression!(expr_dn[area_name, t], -1.0, R_dn_emergency[name, t]) end end return end function objective_function!( container::OptimizationContainer, devs::IS.FlattenIteratorWrapper{T}, ::DeviceModel{T, U}, ) where { T <: PSY.RegulationDevice{V}, U <: AbstractRegulationFormulation, } where {V <: PSY.StaticInjection} add_proportional_cost!(container, AdditionalDeltaActivePowerUpVariable(), devs, U()) add_proportional_cost!(container, AdditionalDeltaActivePowerDownVariable(), devs, U()) return end ================================================ FILE: src/devices_models/devices/renewable_generation.jl ================================================ #! format: off get_variable_multiplier(_, ::Type{<:PSY.RenewableGen}, ::AbstractRenewableFormulation) = 1.0 get_expression_type_for_reserve(::ActivePowerReserveVariable, ::Type{<:PSY.RenewableGen}, ::Type{<:PSY.Reserve{PSY.ReserveUp}}) = ActivePowerRangeExpressionUB get_expression_type_for_reserve(::ActivePowerReserveVariable, ::Type{<:PSY.RenewableGen}, ::Type{<:PSY.Reserve{PSY.ReserveDown}}) = ActivePowerRangeExpressionLB ########################### Parameter related set functions ################################ get_parameter_multiplier(::VariableValueParameter, d::PSY.RenewableGen, ::AbstractRenewableFormulation) = 1.0 ########################### ActivePowerVariable, RenewableGen ################################# get_variable_binary(::ActivePowerVariable, ::Type{<:PSY.RenewableGen}, ::AbstractRenewableFormulation) = false get_min_max_limits(d::PSY.RenewableGen, ::Type{ActivePowerVariableLimitsConstraint}, ::Type{<:AbstractRenewableFormulation}) = (min = 0.0, max = PSY.get_max_active_power(d)) get_variable_lower_bound(::ActivePowerVariable, d::PSY.RenewableGen, ::AbstractRenewableFormulation) = 0.0 get_variable_upper_bound(::ActivePowerVariable, d::PSY.RenewableGen, ::AbstractRenewableFormulation) = PSY.get_max_active_power(d) ########################### ReactivePowerVariable, RenewableGen ################################# get_variable_binary(::ReactivePowerVariable, ::Type{<:PSY.RenewableGen}, ::AbstractRenewableFormulation) = false get_multiplier_value(::TimeSeriesParameter, d::PSY.RenewableGen, ::FixedOutput) = PSY.get_max_active_power(d) get_multiplier_value(::TimeSeriesParameter, d::PSY.RenewableGen, ::AbstractRenewableFormulation) = PSY.get_max_active_power(d) # To avoid ambiguity with default_interface_methods.jl: get_multiplier_value(::AbstractPiecewiseLinearBreakpointParameter, ::PSY.RenewableGen, ::FixedOutput) = 1.0 get_multiplier_value(::AbstractPiecewiseLinearBreakpointParameter, ::PSY.RenewableGen, ::AbstractRenewableFormulation) = 1.0 ########################Objective Function################################################## objective_function_multiplier(::ActivePowerVariable, ::AbstractRenewableDispatchFormulation)=OBJECTIVE_FUNCTION_NEGATIVE variable_cost(::Nothing, ::ActivePowerVariable, ::PSY.RenewableDispatch, ::AbstractRenewableDispatchFormulation)=0.0 variable_cost(cost::PSY.OperationalCost, ::ActivePowerVariable, ::PSY.RenewableDispatch, ::AbstractRenewableDispatchFormulation)=PSY.get_variable(cost) #! format: on get_initial_conditions_device_model( ::OperationModel, ::DeviceModel{T, <:AbstractRenewableFormulation}, ) where {T <: PSY.RenewableGen} = DeviceModel(T, RenewableFullDispatch) get_initial_conditions_device_model( ::OperationModel, ::DeviceModel{T, FixedOutput}, ) where {T <: PSY.RenewableGen} = DeviceModel(T, FixedOutput) function get_min_max_limits( device, ::Type{ReactivePowerVariableLimitsConstraint}, ::Type{<:AbstractRenewableFormulation}, ) return PSY.get_reactive_power_limits(device) end function get_default_time_series_names( ::Type{<:PSY.RenewableGen}, ::Type{<:Union{FixedOutput, AbstractRenewableFormulation}}, ) return Dict{Type{<:TimeSeriesParameter}, String}( ActivePowerTimeSeriesParameter => "max_active_power", ReactivePowerTimeSeriesParameter => "max_active_power", ) end function get_default_attributes( ::Type{<:PSY.RenewableGen}, ::Type{<:Union{FixedOutput, AbstractRenewableFormulation}}, ) return Dict{String, Any}() end ####################################### Reactive Power constraint_infos ######################### function add_constraints!( container::OptimizationContainer, T::Type{<:ReactivePowerVariableLimitsConstraint}, U::Type{<:ReactivePowerVariable}, devices::IS.FlattenIteratorWrapper{V}, model::DeviceModel{V, W}, ::NetworkModel{X}, ) where {V <: PSY.RenewableGen, W <: AbstractDeviceFormulation, X <: PM.AbstractPowerModel} add_range_constraints!(container, T, U, devices, model, X) return end """ Reactive Power Constraints on Renewable Gen Constant power_factor """ function add_constraints!( container::OptimizationContainer, ::Type{<:ReactivePowerVariableLimitsConstraint}, ::Type{<:ReactivePowerVariable}, devices::IS.FlattenIteratorWrapper{V}, ::DeviceModel{V, W}, ::NetworkModel{X}, ) where { V <: PSY.RenewableGen, W <: RenewableConstantPowerFactor, X <: PM.AbstractPowerModel, } names = PSY.get_name.(devices) time_steps = get_time_steps(container) p_var = get_variable(container, ActivePowerVariable(), V) q_var = get_variable(container, ReactivePowerVariable(), V) jump_model = get_jump_model(container) constraint = add_constraints_container!(container, EqualityConstraint(), V, names, time_steps) for t in time_steps, d in devices name = PSY.get_name(d) pf = sin(acos(PSY.get_power_factor(d))) constraint[name, t] = JuMP.@constraint(jump_model, q_var[name, t] == p_var[name, t] * pf) end end function add_constraints!( container::OptimizationContainer, ::Type{ActivePowerVariableLimitsConstraint}, U::Type{<:Union{VariableType, ActivePowerRangeExpressionUB}}, devices::IS.FlattenIteratorWrapper{V}, model::DeviceModel{V, W}, ::NetworkModel{X}, ) where { V <: PSY.RenewableGen, W <: AbstractRenewableDispatchFormulation, X <: PM.AbstractPowerModel, } add_parameterized_upper_bound_range_constraints( container, ActivePowerVariableTimeSeriesLimitsConstraint, U, ActivePowerTimeSeriesParameter, devices, model, X, ) return end function add_constraints!( container::OptimizationContainer, T::Type{ActivePowerVariableLimitsConstraint}, U::Type{ActivePowerRangeExpressionLB}, devices::IS.FlattenIteratorWrapper{V}, model::DeviceModel{V, W}, ::NetworkModel{X}, ) where { V <: PSY.RenewableGen, W <: AbstractRenewableDispatchFormulation, X <: PM.AbstractPowerModel, } add_range_constraints!( container, T, U, devices, model, X, ) return end ##################################### renewable generation cost ############################ function objective_function!( container::OptimizationContainer, devices::IS.FlattenIteratorWrapper{T}, ::DeviceModel{T, U}, ::Type{<:PM.AbstractPowerModel}, ) where {T <: PSY.RenewableGen, U <: AbstractRenewableDispatchFormulation} add_variable_cost!(container, ActivePowerVariable(), devices, U()) add_curtailment_cost!(container, ActivePowerVariable(), devices, U()) return end ================================================ FILE: src/devices_models/devices/source.jl ================================================ #! format: off requires_initialization(::ImportExportSourceModel) = false get_variable_multiplier(::ActivePowerOutVariable, ::Type{<:PSY.Source}, ::AbstractSourceFormulation) = 1.0 get_variable_multiplier(::ActivePowerInVariable, ::Type{<:PSY.Source}, ::AbstractSourceFormulation) = -1.0 get_variable_multiplier(::ReactivePowerVariable, ::Type{<:PSY.Source}, ::AbstractSourceFormulation) = 1.0 ############## ActivePowerVariables, Source #################### get_variable_binary(::ActivePowerInVariable, ::Type{<:PSY.Source}, ::AbstractSourceFormulation) = false get_variable_binary(::ActivePowerOutVariable, ::Type{<:PSY.Source}, ::AbstractSourceFormulation) = false get_variable_lower_bound(::ActivePowerInVariable, d::PSY.Source, ::AbstractSourceFormulation) = 0.0 get_variable_lower_bound(::ActivePowerOutVariable, d::PSY.Source, ::AbstractSourceFormulation) = 0.0 get_variable_upper_bound(::ActivePowerInVariable, d::PSY.Source, ::AbstractSourceFormulation) = -PSY.get_active_power_limits(d).min get_variable_upper_bound(::ActivePowerOutVariable, d::PSY.Source, ::AbstractSourceFormulation) = PSY.get_active_power_limits(d).max ############## ReactivePowerVariable, Source #################### get_variable_binary(::ReactivePowerVariable, ::Type{<:PSY.Source}, ::AbstractSourceFormulation) = false get_variable_lower_bound(::ReactivePowerVariable, d::PSY.Source, ::AbstractSourceFormulation) = PSY.get_reactive_power_limits(d).min get_variable_upper_bound(::ReactivePowerVariable, d::PSY.Source, ::AbstractSourceFormulation) = PSY.get_reactive_power_limits(d).max get_multiplier_value(::ActivePowerTimeSeriesParameter, d::PSY.Source, ::AbstractSourceFormulation) = PSY.get_active_power_limits(d).max get_multiplier_value(::ActivePowerOutTimeSeriesParameter, d::PSY.Source, ::AbstractSourceFormulation) = PSY.get_active_power_limits(d).max get_multiplier_value(::ActivePowerInTimeSeriesParameter, d::PSY.Source, ::AbstractSourceFormulation) = PSY.get_active_power_limits(d).max get_multiplier_value(::ActivePowerTimeSeriesParameter, d::PSY.Source, ::FixedOutput) = PSY.get_active_power_limits(d).max get_multiplier_value(::ActivePowerOutTimeSeriesParameter, d::PSY.Source, ::FixedOutput) = PSY.get_active_power_limits(d).max get_multiplier_value(::ActivePowerInTimeSeriesParameter, d::PSY.Source, ::FixedOutput) = PSY.get_active_power_limits(d).min # This additional method definition is used to avoid ambiguity with the method defined in default_interface_methods.jl get_multiplier_value(::AbstractPiecewiseLinearBreakpointParameter, d::PSY.Source, ::AbstractSourceFormulation) = 1.0 ############## ReservationVariable, Source #################### get_variable_binary(::ReservationVariable, ::Type{<:PSY.Source}, ::ImportExportSourceModel) = true #! format: on function get_default_time_series_names( ::Type{U}, ::Type{V}, ) where {U <: PSY.Source, V <: Union{FixedOutput, AbstractSourceFormulation}} return Dict{Type{<:TimeSeriesParameter}, String}( ActivePowerOutTimeSeriesParameter => "max_active_power_out", ActivePowerInTimeSeriesParameter => "max_active_power_in", ) end function get_default_attributes( ::Type{U}, ::Type{V}, ) where {U <: PSY.Source, V <: Union{FixedOutput, AbstractSourceFormulation}} return Dict{String, Any}( "reservation" => true, ) end function get_min_max_limits( device, ::Type{ActivePowerVariableLimitsConstraint}, ::Type{<:AbstractSourceFormulation}, ) return PSY.get_active_power_limits(device) end function get_min_max_limits( device, ::Type{InputActivePowerVariableLimitsConstraint}, ::Type{<:AbstractSourceFormulation}, ) return PSY.get_active_power_limits(device) # TODO do we need a new field in PSY for this -- input active power limits? end function get_min_max_limits( device, ::Type{ReactivePowerVariableLimitsConstraint}, ::Type{<:AbstractSourceFormulation}, ) return PSY.get_reactive_power_limits(device) end ##### Constraints ###### function add_constraints!( container::OptimizationContainer, T::Type{<:PowerVariableLimitsConstraint}, U::Type{<:Union{VariableType, ExpressionType}}, devices::IS.FlattenIteratorWrapper{V}, model::DeviceModel{V, W}, ::NetworkModel{X}, ) where { V <: PSY.Source, W <: AbstractSourceFormulation, X <: PM.AbstractPowerModel, } if get_attribute(model, "reservation") add_reserve_range_constraints!(container, T, U, devices, model, X) else add_range_constraints!(container, T, U, devices, model, X) end return end function add_constraints!( container::OptimizationContainer, T::Type{ImportExportBudgetConstraint}, devices::IS.FlattenIteratorWrapper{U}, model::DeviceModel{U, V}, network_model::NetworkModel{X}, ) where { U <: PSY.Source, V <: AbstractSourceFormulation, X <: PM.AbstractPowerModel, } time_steps = get_time_steps(container) resolution = get_resolution(container) resolution_in_hours = Dates.Hour(resolution).value hours_in_horizon = length(time_steps) * resolution_in_hours p_out = get_variable(container, ActivePowerOutVariable(), U) p_in = get_variable(container, ActivePowerInVariable(), U) names = PSY.get_name.(devices) constraint_export = add_constraints_container!( container, ImportExportBudgetConstraint(), U, names; meta = "export", ) constraint_import = add_constraints_container!( container, ImportExportBudgetConstraint(), U, names; meta = "import", ) for d in devices name = PSY.get_name(d) op_cost = PSY.get_operation_cost(d) week_import_limit = PSY.get_energy_import_weekly_limit(op_cost) week_export_limit = PSY.get_energy_export_weekly_limit(op_cost) constraint_import[name] = JuMP.@constraint( get_jump_model(container), resolution_in_hours * sum(p_out[name, t] for t in time_steps) <= week_import_limit * (hours_in_horizon / HOURS_IN_WEEK) ) constraint_export[name] = JuMP.@constraint( get_jump_model(container), resolution_in_hours * sum(p_in[name, t] for t in time_steps) <= week_export_limit * (hours_in_horizon / HOURS_IN_WEEK) ) end return end function add_constraints!( container::OptimizationContainer, ::Type{ActivePowerOutVariableTimeSeriesLimitsConstraint}, U::Type{<:Union{ActivePowerOutVariable, ActivePowerRangeExpressionUB}}, devices::IS.FlattenIteratorWrapper{V}, model::DeviceModel{V, W}, ::NetworkModel{X}, ) where { V <: PSY.Source, W <: AbstractSourceFormulation, X <: PM.AbstractPowerModel, } add_parameterized_upper_bound_range_constraints( container, ActivePowerOutVariableTimeSeriesLimitsConstraint, U, ActivePowerOutTimeSeriesParameter, devices, model, X, ) return end function add_constraints!( container::OptimizationContainer, ::Type{ActivePowerInVariableTimeSeriesLimitsConstraint}, U::Type{<:Union{ActivePowerInVariable, ActivePowerRangeExpressionUB}}, devices::IS.FlattenIteratorWrapper{V}, model::DeviceModel{V, W}, ::NetworkModel{X}, ) where { V <: PSY.Source, W <: AbstractSourceFormulation, X <: PM.AbstractPowerModel, } add_parameterized_upper_bound_range_constraints( container, ActivePowerInVariableTimeSeriesLimitsConstraint, U, ActivePowerInTimeSeriesParameter, devices, model, X, ) return end function PSI.objective_function!( container::PSI.OptimizationContainer, devices::IS.FlattenIteratorWrapper{T}, ::PSI.DeviceModel{T, U}, ::Type{V}, ) where {T <: PSY.Source, U <: AbstractSourceFormulation, V <: PM.AbstractPowerModel} PSI.add_variable_cost!(container, PSI.ActivePowerOutVariable(), devices, U()) PSI.add_variable_cost!(container, PSI.ActivePowerInVariable(), devices, U()) return end get_initial_conditions_device_model( ::OperationModel, model::DeviceModel{PSY.Source, T}, ) where {T <: AbstractSourceFormulation} = DeviceModel(PSY.Source, T) ================================================ FILE: src/devices_models/devices/thermal_generation.jl ================================================ #! format: off requires_initialization(::AbstractThermalFormulation) = false requires_initialization(::AbstractThermalUnitCommitment) = true requires_initialization(::ThermalStandardDispatch) = true requires_initialization(::ThermalBasicCompactUnitCommitment) = false requires_initialization(::ThermalBasicUnitCommitment) = false get_variable_multiplier(_, ::Type{<:PSY.ThermalGen}, ::AbstractThermalFormulation) = 1.0 get_variable_multiplier(::OnVariable, d::PSY.ThermalGen, ::Union{AbstractCompactUnitCommitment, ThermalCompactDispatch}) = PSY.get_active_power_limits(d).min get_expression_type_for_reserve(::ActivePowerReserveVariable, ::Type{<:PSY.ThermalGen}, ::Type{<:PSY.Reserve{PSY.ReserveUp}}) = ActivePowerRangeExpressionUB get_expression_type_for_reserve(::ActivePowerReserveVariable, ::Type{<:PSY.ThermalGen}, ::Type{<:PSY.Reserve{PSY.ReserveDown}}) = ActivePowerRangeExpressionLB ############## ActivePowerVariable, ThermalGen #################### get_variable_binary(::ActivePowerVariable, ::Type{<:PSY.ThermalGen}, ::AbstractThermalFormulation) = false get_variable_warm_start_value(::ActivePowerVariable, d::PSY.ThermalGen, ::AbstractThermalFormulation) = PSY.get_active_power(d) get_variable_lower_bound(::ActivePowerVariable, d::PSY.ThermalGen, ::AbstractThermalFormulation) = PSY.get_must_run(d) ? PSY.get_active_power_limits(d).min : 0.0 get_variable_upper_bound(::ActivePowerVariable, d::PSY.ThermalGen, ::AbstractThermalFormulation) = PSY.get_active_power_limits(d).max get_variable_lower_bound(::ActivePowerVariable, d::PSY.ThermalGen, ::ThermalDispatchNoMin) = 0.0 ############## PowerAboveMinimumVariable, ThermalGen #################### get_variable_binary(::PowerAboveMinimumVariable, ::Type{<:PSY.ThermalGen}, ::AbstractThermalFormulation) = false get_variable_warm_start_value(::PowerAboveMinimumVariable, d::PSY.ThermalGen, ::AbstractCompactUnitCommitment) = max(0.0, PSY.get_active_power(d) - PSY.get_active_power_limits(d).min) get_variable_lower_bound(::PowerAboveMinimumVariable, d::PSY.ThermalGen, ::AbstractThermalFormulation) = 0.0 get_variable_upper_bound(::PowerAboveMinimumVariable, d::PSY.ThermalGen, ::AbstractThermalFormulation) = PSY.get_active_power_limits(d).max - PSY.get_active_power_limits(d).min ############## ReactivePowerVariable, ThermalGen #################### get_variable_binary(::ReactivePowerVariable, ::Type{<:PSY.ThermalGen}, ::AbstractThermalFormulation) = false get_variable_warm_start_value(::ReactivePowerVariable, d::PSY.ThermalGen, ::AbstractThermalFormulation) = PSY.get_reactive_power(d) get_variable_lower_bound(::ReactivePowerVariable, d::PSY.ThermalGen, ::AbstractThermalFormulation) = PSY.get_reactive_power_limits(d).min get_variable_upper_bound(::ReactivePowerVariable, d::PSY.ThermalGen, ::AbstractThermalFormulation) = PSY.get_reactive_power_limits(d).max ############## OnVariable, ThermalGen #################### get_variable_binary(::OnVariable, ::Type{<:PSY.ThermalGen}, ::AbstractThermalFormulation) = true get_variable_warm_start_value(::OnVariable, d::PSY.ThermalGen, ::AbstractThermalFormulation) = PSY.get_status(d) ? 1.0 : 0.0 get_variable_lower_bound(::OnVariable, d::PSY.ThermalGen, ::AbstractThermalUnitCommitment) = PSY.get_must_run(d) ? 1.0 : 0.0 ############## StopVariable, ThermalGen #################### get_variable_binary(::StopVariable, ::Type{<:PSY.ThermalGen}, ::AbstractThermalFormulation) = true get_variable_lower_bound(::StopVariable, d::PSY.ThermalGen, ::AbstractThermalFormulation) = 0.0 get_variable_upper_bound(::StopVariable, d::PSY.ThermalGen, ::AbstractThermalFormulation) = 1.0 ############## StartVariable, ThermalGen #################### get_variable_binary(::StartVariable, d::Type{<:PSY.ThermalGen}, ::AbstractThermalFormulation) = true get_variable_lower_bound(::StartVariable, d::PSY.ThermalGen, ::AbstractThermalFormulation) = 0.0 get_variable_upper_bound(::StartVariable, d::PSY.ThermalGen, ::AbstractThermalFormulation) = 1.0 ############## ColdStartVariable, WarmStartVariable, HotStartVariable ############ get_variable_binary(::Union{ColdStartVariable, WarmStartVariable, HotStartVariable}, ::Type{PSY.ThermalMultiStart}, ::AbstractThermalFormulation) = true ############## SlackVariables, ThermalGen #################### # LB Slack # get_variable_binary(::RateofChangeConstraintSlackDown, ::Type{<:PSY.ThermalGen}, ::AbstractThermalFormulation) = false get_variable_lower_bound(::RateofChangeConstraintSlackDown, d::PSY.ThermalGen, ::AbstractThermalFormulation) = 0.0 # UB Slack # get_variable_binary(::RateofChangeConstraintSlackUp, ::Type{<:PSY.ThermalGen}, ::AbstractThermalFormulation) = false get_variable_lower_bound(::RateofChangeConstraintSlackUp, d::PSY.ThermalGen, ::AbstractThermalFormulation) = 0.0 ########################### Parameter related set functions ################################ get_multiplier_value(::ActivePowerTimeSeriesParameter, d::PSY.ThermalGen, ::AbstractThermalFormulation) = PSY.get_max_active_power(d) get_multiplier_value(::FuelCostParameter, d::PSY.ThermalGen, ::AbstractThermalFormulation) = 1.0 get_parameter_multiplier(::VariableValueParameter, d::PSY.ThermalGen, ::AbstractThermalFormulation) = 1.0 get_initial_parameter_value(::VariableValueParameter, d::PSY.ThermalGen, ::AbstractThermalFormulation) = 1.0 get_expression_multiplier(::OnStatusParameter, ::ActivePowerRangeExpressionUB, d::PSY.ThermalGen, ::AbstractThermalFormulation) = PSY.get_active_power_limits(d).max get_expression_multiplier(::OnStatusParameter, ::ActivePowerRangeExpressionLB, d::PSY.ThermalGen, ::AbstractThermalFormulation) = PSY.get_active_power_limits(d).min get_expression_multiplier(::OnStatusParameter, ::ActivePowerRangeExpressionUB, d::PSY.ThermalGen, ::AbstractCompactUnitCommitment) = PSY.get_active_power_limits(d).max - PSY.get_active_power_limits(d).min get_expression_multiplier(::OnStatusParameter, ::ActivePowerRangeExpressionLB, d::PSY.ThermalGen, ::AbstractCompactUnitCommitment) = 0.0 get_expression_multiplier(::OnStatusParameter, ::ActivePowerRangeExpressionUB, d::PSY.ThermalGen, ::ThermalCompactDispatch) = PSY.get_active_power_limits(d).max - PSY.get_active_power_limits(d).min get_expression_multiplier(::OnStatusParameter, ::ActivePowerRangeExpressionLB, d::PSY.ThermalGen, ::ThermalCompactDispatch) = 0.0 get_expression_multiplier(::OnStatusParameter, ::ActivePowerBalance, d::PSY.ThermalGen, ::AbstractThermalFormulation) = PSY.get_active_power_limits(d).min #################### Initial Conditions for models ############### initial_condition_default(::DeviceStatus, d::PSY.ThermalGen, ::AbstractThermalFormulation) = PSY.get_status(d) ? 1.0 : 0.0 initial_condition_variable(::DeviceStatus, d::PSY.ThermalGen, ::AbstractThermalFormulation) = OnVariable() initial_condition_default(::DevicePower, d::PSY.ThermalGen, ::AbstractThermalFormulation) = PSY.get_active_power(d) initial_condition_variable(::DevicePower, d::PSY.ThermalGen, ::AbstractThermalFormulation) = ActivePowerVariable() initial_condition_default(::DeviceAboveMinPower, d::PSY.ThermalGen, ::AbstractThermalFormulation) = max(0.0, PSY.get_active_power(d) - PSY.get_active_power_limits(d).min) initial_condition_variable(::DeviceAboveMinPower, d::PSY.ThermalGen, ::AbstractCompactUnitCommitment) = PowerAboveMinimumVariable() initial_condition_variable(::DeviceAboveMinPower, d::PSY.ThermalGen, ::ThermalCompactDispatch) = PowerAboveMinimumVariable() initial_condition_default(::InitialTimeDurationOn, d::PSY.ThermalGen, ::AbstractThermalFormulation) = PSY.get_status(d) ? PSY.get_time_at_status(d) : 0.0 initial_condition_variable(::InitialTimeDurationOn, d::PSY.ThermalGen, ::AbstractThermalFormulation) = OnVariable() initial_condition_default(::InitialTimeDurationOff, d::PSY.ThermalGen, ::AbstractThermalFormulation) = !PSY.get_status(d) ? PSY.get_time_at_status(d) : 0.0 initial_condition_variable(::InitialTimeDurationOff, d::PSY.ThermalGen, ::AbstractThermalFormulation) = OnVariable() ########################Objective Function################################################## # TODO: Decide what is the cost for OnVariable, if fixed or constant term in variable function proportional_cost(container::OptimizationContainer, cost::PSY.ThermalGenerationCost, S::OnVariable, T::PSY.ThermalGen, U::AbstractThermalFormulation, t::Int) return onvar_cost(container, cost, S, T, U, t) + PSY.get_constant_term(PSY.get_vom_cost(PSY.get_variable(cost))) + PSY.get_fixed(cost) end is_time_variant_term(::OptimizationContainer, ::PSY.ThermalGenerationCost, ::OnVariable, ::PSY.ThermalGen, ::AbstractThermalFormulation, t::Int) = false proportional_cost(container::OptimizationContainer, cost::PSY.MarketBidCost, ::OnVariable, comp::PSY.ThermalGen, ::AbstractThermalFormulation, t::Int) = _lookup_maybe_time_variant_param(container, comp, t, Val(is_time_variant(PSY.get_incremental_initial_input(cost))), PSY.get_initial_input ∘ PSY.get_incremental_offer_curves ∘ PSY.get_operation_cost, IncrementalCostAtMinParameter()) is_time_variant_term(::OptimizationContainer, cost::PSY.MarketBidCost, ::OnVariable, ::PSY.ThermalGen, ::AbstractThermalFormulation, t::Int) = is_time_variant(PSY.get_incremental_initial_input(cost)) proportional_cost(::Union{PSY.MarketBidCost, PSY.ThermalGenerationCost}, ::Union{RateofChangeConstraintSlackUp, RateofChangeConstraintSlackDown}, ::PSY.ThermalGen, ::AbstractThermalFormulation) = CONSTRAINT_VIOLATION_SLACK_COST has_multistart_variables(::PSY.ThermalGen, ::AbstractThermalFormulation)=false has_multistart_variables(::PSY.ThermalMultiStart, ::ThermalMultiStartUnitCommitment)=true objective_function_multiplier(::VariableType, ::AbstractThermalFormulation)=OBJECTIVE_FUNCTION_POSITIVE 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 # Startup cost interpretations! # Validators: check that the types match (formulation is optional) and redirect to the simpler methods start_up_cost(cost, ::PSY.ThermalGen, ::T, ::Union{AbstractThermalFormulation, Nothing} = nothing) where {T <: StartVariable} = start_up_cost(cost, T()) start_up_cost(cost, ::PSY.ThermalMultiStart, ::T, ::ThermalMultiStartUnitCommitment = ThermalMultiStartUnitCommitment()) where {T <: MultiStartVariable} = start_up_cost(cost, T()) # Implementations: given a single number, tuple, or StartUpStages and a variable, do the right thing # Single number to anything start_up_cost(cost::Float64, ::StartVariable) = cost # TODO in the case where we have a single number startup cost and we're modeling a multi-start, do we set all the values to that number? start_up_cost(cost::Float64, ::T) where {T <: MultiStartVariable} = start_up_cost((hot = cost, warm = cost, cold = cost), T()) # 3-tuple to anything start_up_cost(cost::NTuple{3, Float64}, ::T) where {T <: VariableType} = start_up_cost(StartUpStages(cost), T()) # `StartUpStages` to anything start_up_cost(cost::StartUpStages, ::ColdStartVariable) = cost.cold start_up_cost(cost::StartUpStages, ::WarmStartVariable) = cost.warm start_up_cost(cost::StartUpStages, ::HotStartVariable) = cost.hot # TODO in the opposite case, do we want to get the maximum or the hot? start_up_cost(cost::StartUpStages, ::StartVariable) = maximum(cost) uses_compact_power(::PSY.ThermalGen, ::AbstractThermalFormulation)=false uses_compact_power(::PSY.ThermalGen, ::AbstractCompactUnitCommitment )=true 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) """ Theoretical Cost at power output zero. Mathematically is the intercept with the y-axis """ function onvar_cost(container::OptimizationContainer, cost::PSY.ThermalGenerationCost, ::OnVariable, d::PSY.ThermalGen, ::AbstractThermalFormulation, t::Int) return _onvar_cost(container, PSY.get_variable(cost), d, t) end function _onvar_cost(::OptimizationContainer, cost_function::PSY.FuelCurve{PSY.PiecewisePointCurve}, d::PSY.ThermalGen, ::Int) # OnVariableCost is included in the Point itself for PiecewisePointCurve return 0.0 end function _onvar_cost(::OptimizationContainer, cost_function::PSY.FuelCurve{PSY.PiecewiseIncrementalCurve}, d::PSY.ThermalGen, ::Int) # Input at min is used to transform to InputOutputCurve return 0.0 end function _onvar_cost(::OptimizationContainer, cost_function::PSY.FuelCurve{PSY.PiecewiseAverageCurve}, d::PSY.ThermalGen, ::Int) # Converted to InputOutputCurve, OnVariableCost handled in transformation return 0.0 end # this one implementation is thermal-specific, and requires the component. function _onvar_cost(container::OptimizationContainer, cost_function::Union{PSY.FuelCurve{PSY.LinearCurve}, PSY.FuelCurve{PSY.QuadraticCurve}}, d::T, t::Int) where {T <: PSY.ThermalGen} value_curve = PSY.get_value_curve(cost_function) cost_component = PSY.get_function_data(value_curve) # In Unit/h 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 parameter_array = get_parameter_array(container, FuelCostParameter(), T) parameter_multiplier = get_parameter_multiplier_array(container, FuelCostParameter(), T) name = PSY.get_name(d) return constant_term * parameter_array[name, t] * parameter_multiplier[name, t] end end #! format: on function get_initial_conditions_device_model( model::OperationModel, ::DeviceModel{T, D}, ) where {T <: PSY.ThermalGen, D <: AbstractThermalDispatchFormulation} if supports_milp(get_optimization_container(model)) return DeviceModel(T, ThermalBasicUnitCommitment) else return DeviceModel(T, ThermalBasicDispatch) end end function get_initial_conditions_device_model( ::OperationModel, ::DeviceModel{T, D}, ) where {T <: PSY.ThermalGen, D <: ThermalDispatchNoMin} return DeviceModel(T, ThermalDispatchNoMin) end function get_initial_conditions_device_model( ::OperationModel, ::DeviceModel{T, D}, ) where {T <: PSY.ThermalGen, D <: AbstractThermalUnitCommitment} return DeviceModel(T, ThermalBasicUnitCommitment) end function get_initial_conditions_device_model( ::OperationModel, ::DeviceModel{T, D}, ) where {T <: PSY.ThermalGen, D <: AbstractCompactUnitCommitment} return DeviceModel(T, ThermalBasicCompactUnitCommitment) end function get_default_time_series_names( ::Type{U}, ::Type{V}, ) where {U <: PSY.ThermalGen, V <: Union{FixedOutput, AbstractThermalFormulation}} return Dict{Any, String}( FuelCostParameter => "fuel_cost", ) end function get_default_attributes( ::Type{U}, ::Type{V}, ) where {U <: PSY.ThermalGen, V <: Union{FixedOutput, AbstractThermalFormulation}} return Dict{String, Any}() end ######## THERMAL GENERATION CONSTRAINTS ############ # active power limits of generators when there are no CommitmentVariables """ Min and max active power limits of generators for thermal dispatch formulations """ function get_min_max_limits( device, ::Type{ActivePowerVariableLimitsConstraint}, ::Type{<:AbstractThermalDispatchFormulation}, ) return PSY.get_active_power_limits(device) end # active power limits of generators when there are CommitmentVariables """ Min and max active power limits of generators for thermal unit commitment formulations """ function get_min_max_limits( device, ::Type{ActivePowerVariableLimitsConstraint}, ::Type{<:AbstractThermalUnitCommitment}, ) return PSY.get_active_power_limits(device) end """ Range constraints for thermal compact dispatch """ function add_constraints!( container::OptimizationContainer, T::Type{<:PowerVariableLimitsConstraint}, U::Type{<:Union{PowerAboveMinimumVariable, ExpressionType}}, devices::IS.FlattenIteratorWrapper{V}, model::DeviceModel{V, W}, network_model::NetworkModel{X}, ) where {V <: PSY.ThermalGen, W <: ThermalCompactDispatch, X <: PM.AbstractPowerModel} if !has_semicontinuous_feedforward(model, PowerAboveMinimumVariable) add_range_constraints!(container, T, U, devices, model, X) end return end """ Min and max active power limits of generators for thermal dispatch compact formulations """ function get_min_max_limits( device, ::Type{ActivePowerVariableLimitsConstraint}, ::Type{ThermalCompactDispatch}, ) return ( min = 0.0, max = PSY.get_active_power_limits(device).max - PSY.get_active_power_limits(device).min, ) end """ Min and max active power limits of generators for thermal dispatch no minimum formulations """ function get_min_max_limits( device, ::Type{ActivePowerVariableLimitsConstraint}, ::Type{ThermalDispatchNoMin}, ) return (min = 0.0, max = PSY.get_active_power_limits(device).max) end """ Semicontinuous range constraints for thermal dispatch formulations """ function add_constraints!( container::OptimizationContainer, T::Type{<:PowerVariableLimitsConstraint}, U::Type{<:Union{VariableType, ExpressionType}}, devices::IS.FlattenIteratorWrapper{V}, model::DeviceModel{V, W}, ::NetworkModel{X}, ) where { V <: PSY.ThermalGen, W <: AbstractThermalDispatchFormulation, X <: PM.AbstractPowerModel, } if !has_semicontinuous_feedforward(model, U) add_range_constraints!(container, T, U, devices, model, X) end return end """ Min and max active power limits for multi-start unit commitment formulations """ function get_min_max_limits( device, ::Type{ActivePowerVariableLimitsConstraint}, ::Type{ThermalMultiStartUnitCommitment}, ) # -> Union{Nothing, NamedTuple{(:startup, :shutdown), Tuple{Float64, Float64}}} return ( min = 0.0, max = PSY.get_active_power_limits(device).max - PSY.get_active_power_limits(device).min, ) end """ Adds a variable to the optimization model for the OnVariable of Thermal Units """ function add_variable!( container::OptimizationContainer, variable_type::T, devices::U, formulation::AbstractThermalFormulation, ) where { T <: Union{OnVariable, StartVariable, StopVariable}, U <: Union{Vector{D}, IS.FlattenIteratorWrapper{D}}, } where {D <: PSY.ThermalGen} @assert !isempty(devices) time_steps = get_time_steps(container) settings = get_settings(container) binary = get_variable_binary(variable_type, D, formulation) variable = add_variable_container!( container, variable_type, D, [PSY.get_name(d) for d in devices if !PSY.get_must_run(d)], time_steps, ) for d in devices if PSY.get_must_run(d) continue end name = PSY.get_name(d) for t in time_steps variable[name, t] = JuMP.@variable( get_jump_model(container), base_name = "$(T)_$(D)_{$(name), $(t)}", binary = binary ) if get_warm_start(settings) init = get_variable_warm_start_value(variable_type, d, formulation) init !== nothing && JuMP.set_start_value(variable[name, t], init) end end end return end """ Semicontinuous range constraints for unit commitment formulations """ function add_constraints!( container::OptimizationContainer, T::Type{<:PowerVariableLimitsConstraint}, U::Type{<:Union{VariableType, ExpressionType}}, devices::IS.FlattenIteratorWrapper{V}, model::DeviceModel{V, W}, ::NetworkModel{X}, ) where { V <: PSY.ThermalGen, W <: AbstractThermalUnitCommitment, X <: PM.AbstractPowerModel, } add_semicontinuous_range_constraints!(container, T, U, devices, model, X) return end """ Startup and shutdown active power limits for Compact Unit Commitment """ function get_startup_shutdown_limits( device::PSY.ThermalMultiStart, ::Type{ActivePowerVariableLimitsConstraint}, ::Type{ThermalMultiStartUnitCommitment}, ) startup_shutdown = PSY.get_power_trajectory(device) if isnothing(startup_shutdown) @warn( "Generator $(summary(device)) has a Nothing startup_shutdown property. Using active power limits." ) return ( startup = PSY.get_active_power_limits(device).max, shutdown = PSY.get_active_power_limits(device).max, ) end return startup_shutdown end """ Min and Max active power limits for Compact Unit Commitment """ function get_min_max_limits( device, ::Type{ActivePowerVariableLimitsConstraint}, ::Type{<:AbstractCompactUnitCommitment}, ) # -> Union{Nothing, NamedTuple{(:min, :max), Tuple{Float64, Float64}}} return ( min = 0, max = PSY.get_active_power_limits(device).max - PSY.get_active_power_limits(device).min, ) end """ Startup shutdown limits for Compact Unit Commitment """ function get_startup_shutdown_limits( device, ::Type{ActivePowerVariableLimitsConstraint}, ::Type{<:AbstractCompactUnitCommitment}, ) return ( startup = PSY.get_active_power_limits(device).max, shutdown = PSY.get_active_power_limits(device).max, ) end function _get_data_for_range_ic( initial_conditions_power::Vector{<:InitialCondition}, initial_conditions_status::Vector{<:InitialCondition}, ) lenght_devices_power = length(initial_conditions_power) lenght_devices_status = length(initial_conditions_status) IS.@assert_op lenght_devices_power == lenght_devices_status ini_conds = Matrix{InitialCondition}(undef, lenght_devices_power, 2) idx = 0 for (ix, ic) in enumerate(initial_conditions_power) g = get_component(ic) IS.@assert_op g == get_component(initial_conditions_status[ix]) idx += 1 ini_conds[idx, 1] = ic ini_conds[idx, 2] = initial_conditions_status[ix] end return ini_conds end function add_constraints!( container::OptimizationContainer, ::Type{ActivePowerVariableTimeSeriesLimitsConstraint}, U::Type{<:Union{ActivePowerVariable, ActivePowerRangeExpressionUB}}, devices::IS.FlattenIteratorWrapper{V}, model::DeviceModel{V, W}, ::NetworkModel{X}, ) where { V <: PSY.ThermalGen, W <: AbstractThermalUnitCommitment, X <: PM.AbstractPowerModel, } add_parameterized_upper_bound_range_constraints( container, ActivePowerVariableTimeSeriesLimitsConstraint, U, ActivePowerTimeSeriesParameter, devices, model, X, ) return end """ This function adds range constraint for the first time period. Constraint (10) from PGLIB formulation """ function add_constraints!( container::OptimizationContainer, T::Type{<:ActivePowerVariableLimitsConstraint}, U::Type{<:VariableType}, devices::IS.FlattenIteratorWrapper{V}, ::DeviceModel{V, W}, ::NetworkModel{X}, ) where { V <: PSY.ThermalMultiStart, W <: ThermalMultiStartUnitCommitment, X <: PM.AbstractPowerModel, } time_steps = get_time_steps(container) constraint_type = T() variable_type = U() component_type = V varp = get_variable(container, variable_type, component_type) varstatus = get_variable(container, OnVariable(), component_type) varon = get_variable(container, StartVariable(), component_type) varoff = get_variable(container, StopVariable(), component_type) names = [PSY.get_name(x) for x in devices] con_on = add_constraints_container!( container, constraint_type, component_type, names, time_steps; meta = "on", ) con_off = add_constraints_container!( container, constraint_type, component_type, names, time_steps[1:(end - 1)]; meta = "off", ) con_lb = add_constraints_container!( container, constraint_type, component_type, names, time_steps; meta = "lb", ) for device in devices name = PSY.get_name(device) limits = get_min_max_limits(device, T, W) # depends on constraint type and formulation type startup_shutdown_limits = get_startup_shutdown_limits(device, T, W) if JuMP.has_lower_bound(varp[name, t]) JuMP.set_lower_bound(varp[name, t], 0.0) end for t in time_steps con_on[name, t] = JuMP.@constraint( get_jump_model(container), varp[name, t] <= (limits.max - limits.min) * varstatus[name, t] - max(limits.max - startup_shutdown_limits.startup, 0.0) * varon[name, t] ) con_lb[name, t] = JuMP.@constraint(get_jump_model(container), varp[name, t] >= 0.0) if t != length(time_steps) con_off[name, t] = JuMP.@constraint( get_jump_model(container), varp[name, t] <= (limits.max - limits.min) * varstatus[name, t] - max(limits.max - startup_shutdown_limits.shutdown, 0.0) * varoff[name, t + 1] ) end end end return end function add_constraints!( container::OptimizationContainer, T::Type{<:ActivePowerVariableLimitsConstraint}, U::Type{ActivePowerRangeExpressionLB}, devices::IS.FlattenIteratorWrapper{V}, ::DeviceModel{V, W}, ::NetworkModel{X}, ) where { V <: PSY.ThermalMultiStart, W <: ThermalMultiStartUnitCommitment, X <: PM.AbstractPowerModel, } time_steps = get_time_steps(container) constraint_type = T() expression_type = U() component_type = V expression_products = get_expression(container, expression_type, component_type) varp = get_variable(container, PowerAboveMinimumVariable(), component_type) names = [PSY.get_name(x) for x in devices] con_lb = add_constraints_container!( container, constraint_type, component_type, names, time_steps; meta = "lb", ) for device in devices name = PSY.get_name(device) for t in time_steps if JuMP.has_lower_bound(varp[name, t]) JuMP.set_lower_bound(varp[name, t], 0.0) end con_lb[name, t] = JuMP.@constraint( get_jump_model(container), expression_products[name, t] >= 0 ) end end return end function add_constraints!( container::OptimizationContainer, T::Type{<:ActivePowerVariableLimitsConstraint}, U::Type{ActivePowerRangeExpressionUB}, devices::IS.FlattenIteratorWrapper{V}, ::DeviceModel{V, W}, ::NetworkModel{X}, ) where { V <: PSY.ThermalMultiStart, W <: ThermalMultiStartUnitCommitment, X <: PM.AbstractPowerModel, } time_steps = get_time_steps(container) constraint_type = T() expression_type = U() component_type = V expression_products = get_expression(container, expression_type, component_type) varstatus = get_variable(container, OnVariable(), component_type) varon = get_variable(container, StartVariable(), component_type) varoff = get_variable(container, StopVariable(), component_type) varp = get_variable(container, PowerAboveMinimumVariable(), component_type) names = [PSY.get_name(x) for x in devices] con_on = add_constraints_container!( container, constraint_type, component_type, names, time_steps; meta = "ubon", ) con_off = add_constraints_container!( container, constraint_type, component_type, names, time_steps[1:(end - 1)]; meta = "uboff", ) for device in devices name = PSY.get_name(device) limits = get_min_max_limits(device, T, W) # depends on constraint type and formulation type startup_shutdown_limits = get_startup_shutdown_limits(device, T, W) @assert !isnothing(startup_shutdown_limits) "$(name)" for t in time_steps if JuMP.has_lower_bound(varp[name, t]) JuMP.set_lower_bound(varp[name, t], 0.0) end con_on[name, t] = JuMP.@constraint( get_jump_model(container), expression_products[name, t] <= (limits.max - limits.min) * varstatus[name, t] - max(limits.max - startup_shutdown_limits.startup, 0) * varon[name, t] ) if t != length(time_steps) con_off[name, t] = JuMP.@constraint( get_jump_model(container), expression_products[name, t] <= (limits.max - limits.min) * varstatus[name, t] - max(limits.max - startup_shutdown_limits.shutdown, 0) * varoff[name, t + 1] ) end end end return end function add_constraints!( container::OptimizationContainer, ::Type{ActiveRangeICConstraint}, devices::IS.FlattenIteratorWrapper{T}, model::DeviceModel{T, S}, network_model::NetworkModel{X}, ) where { T <: PSY.ThermalGen, S <: AbstractCompactUnitCommitment, X <: PM.AbstractPowerModel, } initial_conditions_power = get_initial_condition(container, DeviceAboveMinPower(), T) initial_conditions_status = get_initial_condition(container, DeviceStatus(), T) ini_conds = _get_data_for_range_ic(initial_conditions_power, initial_conditions_status) if !isempty(ini_conds) varstop = get_variable(container, StopVariable(), T) device_name_set = PSY.get_name.(devices) con = add_constraints_container!( container, ActiveRangeICConstraint(), T, device_name_set, ) for (ix, ic) in enumerate(ini_conds[:, 1]) name = get_component_name(ic) device = get_component(ic) limits = PSY.get_active_power_limits(device) lag_ramp_limits = PSY.get_power_trajectory(device) val = max(limits.max - lag_ramp_limits.shutdown, 0) con[name] = JuMP.@constraint( get_jump_model(container), val * varstop[name, 1] <= ini_conds[ix, 2].value * (limits.max - limits.min) - get_value(ic) ) end else @warn "Data doesn't contain generators with ramp limits, consider adjusting your formulation" end return end """ Reactive power limits of generators for all dispatch formulations """ function get_min_max_limits( device, ::Type{ReactivePowerVariableLimitsConstraint}, ::Type{<:AbstractThermalDispatchFormulation}, ) return PSY.get_reactive_power_limits(device) end """ Reactive power limits of generators when there CommitmentVariables """ function get_min_max_limits( device, ::Type{ReactivePowerVariableLimitsConstraint}, ::Type{<:AbstractThermalUnitCommitment}, ) return PSY.get_reactive_power_limits(device) end function add_constraints!( container::OptimizationContainer, T::Type{CommitmentConstraint}, devices::IS.FlattenIteratorWrapper{U}, model::DeviceModel{U, V}, network_model::NetworkModel{X}, ) where { U <: PSY.ThermalGen, V <: AbstractThermalUnitCommitment, X <: PM.AbstractPowerModel, } time_steps = get_time_steps(container) varstart = get_variable(container, StartVariable(), U) varstop = get_variable(container, StopVariable(), U) varon = get_variable(container, OnVariable(), U) names = axes(varstart, 1) initial_conditions = get_initial_condition(container, DeviceStatus(), U) constraint = add_constraints_container!(container, CommitmentConstraint(), U, names, time_steps) aux_constraint = add_constraints_container!( container, CommitmentConstraint(), U, names, time_steps; meta = "aux", ) for ic in initial_conditions name = PSY.get_name(get_component(ic)) if !PSY.get_must_run(get_component(ic)) constraint[name, 1] = JuMP.@constraint( get_jump_model(container), varon[name, 1] == get_value(ic) + varstart[name, 1] - varstop[name, 1] ) aux_constraint[name, 1] = JuMP.@constraint( get_jump_model(container), varstart[name, 1] + varstop[name, 1] <= 1.0 ) end end for ic in initial_conditions if PSY.get_must_run(get_component(ic)) continue else name = get_component_name(ic) for t in time_steps[2:end] constraint[name, t] = JuMP.@constraint( get_jump_model(container), varon[name, t] == varon[name, t - 1] + varstart[name, t] - varstop[name, t] ) aux_constraint[name, t] = JuMP.@constraint( get_jump_model(container), varstart[name, t] + varstop[name, t] <= 1.0 ) end end end return end ########################## Make initial Conditions for a Model ############################# function initial_conditions!( container::OptimizationContainer, devices::IS.FlattenIteratorWrapper{T}, formulation::AbstractThermalUnitCommitment, ) where {T <: PSY.ThermalGen} add_initial_condition!(container, devices, formulation, DeviceStatus()) add_initial_condition!(container, devices, formulation, DevicePower()) add_initial_condition!(container, devices, formulation, InitialTimeDurationOn()) add_initial_condition!(container, devices, formulation, InitialTimeDurationOff()) return end function initial_conditions!( container::OptimizationContainer, devices::IS.FlattenIteratorWrapper{T}, formulation::AbstractCompactUnitCommitment, ) where {T <: PSY.ThermalGen} add_initial_condition!(container, devices, formulation, DeviceStatus()) add_initial_condition!(container, devices, formulation, DeviceAboveMinPower()) add_initial_condition!(container, devices, formulation, InitialTimeDurationOn()) add_initial_condition!(container, devices, formulation, InitialTimeDurationOff()) return end function initial_conditions!( container::OptimizationContainer, devices::IS.FlattenIteratorWrapper{T}, formulation::Union{ThermalBasicUnitCommitment, ThermalBasicCompactUnitCommitment}, ) where {T <: PSY.ThermalGen} add_initial_condition!(container, devices, formulation, DeviceStatus()) add_initial_condition!(container, devices, formulation, InitialTimeDurationOn()) add_initial_condition!(container, devices, formulation, InitialTimeDurationOff()) return end function initial_conditions!( container::OptimizationContainer, devices::IS.FlattenIteratorWrapper{T}, formulation::AbstractThermalDispatchFormulation, ) where {T <: PSY.ThermalGen} add_initial_condition!(container, devices, formulation, DevicePower()) return end function initial_conditions!( container::OptimizationContainer, devices::IS.FlattenIteratorWrapper{T}, formulation::ThermalCompactDispatch, ) where {T <: PSY.ThermalGen} add_initial_condition!(container, devices, formulation, DeviceAboveMinPower()) return end ############################ Auxiliary Variables Calculation ################################ function calculate_aux_variable_value!( container::OptimizationContainer, ::AuxVarKey{TimeDurationOn, T}, ::PSY.System, ) where {T <: PSY.ThermalGen} on_variable_results = get_variable(container, OnVariable(), T) aux_variable_container = get_aux_variable(container, TimeDurationOn(), T) ini_cond = get_initial_condition(container, InitialTimeDurationOn(), T) time_steps = get_time_steps(container) for ix in eachindex(JuMP.axes(aux_variable_container)[1]) # if its nothing it means the thermal unit was on must run # so there is nothing to do but to add the total number of time steps # to the count if isnothing(get_value(ini_cond[ix])) sum_on_var = time_steps[end] else on_var_name = get_component_name(ini_cond[ix]) ini_cond_value = get_condition(ini_cond[ix]) # On Var doesn't exist for a unit that has must_run = true on_var = jump_value.(on_variable_results[on_var_name, :]) aux_variable_container.data[ix, :] .= ini_cond_value sum_on_var = sum(on_var) end if sum_on_var == time_steps[end] # Unit was always on aux_variable_container.data[ix, :] += time_steps elseif sum_on_var == 0.0 # Unit was always off aux_variable_container.data[ix, :] .= 0.0 else previous_condition = ini_cond_value for (t, v) in enumerate(on_var) if v < 0.99 # Unit turn off time_value = 0.0 elseif isapprox(v, 1.0; atol = ABSOLUTE_TOLERANCE) # Unit is on time_value = previous_condition + 1.0 else error("Binary condition returned $v") end previous_condition = aux_variable_container.data[ix, t] = time_value end end end return end function calculate_aux_variable_value!( container::OptimizationContainer, ::AuxVarKey{TimeDurationOff, T}, ::PSY.System, ) where {T <: PSY.ThermalGen} on_variable_results = get_variable(container, OnVariable(), T) aux_variable_container = get_aux_variable(container, TimeDurationOff(), T) ini_cond = get_initial_condition(container, InitialTimeDurationOff(), T) time_steps = get_time_steps(container) for ix in eachindex(JuMP.axes(aux_variable_container)[1]) # if its nothing it means the thermal unit was on must_run = true # so there is nothing to do but continue if isnothing(get_value(ini_cond[ix])) sum_on_var = 0.0 else on_var_name = get_component_name(ini_cond[ix]) # On Var doesn't exist for a unit that has must run on_var = jump_value.(on_variable_results[on_var_name, :]) ini_cond_value = get_condition(ini_cond[ix]) aux_variable_container.data[ix, :] .= ini_cond_value sum_on_var = sum(on_var) end if sum_on_var == time_steps[end] # Unit was always on aux_variable_container.data[ix, :] .= 0.0 elseif sum_on_var == 0.0 # Unit was always off aux_variable_container.data[ix, :] += time_steps else previous_condition = ini_cond_value for (t, v) in enumerate(on_var) if v < 0.99 # Unit turn off time_value = previous_condition + 1.0 elseif isapprox(v, 1.0; atol = ABSOLUTE_TOLERANCE) # Unit is on time_value = 0.0 else error("Binary condition returned $v") end previous_condition = aux_variable_container.data[ix, t] = time_value end end end return end function calculate_aux_variable_value!( container::OptimizationContainer, ::AuxVarKey{PowerOutput, T}, system::PSY.System, ) where {T <: PSY.ThermalGen} time_steps = get_time_steps(container) if has_container_key(container, OnVariable, T) on_variable_results = get_variable(container, OnVariable(), T) elseif has_container_key(container, OnStatusParameter, T) on_variable_results = get_parameter_array(container, OnStatusParameter(), T) else error( "$T formulation is NOT supported without a Feedforward for CommitmentDecisions, please consider changing your simulation setup or adding a SemiContinuousFeedforward.", ) end p_variable_results = get_variable(container, PowerAboveMinimumVariable(), T) device_name = axes(p_variable_results, 1) aux_variable_container = get_aux_variable(container, PowerOutput(), T) for d_name in device_name d = PSY.get_component(T, system, d_name) name = PSY.get_name(d) min = PSY.get_active_power_limits(d).min for t in time_steps aux_variable_container[name, t] = jump_value(on_variable_results[name, t]) * min + jump_value(p_variable_results[name, t]) end end return end ########################### Ramp/Rate of Change Constraints ################################ """ This function gets the data for the generators for ramping constraints of thermal generators """ _get_initial_condition_type( ::Type{RampConstraint}, ::Type{<:PSY.ThermalGen}, ::Type{<:AbstractThermalFormulation}, ) = DevicePower _get_initial_condition_type( ::Type{RampConstraint}, ::Type{<:PSY.ThermalGen}, ::Type{<:AbstractCompactUnitCommitment}, ) = DeviceAboveMinPower _get_initial_condition_type( ::Type{RampConstraint}, ::Type{<:PSY.ThermalGen}, ::Type{ThermalCompactDispatch}, ) = DeviceAboveMinPower """ This function adds the ramping limits of generators when there are CommitmentVariables """ function add_constraints!( container::OptimizationContainer, T::Type{RampConstraint}, devices::IS.FlattenIteratorWrapper{U}, model::DeviceModel{U, V}, ::NetworkModel{W}, ) where { U <: PSY.ThermalGen, V <: AbstractThermalUnitCommitment, W <: PM.AbstractPowerModel, } add_semicontinuous_ramp_constraints!( container, T, ActivePowerVariable, devices, model, W, ) return end function add_constraints!( container::OptimizationContainer, T::Type{RampConstraint}, devices::IS.FlattenIteratorWrapper{U}, model::DeviceModel{U, V}, ::NetworkModel{W}, ) where { U <: PSY.ThermalGen, V <: AbstractCompactUnitCommitment, W <: PM.AbstractPowerModel, } add_semicontinuous_ramp_constraints!( container, T, PowerAboveMinimumVariable, devices, model, W, ) return end function add_constraints!( container::OptimizationContainer, T::Type{RampConstraint}, devices::IS.FlattenIteratorWrapper{U}, model::DeviceModel{U, ThermalCompactDispatch}, ::NetworkModel{V}, ) where {U <: PSY.ThermalGen, V <: PM.AbstractPowerModel} add_linear_ramp_constraints!(container, T, PowerAboveMinimumVariable, devices, model, V) return end function add_constraints!( container::OptimizationContainer, T::Type{RampConstraint}, devices::IS.FlattenIteratorWrapper{U}, model::DeviceModel{U, V}, ::NetworkModel{W}, ) where { U <: PSY.ThermalGen, V <: AbstractThermalDispatchFormulation, W <: PM.AbstractPowerModel, } add_linear_ramp_constraints!(container, T, ActivePowerVariable, devices, model, W) return end function add_constraints!( container::OptimizationContainer, T::Type{RampConstraint}, devices::IS.FlattenIteratorWrapper{PSY.ThermalMultiStart}, model::DeviceModel{PSY.ThermalMultiStart, ThermalMultiStartUnitCommitment}, ::NetworkModel{U}, ) where {U <: PM.AbstractPowerModel} add_linear_ramp_constraints!(container, T, PowerAboveMinimumVariable, devices, model, U) return end ########################### start up trajectory constraints ###################################### function _convert_hours_to_timesteps( start_times_hr::StartUpStages, resolution::Dates.TimePeriod, ) _start_times_ts = ( round((hr * MINUTES_IN_HOUR) / Dates.value(Dates.Minute(resolution)), RoundUp) for hr in start_times_hr ) start_times_ts = StartUpStages(_start_times_ts) return start_times_ts end @doc raw""" Constructs contraints for different types of starts based on generator down-time # Equations for t in time_limits[s+1]:T ``` var_starts[name, s, t] <= sum( var_stop[name, t-i] for i in time_limits[s]:(time_limits[s+1]-1) ``` # LaTeX `` δ^{s}(t) \leq \sum_{i=TS^{s}_{g}}^{TS^{s+1}_{g}} x^{stop}(t-i) `` """ function add_constraints!( container::OptimizationContainer, ::Type{StartupTimeLimitTemperatureConstraint}, devices::IS.FlattenIteratorWrapper{T}, model::DeviceModel{T, ThermalMultiStartUnitCommitment}, ::NetworkModel{<:PM.AbstractPowerModel}, ) where {T <: PSY.ThermalMultiStart} resolution = get_resolution(container) time_steps = get_time_steps(container) start_vars = [ get_variable(container, HotStartVariable(), T), get_variable(container, WarmStartVariable(), T), ] varstop = get_variable(container, StopVariable(), T) names = PSY.get_name.(devices) con = [ add_constraints_container!( container, StartupTimeLimitTemperatureConstraint(), T, names, time_steps; sparse = true, meta = "hot", ), add_constraints_container!( container, StartupTimeLimitTemperatureConstraint(), T, names, time_steps; sparse = true, meta = "warm", ), ] for t in time_steps, d in devices name = PSY.get_name(d) startup_types = PSY.get_start_types(d) time_limits = _convert_hours_to_timesteps(PSY.get_start_time_limits(d), resolution) for ix in 1:(startup_types - 1) if t >= time_limits[ix + 1] con[ix][name, t] = JuMP.@constraint( get_jump_model(container), start_vars[ix][name, t] <= sum( varstop[name, t - i] for i in UnitRange{Int}( Int(time_limits[ix]), Int(time_limits[ix + 1] - 1), ) ) ) end end end for c in con # Workaround to remove invalid key combinations filter!(x -> x.second !== nothing, c.data) end return end @doc raw""" Constructs contraints that restricts devices to one type of start at a time # Equations ``` sum(var_starts[name, s, t] for s in starts) = var_start[name, t] ``` # LaTeX `` \sum^{S_g}_{s=1} δ^{s}(t) \eq x^{start}(t) `` """ function add_constraints!( container::OptimizationContainer, ::Type{StartTypeConstraint}, devices::IS.FlattenIteratorWrapper{T}, model::DeviceModel{T, ThermalMultiStartUnitCommitment}, ::NetworkModel{<:PM.AbstractPowerModel}, ) where {T <: PSY.ThermalMultiStart} time_steps = get_time_steps(container) varstart = get_variable(container, StartVariable(), T) start_vars = [ get_variable(container, HotStartVariable(), T), get_variable(container, WarmStartVariable(), T), get_variable(container, ColdStartVariable(), T), ] device_name_set = PSY.get_name.(devices) con = add_constraints_container!( container, StartTypeConstraint(), T, device_name_set, time_steps, ) for t in time_steps, d in devices name = PSY.get_name(d) startup_types = PSY.get_start_types(d) con[name, t] = JuMP.@constraint( get_jump_model(container), varstart[name, t] == sum(start_vars[ix][name, t] for ix in 1:(startup_types)) ) end return end @doc raw""" Constructs contraints that restricts devices to one type of start at a time # Equations ub: ``` (time_limits[st+1]-1)*δ^{s}(t) + (1 - δ^{s}(t)) * M_VALUE >= sum(1-varbin[name, i]) for i in 1:t) + initial_condition_offtime ``` lb: ``` (time_limits[st]-1)*δ^{s}(t) =< sum(1-varbin[name, i]) for i in 1:t) + initial_condition_offtime ``` # LaTeX `` TS^{s+1}_{g} δ^{s}(t) + (1-δ^{s}(t)) M_VALUE \geq \sum^{t}_{i=1} x^{status}(i) + DT_{g}^{0} \forall t in \{1, \ldots, TS^{s+1}_{g}`` `` TS^{s}_{g} δ^{s}(t) \leq \sum^{t}_{i=1} x^{status}(i) + DT_{g}^{0} \forall t in \{1, \ldots, TS^{s+1}_{g}`` """ function add_constraints!( container::OptimizationContainer, ::Type{StartupInitialConditionConstraint}, devices::IS.FlattenIteratorWrapper{T}, model::DeviceModel{T, ThermalMultiStartUnitCommitment}, ::NetworkModel{<:PM.AbstractPowerModel}, ) where {T <: PSY.ThermalMultiStart} resolution = get_resolution(container) initial_conditions_offtime = get_initial_condition(container, InitialTimeDurationOff(), PSY.ThermalMultiStart) time_steps = get_time_steps(container) device_name_set = [get_component_name(ic) for ic in initial_conditions_offtime] varbin = get_variable(container, OnVariable(), T) varstarts = [ get_variable(container, HotStartVariable(), T), get_variable(container, WarmStartVariable(), T), ] con_ub = add_constraints_container!( container, StartupInitialConditionConstraint(), T, device_name_set, time_steps, 1:(MAX_START_STAGES - 1); sparse = true, meta = "ub", ) con_lb = add_constraints_container!( container, StartupInitialConditionConstraint(), T, device_name_set, time_steps, 1:(MAX_START_STAGES - 1); sparse = true, meta = "lb", ) for t in time_steps, (ix, ic) in enumerate(initial_conditions_offtime) name = PSY.get_name(get_component(ic)) startup_types = PSY.get_start_types(get_component(ic)) time_limits = _convert_hours_to_timesteps( PSY.get_start_time_limits(get_component(ic)), resolution, ) ic = initial_conditions_offtime[ix] for st in 1:(startup_types - 1) var = varstarts[st] if t < (time_limits[st + 1] - 1) con_ub[name, t, st] = JuMP.@constraint( get_jump_model(container), (time_limits[st + 1] - 1) * var[name, t] + (1 - var[name, t]) * M_VALUE >= sum((1 - varbin[name, i]) for i in 1:t) + get_value(ic) ) con_lb[name, t, st] = JuMP.@constraint( get_jump_model(container), time_limits[st] * var[name, t] <= sum((1 - varbin[name, i]) for i in 1:t) + get_value(ic) ) end end end for c in [con_ub, con_lb] # Workaround to remove invalid key combinations filter!(x -> x.second !== nothing, c.data) end return end ########################### time duration constraints ###################################### """ If the fraction of hours that a generator has a duration constraint is less than the fraction of hours that a single time_step represents then it is not binding. """ function _get_data_for_tdc( initial_conditions_on::Vector{T}, initial_conditions_off::Vector{U}, resolution::Dates.TimePeriod, ) where {T <: InitialCondition, U <: InitialCondition} steps_per_hour = 60 / Dates.value(Dates.Minute(resolution)) fraction_of_hour = 1 / steps_per_hour lenght_devices_on = length(initial_conditions_on) lenght_devices_off = length(initial_conditions_off) IS.@assert_op lenght_devices_off == lenght_devices_on time_params = Vector{UpDown}(undef, lenght_devices_on) ini_conds = Matrix{InitialCondition}(undef, lenght_devices_on, 2) idx = 0 for (ix, ic) in enumerate(initial_conditions_on) g = get_component(ic) IS.@assert_op g == get_component(initial_conditions_off[ix]) time_limits = PSY.get_time_limits(g) name = PSY.get_name(g) if time_limits !== nothing if (time_limits.up <= fraction_of_hour) & (time_limits.down <= fraction_of_hour) @debug "Generator $(name) has a nonbinding time limits. Constraints Skipped" continue else idx += 1 end ini_conds[idx, 1] = ic ini_conds[idx, 2] = initial_conditions_off[ix] up_val = round(time_limits.up * steps_per_hour, RoundUp) down_val = round(time_limits.down * steps_per_hour, RoundUp) time_params[idx] = (up = up_val, down = down_val) end end if idx < lenght_devices_on ini_conds = ini_conds[1:idx, :] deleteat!(time_params, (idx + 1):lenght_devices_on) end return ini_conds, time_params end function add_constraints!( container::OptimizationContainer, ::Type{DurationConstraint}, ::IS.FlattenIteratorWrapper{U}, ::DeviceModel{U, V}, ::NetworkModel{<:PM.AbstractPowerModel}, ) where {U <: PSY.ThermalGen, V <: AbstractThermalUnitCommitment} parameters = built_for_recurrent_solves(container) resolution = get_resolution(container) # Use getter functions that don't require creating the keys here initial_conditions_on = get_initial_condition(container, InitialTimeDurationOn(), U) initial_conditions_off = get_initial_condition(container, InitialTimeDurationOff(), U) ini_conds, time_params = _get_data_for_tdc(initial_conditions_on, initial_conditions_off, resolution) if !(isempty(ini_conds)) if parameters device_duration_parameters!( container, time_params, ini_conds, DurationConstraint(), (OnVariable(), StartVariable(), StopVariable()), U, ) else device_duration_retrospective!( container, time_params, ini_conds, DurationConstraint(), (OnVariable(), StartVariable(), StopVariable()), U, ) end else @warn "Data doesn't contain generators with time-up/down limits, consider adjusting your formulation" end return end function add_constraints!( container::OptimizationContainer, ::Type{DurationConstraint}, devices::IS.FlattenIteratorWrapper{U}, model::DeviceModel{U, ThermalMultiStartUnitCommitment}, ::NetworkModel{<:PM.AbstractPowerModel}, ) where {U <: PSY.ThermalGen} parameters = built_for_recurrent_solves(container) resolution = get_resolution(container) initial_conditions_on = get_initial_condition(container, InitialTimeDurationOn(), U) initial_conditions_off = get_initial_condition(container, InitialTimeDurationOff(), U) ini_conds, time_params = _get_data_for_tdc(initial_conditions_on, initial_conditions_off, resolution) if !(isempty(ini_conds)) if parameters device_duration_parameters!( container, time_params, ini_conds, DurationConstraint(), (OnVariable(), StartVariable(), StopVariable()), U, ) else device_duration_compact_retrospective!( container, time_params, ini_conds, DurationConstraint(), (OnVariable(), StartVariable(), StopVariable()), U, ) end else @warn "Data doesn't contain generators with time-up/down limits, consider adjusting your formulation" end return end ########################### Objective Function Calls############################################# # These functions are custom implementations of the cost data. In the file objective_functions.jl there are default implementations. Define these only if needed. function objective_function!( container::OptimizationContainer, devices::IS.FlattenIteratorWrapper{T}, device_model::DeviceModel{T, U}, ::Type{<:PM.AbstractPowerModel}, ) where {T <: PSY.ThermalGen, U <: AbstractThermalUnitCommitment} add_variable_cost!(container, ActivePowerVariable(), devices, U()) add_start_up_cost!(container, StartVariable(), devices, U()) add_shut_down_cost!(container, StopVariable(), devices, U()) add_proportional_cost!(container, OnVariable(), devices, U()) if get_use_slacks(device_model) add_proportional_cost!(container, RateofChangeConstraintSlackUp(), devices, U()) add_proportional_cost!(container, RateofChangeConstraintSlackDown(), devices, U()) end return end function objective_function!( container::OptimizationContainer, devices::IS.FlattenIteratorWrapper{T}, device_model::DeviceModel{T, U}, ::Type{<:PM.AbstractPowerModel}, ) where {T <: PSY.ThermalGen, U <: AbstractCompactUnitCommitment} add_variable_cost!(container, PowerAboveMinimumVariable(), devices, U()) add_start_up_cost!(container, StartVariable(), devices, U()) add_shut_down_cost!(container, StopVariable(), devices, U()) add_proportional_cost!(container, OnVariable(), devices, U()) if get_use_slacks(device_model) add_proportional_cost!(container, RateofChangeConstraintSlackUp(), devices, U()) add_proportional_cost!(container, RateofChangeConstraintSlackDown(), devices, U()) end return end function objective_function!( container::OptimizationContainer, devices::IS.FlattenIteratorWrapper{PSY.ThermalMultiStart}, device_model::DeviceModel{PSY.ThermalMultiStart, U}, ::Type{<:PM.AbstractPowerModel}, ) where {U <: ThermalMultiStartUnitCommitment} add_variable_cost!(container, PowerAboveMinimumVariable(), devices, U()) for var_type in MULTI_START_VARIABLES add_start_up_cost!(container, var_type(), devices, U()) end add_shut_down_cost!(container, StopVariable(), devices, U()) add_proportional_cost!(container, OnVariable(), devices, U()) if get_use_slacks(device_model) add_proportional_cost!(container, RateofChangeConstraintSlackUp(), devices, U()) add_proportional_cost!(container, RateofChangeConstraintSlackDown(), devices, U()) end return end function objective_function!( container::OptimizationContainer, devices::IS.FlattenIteratorWrapper{T}, device_model::DeviceModel{T, U}, ::Type{<:PM.AbstractPowerModel}, ) where {T <: PSY.ThermalGen, U <: AbstractThermalDispatchFormulation} add_variable_cost!(container, ActivePowerVariable(), devices, U()) if get_use_slacks(device_model) add_proportional_cost!(container, RateofChangeConstraintSlackUp(), devices, U()) add_proportional_cost!(container, RateofChangeConstraintSlackDown(), devices, U()) end return end function objective_function!( container::OptimizationContainer, devices::IS.FlattenIteratorWrapper{T}, device_model::DeviceModel{T, U}, ::Type{<:PM.AbstractPowerModel}, ) where {T <: PSY.ThermalGen, U <: ThermalCompactDispatch} add_variable_cost!(container, PowerAboveMinimumVariable(), devices, U()) if get_use_slacks(device_model) add_proportional_cost!(container, RateofChangeConstraintSlackUp(), devices, U()) add_proportional_cost!(container, RateofChangeConstraintSlackDown(), devices, U()) end return end function objective_function!( ::OptimizationContainer, ::IS.FlattenIteratorWrapper{PSY.ThermalMultiStart}, ::DeviceModel{PSY.ThermalMultiStart, ThermalDispatchNoMin}, ::Type{<:PM.AbstractPowerModel}, ) throw( IS.ConflictingInputsError( "ThermalDispatchNoMin cost function is not compatible with ThermalMultiStart Devices.", ), ) end ================================================ FILE: src/feedforward/feedforward_arguments.jl ================================================ function add_feedforward_arguments!( container::OptimizationContainer, model::DeviceModel, devices::IS.FlattenIteratorWrapper{V}, ) where {V <: PSY.Component} for ff in get_feedforwards(model) @debug "arguments" ff V _group = LOG_GROUP_FEEDFORWARDS_CONSTRUCTION _add_feedforward_arguments!(container, model, devices, ff) end return end function add_feedforward_arguments!( container::OptimizationContainer, model::ServiceModel, service::V, ) where {V <: PSY.AbstractReserve} for ff in get_feedforwards(model) @debug "arguments" ff V _group = LOG_GROUP_FEEDFORWARDS_CONSTRUCTION contributing_devices = get_contributing_devices(model) _add_feedforward_arguments!(container, model, contributing_devices, ff) end return end function add_feedforward_arguments!( ::OptimizationContainer, model::ServiceModel, ::PSY.TransmissionInterface, ) # Currently we do not support feedforwards for TransmissionInterface ffs = get_feedforwards(model) if !isempty(ffs) throw( ArgumentError( "TransmissionInterface data types currently do not support feedforwards.", ), ) end return end function _add_feedforward_arguments!( container::OptimizationContainer, model::DeviceModel{T, U}, devices::IS.FlattenIteratorWrapper{T}, ff::AbstractAffectFeedforward, ) where {T <: PSY.Device, U <: AbstractDeviceFormulation} parameter_type = get_default_parameter_type(ff, T) add_parameters!(container, parameter_type, ff, model, devices) return end function _add_feedforward_arguments!( container::OptimizationContainer, model::ServiceModel{T, U}, contributing_devices::Vector, ff::AbstractAffectFeedforward, ) where {T <: PSY.AbstractReserve, U <: AbstractServiceFormulation} parameter_type = get_default_parameter_type(ff, U) add_parameters!(container, parameter_type, ff, model, contributing_devices) return end function _add_feedforward_slack_variables!(container::OptimizationContainer, ::T, ff::Union{LowerBoundFeedforward, UpperBoundFeedforward}, model::ServiceModel{U, V}, devices::Vector, ) where { T <: Union{LowerBoundFeedForwardSlack, UpperBoundFeedForwardSlack}, U <: PSY.AbstractReserve, V <: AbstractReservesFormulation, } time_steps = get_time_steps(container) for var in get_affected_values(ff) variable = get_variable(container, var) device_name_set, set_time = JuMP.axes(variable) device_names = PSY.get_name.(devices) @assert issetequal(device_name_set, device_names) IS.@assert_op set_time == time_steps service_name = get_service_name(model) var_type = get_entry_type(var) variable_container = add_variable_container!( container, T(), U, device_names, time_steps; meta = "$(var_type)_$(service_name)", ) for t in time_steps, name in device_name_set variable_container[name, t] = JuMP.@variable( get_jump_model(container), base_name = "$(T)_$(U)_{$(name), $(t)}", lower_bound = 0.0 ) add_to_objective_invariant_expression!( container, variable_container[name, t] * BALANCE_SLACK_COST, ) end end return end function _add_feedforward_slack_variables!( container::OptimizationContainer, ::T, ff::Union{LowerBoundFeedforward, UpperBoundFeedforward}, model::DeviceModel{U, V}, devices::IS.FlattenIteratorWrapper{U}, ) where { T <: Union{LowerBoundFeedForwardSlack, UpperBoundFeedForwardSlack}, U <: PSY.Device, V <: AbstractDeviceFormulation, } time_steps = get_time_steps(container) for var in get_affected_values(ff) variable = get_variable(container, var) device_name_set, set_time = JuMP.axes(variable) devices_names = PSY.get_name.(devices) @assert issetequal(device_name_set, devices_names) IS.@assert_op set_time == time_steps var_type = get_entry_type(var) variable = add_variable_container!( container, T(), U, devices_names, time_steps; meta = "$(var_type)", ) for t in time_steps, name in device_name_set variable[name, t] = JuMP.@variable( get_jump_model(container), base_name = "$(T)_$(U)_{$(name), $(t)}", lower_bound = 0.0 ) end end return end function _add_feedforward_arguments!( container::OptimizationContainer, model::DeviceModel{T, U}, devices::IS.FlattenIteratorWrapper{T}, ff::UpperBoundFeedforward, ) where {T <: PSY.Device, U <: AbstractDeviceFormulation} parameter_type = get_default_parameter_type(ff, T) add_parameters!(container, parameter_type, ff, model, devices) if get_slacks(ff) _add_feedforward_slack_variables!( container, UpperBoundFeedForwardSlack(), ff, model, devices, ) end return end function _add_feedforward_arguments!( container::OptimizationContainer, model::ServiceModel{T, U}, contributing_devices::Vector, ff::UpperBoundFeedforward, ) where {T <: PSY.AbstractReserve, U <: AbstractServiceFormulation} parameter_type = get_default_parameter_type(ff, SR) add_parameters!(container, parameter_type, ff, model, contributing_devices) if get_slacks(ff) _add_feedforward_slack_variables!( container, UpperBoundFeedForwardSlack(), ff, model, contributing_devices, ) end return end function _add_feedforward_arguments!( container::OptimizationContainer, model::DeviceModel{T, U}, devices::IS.FlattenIteratorWrapper{T}, ff::LowerBoundFeedforward, ) where {T <: PSY.Device, U <: AbstractDeviceFormulation} parameter_type = get_default_parameter_type(ff, T) add_parameters!(container, parameter_type, ff, model, devices) if get_slacks(ff) _add_feedforward_slack_variables!( container, LowerBoundFeedForwardSlack(), ff, model, devices, ) end return end function _add_feedforward_arguments!( container::OptimizationContainer, model::ServiceModel{T, U}, contributing_devices::Vector{V}, ff::LowerBoundFeedforward, ) where {T <: PSY.AbstractReserve, U <: AbstractReservesFormulation, V <: PSY.Component} parameter_type = get_default_parameter_type(ff, T) add_parameters!(container, parameter_type, ff, model, contributing_devices) if get_slacks(ff) _add_feedforward_slack_variables!( container, LowerBoundFeedForwardSlack(), ff, model, contributing_devices, ) end return end function _add_feedforward_arguments!( container::OptimizationContainer, model::DeviceModel{T, U}, devices::IS.FlattenIteratorWrapper{T}, ff::SemiContinuousFeedforward, ) where {T <: PSY.Device, U <: AbstractDeviceFormulation} parameter_type = get_default_parameter_type(ff, T) add_parameters!(container, parameter_type, ff, model, devices) add_to_expression!( container, ActivePowerRangeExpressionUB, parameter_type(), devices, model, ) add_to_expression!( container, ActivePowerRangeExpressionLB, parameter_type(), devices, model, ) return end ================================================ FILE: src/feedforward/feedforward_constraints.jl ================================================ function add_feedforward_constraints!( container::OptimizationContainer, model::DeviceModel, devices::IS.FlattenIteratorWrapper{V}, ) where {V <: PSY.Component} for ff in get_feedforwards(model) @debug "constraints" ff V _group = LOG_GROUP_FEEDFORWARDS_CONSTRUCTION add_feedforward_constraints!(container, model, devices, ff) end return end function add_feedforward_constraints!( container::OptimizationContainer, model::ServiceModel{V, <:AbstractReservesFormulation}, ::V, ) where {V <: PSY.AbstractReserve} for ff in get_feedforwards(model) @debug "constraints" ff V _group = LOG_GROUP_FEEDFORWARDS_CONSTRUCTION contributing_devices = get_contributing_devices(model) add_feedforward_constraints!(container, model, contributing_devices, ff) end return end function add_feedforward_constraints!( container::OptimizationContainer, model::ServiceModel, ::V, ) where {V <: PSY.Service} for ff in get_feedforwards(model) @debug "constraints" ff V _group = LOG_GROUP_FEEDFORWARDS_CONSTRUCTION contributing_devices = get_contributing_devices(model) add_feedforward_constraints!(container, model, contributing_devices, ff) end return end function _add_feedforward_constraints!( container::OptimizationContainer, ::Type{T}, param::P, ::VariableKey{U, V}, devices::IS.FlattenIteratorWrapper{V}, model::DeviceModel, ) where { T <: ConstraintType, P <: ParameterType, U <: VariableType, V <: PSY.Component, } time_steps = get_time_steps(container) names = PSY.get_name.(devices) constraint_lb = add_constraints_container!(container, T(), V, names, time_steps; meta = "$(U)_lb") constraint_ub = add_constraints_container!(container, T(), V, names, time_steps; meta = "$(U)_ub") array = get_variable(container, U(), V) upper_bound_range_with_parameter!( container, constraint_ub, array, param, devices, model, ) lower_bound_range_with_parameter!( container, constraint_lb, array, param, devices, model, ) return end function _add_sc_feedforward_constraints!( container::OptimizationContainer, ::Type{T}, ::P, ::VariableKey{U, V}, devices::IS.FlattenIteratorWrapper{V}, model::DeviceModel{V, W}, ) where { T <: FeedforwardSemiContinuousConstraint, P <: OnStatusParameter, U <: Union{ActivePowerVariable, PowerAboveMinimumVariable}, V <: PSY.Component, W <: AbstractDeviceFormulation, } time_steps = get_time_steps(container) names = PSY.get_name.(devices) constraint_lb = add_constraints_container!(container, T(), V, names, time_steps; meta = "$(U)_lb") constraint_ub = add_constraints_container!(container, T(), V, names, time_steps; meta = "$(U)_ub") array_lb = get_expression(container, ActivePowerRangeExpressionLB(), V) array_ub = get_expression(container, ActivePowerRangeExpressionUB(), V) parameter = get_parameter_array(container, P(), V) mult_ub = DenseAxisArray(zeros(length(devices), time_steps[end]), names, time_steps) mult_lb = DenseAxisArray(zeros(length(devices), time_steps[end]), names, time_steps) jump_model = get_jump_model(container) _upper_bound_range_with_parameter!( jump_model, constraint_ub, array_ub, mult_ub, parameter, devices, ) _lower_bound_range_with_parameter!( jump_model, constraint_lb, array_lb, mult_lb, parameter, devices, ) return end function _add_sc_feedforward_constraints!( container::OptimizationContainer, ::Type{T}, ::P, ::VariableKey{U, V}, devices::IS.FlattenIteratorWrapper{V}, model::DeviceModel{V, W}, ) where { T <: FeedforwardSemiContinuousConstraint, P <: ParameterType, U <: VariableType, V <: PSY.Component, W <: AbstractDeviceFormulation, } time_steps = get_time_steps(container) names = PSY.get_name.(devices) constraint_lb = add_constraints_container!(container, T(), V, names, time_steps; meta = "$(U)_lb") constraint_ub = add_constraints_container!(container, T(), V, names, time_steps; meta = "$(U)_ub") variable = get_variable(container, U(), V) parameter = get_parameter_array(container, P(), V) upper_bounds = [get_variable_upper_bound(U(), d, W()) for d in devices] lower_bounds = [get_variable_lower_bound(U(), d, W()) for d in devices] if any(isnothing.(upper_bounds)) || any(isnothing.(lower_bounds)) throw(IS.InvalidValueError("Bounds for variable $U $V not defined correctly")) end mult_ub = DenseAxisArray(repeat(upper_bounds, 1, time_steps[end]), names, time_steps) mult_lb = DenseAxisArray(repeat(lower_bounds, 1, time_steps[end]), names, time_steps) jump_model = get_jump_model(container) _upper_bound_range_with_parameter!( jump_model, constraint_ub, variable, mult_ub, parameter, devices, ) _lower_bound_range_with_parameter!( jump_model, constraint_lb, variable, mult_lb, parameter, devices, ) return end function _lower_bound_range_with_parameter!( jump_model::JuMP.Model, constraint_container::JuMPConstraintArray, lhs_array, param_multiplier::JuMPFloatArray, param_array::Union{JuMPVariableArray, JuMPFloatArray}, devices::IS.FlattenIteratorWrapper{V}, ) where {V <: PSY.Component} time_steps = axes(constraint_container)[2] for device in devices if hasmethod(PSY.get_must_run, Tuple{V}) PSY.get_must_run(device) && continue end name = PSY.get_name(device) for t in time_steps constraint_container[name, t] = JuMP.@constraint( jump_model, lhs_array[name, t] >= param_multiplier[name, t] * param_array[name, t] ) end end return end function _upper_bound_range_with_parameter!( jump_model::JuMP.Model, constraint_container::JuMPConstraintArray, lhs_array, param_multiplier::JuMPFloatArray, param_array::Union{JuMPVariableArray, JuMPFloatArray}, devices::IS.FlattenIteratorWrapper{V}, ) where {V <: PSY.Component} time_steps = axes(constraint_container)[2] for device in devices if hasmethod(PSY.get_must_run, Tuple{V}) PSY.get_must_run(device) && continue end name = PSY.get_name(device) for t in time_steps constraint_container[name, t] = JuMP.@constraint( jump_model, lhs_array[name, t] <= param_multiplier[name, t] * param_array[name, t] ) end end return end function add_feedforward_constraints!( container::OptimizationContainer, model::DeviceModel, devices::IS.FlattenIteratorWrapper{T}, ff::SemiContinuousFeedforward, ) where {T <: PSY.Component} parameter_type = get_default_parameter_type(ff, T) time_steps = get_time_steps(container) for var in get_affected_values(ff) variable = get_variable(container, var) axes = JuMP.axes(variable) @assert issetequal(axes[1], PSY.get_name.(devices)) IS.@assert_op axes[2] == time_steps # If the variable was a lower bound != 0, not removing the LB can cause infeasibilities for v in variable if JuMP.has_lower_bound(v) && JuMP.lower_bound(v) > 0.0 @debug "lb reset" JuMP.lower_bound(v) v _group = LOG_GROUP_FEEDFORWARDS_CONSTRUCTION JuMP.set_lower_bound(v, 0.0) end end _add_sc_feedforward_constraints!( container, FeedforwardSemiContinuousConstraint, parameter_type(), var, devices, model, ) end return end function add_feedforward_constraints!( container::OptimizationContainer, model::DeviceModel{T, U}, devices::IS.FlattenIteratorWrapper{T}, ff::SemiContinuousFeedforward, ) where {T <: PSY.ThermalGen, U <: AbstractThermalFormulation} parameter_type = get_default_parameter_type(ff, T) time_steps = get_time_steps(container) for var in get_affected_values(ff) variable = get_variable(container, var) axes = JuMP.axes(variable) @assert issetequal(axes[1], PSY.get_name.(devices)) IS.@assert_op axes[2] == time_steps # If the variable was a lower bound != 0, not removing the LB can cause infeasibilities for d in devices PSY.get_must_run(d) && continue for v in variable[PSY.get_name(d), :] if JuMP.has_lower_bound(v) && JuMP.lower_bound(v) > 0.0 @debug "lb reset $(PSY.get_name(d))" JuMP.lower_bound(v) v _group = LOG_GROUP_FEEDFORWARDS_CONSTRUCTION JuMP.set_lower_bound(v, 0.0) end end end _add_sc_feedforward_constraints!( container, FeedforwardSemiContinuousConstraint, parameter_type(), var, devices, model, ) end return end @doc raw""" ub_ff(container::OptimizationContainer, cons_name::Symbol, constraint_infos, param_reference, var_key::VariableKey) Constructs a parameterized upper bound constraint to implement feedforward from other models. The Parameters are initialized using the uppper boundary values of the provided variables. ``` variable[var_name, t] <= param_reference[var_name] ``` # LaTeX `` x \leq param^{max}`` # Arguments * container::OptimizationContainer : the optimization_container model built in PowerSimulations * cons_name::Symbol : name of the constraint * param_reference : Reference to the JuMP.VariableRef used to determine the upperbound * var_key::VariableKey : the name of the continuous variable """ function add_feedforward_constraints!( container::OptimizationContainer, ::DeviceModel, devices::IS.FlattenIteratorWrapper{T}, ff::UpperBoundFeedforward, ) where {T <: PSY.Component} time_steps = get_time_steps(container) parameter_type = get_default_parameter_type(ff, T) param_ub = get_parameter_array(container, parameter_type(), T) multiplier_ub = get_parameter_multiplier_array(container, parameter_type(), T) for var in get_affected_values(ff) variable = get_variable(container, var) device_name_set, set_time = JuMP.axes(variable) @assert issetequal(device_name_set, PSY.get_name.(devices)) IS.@assert_op set_time == time_steps var_type = get_entry_type(var) con_ub = add_constraints_container!( container, FeedforwardUpperBoundConstraint(), T, device_name_set, time_steps; meta = "$(var_type)ub", ) for t in time_steps, name in device_name_set con_ub[name, t] = JuMP.@constraint( container.JuMPmodel, variable[name, t] <= param_ub[name, t] * multiplier_ub[name, t] ) end end return end @doc raw""" lb_ff(container::OptimizationContainer, cons_name::Symbol, constraint_infos, param_reference, var_key::VariableKey) Constructs a parameterized upper bound constraint to implement feedforward from other models. The Parameters are initialized using the uppper boundary values of the provided variables. ``` variable[var_name, t] <= param_reference[var_name] ``` # LaTeX `` x \leq param^{max}`` # Arguments * container::OptimizationContainer : the optimization_container model built in PowerSimulations * cons_name::Symbol : name of the constraint * param_reference : Reference to the JuMP.VariableRef used to determine the upperbound * var_key::VariableKey : the name of the continuous variable """ function add_feedforward_constraints!( container::OptimizationContainer, ::DeviceModel{T, U}, devices::IS.FlattenIteratorWrapper{T}, ff::LowerBoundFeedforward, ) where {T <: PSY.Component, U <: AbstractDeviceFormulation} time_steps = get_time_steps(container) parameter_type = get_default_parameter_type(ff, T) param_ub = get_parameter_array(container, parameter_type(), T) multiplier_ub = get_parameter_multiplier_array(container, parameter_type(), T) for var in get_affected_values(ff) variable = get_variable(container, var) device_name_set, set_time = JuMP.axes(variable) @assert issetequal(device_name_set, PSY.get_name.(devices)) IS.@assert_op set_time == time_steps var_type = get_entry_type(var) con_ub = add_constraints_container!( container, FeedforwardLowerBoundConstraint(), T, device_name_set, time_steps; meta = "$(var_type)lb", ) use_slacks = get_slacks(ff) for t in time_steps, name in device_name_set if use_slacks slack_var = get_variable(container, LowerBoundFeedForwardSlack(), T, "$(var_type)") con_ub[name, t] = JuMP.@constraint( get_jump_model(container), variable[name, t] + slack_var[name, t] >= param_ub[name, t] * multiplier_ub[name, t] ) else con_ub[name, t] = JuMP.@constraint( get_jump_model(container), variable[name, t] >= param_ub[name, t] * multiplier_ub[name, t] ) end end end return end function add_feedforward_constraints!( container::OptimizationContainer, model::ServiceModel{T, U}, contributing_devices::Vector{V}, ff::LowerBoundFeedforward, ) where {T <: PSY.Service, U <: AbstractServiceFormulation, V <: PSY.Component} time_steps = get_time_steps(container) parameter_type = get_default_parameter_type(ff, T) param_ub = get_parameter_array(container, parameter_type(), T, get_service_name(model)) service_name = get_service_name(model) multiplier_ub = get_parameter_multiplier_array( container, parameter_type(), T, service_name, ) use_slacks = get_slacks(ff) for var in get_affected_values(ff) variable = get_variable(container, var) device_name_set, set_time = JuMP.axes(variable) IS.@assert_op device_name_set == [PSY.get_name(d) for d in contributing_devices] IS.@assert_op set_time == time_steps var_type = get_entry_type(var) con_lb = add_constraints_container!( container, FeedforwardLowerBoundConstraint(), T, device_name_set, time_steps; meta = "$(var_type)_$(service_name)", ) for t in time_steps, name in device_name_set if use_slacks slack_var = get_variable( container, LowerBoundFeedForwardSlack(), T, "$(var_type)_$(service_name)", ) slack_var[name, t] con_lb[name, t] = JuMP.@constraint( get_jump_model(container), variable[name, t] + slack_var[name, t] >= param_ub[name, t] * multiplier_ub[name, t] ) else con_lb[name, t] = JuMP.@constraint( get_jump_model(container), variable[name, t] >= param_ub[name, t] * multiplier_ub[name, t] ) end end end return end @doc raw""" add_feedforward_constraints( container::OptimizationContainer, ::DeviceModel, devices::IS.FlattenIteratorWrapper{T}, ff::FixValueFeedforward, ) where {T <: PSY.Component} Constructs a equality constraint to a fix a variable in one model using the variable value from other model results. ``` variable[var_name, t] == param[var_name, t] ``` # LaTeX `` x == param`` # Arguments * container::OptimizationContainer : the optimization_container model built in PowerSimulations * model::DeviceModel : the device model * devices::IS.FlattenIteratorWrapper{T} : list of devices * ff::FixValueFeedforward : a instance of the FixValue Feedforward """ function add_feedforward_constraints!( container::OptimizationContainer, ::DeviceModel, devices::Union{Vector{T}, IS.FlattenIteratorWrapper{T}}, ff::FixValueFeedforward, ) where {T <: PSY.Component} parameter_type = get_default_parameter_type(ff, T) source_key = get_optimization_container_key(ff) var_type = get_entry_type(source_key) param = get_parameter_array(container, parameter_type(), T, "$var_type") multiplier = get_parameter_multiplier_array(container, parameter_type(), T, "$var_type") for var in get_affected_values(ff) variable = get_variable(container, var) device_name_set, set_time = JuMP.axes(variable) IS.@assert_op device_name_set == PSY.get_name.(devices) for t in set_time, name in device_name_set JuMP.fix(variable[name, t], param[name, t] * multiplier[name, t]; force = true) end end return end function add_feedforward_constraints!( container::OptimizationContainer, model::ServiceModel{T, U}, devices::Vector{V}, ff::FixValueFeedforward, ) where {T, U, V <: PSY.Component} time_steps = get_time_steps(container) parameter_type = get_default_parameter_type(ff, T) param = get_parameter_array(container, parameter_type(), T, get_service_name(model)) multiplier = get_parameter_multiplier_array( container, parameter_type(), T, get_service_name(model), ) for var in get_affected_values(ff) variable = get_variable(container, var) device_name_set, set_time = JuMP.axes(variable) @assert issetequal(device_name_set, PSY.get_name.(devices)) IS.@assert_op set_time == time_steps for t in time_steps, name in device_name_set JuMP.fix(variable[name, t], param[name, t] * multiplier[name, t]; force = true) end end return end ================================================ FILE: src/feedforward/feedforwards.jl ================================================ function get_affected_values(ff::AbstractAffectFeedforward) return ff.affected_values end function attach_feedforward!( model::DeviceModel, ff::T, ) where {T <: AbstractAffectFeedforward} if !isempty(model.feedforwards) ff_k = [get_optimization_container_key(v) for v in model.feedforwards if isa(v, T)] if get_optimization_container_key(ff) ∈ ff_k return end end push!(model.feedforwards, ff) return end function attach_feedforward!( model::ServiceModel, ff::T, ) where {T <: AbstractAffectFeedforward} if get_feedforward_meta(ff) != NO_SERVICE_NAME_PROVIDED ff_ = ff else ff_ = T(; component_type = get_component_type(ff), source = get_entry_type(get_optimization_container_key(ff)), affected_values = affected_values = get_entry_type.(get_affected_values(ff)), meta = model.service_name, ) end if !isempty(model.feedforwards) ff_k = [get_optimization_container_key(v) for v in model.feedforwards if isa(v, T)] if get_optimization_container_key(ff_) ∈ ff_k return end end push!(model.feedforwards, ff_) return end function get_component_type(ff::AbstractAffectFeedforward) return get_component_type(get_optimization_container_key(ff)) end function get_feedforward_meta(ff::AbstractAffectFeedforward) return get_optimization_container_key(ff).meta end """ 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 affected_values::Vector add_slacks::Bool function UpperBoundFeedforward(; component_type::Type{<:PSY.Component}, source::Type{T}, affected_values::Vector{DataType}, add_slacks::Bool = false, meta = ISOPT.CONTAINER_KEY_EMPTY_META, ) where {T} values_vector = Vector(undef, length(affected_values)) for (ix, v) in enumerate(affected_values) if v <: VariableType values_vector[ix] = get_optimization_container_key(v(), component_type, meta) else error( "UpperBoundFeedforward is only compatible with VariableType affected values", ) end end new( get_optimization_container_key(T(), component_type, meta), values_vector, add_slacks, ) end end get_default_parameter_type(::UpperBoundFeedforward, _) = UpperBoundValueParameter get_optimization_container_key(ff::UpperBoundFeedforward) = ff.optimization_container_key get_slacks(ff::UpperBoundFeedforward) = ff.add_slacks """ 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 affected_values::Vector{<:OptimizationContainerKey} add_slacks::Bool function LowerBoundFeedforward(; component_type::Type{<:PSY.Component}, source::Type{T}, affected_values::Vector{DataType}, add_slacks::Bool = false, meta = ISOPT.CONTAINER_KEY_EMPTY_META, ) where {T} values_vector = Vector{VariableKey}(undef, length(affected_values)) for (ix, v) in enumerate(affected_values) if v <: VariableType values_vector[ix] = get_optimization_container_key(v(), component_type, meta) else error( "LowerBoundFeedforward is only compatible with VariableType affected values", ) end end new( get_optimization_container_key(T(), component_type, meta), values_vector, add_slacks, ) end end get_default_parameter_type(::LowerBoundFeedforward, _) = LowerBoundValueParameter get_optimization_container_key(ff::LowerBoundFeedforward) = ff.optimization_container_key get_slacks(ff::LowerBoundFeedforward) = ff.add_slacks function attach_feedforward!( model::ServiceModel, ff::T, ) where {T <: Union{LowerBoundFeedforward, UpperBoundFeedforward}} if get_feedforward_meta(ff) != NO_SERVICE_NAME_PROVIDED ff_ = ff else ff_ = T(; component_type = get_component_type(ff), source = get_entry_type(get_optimization_container_key(ff)), affected_values = get_entry_type.(get_affected_values(ff)), meta = model.service_name, add_slacks = ff.add_slacks, ) end if !isempty(model.feedforwards) ff_k = [get_optimization_container_key(v) for v in model.feedforwards if isa(v, T)] if get_optimization_container_key(ff_) ∈ ff_k return end end push!(model.feedforwards, ff_) return end """ 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 affected_values::Vector{<:OptimizationContainerKey} function SemiContinuousFeedforward(; component_type::Type{<:PSY.Component}, source::Type{T}, affected_values::Vector{DataType}, meta = ISOPT.CONTAINER_KEY_EMPTY_META, ) where {T} values_vector = Vector{VariableKey}(undef, length(affected_values)) for (ix, v) in enumerate(affected_values) if v <: VariableType values_vector[ix] = get_optimization_container_key(v(), component_type, meta) else error( "SemiContinuousFeedforward is only compatible with VariableType affected values", ) end end new(get_optimization_container_key(T(), component_type, meta), values_vector) end end get_default_parameter_type(::SemiContinuousFeedforward, _) = OnStatusParameter get_optimization_container_key(f::SemiContinuousFeedforward) = f.optimization_container_key function has_semicontinuous_feedforward( model::DeviceModel, ::Type{T}, )::Bool where {T <: Union{VariableType, ExpressionType}} if isempty(model.feedforwards) return false end sc_feedforwards = [x for x in model.feedforwards if isa(x, SemiContinuousFeedforward)] if isempty(sc_feedforwards) return false end keys = get_affected_values(sc_feedforwards[1]) return T ∈ get_entry_type.(keys) end function has_semicontinuous_feedforward( model::DeviceModel, ::Type{T}, )::Bool where {T <: Union{ActivePowerRangeExpressionUB, ActivePowerRangeExpressionLB}} return has_semicontinuous_feedforward(model, ActivePowerVariable) end """ 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 affected_values::Vector function FixValueFeedforward(; component_type::Type{<:PSY.Component}, source::Type{T}, affected_values::Vector{DataType}, meta = ISOPT.CONTAINER_KEY_EMPTY_META, ) where {T} values_vector = Vector(undef, length(affected_values)) for (ix, v) in enumerate(affected_values) if v <: VariableType || v <: ParameterType values_vector[ix] = get_optimization_container_key(v(), component_type, meta) else error( "UpperBoundFeedforward is only compatible with VariableType affected values", ) end end new(get_optimization_container_key(T(), component_type, meta), values_vector) end end get_default_parameter_type(::FixValueFeedforward, _) = FixValueParameter get_optimization_container_key(ff::FixValueFeedforward) = ff.optimization_container_key ================================================ FILE: src/initial_conditions/add_initial_condition.jl ================================================ function _get_initial_conditions_value( ::Vector{T}, component::W, ::U, ::V, container::OptimizationContainer, ) where { T <: InitialCondition{U, Nothing}, V <: Union{AbstractDeviceFormulation, AbstractServiceFormulation}, W <: PSY.Component, } where {U <: InitialConditionType} return InitialCondition{U, Nothing}(component, nothing) end function _get_initial_conditions_value( ::Vector{T}, component::W, ::U, ::V, container::OptimizationContainer, ) where { T <: Union{InitialCondition{U, Float64}, InitialCondition{U, Nothing}}, V <: Union{AbstractDeviceFormulation, AbstractServiceFormulation}, W <: PSY.Component, } where {U <: InitialConditionType} ic_data = get_initial_conditions_data(container) var_type = initial_condition_variable(U(), component, V()) if !has_initial_condition_value(ic_data, var_type, W) val = initial_condition_default(U(), component, V()) else val = get_initial_condition_value(ic_data, var_type, W)[PSY.get_name(component), 1] end @debug "Device $(PSY.get_name(component)) initialized $U as $val" _group = LOG_GROUP_BUILD_INITIAL_CONDITIONS return InitialCondition{U, Float64}(component, val) end function _get_initial_conditions_value( ::Vector{T}, component::W, ::U, ::V, container::OptimizationContainer, ) where { T <: Union{InitialCondition{U, JuMP.VariableRef}, InitialCondition{U, Nothing}}, V <: AbstractDeviceFormulation, W <: PSY.Component, } where {U <: InitialConditionType} ic_data = get_initial_conditions_data(container) var_type = initial_condition_variable(U(), component, V()) if !has_initial_condition_value(ic_data, var_type, W) val = initial_condition_default(U(), component, V()) else val = get_initial_condition_value(ic_data, var_type, W)[PSY.get_name(component), 1] end @debug "Device $(PSY.get_name(component)) initialized $U as $val" _group = LOG_GROUP_BUILD_INITIAL_CONDITIONS return InitialCondition{U, JuMP.VariableRef}( component, add_jump_parameter(get_jump_model(container), val), ) end function _get_initial_conditions_value( ::Vector{Union{InitialCondition{U, Float64}, InitialCondition{U, Nothing}}}, component::W, ::U, ::V, container::OptimizationContainer, ) where { V <: AbstractThermalFormulation, W <: PSY.Component, } where {U <: InitialTimeDurationOff} ic_data = get_initial_conditions_data(container) var_type = initial_condition_variable(U(), component, V()) if !has_initial_condition_value(ic_data, var_type, W) val = initial_condition_default(U(), component, V()) else var = get_initial_condition_value(ic_data, var_type, W)[PSY.get_name(component), 1] val = 0.0 if !PSY.get_status(component) && !(var > ABSOLUTE_TOLERANCE) val = PSY.get_time_at_status(component) end end @debug "Device $(PSY.get_name(component)) initialized $U as $val" _group = LOG_GROUP_BUILD_INITIAL_CONDITIONS return InitialCondition{U, Float64}(component, val) end function _get_initial_conditions_value( ::Vector{Union{InitialCondition{U, JuMP.VariableRef}, InitialCondition{U, Nothing}}}, component::W, ::U, ::V, container::OptimizationContainer, ) where { V <: AbstractThermalFormulation, W <: PSY.ThermalGen, } where {U <: InitialTimeDurationOff} ic_data = get_initial_conditions_data(container) var_type = initial_condition_variable(U(), component, V()) if !has_initial_condition_value(ic_data, var_type, W) val = initial_condition_default(U(), component, V()) else var = get_initial_condition_value(ic_data, var_type, W)[PSY.get_name(component), 1] val = 0.0 if !PSY.get_status(component) && !(var > ABSOLUTE_TOLERANCE) val = PSY.get_time_at_status(component) end end @debug "Device $(PSY.get_name(component)) initialized $U as $val" _group = LOG_GROUP_BUILD_INITIAL_CONDITIONS return InitialCondition{U, JuMP.VariableRef}( component, add_jump_parameter(get_jump_model(container), val), ) end function _get_initial_conditions_value( ::Vector{Union{InitialCondition{U, Float64}, InitialCondition{U, Nothing}}}, component::W, ::U, ::V, container::OptimizationContainer, ) where { V <: AbstractThermalFormulation, W <: PSY.ThermalGen, } where {U <: InitialTimeDurationOn} ic_data = get_initial_conditions_data(container) var_type = initial_condition_variable(U(), component, V()) if !has_initial_condition_value(ic_data, var_type, W) val = initial_condition_default(U(), component, V()) else var = get_initial_condition_value(ic_data, var_type, W)[PSY.get_name(component), 1] val = 0.0 if PSY.get_status(component) && (var > ABSOLUTE_TOLERANCE) val = PSY.get_time_at_status(component) end end @debug "Device $(PSY.get_name(component)) initialized $U as $val" _group = LOG_GROUP_BUILD_INITIAL_CONDITIONS return InitialCondition{U, Float64}(component, val) end function _get_initial_conditions_value( ::Vector{Union{InitialCondition{U, JuMP.VariableRef}, InitialCondition{U, Nothing}}}, component::W, ::U, ::V, container::OptimizationContainer, ) where { V <: AbstractThermalFormulation, W <: PSY.ThermalGen, } where {U <: InitialTimeDurationOn} ic_data = get_initial_conditions_data(container) var_type = initial_condition_variable(U(), component, V()) if !has_initial_condition_value(ic_data, var_type, W) val = initial_condition_default(U(), component, V()) else var = get_initial_condition_value(ic_data, var_type, W)[PSY.get_name(component), 1] val = 0.0 if PSY.get_status(component) && (var > ABSOLUTE_TOLERANCE) val = PSY.get_time_at_status(component) end end @debug "Device $(PSY.get_name(component)) initialized $U as $val" _group = LOG_GROUP_BUILD_INITIAL_CONDITIONS return InitialCondition{U, JuMP.VariableRef}( component, add_jump_parameter(get_jump_model(container), val), ) end function _get_initial_conditions_value( ::Vector{T}, component::W, ::U, ::V, container::OptimizationContainer, ) where { T <: InitialCondition{U, JuMP.VariableRef}, V <: AbstractDeviceFormulation, W <: PSY.Component, } where {U <: InitialEnergyLevel} var_type = initial_condition_variable(U(), component, V()) val = initial_condition_default(U(), component, V()) @debug "Device $(PSY.get_name(component)) initialized $U as $val" _group = LOG_GROUP_BUILD_INITIAL_CONDITIONS return T(component, add_jump_parameter(get_jump_model(container), val)) end function _get_initial_conditions_value( ::Vector{T}, component::W, ::U, ::V, container::OptimizationContainer, ) where { T <: InitialCondition{U, Float64}, V <: AbstractDeviceFormulation, W <: PSY.Component, } where {U <: InitialEnergyLevel} var_type = initial_condition_variable(U(), component, V()) val = initial_condition_default(U(), component, V()) @debug "Device $(PSY.get_name(component)) initialized $U as $val" _group = LOG_GROUP_BUILD_INITIAL_CONDITIONS return T(component, val) end function add_initial_condition!( container::OptimizationContainer, components::Union{Vector{T}, IS.FlattenIteratorWrapper{T}}, ::U, ::D, ) where { T <: PSY.Component, U <: Union{AbstractDeviceFormulation, AbstractServiceFormulation}, D <: InitialConditionType, } if get_rebuild_model(get_settings(container)) && has_container_key(container, D, T) return end ini_cond_vector = add_initial_condition_container!(container, D(), T, components) for (ix, component) in enumerate(components) ini_cond_vector[ix] = _get_initial_conditions_value(ini_cond_vector, component, D(), U(), container) end return end function add_initial_condition!( container::OptimizationContainer, components::Union{Vector{T}, IS.FlattenIteratorWrapper{T}}, ::U, ::D, ) where { T <: PSY.ThermalGen, U <: AbstractThermalFormulation, D <: Union{InitialTimeDurationOff, InitialTimeDurationOn, DeviceStatus}, } if get_rebuild_model(get_settings(container)) && has_container_key(container, D, T) return end ini_cond_vector = add_initial_condition_container!(container, D(), T, components) for (ix, component) in enumerate(components) if PSY.get_must_run(component) ini_cond_vector[ix] = InitialCondition{D, Nothing}(component, nothing) else ini_cond_vector[ix] = _get_initial_conditions_value( ini_cond_vector, component, D(), U(), container, ) end end return end ================================================ FILE: src/initial_conditions/calculate_initial_condition.jl ================================================ """ Default implementation of set_initial_condition_value """ function set_ic_quantity!( ic::InitialCondition{T, JuMP.VariableRef}, var_value::Float64, ) where {T <: InitialConditionType} @assert isfinite(var_value) ic fix_parameter_value(ic.value, var_value) return end """ Default implementation of set_initial_condition_value """ function set_ic_quantity!( ic::InitialCondition{T, Float64}, var_value::Float64, ) where {T <: InitialConditionType} @assert isfinite(var_value) ic @debug "Initial condition value set with Float64. Won't update the model until rebuild" _group = LOG_GROUP_BUILD_INITIAL_CONDITIONS ic.value = var_value return end function set_ic_quantity!( ::InitialCondition{T, Nothing}, ::Float64, ) where {T <: InitialConditionType} return end ================================================ FILE: src/initial_conditions/initial_condition_chronologies.jl ================================================ """ Supertype for initial condition chronologies """ abstract type InitialConditionChronology end """ InterProblemChronology() Type struct to select an information sharing model between stages that uses results from the most recent stage executed to calculate the initial conditions. This model takes into account solutions from stages defined with finer temporal resolutions See also: [`IntraProblemChronology`](@ref) """ struct InterProblemChronology <: InitialConditionChronology end """ IntraProblemChronology() Type struct to select an information sharing model between stages that uses results from the same recent stage to calculate the initial conditions. This model ignores solutions from stages defined with finer temporal resolutions. See also: [`InterProblemChronology`](@ref) """ struct IntraProblemChronology <: InitialConditionChronology end ================================================ FILE: src/initial_conditions/initialization.jl ================================================ function get_initial_conditions_template(model::OperationModel, number_of_steps::Int) # This is done to avoid passing the duals but also not re-allocating the PTDF when it # exists network_model = NetworkModel( get_network_formulation(model.template); use_slacks = get_use_slacks(get_network_model(model.template)), PTDF_matrix = get_PTDF_matrix(get_network_model(model.template)), reduce_radial_branches = get_reduce_radial_branches( get_network_model(model.template), ), ) set_hvdc_network_model!( network_model, deepcopy(get_hvdc_network_model(model.template)), ) network_model.network_reduction = deepcopy(get_network_reduction(get_network_model(model.template))) network_model.subnetworks = get_subnetworks(get_network_model(model.template)) # Initialization does not support PowerFlow evaluation network_model.power_flow_evaluation = Vector{PFS.PowerFlowEvaluationModel}[] bus_area_map = get_bus_area_map(get_network_model(model.template)) if !isempty(bus_area_map) network_model.bus_area_map = get_bus_area_map(get_network_model(model.template)) end network_model.modeled_ac_branch_types = get_network_model(model.template).modeled_ac_branch_types ic_template = ProblemTemplate(network_model) # Do not copy events here for initialization for device_model in values(model.template.devices) base_model = get_initial_conditions_device_model(model, device_model) base_model.use_slacks = device_model.use_slacks base_model.time_series_names = device_model.time_series_names base_model.attributes = device_model.attributes set_device_model!(ic_template, base_model) end for device_model in values(model.template.branches) base_model = get_initial_conditions_device_model(model, device_model) base_model.use_slacks = device_model.use_slacks base_model.time_series_names = device_model.time_series_names base_model.attributes = device_model.attributes set_device_model!(ic_template, base_model) end for service_model in values(model.template.services) base_model = get_initial_conditions_service_model(model, service_model) base_model.service_name = service_model.service_name base_model.contributing_devices_map = service_model.contributing_devices_map 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, get_service_name(service_model), base_model) end set_number_of_steps!(network_model.reduced_branch_tracker, number_of_steps) if !isempty(model.template.services) _add_services_to_device_model!(ic_template) end return ic_template end function _make_init_jump_model(ic_settings::Settings) optimizer = get_optimizer(ic_settings) JuMPmodel = JuMP.Model(optimizer) warm_start_enabled = get_warm_start(ic_settings) solver_supports_warm_start = _validate_warm_start_support(JuMPmodel, warm_start_enabled) set_warm_start!(ic_settings, solver_supports_warm_start) if get_optimizer_solve_log_print(ic_settings) JuMP.unset_silent(JuMPmodel) @debug "optimizer unset to silent" _group = LOG_GROUP_OPTIMIZATION_CONTAINER else JuMP.set_silent(JuMPmodel) @debug "optimizer set to silent" _group = LOG_GROUP_OPTIMIZATION_CONTAINER end return JuMPmodel end function build_initial_conditions_model!(model::T) where {T <: OperationModel} internal = get_internal(model) ISOPT.set_initial_conditions_model_container!( internal, deepcopy(get_optimization_container(model)), ) ic_container = ISOPT.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 ic_container.JuMPmodel = _make_init_jump_model(ic_settings) resolution = get_resolution(ic_settings) init_horizon = INITIALIZATION_PROBLEM_HORIZON_COUNT * resolution number_of_steps = min(init_horizon, main_problem_horizon) template = get_initial_conditions_template(model, number_of_steps ÷ resolution) ic_container.settings = ic_settings ic_container.built_for_recurrent_solves = false set_horizon!(ic_settings, number_of_steps) init_optimization_container!( ISOPT.get_initial_conditions_model_container(internal), get_network_model(get_template(model)), get_system(model), ) JuMP.set_string_names_on_creation( get_jump_model(ISOPT.get_initial_conditions_model_container(internal)), false, ) 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 ================================================ FILE: src/initial_conditions/update_initial_conditions.jl ================================================ function _update_initial_conditions!( model::OperationModel, 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(get_store_params(model)) ini_conditions_vector = get_initial_condition(container, key) timestamp = get_current_timestamp(model) previous_values = get_condition.(ini_conditions_vector) # The implementation of specific update_initial_conditions! is located in the files # update_initial_conditions_in_memory_store.jl and update_initial_conditions_simulation.jl update_initial_conditions!(ini_conditions_vector, source, model_resolution) for (i, initial_condition) in enumerate(ini_conditions_vector) IS.@record :execution InitialConditionUpdateEvent( timestamp, initial_condition, previous_values[i], get_name(model), ) end return end # Note to devs: Implemented this way to avoid ambiguities and future proof custom ic updating function update_initial_conditions!( model::DecisionModel, 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) return end function update_initial_conditions!( model::EmulationModel, 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) return end ================================================ FILE: src/network_models/area_balance_model.jl ================================================ 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}}, branches, ) time_steps = get_time_steps(container) nodal_net_balance = get_expression(container, expression) constraint = add_constraints_container!( container, CopperPlateBalanceConstraint(), PSY.Area, keys(area_mapping), time_steps, ) area_balance = get_variable(container, ActivePowerVariable(), PSY.Area) for (k, buses_in_area) in area_mapping for t in time_steps area_net = JuMP.AffExpr(0.0) for b in buses_in_area JuMP.add_to_expression!(area_net, nodal_net_balance[PSY.get_number(b), t]) end constraint[k, t] = JuMP.@constraint(get_jump_model(container), area_balance[k, t] == area_net) end end expr_up = get_expression(container, EmergencyUp(), PSY.Area) expr_dn = get_expression(container, EmergencyDown(), PSY.Area) participation_assignment_up = add_constraints_container!( container, AreaParticipationAssignmentConstraint(), PSY.Area, keys(area_mapping), time_steps; meta = "up", ) participation_assignment_dn = add_constraints_container!( container, AreaParticipationAssignmentConstraint(), PSY.Area, keys(area_mapping), time_steps; meta = "dn", ) for area in keys(area_mapping), t in time_steps participation_assignment_up[area, t] = JuMP.@constraint(get_jump_model(container), expr_up[area, t] == 0) participation_assignment_dn[area, t] = JuMP.@constraint(get_jump_model(container), expr_dn[area, t] == 0) end return end =# ================================================ FILE: src/network_models/copperplate_model.jl ================================================ function add_constraints!( container::OptimizationContainer, ::Type{T}, sys::U, model::NetworkModel{V}, ) where { T <: CopperPlateBalanceConstraint, U <: PSY.System, V <: Union{CopperPlatePowerModel, PTDFPowerModel}, } time_steps = get_time_steps(container) expressions = get_expression(container, ActivePowerBalance(), U) subnets = collect(keys(model.subnetworks)) constraint = add_constraints_container!(container, T(), U, subnets, time_steps) for t in time_steps, k in keys(model.subnetworks) constraint[k, t] = JuMP.@constraint(get_jump_model(container), expressions[k, t] == 0) end return end function add_constraints!( container::OptimizationContainer, ::Type{T}, sys::U, network_model::NetworkModel{V}, ) where { T <: CopperPlateBalanceConstraint, U <: PSY.System, V <: AreaPTDFPowerModel, } 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 ================================================ FILE: src/network_models/hvdc_network_constructor.jl ================================================ function construct_hvdc_network!( container::OptimizationContainer, sys::PSY.System, transmission_model::NetworkModel{T}, hvdc_model::Nothing, ::ProblemTemplate, ) where {T <: PM.AbstractPowerModel} return end function construct_hvdc_network!( container::OptimizationContainer, sys::PSY.System, transmission_model::NetworkModel{T}, hvdc_model::TransportHVDCNetworkModel, ::ProblemTemplate, ) where {T <: PM.AbstractPowerModel} add_constraints!( container, NodalBalanceActiveConstraint, sys, transmission_model, hvdc_model, ) # TODO: duals #add_constraint_dual!(container, sys, hvdc_model) return end function construct_hvdc_network!( container::OptimizationContainer, sys::PSY.System, transmission_model::NetworkModel{T}, hvdc_model::VoltageDispatchHVDCNetworkModel, ::ProblemTemplate, ) where {T <: PM.AbstractPowerModel} add_constraints!( container, NodalBalanceCurrentConstraint, sys, transmission_model, hvdc_model, ) # TODO: duals #add_constraint_dual!(container, sys, hvdc_model) return end ================================================ FILE: src/network_models/hvdc_networks.jl ================================================ ## To add method of upper_bounds and lower_bounds for DCVoltage get_variable_binary(::DCVoltage, ::Type{PSY.DCBus}, ::AbstractHVDCNetworkModel) = false get_variable_lower_bound(::DCVoltage, d::PSY.DCBus, ::AbstractHVDCNetworkModel) = PSY.get_voltage_limits(d).min get_variable_upper_bound(::DCVoltage, d::PSY.DCBus, ::AbstractHVDCNetworkModel) = PSY.get_voltage_limits(d).max function add_constraints!( container::OptimizationContainer, ::Type{NodalBalanceActiveConstraint}, sys::PSY.System, model::NetworkModel{V}, hvdc_model::W, ) where {V <: PM.AbstractPowerModel, W <: TransportHVDCNetworkModel} dc_buses = PSY.get_components(PSY.DCBus, sys) if isempty(dc_buses) return end time_steps = get_time_steps(container) dc_expr = get_expression(container, ActivePowerBalance(), PSY.DCBus) balance_constraint = add_constraints_container!( container, NodalBalanceActiveConstraint(), PSY.DCBus, axes(dc_expr)[1], time_steps, ) for d in dc_buses dc_bus_no = PSY.get_number(d) for t in time_steps balance_constraint[dc_bus_no, t] = JuMP.@constraint(get_jump_model(container), dc_expr[dc_bus_no, t] == 0) end end return end function add_constraints!( container::OptimizationContainer, ::Type{NodalBalanceCurrentConstraint}, sys::PSY.System, model::NetworkModel{V}, hvdc_model::W, ) where {V <: PM.AbstractPowerModel, W <: VoltageDispatchHVDCNetworkModel} dc_buses = PSY.get_components(PSY.DCBus, sys) if isempty(dc_buses) return end time_steps = get_time_steps(container) dc_expr = get_expression(container, DCCurrentBalance(), PSY.DCBus) balance_constraint = add_constraints_container!( container, NodalBalanceCurrentConstraint(), PSY.DCBus, axes(dc_expr)[1], time_steps, ) for d in dc_buses dc_bus_no = PSY.get_number(d) for t in time_steps balance_constraint[dc_bus_no, t] = JuMP.@constraint(get_jump_model(container), dc_expr[dc_bus_no, t] == 0) end end return end ================================================ FILE: src/network_models/network_constructor.jl ================================================ function construct_network!( container::OptimizationContainer, sys::PSY.System, model::NetworkModel{CopperPlatePowerModel}, ::ProblemTemplate, ) if get_use_slacks(model) 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 add_constraints!(container, CopperPlateBalanceConstraint, sys, model) add_constraint_dual!(container, sys, model) return end function construct_network!( container::OptimizationContainer, sys::PSY.System, model::NetworkModel{AreaBalancePowerModel}, ::ProblemTemplate, ) if get_use_slacks(model) 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 add_constraints!(container, CopperPlateBalanceConstraint, sys, model) add_constraint_dual!(container, sys, model) return end function construct_network!( container::OptimizationContainer, sys::PSY.System, model::NetworkModel{<:AbstractPTDFModel}, ::ProblemTemplate, ) if get_use_slacks(model) 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 add_constraints!(container, CopperPlateBalanceConstraint, sys, model) add_constraint_dual!(container, sys, model) return end function construct_network!( container::OptimizationContainer, sys::PSY.System, model::NetworkModel{T}, template::ProblemTemplate; ) where {T <: PM.AbstractActivePowerModel} if T in UNSUPPORTED_POWERMODELS throw( ArgumentError( "$(T) formulation is not currently supported in PowerSimulations", ), ) end if get_use_slacks(model) 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 @debug "Building the $T network with instantiate_nip_expr_model method" _group = LOG_GROUP_NETWORK_CONSTRUCTION powermodels_network!(container, T, sys, template, instantiate_nip_expr_model) add_pm_variable_refs!(container, T, sys, model) add_pm_constraint_refs!(container, T, sys) add_constraint_dual!(container, sys, model) return end function construct_network!( container::OptimizationContainer, sys::PSY.System, model::NetworkModel{T}, template::ProblemTemplate; ) where {T <: PM.AbstractPowerModel} if T in UNSUPPORTED_POWERMODELS throw( ArgumentError( "$(T) formulation is not currently supported in PowerSimulations", ), ) end if get_use_slacks(model) 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, ) add_to_expression!( container, ReactivePowerBalance, SystemBalanceSlackUp, sys, model, ) add_to_expression!( container, ReactivePowerBalance, SystemBalanceSlackDown, sys, model, ) objective_function!(container, sys, model) end @debug "Building the $T network with instantiate_nip_expr_model method" _group = LOG_GROUP_NETWORK_CONSTRUCTION powermodels_network!(container, T, sys, template, instantiate_nip_expr_model) add_pm_variable_refs!(container, T, sys, model) add_pm_constraint_refs!(container, T, sys) add_constraint_dual!(container, sys, model) return end function construct_network!( container::OptimizationContainer, sys::PSY.System, model::NetworkModel{T}, template::ProblemTemplate, ) where {T <: PM.AbstractBFModel} if T in UNSUPPORTED_POWERMODELS throw( ArgumentError( "$(T) formulation is not currently supported in PowerSimulations", ), ) end if get_use_slacks(model) 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, ) add_to_expression!( container, ReactivePowerBalance, SystemBalanceSlackUp, sys, model, ) add_to_expression!( container, ReactivePowerBalance, SystemBalanceSlackDown, sys, model, ) objective_function!(container, sys, model) end @debug "Building the $T network with instantiate_bfp_expr_model method" _group = LOG_GROUP_NETWORK_CONSTRUCTION powermodels_network!(container, T, sys, template, instantiate_bfp_expr_model) add_pm_variable_refs!(container, T, sys, model) add_pm_constraint_refs!(container, T, sys) add_constraint_dual!(container, sys, model) return end #= # AbstractIVRModel models not currently supported function construct_network!( container::OptimizationContainer, sys::PSY.System, model::NetworkModel{T}, template::ProblemTemplate; ) where {T <: PM.AbstractIVRModel} if T in UNSUPPORTED_POWERMODELS throw( ArgumentError( "$(T) formulation is not currently supported in PowerSimulations", ), ) end if get_use_slacks(model) add_variables!(container, SystemBalanceSlackUp, sys, T) add_variables!(container, SystemBalanceSlackDown, sys, T) add_to_expression!( container, ActivePowerBalance, SystemBalanceSlackUp, sys, model, T, ) add_to_expression!( container, ActivePowerBalance, SystemBalanceSlackDown, sys, model, T, ) add_to_expression!( container, ReactivePowerBalance, SystemBalanceSlackUp, sys, model, T, ) add_to_expression!( container, ReactivePowerBalance, SystemBalanceSlackDown, sys, model, T, ) objective_function!(container, sys, model) end @debug "Building the $T network with instantiate_vip_expr_model method" _group = LOG_GROUP_NETWORK_CONSTRUCTION #Constraints in case the model has DC Buses add_constraints!(container, NodalBalanceActiveConstraint, sys, model) powermodels_network!(container, T, sys, template, instantiate_vip_expr_model) add_pm_variable_refs!(container, T, sys, model) add_pm_constraint_refs!(container, T, sys) add_constraint_dual!(container, sys, model) return end =# ================================================ FILE: src/network_models/network_slack_variables.jl ================================================ #! format: off 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!( container::OptimizationContainer, ::Type{T}, ::PSY.System, network_model::NetworkModel{U}, ) where { T <: Union{SystemBalanceSlackUp, SystemBalanceSlackDown}, U <: Union{CopperPlatePowerModel, PTDFPowerModel}, } time_steps = get_time_steps(container) reference_buses = get_reference_buses(network_model) variable = add_variable_container!(container, T(), PSY.System, reference_buses, time_steps) for t in time_steps, bus in reference_buses variable[bus, t] = JuMP.@variable( get_jump_model(container), base_name = "slack_{$(T), $(bus), $t}", lower_bound = 0.0 ) end 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}, sys::PSY.System, network_model::NetworkModel{U}, ) where { T <: Union{SystemBalanceSlackUp, SystemBalanceSlackDown}, U <: PM.AbstractActivePowerModel, } time_steps = get_time_steps(container) network_reduction = get_network_reduction(network_model) if isempty(network_reduction) bus_numbers = PSY.get_number.(get_available_components(network_model, PSY.ACBus, sys)) else bus_numbers = collect(keys(PNM.get_bus_reduction_map(network_reduction))) end variable = add_variable_container!(container, T(), PSY.ACBus, bus_numbers, time_steps) for t in time_steps, n in bus_numbers variable[n, t] = JuMP.@variable( get_jump_model(container), base_name = "slack_{$(T), $n, $t}", lower_bound = 0.0 ) end return end function add_variables!( container::OptimizationContainer, ::Type{T}, sys::PSY.System, network_model::NetworkModel{U}, ) where { T <: Union{SystemBalanceSlackUp, SystemBalanceSlackDown}, U <: PM.AbstractPowerModel, } time_steps = get_time_steps(container) network_reduction = get_network_reduction(network_model) if isempty(network_reduction) bus_numbers = PSY.get_number.(get_available_components(network_model, PSY.ACBus, sys)) else bus_numbers = collect(keys(PNM.get_bus_reduction_map(network_reduction))) end variable_active = add_variable_container!(container, T(), PSY.ACBus, "P", bus_numbers, time_steps) variable_reactive = add_variable_container!(container, T(), PSY.ACBus, "Q", bus_numbers, time_steps) for t in time_steps, n in bus_numbers variable_active[n, t] = JuMP.@variable( get_jump_model(container), base_name = "slack_{p, $(T), $n, $t}", lower_bound = 0.0 ) variable_reactive[n, t] = JuMP.@variable( get_jump_model(container), base_name = "slack_{q, $(T), $n, $t}", lower_bound = 0.0 ) end return end function objective_function!( container::OptimizationContainer, sys::PSY.System, network_model::NetworkModel{T}, ) where {T <: Union{CopperPlatePowerModel, PTDFPowerModel}} variable_up = get_variable(container, SystemBalanceSlackUp(), PSY.System) variable_dn = get_variable(container, SystemBalanceSlackDown(), PSY.System) reference_buses = get_reference_buses(network_model) for t in get_time_steps(container), n in reference_buses 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 <: 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) variable_dn = get_variable(container, SystemBalanceSlackDown(), PSY.ACBus) bus_numbers = axes(variable_up)[1] @assert_op bus_numbers == axes(variable_dn)[1] for t in get_time_steps(container), n in bus_numbers 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.AbstractPowerModel} variable_p_up = get_variable(container, SystemBalanceSlackUp(), PSY.ACBus, "P") variable_p_dn = get_variable(container, SystemBalanceSlackDown(), PSY.ACBus, "P") variable_q_up = get_variable(container, SystemBalanceSlackUp(), PSY.ACBus, "Q") variable_q_dn = get_variable(container, SystemBalanceSlackDown(), PSY.ACBus, "Q") bus_numbers = axes(variable_p_up)[1] @assert_op bus_numbers == axes(variable_q_dn)[1] for t in get_time_steps(container), n in bus_numbers add_to_objective_invariant_expression!( container, ( variable_p_dn[n, t] + variable_p_up[n, t] + variable_q_dn[n, t] + variable_q_up[n, t] ) * BALANCE_SLACK_COST, ) end return end ================================================ FILE: src/network_models/pm_translator.jl ================================================ const PM_MAP_TUPLE = NamedTuple{(:from_to, :to_from), Tuple{Tuple{Int, Int, Int}, Tuple{Int, Int, Int}}} const PM_BUSTYPES = Dict{PSY.ACBusTypes, Int}( PSY.ACBusTypes.ISOLATED => 4, PSY.ACBusTypes.PQ => 1, PSY.ACBusTypes.PV => 2, PSY.ACBusTypes.REF => 3, PSY.ACBusTypes.SLACK => 3, ) struct PMmap bus::Dict{Int, PSY.ACBus} arcs::Dict{Tuple{Int, Int}, PM_MAP_TUPLE} arcs_dc::Dict{PM_MAP_TUPLE, PSY.TwoTerminalHVDC} end function get_branch_to_pm( ix::Int, ::Tuple{Int, Int}, branch::PSY.PhaseShiftingTransformer, ::Type{PhaseAngleControl}, ::Type{<:PM.AbstractDCPModel}, ) # we allocate the transformer shunt values to primary side only yt = PSY.get_primary_shunt(branch) PM_branch = Dict{String, Any}( "br_r" => PSY.get_r(branch), "rate_a" => PSY.get_rating(branch), "shift" => PSY.get_α(branch), "rate_b" => PSY.get_rating(branch), "br_x" => PSY.get_x(branch), "rate_c" => PSY.get_rating(branch), "g_to" => 0.0, "b_to" => 0.0, "g_fr" => real(yt), "b_fr" => imag(yt), "f_bus" => PSY.get_number(PSY.get_arc(branch).from), "br_status" => 0.0, # Turn off the branch while keeping the function type stable "t_bus" => PSY.get_number(PSY.get_arc(branch).to), "index" => ix, "angmin" => -π / 2, "angmax" => π / 2, "transformer" => true, "tap" => PSY.get_tap(branch), ) return PM_branch end function get_branch_to_pm( ix::Int, ::Tuple{Int, Int}, branch::PSY.PhaseShiftingTransformer, ::Type{D}, ::Type{<:PM.AbstractPowerModel}, ) where {D <: AbstractBranchFormulation} # we allocate the transformer shunt values to primary side only yt = PSY.get_primary_shunt(branch) PM_branch = Dict{String, Any}( "br_r" => PSY.get_r(branch), "rate_a" => PSY.get_rating(branch), "shift" => PSY.get_α(branch), "rate_b" => PSY.get_rating(branch), "br_x" => PSY.get_x(branch), "rate_c" => PSY.get_rating(branch), "g_to" => 0.0, "b_to" => 0.0, "g_fr" => real(yt), "b_fr" => imag(yt), "f_bus" => PSY.get_number(PSY.get_arc(branch).from), "br_status" => Float64(PSY.get_available(branch)), "t_bus" => PSY.get_number(PSY.get_arc(branch).to), "index" => ix, "angmin" => -π / 2, "angmax" => π / 2, "transformer" => true, "tap" => PSY.get_tap(branch), ) return PM_branch end function get_branch_to_pm( ix::Int, ::Tuple{Int, Int}, branch::PSY.PhaseShiftingTransformer, ::Type{StaticBranchUnbounded}, ::Type{<:PM.AbstractPowerModel}, ) # we allocate the transformer shunt values to primary side only yt = PSY.get_primary_shunt(branch) PM_branch = Dict{String, Any}( "br_r" => PSY.get_r(branch), "shift" => PSY.get_α(branch), "br_x" => PSY.get_x(branch), "g_to" => 0.0, "b_to" => 0.0, "g_fr" => real(yt), "b_fr" => imag(yt), "f_bus" => PSY.get_number(PSY.get_arc(branch).from), "br_status" => Float64(PSY.get_available(branch)), "t_bus" => PSY.get_number(PSY.get_arc(branch).to), "index" => ix, "angmin" => -π / 2, "angmax" => π / 2, "transformer" => true, "tap" => PSY.get_tap(branch), ) return PM_branch end function get_branch_to_pm( ix::Int, ::Tuple{Int, Int}, branch::PSY.Transformer2W, ::Type{<:AbstractBranchFormulation}, ::Type{<:PM.AbstractPowerModel}, ) # we allocate the transformer shunt values to primary side only yt = PSY.get_primary_shunt(branch) PM_branch = Dict{String, Any}( "br_r" => PSY.get_r(branch), "rate_a" => PSY.get_rating(branch), "shift" => 0.0, "rate_b" => PSY.get_rating(branch), "br_x" => PSY.get_x(branch), "rate_c" => PSY.get_rating(branch), "g_to" => 0.0, "b_to" => 0.0, "g_fr" => real(yt), "b_fr" => imag(yt), "f_bus" => PSY.get_number(PSY.get_arc(branch).from), "br_status" => Float64(PSY.get_available(branch)), "t_bus" => PSY.get_number(PSY.get_arc(branch).to), "index" => ix, "angmin" => -π / 2, "angmax" => π / 2, "transformer" => true, "tap" => 1.0, ) return PM_branch end function get_branch_to_pm( ix::Int, ::Tuple{Int, Int}, branch::PSY.Transformer2W, ::Type{StaticBranchUnbounded}, ::Type{<:PM.AbstractPowerModel}, ) # we allocate the transformer shunt values to primary side only yt = PSY.get_primary_shunt(branch) PM_branch = Dict{String, Any}( "br_r" => PSY.get_r(branch), "shift" => 0.0, "br_x" => PSY.get_x(branch), "g_to" => 0.0, "b_to" => 0.0, "g_fr" => real(yt), "b_fr" => imag(yt), "f_bus" => PSY.get_number(PSY.get_arc(branch).from), "br_status" => Float64(PSY.get_available(branch)), "t_bus" => PSY.get_number(PSY.get_arc(branch).to), "index" => ix, "angmin" => -π / 2, "angmax" => π / 2, "transformer" => true, "tap" => 1.0, ) return PM_branch end function get_branch_to_pm( ix::Int, ::Tuple{Int, Int}, branch::PSY.TapTransformer, ::Type{<:AbstractBranchFormulation}, ::Type{<:PM.AbstractPowerModel}, ) # we allocate the transformer shunt values to primary side only yt = PSY.get_primary_shunt(branch) PM_branch = Dict{String, Any}( "br_r" => PSY.get_r(branch), "rate_a" => PSY.get_rating(branch), "shift" => 0.0, "rate_b" => PSY.get_rating(branch), "br_x" => PSY.get_x(branch), "rate_c" => PSY.get_rating(branch), "g_to" => 0.0, "b_to" => 0.0, "g_fr" => real(yt), "b_fr" => imag(yt), "f_bus" => PSY.get_number(PSY.get_arc(branch).from), "br_status" => Float64(PSY.get_available(branch)), "t_bus" => PSY.get_number(PSY.get_arc(branch).to), "index" => ix, "angmin" => -π / 2, "angmax" => π / 2, "transformer" => true, "tap" => PSY.get_tap(branch), ) return PM_branch end function get_branch_to_pm( ix::Int, ::Tuple{Int, Int}, branch::PSY.TapTransformer, ::Type{StaticBranchUnbounded}, ::Type{<:PM.AbstractPowerModel}, ) # we allocate the transformer shunt values to primary side only yt = PSY.get_primary_shunt(branch) PM_branch = Dict{String, Any}( "br_r" => PSY.get_r(branch), "shift" => 0.0, "br_x" => PSY.get_x(branch), "g_to" => 0.0, "b_to" => 0.0, "g_fr" => real(yt), "b_fr" => imag(yt), "f_bus" => PSY.get_number(PSY.get_arc(branch).from), "br_status" => Float64(PSY.get_available(branch)), "t_bus" => PSY.get_number(PSY.get_arc(branch).to), "index" => ix, "angmin" => -π / 2, "angmax" => π / 2, "transformer" => true, "tap" => PSY.get_tap(branch), ) return PM_branch end function get_branch_to_pm( ix::Int, ::Tuple{Int, Int}, branch::PSY.ACTransmission, ::Type{<:AbstractBranchFormulation}, ::Type{<:PM.AbstractPowerModel}, ) PM_branch = Dict{String, Any}( "br_r" => PSY.get_r(branch), "rate_a" => PSY.get_rating(branch), "shift" => 0.0, "rate_b" => PSY.get_rating(branch), "br_x" => PSY.get_x(branch), "rate_c" => PSY.get_rating(branch), "g_to" => 0.0, "g_fr" => 0.0, "b_fr" => PSY.get_b(branch).from, "f_bus" => PSY.get_number(PSY.get_arc(branch).from), "br_status" => Float64(PSY.get_available(branch)), "t_bus" => PSY.get_number(PSY.get_arc(branch).to), "b_to" => PSY.get_b(branch).to, "index" => ix, "angmin" => PSY.get_angle_limits(branch).min, "angmax" => PSY.get_angle_limits(branch).max, "transformer" => false, "tap" => 1.0, ) return PM_branch end function get_branch_to_pm( ix::Int, ::Tuple{Int, Int}, branch::PSY.ACTransmission, ::Type{StaticBranchUnbounded}, ::Type{<:PM.AbstractPowerModel}, ) PM_branch = Dict{String, Any}( "br_r" => PSY.get_r(branch), "shift" => 0.0, "br_x" => PSY.get_x(branch), "g_to" => 0.0, "g_fr" => 0.0, "b_fr" => PSY.get_b(branch).from, "f_bus" => PSY.get_number(PSY.get_arc(branch).from), "br_status" => Float64(PSY.get_available(branch)), "t_bus" => PSY.get_number(PSY.get_arc(branch).to), "b_to" => PSY.get_b(branch).to, "index" => ix, "angmin" => PSY.get_angle_limits(branch).min, "angmax" => PSY.get_angle_limits(branch).max, "transformer" => false, "tap" => 1.0, ) return PM_branch end function get_branch_to_pm( ix::Int, arc_tuple::Tuple{Int, Int}, branch::PNM.ThreeWindingTransformerWinding{PSY.Transformer3W}, ::Type{StaticBranchUnbounded}, ::Type{<:PM.AbstractPowerModel}, ) arc_tuple PM_branch = Dict{String, Any}( "br_r" => PNM.get_equivalent_r(branch), "shift" => 0.0, "br_x" => PNM.get_equivalent_x(branch), "g_to" => 0.0, "g_fr" => 0.0, "b_fr" => PNM.get_equivalent_b(branch).from, "f_bus" => arc_tuple[1], "br_status" => Float64(PNM.get_equivalent_available(branch)), "t_bus" => arc_tuple[2], "b_to" => PNM.get_equivalent_b(branch).to, "index" => ix, "angmin" => -π / 2, "angmax" => π / 2, "transformer" => true, "tap" => 1.0, ) return PM_branch end function get_branch_to_pm( ix::Int, arc_tuple::Tuple{Int, Int}, branch::PNM.ThreeWindingTransformerWinding{PSY.Transformer3W}, ::Type{<:AbstractBranchFormulation}, ::Type{<:PM.AbstractPowerModel}, ) PM_branch = Dict{String, Any}( "br_r" => PNM.get_equivalent_r(branch), "rate_a" => PNM.get_equivalent_rating(branch), "shift" => 0.0, "rate_b" => PNM.get_equivalent_rating(branch), "br_x" => PNM.get_equivalent_x(branch), "rate_c" => PNM.get_equivalent_rating(branch), "g_to" => 0.0, "g_fr" => 0.0, "b_fr" => PNM.get_equivalent_b(branch).from, "f_bus" => arc_tuple[1], "br_status" => Float64(PNM.get_equivalent_available(branch)), "t_bus" => arc_tuple[2], "b_to" => PNM.get_equivalent_b(branch).to, "index" => ix, "angmin" => -π / 2, "angmax" => π / 2, "transformer" => true, "tap" => 1.0, ) return PM_branch end function get_branch_to_pm( ix::Int, arc_tuple::Tuple{Int, Int}, branch::PNM.ThreeWindingTransformerWinding{PSY.PhaseShiftingTransformer3W}, ::Type{<:AbstractBranchFormulation}, ::Type{<:PM.AbstractPowerModel}, ) PM_branch = Dict{String, Any}( "br_r" => PNM.get_equivalent_r(branch), "rate_a" => PNM.get_equivalent_rating(branch), "shift" => 0.0, "rate_b" => PNM.get_equivalent_rating(branch), "br_x" => PNM.get_equivalent_x(branch), "rate_c" => PNM.get_equivalent_rating(branch), "g_to" => 0.0, "g_fr" => 0.0, "b_fr" => PNM.get_equivalent_b(branch).from, "f_bus" => arc_tuple[1], "br_status" => Float64(PNM.get_equivalent_available(branch)), "t_bus" => arc_tuple[2], "b_to" => PNM.get_equivalent_b(branch).to, "index" => ix, "angmin" => -π / 2, "angmax" => π / 2, "transformer" => true, "tap" => PNM.get_equivalent_tap(branch), ) return PM_branch end function get_branch_to_pm( ix::Int, arc_tuple::Tuple{Int, Int}, branch::PNM.ThreeWindingTransformerWinding{PSY.PhaseShiftingTransformer3W}, ::Type{StaticBranchUnbounded}, ::Type{<:PM.AbstractPowerModel}, ) PM_branch = Dict{String, Any}( "br_r" => PNM.get_equivalent_r(branch), "shift" => 0.0, "br_x" => PNM.get_equivalent_x(branch), "g_to" => 0.0, "g_fr" => 0.0, "b_fr" => PNM.get_equivalent_b(branch).from, "f_bus" => arc_tuple[1], "br_status" => Float64(PNM.get_equivalent_available(branch)), "t_bus" => arc_tuple[2], "b_to" => PNM.get_equivalent_b(branch).to, "index" => ix, "angmin" => -π / 2, "angmax" => π / 2, "transformer" => true, "tap" => PNM.get_equivalent_tap(branch), ) return PM_branch end function get_branch_to_pm( ix::Int, arc_tuple::Tuple{Int, Int}, double_circuit::PNM.BranchesParallel, T::Type{<:AbstractBranchFormulation}, U::Type{<:PM.AbstractPowerModel}, ) equivalent_branch = PNM.get_equivalent_physical_branch_parameters(double_circuit) PM_branch = Dict{String, Any}( "br_r" => PNM.get_equivalent_r(equivalent_branch), "shift" => 0.0, "rate_a" => PNM.get_equivalent_rating(double_circuit), "rate_b" => PNM.get_equivalent_rating(double_circuit), "rate_c" => PNM.get_equivalent_rating(double_circuit), "br_x" => PNM.get_equivalent_x(equivalent_branch), "g_to" => 0.0, "g_fr" => 0.0, "b_fr" => PNM.get_equivalent_b_from(equivalent_branch), "f_bus" => arc_tuple[1], "br_status" => Float64(PNM.get_equivalent_available(double_circuit)), "t_bus" => arc_tuple[2], "b_to" => PNM.get_equivalent_b_to(equivalent_branch), "index" => ix, "angmin" => -π / 2, "angmax" => π / 2, "transformer" => false, "tap" => 1.0, ) return PM_branch end function get_branch_to_pm( ix::Int, arc_tuple::Tuple{Int, Int}, double_circuit::PNM.BranchesParallel, T::Type{StaticBranchUnbounded}, U::Type{<:PM.AbstractPowerModel}, ) equivalent_branch = PNM.get_equivalent_physical_branch_parameters(double_circuit) PM_branch = Dict{String, Any}( "br_r" => PNM.get_equivalent_r(equivalent_branch), "shift" => 0.0, "br_x" => PNM.get_equivalent_x(equivalent_branch), "g_to" => 0.0, "g_fr" => 0.0, "rate_a" => PNM.get_equivalent_rating(double_circuit), "rate_b" => PNM.get_equivalent_rating(double_circuit), "rate_c" => PNM.get_equivalent_rating(double_circuit), "b_fr" => PNM.get_equivalent_b_from(equivalent_branch), "f_bus" => arc_tuple[1], "br_status" => Float64(PNM.get_equivalent_available(double_circuit)), "t_bus" => arc_tuple[2], "b_to" => PNM.get_equivalent_b_to(equivalent_branch), "index" => ix, "angmin" => -π / 2, "angmax" => π / 2, "transformer" => false, "tap" => 1.0, ) return PM_branch end function get_branch_to_pm( ix::Int, arc_tuple::Tuple{Int, Int}, series_chain::PNM.BranchesSeries, T::Type{<:AbstractBranchFormulation}, U::Type{<:PM.AbstractPowerModel}, ) equivalent_branch = PNM.get_equivalent_physical_branch_parameters(series_chain) PM_branch = Dict{String, Any}( "br_r" => PNM.get_equivalent_r(equivalent_branch), "shift" => 0.0, "br_x" => PNM.get_equivalent_x(equivalent_branch), "g_to" => 0.0, "g_fr" => 0.0, "b_fr" => PNM.get_equivalent_b_from(equivalent_branch), "f_bus" => arc_tuple[1], "br_status" => Float64(PNM.get_equivalent_available(series_chain)), "t_bus" => arc_tuple[2], "b_to" => PNM.get_equivalent_b_to(equivalent_branch), "index" => ix, "angmin" => -π / 2, "angmax" => π / 2, "transformer" => false, "tap" => 1.0, ) return PM_branch end function get_branch_to_pm( ix::Int, arc_tuple::Tuple{Int, Int}, series_chain::PNM.BranchesSeries, T::Type{StaticBranchUnbounded}, U::Type{<:PM.AbstractPowerModel}, ) equivalent_branch = PNM.get_equivalent_physical_branch_parameters(series_chain) PM_branch = Dict{String, Any}( "br_r" => PNM.get_equivalent_r(equivalent_branch), "shift" => 0.0, "br_x" => PNM.get_equivalent_x(equivalent_branch), "g_to" => 0.0, "g_fr" => 0.0, "b_fr" => PNM.get_equivalent_b_from(equivalent_branch), "f_bus" => arc_tuple[1], "br_status" => Float64(PNM.get_equivalent_available(double_circuit)), "t_bus" => arc_tuple[2], "b_to" => PNM.get_equivalent_b_to(equivalent_branch), "index" => ix, "angmin" => -π / 2, "angmax" => π / 2, "transformer" => false, "tap" => 1.0, ) return PM_branch end function get_branch_to_pm( ix::Int, branch::PSY.TwoTerminalGenericHVDCLine, ::Type{HVDCTwoTerminalDispatch}, ::Type{<:PM.AbstractPowerModel}, ) check_hvdc_line_limits_unidirectional(branch) PM_branch = Dict{String, Any}( "loss1" => PSY.get_proportional_term(PSY.get_loss(branch)), "mp_pmax" => PSY.get_reactive_power_limits_from(branch).max, "model" => 2, "shutdown" => 0.0, "pmaxt" => PSY.get_active_power_limits_to(branch).max, "pmaxf" => PSY.get_active_power_limits_from(branch).max, "startup" => 0.0, "loss0" => PSY.get_constant_term(PSY.get_loss(branch)), "pt" => 0.0, "vt" => PSY.get_magnitude(PSY.get_arc(branch).to), "qmaxf" => PSY.get_reactive_power_limits_from(branch).max, "pmint" => PSY.get_active_power_limits_to(branch).min, "f_bus" => PSY.get_number(PSY.get_arc(branch).from), "mp_pmin" => PSY.get_reactive_power_limits_from(branch).min, "br_status" => Float64(PSY.get_available(branch)), "t_bus" => PSY.get_number(PSY.get_arc(branch).to), "index" => ix, "qmint" => PSY.get_reactive_power_limits_to(branch).min, "qf" => 0.0, "cost" => 0.0, "pminf" => PSY.get_active_power_limits_from(branch).min, "qt" => 0.0, "qminf" => PSY.get_reactive_power_limits_from(branch).min, "vf" => PSY.get_magnitude(PSY.get_arc(branch).from), "qmaxt" => PSY.get_reactive_power_limits_to(branch).max, "ncost" => 0, "pf" => 0.0, ) return PM_branch end function get_branch_to_pm( ix::Int, branch::PSY.TwoTerminalGenericHVDCLine, ::Type{<:AbstractTwoTerminalDCLineFormulation}, ::Type{<:PM.AbstractPowerModel}, ) PM_branch = Dict{String, Any}( "loss1" => PSY.get_proportional_term(PSY.get_loss(branch)), "mp_pmax" => PSY.get_reactive_power_limits_from(branch).max, "model" => 2, "shutdown" => 0.0, "pmaxt" => PSY.get_active_power_limits_to(branch).max, "pmaxf" => PSY.get_active_power_limits_from(branch).max, "startup" => 0.0, "loss0" => PSY.get_constant_term(PSY.get_loss(branch)), "pt" => 0.0, "vt" => PSY.get_magnitude(PSY.get_arc(branch).to), "qmaxf" => PSY.get_reactive_power_limits_from(branch).max, "pmint" => PSY.get_active_power_limits_to(branch).min, "f_bus" => PSY.get_number(PSY.get_arc(branch).from), "mp_pmin" => PSY.get_reactive_power_limits_from(branch).min, "br_status" => Float64(PSY.get_available(branch)), "t_bus" => PSY.get_number(PSY.get_arc(branch).to), "index" => ix, "qmint" => PSY.get_reactive_power_limits_to(branch).min, "qf" => 0.0, "cost" => 0.0, "pminf" => PSY.get_active_power_limits_from(branch).min, "qt" => 0.0, "qminf" => PSY.get_reactive_power_limits_from(branch).min, "vf" => PSY.get_magnitude(PSY.get_arc(branch).from), "qmaxt" => PSY.get_reactive_power_limits_to(branch).max, "ncost" => 0, "pf" => 0.0, ) return PM_branch end function get_branch_to_pm( ix::Int, branch::PSY.TwoTerminalGenericHVDCLine, ::Type{HVDCTwoTerminalDispatch}, ::Type{<:PM.AbstractDCPModel}, ) PM_branch = Dict{String, Any}( "loss1" => PSY.get_proportional_term(PSY.get_loss(branch)), "mp_pmax" => PSY.get_reactive_power_limits_from(branch).max, "model" => 2, "shutdown" => 0.0, "pmaxt" => PSY.get_active_power_limits_to(branch).max, "pmaxf" => PSY.get_active_power_limits_from(branch).max, "startup" => 0.0, "loss0" => PSY.get_constant_term(PSY.get_loss(branch)), "pt" => 0.0, "vt" => PSY.get_magnitude(PSY.get_arc(branch).to), "qmaxf" => PSY.get_reactive_power_limits_from(branch).max, "pmint" => PSY.get_active_power_limits_to(branch).min, "f_bus" => PSY.get_number(PSY.get_arc(branch).from), "mp_pmin" => PSY.get_reactive_power_limits_from(branch).min, "br_status" => 0.0, "t_bus" => PSY.get_number(PSY.get_arc(branch).to), "index" => ix, "qmint" => PSY.get_reactive_power_limits_to(branch).min, "qf" => 0.0, "cost" => 0.0, "pminf" => PSY.get_active_power_limits_from(branch).min, "qt" => 0.0, "qminf" => PSY.get_reactive_power_limits_from(branch).min, "vf" => PSY.get_magnitude(PSY.get_arc(branch).from), "qmaxt" => PSY.get_reactive_power_limits_to(branch).max, "ncost" => 0, "pf" => 0.0, ) return PM_branch end function get_branch_to_pm( ix::Int, branch::PSY.TwoTerminalLCCLine, ::Type{HVDCTwoTerminalLCC}, ::Type{<:PM.AbstractPowerModel}, ) return Dict{String, Any}() end function get_branches_to_pm( sys::PSY.System, network_model::NetworkModel{S}, ::Type{T}, branch_template::BranchModelContainer, start_idx = 0, ) where {T <: PSY.ACTransmission, S <: PM.AbstractPowerModel} PM_branches = Dict{String, Any}() PMmap_br = Dict{Tuple{Int, Int}, PM_MAP_TUPLE}() net_reduction_data = get_network_reduction(network_model) all_branch_maps_by_type = net_reduction_data.all_branch_maps_by_type name_to_arc_maps = PNM.get_name_to_arc_maps(net_reduction_data) ix = 1 @assert !isempty(branch_template) modeled_arc_tuples = Set{Tuple{Int, Int}}() for (d, device_model) in branch_template comp_type = get_component_type(device_model) if comp_type <: PSY.TwoTerminalHVDC || !haskey(name_to_arc_maps, comp_type) @info "No $d Branches to process in PowerModels data." continue end name_to_arc_map = PNM.get_name_to_arc_map(net_reduction_data, comp_type) for (_, (arc_tuple, reduction)) in name_to_arc_map arc_tuple ∈ modeled_arc_tuples && continue # This is the PowerModels equivalent of the branch and constraint tracker. reduction_entry = all_branch_maps_by_type[reduction][comp_type][arc_tuple] PM_branches["$(ix)"] = get_branch_to_pm( ix, arc_tuple, reduction_entry, get_formulation(device_model), S, ) if PM_branches["$(ix)"]["br_status"] == true f = PM_branches["$(ix)"]["f_bus"] t = PM_branches["$(ix)"]["t_bus"] PMmap_br[arc_tuple] = (from_to = (ix, f, t), to_from = (ix, t, f)) end push!(modeled_arc_tuples, arc_tuple) ix += 1 end end return PM_branches, PMmap_br end function get_branches_to_pm( sys::PSY.System, network_model::NetworkModel{S}, ::Type{T}, branch_template::BranchModelContainer, start_idx = 0, ) where {T <: PSY.TwoTerminalHVDC, S <: PM.AbstractPowerModel} PM_branches = Dict{String, Any}() PMmap_br = Dict{PM_MAP_TUPLE, T}() for (d, device_model) in branch_template comp_type = get_component_type(device_model) !(comp_type <: T) && continue if comp_type <: PSY.TwoTerminalLCCLine && get_formulation(device_model) <: HVDCTwoTerminalLCC continue end start_idx += length(PM_branches) for (i, branch) in enumerate(get_available_components(device_model, sys)) ix = i + start_idx PM_branches["$(ix)"] = get_branch_to_pm(ix, branch, get_formulation(device_model), S) if PM_branches["$(ix)"]["br_status"] == true f = PM_branches["$(ix)"]["f_bus"] t = PM_branches["$(ix)"]["t_bus"] PMmap_br[(from_to = (ix, f, t), to_from = (ix, t, f))] = branch end end end return PM_branches, PMmap_br end function get_buses_to_pm(buses::IS.FlattenIteratorWrapper{PSY.ACBus}) PM_buses = Dict{String, Any}() PMmap_buses = Dict{Int, PSY.ACBus}() for bus in buses if PSY.get_bustype(bus) == PSY.ACBusTypes.ISOLATED continue end number = PSY.get_number(bus) PM_bus = Dict{String, Any}( "zone" => 1, "bus_i" => number, "bus_type" => PM_BUSTYPES[PSY.get_bustype(bus)], "vmax" => PSY.get_voltage_limits(bus).max, "area" => 1, "vmin" => PSY.get_voltage_limits(bus).min, "index" => PSY.get_number(bus), "va" => PSY.get_angle(bus), "vm" => PSY.get_magnitude(bus), "base_kv" => PSY.get_base_voltage(bus), "inj_p" => 0.0, "inj_q" => 0.0, "name" => PSY.get_name(bus), ) PM_buses["$(number)"] = PM_bus PMmap_buses[number] = bus end return PM_buses, PMmap_buses end function pass_to_pm(sys::PSY.System, template::ProblemTemplate, time_periods::Int) ac_lines, PMmap_ac = get_branches_to_pm( sys, get_network_model(template), PSY.ACTransmission, template.branches, ) two_terminal_dc_lines, PMmap_dc = get_branches_to_pm( sys, get_network_model(template), PSY.TwoTerminalHVDC, template.branches, length(ac_lines), ) network_model = get_network_model(template) buses = get_available_components(network_model, PSY.ACBus, sys) pm_buses, PMmap_buses = get_buses_to_pm(buses) PM_translation = Dict{String, Any}( "bus" => pm_buses, "branch" => ac_lines, "baseMVA" => PSY.get_base_power(sys), "per_unit" => true, "storage" => Dict{String, Any}(), "dcline" => two_terminal_dc_lines, "gen" => Dict{String, Any}(), "switch" => Dict{String, Any}(), "shunt" => Dict{String, Any}(), "load" => Dict{String, Any}(), ) # TODO: this function adds overhead in large number of time_steps # We can do better later. PM_translation = PM.replicate(PM_translation, time_periods) PM_map = PMmap(PMmap_buses, PMmap_ac, PMmap_dc) return PM_translation, PM_map end ================================================ FILE: src/network_models/power_flow_evaluation.jl ================================================ # Defines the order of precedence for each type of information that could be sent to PowerFlows.jl const PF_INPUT_KEY_PRECEDENCES = Dict( :active_power => [ActivePowerVariable, PowerOutput, ActivePowerTimeSeriesParameter], :active_power_in => [ActivePowerInVariable, ActivePowerInTimeSeriesParameter], :active_power_out => [ActivePowerOutVariable, ActivePowerOutTimeSeriesParameter], :reactive_power => [ReactivePowerVariable, ReactivePowerTimeSeriesParameter], :voltage_angle_export => [PowerFlowVoltageAngle, VoltageAngle], :voltage_magnitude_export => [PowerFlowVoltageMagnitude, VoltageMagnitude], :voltage_angle_opf => [VoltageAngle], :voltage_magnitude_opf => [VoltageMagnitude], :active_power_hvdc_pst_from_to => [FlowActivePowerFromToVariable, FlowActivePowerVariable], :active_power_hvdc_pst_to_from => [FlowActivePowerToFromVariable, FlowActivePowerVariable], ) const RELEVANT_COMPONENTS_SELECTOR = PSY.make_selector(Union{PSY.StaticInjection, PSY.Bus, PSY.Branch}) function _add_aux_variables!( container::OptimizationContainer, component_map::Dict{Type{<:AuxVariableType}, <:Set{<:Tuple{DataType, Any}}}, ) for (var_type, components) in pairs(component_map) component_types = unique(first.(components)) for component_type in component_types component_names = [v for (k, v) in components if k <: component_type] sort!(component_names) add_aux_variable_container!( container, var_type(), component_type, component_names, get_time_steps(container), ) end end end # Trait that determines which types of information are needed for each type of power flow pf_input_keys(::PFS.ABAPowerFlowData) = [:active_power, :active_power_in, :active_power_out] pf_input_keys(::PFS.PTDFPowerFlowData) = [:active_power, :active_power_in, :active_power_out] pf_input_keys(::PFS.vPTDFPowerFlowData) = [:active_power, :active_power_in, :active_power_out] pf_input_keys(::PFS.ACPowerFlowData) = [ :active_power, :active_power_in, :active_power_out, :reactive_power, :voltage_angle_opf, :voltage_magnitude_opf, ] pf_input_keys(::PFS.PSSEExporter) = [ :active_power, :active_power_in, :active_power_out, :reactive_power, :voltage_angle_export, :voltage_magnitude_export, ] pf_input_keys_hvdc_pst(::PFS.PowerFlowData) = DataType[] pf_input_keys_hvdc_pst(::PFS.ACPowerFlowData) = [:active_power_hvdc_pst_from_to, :active_power_hvdc_pst_to_from] _get_component_bus_for_map(component::PSY.Branch, ::Val{:from}) = PSY.get_from_bus(component) _get_component_bus_for_map(component::PSY.Branch, ::Val{:to}) = PSY.get_to_bus(component) _get_component_bus_for_map(component::PSY.Component, ::Nothing) = PSY.get_bus(component) # Generalized function to create component maps by name to the index in the PowerFlowData bus arrays function _make_temp_component_map( pf_data::PFS.PowerFlowData, sys::PSY.System, component_type::DataType, side::Union{Val{:from}, Val{:to}, Nothing}, ) nrd = PFS.get_network_reduction_data(pf_data) temp_component_map = Dict{DataType, Dict{String, Int}}() components = PSY.get_available_components(component_type, sys) bus_lookup = PFS.get_bus_lookup(pf_data) for comp in components comp_type = typeof(comp) bus_dict = get!(temp_component_map, comp_type, Dict{String, Int}()) bus_number = PSY.get_number(_get_component_bus_for_map(comp, side)) bus_dict[PSY.get_name(comp)] = PNM.get_bus_index(bus_number, bus_lookup, nrd) end return temp_component_map end # Maps the StaticInjection component type by name to the # index in the PowerFlow data arrays going from Bus number to bus index function _make_temp_component_map(pf_data::PFS.PowerFlowData, sys::PSY.System) temp_component_map = _make_temp_component_map( pf_data, sys, PSY.StaticInjection, nothing, ) # Add ACBus components for voltage magnitude and angle export bus_lookup = PFS.get_bus_lookup(pf_data) nrd = PFS.get_network_reduction_data(pf_data) temp_component_map[PSY.ACBus] = Dict( PSY.get_name(c) => PNM.get_bus_index(PSY.get_number(c), bus_lookup, nrd) for c in get_available_components(PSY.ACBus, sys) ) return temp_component_map end _get_temp_component_map_lhs(comp::PSY.Component) = PSY.get_name(comp) _get_temp_component_map_lhs(comp::PSY.Bus) = PSY.get_number(comp) # Creates dicts of components by type function _make_temp_component_map(::PFS.SystemPowerFlowContainer, sys::PSY.System) temp_component_map = Dict{DataType, Dict{Union{String, Int64}, String}}() relevant_components = PSY.get_available_components(RELEVANT_COMPONENTS_SELECTOR, sys) for comp_type in unique(typeof.(relevant_components)) # NOTE we avoid using bus numbers here because PSY.get_bus(system, number) is O(n) temp_component_map[comp_type] = Dict( _get_temp_component_map_lhs(c) => PSY.get_name(c) for c in relevant_components if c isa comp_type ) end return temp_component_map end function _make_pf_input_map!( pf_e_data::PowerFlowEvaluationData, container::OptimizationContainer, sys::PSY.System, ) pf_data = get_power_flow_data(pf_e_data) temp_component_map = _make_temp_component_map(pf_data, sys) map_type = valtype(temp_component_map) # Dict{String, Int} for PowerFlowData, Dict{Union{String, Int64}, String} for SystemPowerFlowContainer pf_e_data.input_key_map = Dict{Symbol, Dict{OptimizationContainerKey, map_type}}() # available_keys is a vector of Pair{OptimizationContainerKey, data} containing all possibly relevant data sources to iterate over available_keys = vcat( [ collect(pairs(f(container))) for f in [get_variables, get_aux_variables, get_parameters] ]..., ) # Separate map for each category for category in pf_input_keys(pf_data) # Map that persists to store the bus index to which the variable maps in the PowerFlowData, etc. pf_data_opt_container_map = Dict{OptimizationContainerKey, map_type}() @info "Adding input map to send $category to $(nameof(typeof(pf_data)))" precedence = PF_INPUT_KEY_PRECEDENCES[category] _add_category_to_map!( precedence, available_keys, temp_component_map, pf_data_opt_container_map, ) pf_e_data.input_key_map[category] = pf_data_opt_container_map end _add_two_terminal_elements_map!(sys, pf_data, available_keys, pf_e_data.input_key_map) return end """ _add_category_to_map!( precedence::Vector{DataType}, available_keys::Vector{Pair{OptimizationContainerKey, Any}}, temp_component_map::Union{ Dict{DataType, Dict{String, Int}}, Dict{DataType, Dict{Union{Int64, String}, String}}, }, pf_data_opt_container_map::Union{ Dict{OptimizationContainerKey, Dict{String, Int}}, Dict{OptimizationContainerKey, Dict{Union{Int64, String}, String}}, }, ) Helper function that is used in _make_pf_input_map! and _add_two_terminal_elements_map! to configure which variables from the optimization results get written to the PowerFlowData. For every results variable from the optimization, it finds the corresponding mapping between the optimization variable and the PowerFlowData variable. The mappings are added to the `pf_data_opt_container_map` Dict. This step is executed during the build stage of the optimization. The results are written to the PowerFlowData in the solve stage, before the power flow is solved. # Arguments - `precedence::Vector{DataType}`: A vector of `DataType` objects that defines the order of precedence for the variables that correspond to the category of variables (e.g. `:active_power` - first look for `ActivePowerVariable` for the component type, if not available then `PowerOutput`, and finally `ActivePowerTimeSeriesParameter`). - `available_keys::Vector{Pair{OptimizationContainerKey, Any}}`: A vector of key-value pairs where the key is an `OptimizationContainerKey` and the value contains data associated with the key. - `temp_component_map::Union{Dict{DataType, Dict{String, Int}}, Dict{DataType, Dict{Union{Int64, String}, String}}}`: A mapping for component types to point the component-level results (e.g. as voltage value for bus "A") to the appropriate variable in PowerFlowData (e.g. row 27 in the bus-related matrices). - `pf_data_opt_container_map::Union{Dict{OptimizationContainerKey, Dict{String, Int}}, Dict{OptimizationContainerKey, Dict{Union{Int64, String}, String}}}`: The target Dict that contains mappings for all relevant component types. """ function _add_category_to_map!( precedence::Vector{DataType}, available_keys::Vector{Pair{OptimizationContainerKey, Any}}, temp_component_map::Dict{DataType, <:Dict}, pf_data_opt_container_map::Dict{OptimizationContainerKey, <:Dict}, ) added_injection_types = DataType[] for entry_type in precedence for (key, val) in available_keys if get_entry_type(key) === entry_type comp_type = get_component_type(key) # Skip types that have already been handled by something of higher precedence if comp_type ∈ added_injection_types || comp_type ∉ keys(temp_component_map) continue end push!(added_injection_types, comp_type) name_bus_ix_map = valtype(temp_component_map)() comp_names = if (key isa ParameterKey) get_component_names(get_attributes(val)) else axes(val)[1] end for comp_name in comp_names name_bus_ix_map[comp_name] = temp_component_map[comp_type][comp_name] end pf_data_opt_container_map[key] = name_bus_ix_map end end end end # the function to map HVDC power transfers as bus injections is not applicable to PSSEExporter: _add_two_terminal_elements_map!( ::PSY.System, ::PFS.PSSEExporter, ::Vector{Pair{OptimizationContainerKey, Any}}, ::Dict{Symbol, <:Dict{OptimizationContainerKey, <:Dict}}, ) = nothing """ _add_two_terminal_elements_map!( sys::PSY.System, pf_data::PFS.PowerFlowData, available_keys::Vector{Pair{OptimizationContainerKey, Any}}, input_key_map::Dict{Symbol, Dict{OptimizationContainerKey, Dict{String, Int64}}} ) Adds mappings for two-terminal elements (HVDC components) that connect the power flow results (from -> to, to -> from) to be added to the mappings for all component types. The results for these elements are added as bus injections in the `PowerFlowData` as a simplified representation of these components. # Arguments - `sys::PSY.System`: `System` instance representing the power system model. - `pf_data::PFS.PowerFlowData`: The power flow data used internally for power flow calculations. - `available_keys::Vector{Pair{OptimizationContainerKey, Any}}`: A vector of available optimization container keys and their associated values. - `input_key_map::Dict{Symbol, Dict{OptimizationContainerKey, Dict{String, Int64}}}`: A dictionary mapping categories to optimization container keys and their associated mappings. To be extended in this function by the mappings for the two-terminal elements to the respective buses in the `PowerFlowData` instance. """ function _add_two_terminal_elements_map!( sys::PSY.System, pf_data::PFS.PowerFlowData, available_keys::Vector{Pair{OptimizationContainerKey, Any}}, input_key_map::Dict{Symbol, <:Dict{OptimizationContainerKey, <:Dict}}, ) for element_type in (PSY.TwoTerminalHVDC, PSY.PhaseShiftingTransformer) for (category, side) in zip( [:active_power_hvdc_pst_from_to, :active_power_hvdc_pst_to_from], [Val(:from), Val(:to)], ) category ∈ pf_input_keys_hvdc_pst(pf_data) || continue temp_component_map = _make_temp_component_map( pf_data, sys, element_type, side, ) isempty(temp_component_map) && continue precedence = PF_INPUT_KEY_PRECEDENCES[category] pf_data_opt_container_map = Dict{OptimizationContainerKey, valtype(temp_component_map)}() _add_category_to_map!( precedence, available_keys, temp_component_map, pf_data_opt_container_map, ) category_map = get!( input_key_map, category, Dict{OptimizationContainerKey, valtype(temp_component_map)}(), ) merge!(category_map, pf_data_opt_container_map) end end return end # Trait that determines what branch aux vars we can get from each PowerFlowContainer branch_aux_vars(::PFS.ACPowerFlowData) = [PowerFlowBranchReactivePowerFromTo, PowerFlowBranchReactivePowerToFrom, PowerFlowBranchActivePowerFromTo, PowerFlowBranchActivePowerToFrom, PowerFlowBranchActivePowerLoss] branch_aux_vars(::PFS.ABAPowerFlowData) = [PowerFlowBranchActivePowerFromTo, PowerFlowBranchActivePowerToFrom] branch_aux_vars(::PFS.PTDFPowerFlowData) = [PowerFlowBranchActivePowerFromTo, PowerFlowBranchActivePowerToFrom] branch_aux_vars(::PFS.vPTDFPowerFlowData) = [PowerFlowBranchActivePowerFromTo, PowerFlowBranchActivePowerToFrom] branch_aux_vars(::PFS.PSSEExporter) = DataType[] # Same for bus aux vars function bus_aux_vars(data::PFS.ACPowerFlowData) vars = [PowerFlowVoltageAngle, PowerFlowVoltageMagnitude] if PFS.get_calculate_loss_factors(data) push!(vars, PowerFlowLossFactors) end if PFS.get_calculate_voltage_stability_factors(data) push!(vars, PowerFlowVoltageStabilityFactors) end return vars end bus_aux_vars(::PFS.ABAPowerFlowData) = [PowerFlowVoltageAngle] bus_aux_vars(::PFS.PTDFPowerFlowData) = DataType[] bus_aux_vars(::PFS.vPTDFPowerFlowData) = DataType[] bus_aux_vars(::PFS.PSSEExporter) = DataType[] # TODO: Needs update for MultiTerminal HVDC _get_branch_component_tuples(sys::PSY.System) = [ (typeof(c), get_name(c)) for c in PSY.get_available_components(PSY.ACBranch, sys) ] _get_bus_component_tuples(pfd::PFS.PowerFlowData) = tuple.(PSY.ACBus, keys(PFS.get_bus_lookup(pfd))) # get_bus_type returns a ACBusTypes, not the DataType we need here _get_bus_component_tuples(pfd::PFS.SystemPowerFlowContainer) = [ (typeof(c), PSY.get_number(c)) for c in PSY.get_available_components(PSY.ACBus, PFS.get_system(pfd)) ] function _with_time_steps(pf::T, n::Int) where {T <: PFS.PowerFlowEvaluationModel} fields = Dict(fn => getfield(pf, fn) for fn in fieldnames(T)) fields[:time_steps] = n return T(; fields...) end _with_time_steps(pf::PFS.PSSEExportPowerFlow, ::Int) = pf # exporter doesn't use time_steps function add_power_flow_data!( container::OptimizationContainer, evaluators::Vector{PFS.PowerFlowEvaluationModel}, sys::PSY.System, ) container.power_flow_evaluation_data = Vector{PowerFlowEvaluationData}() sizehint!(container.power_flow_evaluation_data, length(evaluators)) # For each output key, what components are we working with? branch_aux_var_components = Dict{Type{<:AuxVariableType}, Set{Tuple{<:DataType, String}}}() bus_aux_var_components = Dict{Type{<:AuxVariableType}, Set{Tuple{<:DataType, <:Int}}}() # we ought to be providing the time_steps when constructing the PF evaluation model, # but that value isn't known until runtime (and PF evaluation model is immutable). n_time_steps = length(get_time_steps(container)) for evaluator in evaluators evaluator = _with_time_steps(evaluator, n_time_steps) @info "Building PowerFlow evaluator using $(evaluator)" pf_data = PFS.make_power_flow_container(evaluator, sys) pf_e_data = PowerFlowEvaluationData(pf_data) my_branch_aux_vars = branch_aux_vars(pf_data) my_bus_aux_vars = bus_aux_vars(pf_data) my_branch_components = _get_branch_component_tuples(sys) for branch_aux_var in my_branch_aux_vars to_add_to = get!( branch_aux_var_components, branch_aux_var, Set{Tuple{<:DataType, String}}(), ) push!.(Ref(to_add_to), my_branch_components) end my_bus_components = _get_bus_component_tuples(pf_data) for bus_aux_var in my_bus_aux_vars to_add_to = get!(bus_aux_var_components, bus_aux_var, Set{Tuple{<:DataType, <:Int}}()) push!.(Ref(to_add_to), my_bus_components) end push!(container.power_flow_evaluation_data, pf_e_data) end _add_aux_variables!(container, branch_aux_var_components) _add_aux_variables!(container, bus_aux_var_components) # Make the input maps after adding aux vars so output of one power flow can be input of another for pf_e_data in get_power_flow_evaluation_data(container) _make_pf_input_map!(pf_e_data, container, sys) end return end # How to update the PowerFlowData given a component type. A bit duplicative of code in PowerFlows.jl. _update_pf_data_component!( pf_data::PFS.PowerFlowData, ::Val{:active_power}, ::Type{<:PSY.StaticInjection}, index, t, value, ) = (pf_data.bus_active_power_injections[index, t] += value) _update_pf_data_component!( pf_data::PFS.PowerFlowData, ::Val{:active_power}, ::Type{<:PSY.ElectricLoad}, index, t, value, ) = (pf_data.bus_active_power_withdrawals[index, t] -= value) # ActivePowerOutVariable represents power output (positive injection into the grid) _update_pf_data_component!( pf_data::PFS.PowerFlowData, ::Val{:active_power_out}, ::Type{<:PSY.StaticInjection}, index, t, value, ) = (pf_data.bus_active_power_injections[index, t] += value) # ActivePowerInVariable represents power input (withdrawal from the grid, e.g. storage charging) _update_pf_data_component!( pf_data::PFS.PowerFlowData, ::Val{:active_power_in}, ::Type{<:PSY.StaticInjection}, index, t, value, ) = (pf_data.bus_active_power_injections[index, t] -= value) _update_pf_data_component!( pf_data::PFS.PowerFlowData, ::Val{:reactive_power}, ::Type{<:PSY.StaticInjection}, index, t, value, ) = (pf_data.bus_reactive_power_injections[index, t] += value) _update_pf_data_component!( pf_data::PFS.PowerFlowData, ::Val{:reactive_power}, ::Type{<:PSY.ElectricLoad}, index, t, value, ) = (pf_data.bus_reactive_power_withdrawals[index, t] -= value) _update_pf_data_component!( pf_data::PFS.PowerFlowData, ::Union{Val{:voltage_angle_export}, Val{:voltage_angle_opf}}, ::Type{<:PSY.ACBus}, index, t, value, ) = (pf_data.bus_angles[index, t] = value) _update_pf_data_component!( pf_data::PFS.PowerFlowData, ::Union{Val{:voltage_magnitude_export}, Val{:voltage_magnitude_opf}}, ::Type{<:PSY.ACBus}, index, t, value, ) = (pf_data.bus_magnitude[index, t] = value) _update_pf_data_component!( pf_data::PFS.PowerFlowData, ::Val{:active_power_hvdc_pst_from_to}, ::Type{<:PSY.TwoTerminalHVDC}, index, t, value, ) = (pf_data.bus_active_power_injections[index, t] -= value) # FlowActivePowerToFromVariable is signed negative when power flows from→to (since # `tf_var + ft_var == losses ≥ 0`), so subtracting yields the correct positive # injection at the receiving bus. _update_pf_data_component!( pf_data::PFS.PowerFlowData, ::Val{:active_power_hvdc_pst_to_from}, ::Type{<:PSY.TwoTerminalHVDC}, index, t, value, ) = (pf_data.bus_active_power_injections[index, t] -= value) _update_pf_data_component!( pf_data::PFS.PowerFlowData, ::Val{:active_power_hvdc_pst_from_to}, ::Type{<:PSY.PhaseShiftingTransformer}, index, t, value, ) = (pf_data.bus_active_power_injections[index, t] -= value) _update_pf_data_component!( pf_data::PFS.PowerFlowData, ::Val{:active_power_hvdc_pst_to_from}, ::Type{<:PSY.PhaseShiftingTransformer}, index, t, value, ) = (pf_data.bus_active_power_injections[index, t] += value) function _write_value_to_pf_data!( pf_data::PFS.PowerFlowData, category::Symbol, container::OptimizationContainer, key::OptimizationContainerKey, component_map) result = lookup_value(container, key) for (device_name, index) in component_map injection_values = result[device_name, :] for t in get_time_steps(container) value = jump_value(injection_values[t]) _update_pf_data_component!( pf_data, Val(category), get_component_type(key), index, t, value, ) end end return end function update_pf_data!( pf_e_data::PowerFlowEvaluationData{<:PFS.PowerFlowData}, container::OptimizationContainer, ) pf_data = get_power_flow_data(pf_e_data) PFS.clear_injection_data!(pf_data) input_map = get_input_key_map(pf_e_data) for (category, inputs) in input_map @debug "Writing $category to $(nameof(typeof(pf_data)))" for (key, component_map) in inputs _write_value_to_pf_data!(pf_data, category, container, key, component_map) end end return end # PERF we use direct dot access here, and implement our own unit conversions, for performance and convenience _update_component!(comp::PSY.Component, ::Val{:active_power}, value, sys_base) = (comp.active_power = value * sys_base / PSY.get_base_power(comp)) # Sign is flipped for loads (TODO can we rely on some existing function that encodes this information?) _update_component!(comp::PSY.ElectricLoad, ::Val{:active_power}, value, sys_base) = (comp.active_power = -value * sys_base / PSY.get_base_power(comp)) _update_component!(comp::PSY.Component, ::Val{:reactive_power}, value, sys_base) = (comp.reactive_power = value * sys_base / PSY.get_base_power(comp)) _update_component!(comp::PSY.ElectricLoad, ::Val{:reactive_power}, value, sys_base) = (comp.reactive_power = -value * sys_base / PSY.get_base_power(comp)) # ActivePowerOutVariable represents power output (positive contribution to active_power) _update_component!(comp::PSY.Component, ::Val{:active_power_out}, value, sys_base) = (comp.active_power += value * sys_base / PSY.get_base_power(comp)) # ActivePowerInVariable represents power input / withdrawal (negative contribution to active_power) _update_component!(comp::PSY.Component, ::Val{:active_power_in}, value, sys_base) = (comp.active_power -= value * sys_base / PSY.get_base_power(comp)) _update_component!( comp::PSY.ACBus, ::Union{Val{:voltage_angle_export}, Val{:voltage_angle_opf}}, value, sys_base, ) = comp.angle = value _update_component!( comp::PSY.ACBus, ::Union{Val{:voltage_magnitude_export}, Val{:voltage_magnitude_opf}}, value, sys_base, ) = comp.magnitude = value function update_pf_system!( sys::PSY.System, container::OptimizationContainer, input_map::Dict{Symbol, <:Dict{OptimizationContainerKey, <:Any}}, time_step::Int, ) # Reset active_power to zero for components that use separate in/out variables # (e.g. storage, import/export sources) before the additive += / -= updates. # Collect unique (type, name) pairs to avoid resetting the same component twice. reset_components = Set{Tuple{DataType, String}}() for category in (:active_power_in, :active_power_out) haskey(input_map, category) || continue for (key, component_map) in input_map[category] for (_, device_name) in component_map push!(reset_components, (get_component_type(key), device_name)) end end end for (comp_type, device_name) in reset_components comp = PSY.get_component(comp_type, sys, device_name) comp.active_power = 0.0 end for (category, inputs) in input_map @debug "Writing $category to (possibly internal) System" for (key, component_map) in inputs result = lookup_value(container, key) for (device_id, device_name) in component_map injection_values = result[device_id, :] comp = PSY.get_component(get_component_type(key), sys, device_name) val = jump_value(injection_values[time_step]) _update_component!(comp, Val(category), val, get_base_power(container)) end end end end """ Update a `PowerFlowEvaluationData` containing a `PowerFlowContainer` that does not `supports_multi_period` using a single `time_step` of the `OptimizationContainer`. To properly keep track of outer step number, time steps must be passed in sequentially, starting with 1. """ function update_pf_data!( pf_e_data::PowerFlowEvaluationData{PFS.PSSEExporter}, container::OptimizationContainer, time_step::Int, ) pf_data = get_power_flow_data(pf_e_data) input_map = get_input_key_map(pf_e_data) update_pf_system!(PFS.get_system(pf_data), container, input_map, time_step) if !isnothing(pf_data.step) outer_step, _... = pf_data.step # time_step == 1 means we have rolled over to a new outer step # NOTE this is a bit brittle but there is currently no way of getting this # information from upstream, may change in the future (time_step == 1) && (outer_step += 1) pf_data.step = (outer_step, time_step) end return end # ParameterKey → FixedOutput formulation; dispatch is externally determined, # should not participate in slack. _accumulate_headroom!(::PFS.PowerFlowData, ::OptimizationContainer, ::PSY.System, ::OptimizationContainerKey{<:ParameterType, <:PSY.Component}, ::Dict{String, Int}, ::Int, ::Matrix{PSY.ACBusTypes}, ::Vector{Dict{Tuple{DataType, String}, Float64}}, ) = nothing # Storage uses split In/Out active power variables; its headroom contribution comes # from `_accumulate_in_out_headroom!` below. These skips guard against any (currently # unused) `:active_power` mapping for Storage that would otherwise double-count. _accumulate_headroom!(::PFS.PowerFlowData, ::OptimizationContainer, ::PSY.System, ::OptimizationContainerKey{<:ISOPT.OptimizationKeyType, <:PSY.Storage}, ::Dict{String, Int}, ::Int, ::Matrix{PSY.ACBusTypes}, ::Vector{Dict{Tuple{DataType, String}, Float64}}, ) = nothing # needed to fix an ambiguity: ParameterType with Storage. _accumulate_headroom!(::PFS.PowerFlowData, ::OptimizationContainer, ::PSY.System, ::OptimizationContainerKey{<:ISOPT.ParameterType, <:PSY.Storage}, ::Dict{String, Int}, ::Int, ::Matrix{PSY.ACBusTypes}, ::Vector{Dict{Tuple{DataType, String}, Float64}}, ) = nothing """ Accumulate headroom for a single OptimizationContainerKey into `pf_data` and `computed_gspf`. The `where {U}` parameter makes the component type a compile-time constant, so `PSY.get_component(U, ...)`, `has_container_key(..., U)`, and the `(U, device_name)` Dict key all dispatch concretely. Note that `result` and `ts_param_values` remain abstractly typed because `OptimizationContainer.variables` and `.parameters` have abstract value types in their dict signatures — the inner indexing into them still goes through dynamic dispatch. """ function _accumulate_headroom!( pf_data::PFS.PowerFlowData, container::OptimizationContainer, sys::PSY.System, key::OptimizationContainerKey{<:ISOPT.OptimizationKeyType, U}, component_map::Dict{String, Int}, n_time_steps::Int, bus_types::Matrix{PSY.ACBusTypes}, computed_gspf::Vector{Dict{Tuple{DataType, String}, Float64}}, ) where {U <: PSY.Component} result = lookup_value(container, key) # Time-varying active power limits (e.g. renewable availability profiles). # Precompute the axis as a Set so the per-(device, t) membership test is O(1). ts_param_values, ts_axis = if has_container_key( container, ActivePowerTimeSeriesParameter, U) vals = lookup_value(container, ParameterKey(ActivePowerTimeSeriesParameter, U)) (vals, Set{String}(axes(vals, 1))) else (nothing, nothing) end for (device_name, bus_ix) in component_map comp = PSY.get_component(U, sys, device_name) PFS.contributes_active_power(comp) || continue PFS.active_power_contribution_type(comp) == PFS.PowerContributionType.INJECTION || continue # limits.max is already in SYSTEM_BASE because PSI sets units at init p_max_static = PFS.get_active_power_limits_for_power_flow(comp).max has_ts = ts_axis !== nothing && device_name ∈ ts_axis for t in 1:n_time_steps bus_types[bus_ix, t] ∈ (PSY.ACBusTypes.REF, PSY.ACBusTypes.PV) || continue p_setpoint = jump_value(result[device_name, t]) p_max_t = if has_ts min(p_max_static, jump_value(ts_param_values[device_name, t])) else p_max_static end headroom = p_max_t - p_setpoint headroom <= 0.0 && continue computed_gspf[t][(U, device_name)] = headroom pf_data.bus_active_power_range[bus_ix, t] += headroom end end return end # Maximum discharge active power (system-base PU) for devices that use split # `ActivePowerInVariable` / `ActivePowerOutVariable`. PFS's # `get_active_power_limits_for_power_flow(::Source)` returns `(min=-Inf, max=Inf)`, # which is unusable for headroom math, so we read the device-level limits directly. _pf_in_out_discharge_max(comp::PSY.Storage) = PSY.get_output_active_power_limits(comp).max _pf_in_out_discharge_max(comp::PSY.Source) = PSY.get_active_power_limits(comp).max """ Accumulate headroom for devices that use split `ActivePowerInVariable` / `ActivePowerOutVariable` (e.g. Storage `BookKeeping`, Source `ImportExportSourceModel`). `net = p_out - p_in` is the device's signed contribution at time `t`. With net > 0 the device is dispatching and its headroom is `p_max_out - net`; with net <= 0 the device is charging (or idle) and contributes no upward slack. """ function _accumulate_in_out_headroom!( pf_data::PFS.PowerFlowData, container::OptimizationContainer, sys::PSY.System, in_inputs::Dict{OptimizationContainerKey, Dict{String, Int}}, out_inputs::Dict{OptimizationContainerKey, Dict{String, Int}}, n_time_steps::Int, bus_types::Matrix{PSY.ACBusTypes}, computed_gspf::Vector{Dict{Tuple{DataType, String}, Float64}}, ) for (in_key, in_cmap) in in_inputs out_key, out_cmap = _find_paired_out(out_inputs, get_component_type(in_key)) _accumulate_in_out_headroom_one_type!( pf_data, container, sys, in_key, in_cmap, out_key, out_cmap, n_time_steps, bus_types, computed_gspf, ) end return end function _find_paired_out( out_inputs::Dict{OptimizationContainerKey, Dict{String, Int}}, comp_type::DataType, ) for (key, cmap) in out_inputs get_component_type(key) === comp_type && return (key, cmap) end error( "`:active_power_out` map missing for $comp_type — a formulation added " * "`ActivePowerInVariable` without a paired `ActivePowerOutVariable`.", ) end # Function barrier: the parametric key types specialize `lookup_value` and `result[...]` # indexing on the concrete component type `U`. function _accumulate_in_out_headroom_one_type!( pf_data::PFS.PowerFlowData, container::OptimizationContainer, sys::PSY.System, in_key::OptimizationContainerKey{<:ISOPT.OptimizationKeyType, U}, in_cmap::Dict{String, Int}, out_key::OptimizationContainerKey{<:ISOPT.OptimizationKeyType, U}, out_cmap::Dict{String, Int}, n_time_steps::Int, bus_types::Matrix{PSY.ACBusTypes}, computed_gspf::Vector{Dict{Tuple{DataType, String}, Float64}}, ) where {U <: PSY.Component} result_in = lookup_value(container, in_key) result_out = lookup_value(container, out_key) for (device_name, bus_ix) in in_cmap comp = PSY.get_component(U, sys, device_name) PFS.contributes_active_power(comp) || continue PFS.active_power_contribution_type(comp) == PFS.PowerContributionType.INJECTION || continue p_max_out = _pf_in_out_discharge_max(comp) for t in 1:n_time_steps bus_types[bus_ix, t] ∈ (PSY.ACBusTypes.REF, PSY.ACBusTypes.PV) || continue net = jump_value(result_out[device_name, t]) - jump_value(result_in[device_name, t]) # Net <= 0 means charging or idle — per spec, no upward slack contribution. net < 0.0 && continue headroom = p_max_out - net headroom <= 0.0 && continue computed_gspf[t][(U, device_name)] = headroom pf_data.bus_active_power_range[bus_ix, t] += headroom end end return end """ Recompute per-time-step headroom-proportional generator slack participation factors using optimization results. Only runs if headroom proportional slack was enabled during initialization. For each generator at a REF or PV bus, headroom is `P_max(t) - P_setpoint(t)`, where `P_setpoint(t)` comes from the optimization result and `P_max(t)` is the minimum of the static device limit and any `ActivePowerTimeSeriesParameter` at time `t`. Devices that use split In/Out active power variables are handled separately via `_accumulate_in_out_headroom!`. This overwrites the PF-initialized values (which were computed once from static system data) with time-varying factors. """ function _update_headroom_participation_factors!( pf_data::PFS.PowerFlowData, container::OptimizationContainer, sys::PSY.System, input_key_map::Dict{Symbol, Dict{OptimizationContainerKey, Dict{String, Int}}}, ) PFS.get_distribute_slack_proportional_to_headroom(PFS.get_pf(pf_data)) || return computed_gspf = PFS.get_computed_gspf(pf_data)::Vector{Dict{Tuple{DataType, String}, Float64}} n_time_steps = length(get_time_steps(container)) bus_types = PFS.get_bus_type(pf_data)::Matrix{PSY.ACBusTypes} bus_slack_pf = PFS.get_bus_slack_participation_factors( pf_data, )::SparseArrays.SparseMatrixCSC{ Float64, Int, } # Reset with fresh dicts per time step (init may share references) for t in 1:n_time_steps computed_gspf[t] = Dict{Tuple{DataType, String}, Float64}() end pf_data.bus_active_power_range .= 0.0 # Function barrier so `_accumulate_headroom!` specializes per concrete key type # encountered at runtime — the outer Dict iterates abstract `OptimizationContainerKey`s. for (key, component_map) in input_key_map[:active_power] _accumulate_headroom!( pf_data, container, sys, key, component_map, n_time_steps, bus_types, computed_gspf) end # Devices with split `ActivePowerInVariable` / `ActivePowerOutVariable` # (e.g. Storage `BookKeeping`, Source `ImportExportSourceModel`) accumulate # headroom from the net of out − in. _accumulate_in_out_headroom!( pf_data, container, sys, input_key_map[:active_power_in], input_key_map[:active_power_out], n_time_steps, bus_types, computed_gspf) # Rebuild bus_slack_pf in one pass. Per-cell writes into the existing CSC matrix # would trigger O(nnz) structural inserts whenever runtime headroom appears at # (bus, t) pairs outside the t=1-derived sparsity pattern PFS init creates — which # is the common case for renewables with intermittent availability. PowerFlowData # is immutable, so we mutate the CSC's internal arrays in place to preserve identity. n_buses = size(pf_data.bus_active_power_range, 1) nnz_hint = count(>(0.0), pf_data.bus_active_power_range) I_idx = Int[] J_idx = Int[] V_val = Float64[] sizehint!(I_idx, nnz_hint) sizehint!(J_idx, nnz_hint) sizehint!(V_val, nnz_hint) for t in 1:n_time_steps, bus_ix in 1:n_buses R_k = pf_data.bus_active_power_range[bus_ix, t] R_k > 0.0 || continue push!(I_idx, bus_ix) push!(J_idx, t) push!(V_val, R_k) end new_sparse = SparseArrays.sparse(I_idx, J_idx, V_val, n_buses, n_time_steps) resize!(bus_slack_pf.nzval, length(new_sparse.nzval)) copyto!(bus_slack_pf.nzval, new_sparse.nzval) resize!(bus_slack_pf.rowval, length(new_sparse.rowval)) copyto!(bus_slack_pf.rowval, new_sparse.rowval) resize!(bus_slack_pf.colptr, length(new_sparse.colptr)) copyto!(bus_slack_pf.colptr, new_sparse.colptr) return end "Fetch the most recently solved `PowerFlowEvaluationData`" function latest_solved_power_flow_evaluation_data(container::OptimizationContainer) datas = get_power_flow_evaluation_data(container) return datas[findlast(x -> x.is_solved, datas)] end function solve_power_flow!( pf_e_data::PowerFlowEvaluationData, container::OptimizationContainer, sys::PSY.System) pf_data = get_power_flow_data(pf_e_data) if PFS.supports_multi_period(pf_data) update_pf_data!(pf_e_data, container) _update_headroom_participation_factors!( pf_data, container, sys, get_input_key_map(pf_e_data)) PFS.solve_power_flow!(pf_data) else for t in get_time_steps(container) update_pf_data!(pf_e_data, container, t) PFS.solve_power_flow!(pf_data) end end pf_e_data.is_solved = true return end # Currently nothing to write back to the optimization container from a PSSEExporter calculate_aux_variable_value!(::OptimizationContainer, ::AuxVarKey{T, <:Any} where {T <: PowerFlowAuxVariableType}, ::PSY.System, ::PowerFlowEvaluationData{PFS.PSSEExporter}) = nothing _get_pf_result(::Type{PowerFlowVoltageAngle}, pf_data::PFS.PowerFlowData) = PFS.get_bus_angles(pf_data) _get_pf_result(::Type{PowerFlowVoltageMagnitude}, pf_data::PFS.PowerFlowData) = PFS.get_bus_magnitude(pf_data) _get_pf_result(::Type{PowerFlowBranchReactivePowerFromTo}, pf_data::PFS.PowerFlowData) = PFS.get_arc_reactive_power_flow_from_to(pf_data) _get_pf_result(::Type{PowerFlowBranchReactivePowerToFrom}, pf_data::PFS.PowerFlowData) = PFS.get_arc_reactive_power_flow_to_from(pf_data) _get_pf_result(::Type{PowerFlowBranchActivePowerFromTo}, pf_data::PFS.PowerFlowData) = PFS.get_arc_active_power_flow_from_to(pf_data) _get_pf_result(::Type{PowerFlowBranchActivePowerToFrom}, pf_data::PFS.PowerFlowData) = PFS.get_arc_active_power_flow_to_from(pf_data) _get_pf_result(::Type{PowerFlowLossFactors}, pf_data::PFS.PowerFlowData) = PFS.get_loss_factors(pf_data) _get_pf_result(::Type{PowerFlowVoltageStabilityFactors}, pf_data::PFS.PowerFlowData) = PFS.get_voltage_stability_factors(pf_data) # PERF: unlike the others, this one requires a bit of computation. _get_pf_result(::Type{PowerFlowBranchActivePowerLoss}, pf_data::PFS.PowerFlowData) = PFS.get_arc_active_power_flow_from_to(pf_data) .+ PFS.get_arc_active_power_flow_to_from(pf_data) function calculate_aux_variable_value!(container::OptimizationContainer, key::AuxVarKey{T, <:PSY.ACBus}, ::PSY.System, pf_e_data::PowerFlowEvaluationData{<:PFS.PowerFlowData}, ) where {T <: PowerFlowAuxVariableType} @debug "Updating $key from PowerFlowData" pf_data = get_power_flow_data(pf_e_data) nrd = PFS.get_network_reduction_data(pf_data) src = _get_pf_result(T, pf_data) bus_lookup = PFS.get_bus_lookup(pf_data) dest = get_aux_variable(container, key) for bus_number in axes(dest, 1) bus_ix = PNM.get_bus_index(bus_number, bus_lookup, nrd) dest[bus_number, :] = src[bus_ix, :] end return end function calculate_aux_variable_value!(container::OptimizationContainer, key::AuxVarKey{T, U}, ::PSY.System, pf_e_data::PowerFlowEvaluationData{<:PFS.PowerFlowData}, ) where {T <: PowerFlowAuxVariableType, U <: PSY.Branch} @debug "Updating $key from PowerFlowData" pf_data = get_power_flow_data(pf_e_data) src = _get_pf_result(T, pf_data) dest = get_aux_variable(container, key) nrd = PFS.get_network_reduction_data(pf_data) arc_lookup = PFS.get_arc_lookup(pf_data) # PERF: could pre-compute a Dict of branch type to arcs, then intersect the arcs # for the type U with the keys of the branch maps. for (arc, br) in PNM.get_direct_branch_map(nrd) if br isa U # always a concrete type, so same as: typeof(br) == U name = PSY.get_name(br) arc_ix = arc_lookup[arc] dest[name, :] = src[arc_ix, :] end end for (arc, parallel_brs) in PNM.get_parallel_branch_map(nrd) # parallel_brs is Set{ACTransmission} for br in parallel_brs sample_line = first(parallel_brs) impedance = PSY.get_r(sample_line) + im * PSY.get_x(sample_line) first_name = PSY.get_name(sample_line) if br isa U name = PSY.get_name(br) IS.@assert_op T <: BranchFlowAuxVariableType || (T == PowerFlowBranchActivePowerLoss) if !isapprox(PSY.get_r(br) + im * PSY.get_x(br), impedance) @debug "Parallel branches with different impedances found: " * "$name and $first_name. Check your data inputs." end multiplier = PNM.compute_parallel_multiplier(parallel_brs, name) arc_ix = arc_lookup[arc] dest[name, :] = multiplier .* src[arc_ix, :] end end end return end function calculate_aux_variable_value!(container::OptimizationContainer, key::AuxVarKey{<:PowerFlowAuxVariableType, <:PSY.Component}, system::PSY.System) # Skip the aux vars that the current power flow isn't meant to update pf_e_data = latest_solved_power_flow_evaluation_data(container) pf_data = get_power_flow_data(pf_e_data) (key in branch_aux_vars(pf_data) || key in bus_aux_vars(pf_data)) && return calculate_aux_variable_value!(container, key, system, pf_e_data) return end ================================================ FILE: src/network_models/powermodels_interface.jl ================================================ ################################################################################# # Comments # # - Ideally the net_injection variables would be bounded. This can be done using an adhoc data model extension # - the `instantiate_*_expr_model` functions combine `PM.instantiate_model` and the `build_*` methods ################################################################################# # Model Definitions const UNSUPPORTED_POWERMODELS = [ PM.SOCBFPowerModel, PM.SOCBFConicPowerModel, PM.IVRPowerModel, PM.SparseSDPWRMPowerModel, ] function instantiate_nip_expr_model(data::Dict{String, Any}, model_constructor; kwargs...) return PM.instantiate_model(data, model_constructor, instantiate_nip_expr; kwargs...) end # replicates PM.build_mn_opf function instantiate_nip_expr(pm::PM.AbstractPowerModel) for n in eachindex(PM.nws(pm)) PM.variable_bus_voltage(pm; nw = n) PM.variable_branch_power(pm; nw = n, bounded = false) PM.variable_dcline_power(pm; nw = n, bounded = false) PM.constraint_model_voltage(pm; nw = n) for i in PM.ids(pm, :ref_buses; nw = n) PM.constraint_theta_ref(pm, i; nw = n) end for i in PM.ids(pm, :bus; nw = n) constraint_power_balance_ni_expr(pm, i; nw = n) end for i in PM.ids(pm, :branch; nw = n) PM.constraint_ohms_yt_from(pm, i; nw = n) PM.constraint_ohms_yt_to(pm, i; nw = n) PM.constraint_voltage_angle_difference(pm, i; nw = n) end for i in PM.ids(pm, :dcline; nw = n) PM.constraint_dcline_power_losses(pm, i; nw = n) end end return end function instantiate_bfp_expr_model(data::Dict{String, Any}, model_constructor; kwargs...) return PM.instantiate_model(data, model_constructor, instantiate_bfp_expr; kwargs...) end # replicates PM.build_mn_opf_bf_strg function instantiate_bfp_expr(pm::PM.AbstractPowerModel) for n in eachindex(PM.nws(pm)) PM.variable_bus_voltage(pm; nw = n) PM.variable_branch_power(pm; nw = n, bounded = false) PM.variable_dcline_power(pm; nw = n, bounded = false) PM.constraint_model_current(pm; nw = n) for i in PM.ids(pm, :ref_buses; nw = n) PM.constraint_theta_ref(pm, i; nw = n) end for i in PM.ids(pm, :bus; nw = n) constraint_power_balance_ni_expr(pm, i; nw = n) end for i in PM.ids(pm, :branch; nw = n) PM.constraint_power_losses(pm, i; nw = n) PM.constraint_voltage_magnitude_difference(pm, i; nw = n) PM.constraint_voltage_angle_difference(pm, i; nw = n) end for i in PM.ids(pm, :dcline; nw = n) PM.constraint_dcline_power_losses(pm, i; nw = n) end end return end #= # VI Methdos not supported currently function instantiate_vip_expr_model(data::Dict{String, Any}, model_constructor; kwargs...) throw(error("VI Models not currently supported")) end =# ################################################################################# # Model Extention Functions function constraint_power_balance_ni_expr( pm::PM.AbstractPowerModel, i::Int; nw::Int = pm.cnw, ) if !haskey(PM.con(pm, nw), :power_balance_p) PM.con(pm, nw)[:power_balance_p] = Dict{Int, JuMP.ConstraintRef}() end if !haskey(PM.con(pm, nw), :power_balance_q) PM.con(pm, nw)[:power_balance_q] = Dict{Int, JuMP.ConstraintRef}() end bus_arcs = PM.ref(pm, nw, :bus_arcs, i) bus_arcs_dc = PM.ref(pm, nw, :bus_arcs_dc, i) inj_p_expr = PM.ref(pm, nw, :bus, i, "inj_p") inj_q_expr = PM.ref(pm, nw, :bus, i, "inj_q") constraint_power_balance_ni_expr( pm, nw, i, bus_arcs, bus_arcs_dc, inj_p_expr, inj_q_expr, ) return end function constraint_power_balance_ni_expr( pm::PM.AbstractPowerModel, n::Int, i::Int, bus_arcs, bus_arcs_dc, inj_p_expr, inj_q_expr, ) p = PM.var(pm, n, :p) q = PM.var(pm, n, :q) p_dc = PM.var(pm, n, :p_dc) q_dc = PM.var(pm, n, :q_dc) PM.con(pm, n, :power_balance_p)[i] = JuMP.@constraint( pm.model, sum(p[a] for a in bus_arcs) + sum(p_dc[a_dc] for a_dc in bus_arcs_dc) == inj_p_expr ) PM.con(pm, n, :power_balance_q)[i] = JuMP.@constraint( pm.model, sum(q[a] for a in bus_arcs) + sum(q_dc[a_dc] for a_dc in bus_arcs_dc) == inj_q_expr ) return end #= # VI Methdos not supported currently function constraint_current_balance_ni_expr( pm::PM.AbstractPowerModel, i::Int; nw::Int = pm.cnw, ) if !haskey(PM.con(pm, nw), :kcl_cr) PM.con(pm, nw)[:kcl_cr] = Dict{Int, JuMP.ConstraintRef}() end if !haskey(PM.con(pm, nw), :kcl_ci) PM.con(pm, nw)[:kcl_ci] = Dict{Int, JuMP.ConstraintRef}() end bus_arcs = PM.ref(pm, nw, :bus_arcs, i) bus_arcs_dc = PM.ref(pm, nw, :bus_arcs_dc, i) inj_p_expr = PM.ref(pm, nw, :bus, i, "inj_p") inj_q_expr = PM.ref(pm, nw, :bus, i, "inj_q") constraint_current_balance_ni_expr( pm, nw, i, bus_arcs, bus_arcs_dc, inj_p_expr, inj_q_expr, ) return end function constraint_current_balance_ni_expr( pm::PM.AbstractPowerModel, n::Int, i::Int, bus_arcs, bus_arcs_dc, inj_p_expr, inj_q_expr, ) p = PM.var(pm, n, :p) q = PM.var(pm, n, :q) p_dc = PM.var(pm, n, :p_dc) q_dc = PM.var(pm, n, :q_dc) PM.con(pm, n, :power_balance_p)[i] = JuMP.@constraint( pm.model, sum(p[a] for a in bus_arcs) + sum(p_dc[a_dc] for a_dc in bus_arcs_dc) == inj_p_expr ) PM.con(pm, n, :power_balance_q)[i] = JuMP.@constraint( pm.model, sum(q[a] for a in bus_arcs) + sum(q_dc[a_dc] for a_dc in bus_arcs_dc) == inj_q_expr ) return end =# """ active power only models ignore reactive power variables """ function variable_reactive_net_injection(pm::PM.AbstractActivePowerModel; kwargs...) return end function constraint_power_balance_ni_expr( pm::PM.AbstractActivePowerModel, n::Int, i::Int, bus_arcs, bus_arcs_dc, inj_p_expr, _, ) p = PM.var(pm, n, :p) p_dc = PM.var(pm, n, :p_dc) PM.con(pm, n, :power_balance_p)[i] = JuMP.@constraint( pm.model, sum(p[a] for a in bus_arcs) + sum(p_dc[a_dc] for a_dc in bus_arcs_dc) == inj_p_expr ) return end function powermodels_network!( container::OptimizationContainer, system_formulation::Type{S}, sys::PSY.System, template::ProblemTemplate, instantiate_model, ) where {S <: PM.AbstractPowerModel} time_steps = get_time_steps(container) pm_data, PM_map = pass_to_pm(sys, template, time_steps[end]) network_model = get_network_model(template) network_reduction = get_network_reduction(network_model) if isempty(network_reduction) ac_bus_numbers = PSY.get_number.(get_available_components(network_model, PSY.ACBus, sys)) else bus_reduction_map = PNM.get_bus_reduction_map(network_reduction) ac_bus_numbers = collect(keys(bus_reduction_map)) end for t in time_steps, bus_no in ac_bus_numbers pm_data["nw"]["$(t)"]["bus"]["$bus_no"]["inj_p"] = container.expressions[ExpressionKey(ActivePowerBalance, PSY.ACBus)][ bus_no, t, ] pm_data["nw"]["$(t)"]["bus"]["$bus_no"]["inj_q"] = container.expressions[ExpressionKey(ReactivePowerBalance, PSY.ACBus)][ bus_no, t, ] end container.pm = instantiate_model(pm_data, system_formulation; jump_model = container.JuMPmodel) container.pm.ext[:PMmap] = PM_map return end function powermodels_network!( container::OptimizationContainer, system_formulation::Type{S}, sys::PSY.System, template::ProblemTemplate, instantiate_model, ) where {S <: PM.AbstractActivePowerModel} time_steps = get_time_steps(container) pm_data, PM_map = pass_to_pm(sys, template, time_steps[end]) network_model = get_network_model(template) network_reduction = get_network_reduction(network_model) if isempty(network_reduction) ac_bus_numbers = PSY.get_number.(get_available_components(network_model, PSY.ACBus, sys)) else bus_reduction_map = PNM.get_bus_reduction_map(network_reduction) ac_bus_numbers = collect(keys(bus_reduction_map)) end for t in time_steps, bus_no in ac_bus_numbers pm_data["nw"]["$(t)"]["bus"]["$bus_no"]["inj_p"] = container.expressions[ExpressionKey(ActivePowerBalance, PSY.ACBus)][ bus_no, t, ] # pm_data["nw"]["$(t)"]["bus"]["$(bus.number)"]["inj_q"] = 0.0 end container.pm = instantiate_model( pm_data, system_formulation; jump_model = get_jump_model(container), ) container.pm.ext[:PMmap] = PM_map return end #### PM accessor functions ######## function PMvarmap(::Type{S}) where {S <: PM.AbstractDCPModel} pm_variable_map = Dict{Type, Dict{Symbol, Union{VariableType, NamedTuple}}}() pm_variable_map[PSY.ACBus] = Dict(:va => VoltageAngle()) pm_variable_map[PSY.ACTransmission] = Dict(:p => (from_to = FlowActivePowerVariable(), to_from = nothing)) pm_variable_map[PSY.TwoTerminalHVDC] = Dict(:p_dc => (from_to = FlowActivePowerVariable(), to_from = nothing)) return pm_variable_map end function PMvarmap(::Type{S}) where {S <: PM.AbstractActivePowerModel} pm_variable_map = Dict{Type, Dict{Symbol, Union{VariableType, NamedTuple}}}() pm_variable_map[PSY.ACBus] = Dict(:va => VoltageAngle()) pm_variable_map[PSY.ACTransmission] = Dict(:p => FlowActivePowerFromToVariable()) pm_variable_map[PSY.TwoTerminalHVDC] = Dict( :p_dc => ( from_to = FlowActivePowerFromToVariable(), to_from = FlowActivePowerToFromVariable(), ), ) return pm_variable_map end function PMvarmap(::Type{S}) where {S <: PM.AbstractPowerModel} pm_variable_map = Dict{Type, Dict{Symbol, Union{VariableType, NamedTuple}}}() pm_variable_map[PSY.ACBus] = Dict(:va => VoltageAngle(), :vm => VoltageMagnitude()) pm_variable_map[PSY.ACTransmission] = Dict( :p => ( from_to = FlowActivePowerFromToVariable(), to_from = FlowActivePowerToFromVariable(), ), :q => ( from_to = FlowReactivePowerFromToVariable(), to_from = FlowReactivePowerToFromVariable(), ), ) pm_variable_map[PSY.TwoTerminalHVDC] = Dict( :p_dc => (from_to = FlowActivePowerVariable(), to_from = nothing), :q_dc => ( from_to = FlowReactivePowerFromToVariable(), to_from = FlowReactivePowerToFromVariable(), ), ) return pm_variable_map end function PMconmap(::Type{S}) where {S <: PM.AbstractActivePowerModel} pm_constraint_map = Dict{Type, Dict{Symbol, <:ConstraintType}}() pm_constraint_map[PSY.ACBus] = Dict(:power_balance_p => NodalBalanceActiveConstraint()) return pm_constraint_map end function PMconmap(::Type{S}) where {S <: PM.AbstractPowerModel} pm_constraint_map = Dict{Type, Dict{Symbol, ConstraintType}}() pm_constraint_map[PSY.ACBus] = Dict( :power_balance_p => NodalBalanceActiveConstraint(), :power_balance_q => NodalBalanceReactiveConstraint(), ) return pm_constraint_map end function PMexprmap(::Type{S}) where {S <: PM.AbstractPowerModel} pm_expr_map = Dict{ Type, NamedTuple{ (:pm_expr, :psi_con), Tuple{Dict{Symbol, Union{VariableType, NamedTuple}}, Symbol}, }, }() return pm_expr_map end function add_pm_variable_refs!( container::OptimizationContainer, system_formulation::Type{S}, ::PSY.System, model::NetworkModel, ) where {S <: PM.AbstractPowerModel} time_steps = get_time_steps(container) bus_dict = container.pm.ext[:PMmap].bus ACbranch_dict = container.pm.ext[:PMmap].arcs ACbranch_types = PNM.get_ac_transmission_types(model.network_reduction) DCbranch_dict = container.pm.ext[:PMmap].arcs_dc DCbranch_types = Set(typeof.(values(DCbranch_dict))) pm_variable_types = keys(PM.var(container.pm, 1)) pm_variable_map = PMvarmap(system_formulation) bus_names = [PSY.get_name(b) for b in values(bus_dict)] for (pm_v, ps_v) in pm_variable_map[PSY.ACBus] if pm_v in pm_variable_types var_container = add_variable_container!(container, ps_v, PSY.ACBus, bus_names, time_steps) for t in time_steps, (pm_bus, bus) in bus_dict name = PSY.get_name(bus) var_container[name, t] = PM.var(container.pm, t, pm_v)[pm_bus] # pm_vars[pm_v][pm_bus] end end end add_pm_variable_refs!( container, model, PSY.ACTransmission, ACbranch_types, ACbranch_dict, pm_variable_map, pm_variable_types, time_steps, ) add_pm_variable_refs!( container, model, PSY.TwoTerminalHVDC, DCbranch_types, DCbranch_dict, pm_variable_map, pm_variable_types, time_steps, ) return end function add_pm_variable_refs!( container::OptimizationContainer, model::NetworkModel, d_class::Type{PSY.ACTransmission}, device_types::Set, pm_map::Dict, pm_variable_map::Dict, pm_variable_types::Base.KeySet, time_steps::UnitRange{Int}, ) net_reduction_data = get_network_reduction(model) reduced_branch_tracker = get_reduced_branch_tracker(model) for d_type in device_types, (pm_v, ps_v) in pm_variable_map[d_class] if pm_v in pm_variable_types for direction in fieldnames(typeof(ps_v)) var_type = getfield(ps_v, direction) var_type === nothing && continue branch_names = get_branch_argument_variable_axis(net_reduction_data, d_type) var_container = add_variable_container!( container, var_type, d_type, branch_names, time_steps, ) for (name, (arc_tuple, reduction)) in PNM.get_name_to_arc_map(net_reduction_data, d_type) has_entry, tracker_container = search_for_reduced_branch_argument!( reduced_branch_tracker, arc_tuple, typeof(var_type), # TODO: Make the mapping not rely on instances but types ) if has_entry @assert !isempty(tracker_container) name arc_tuple reduction end for t in time_steps if !has_entry pm_d = pm_map[arc_tuple] var = PM.var(container.pm, t, pm_v, getfield(pm_d, direction)) tracker_container[t] = var end var_container[name, t] = tracker_container[t] end end end end end return end function add_pm_variable_refs!( container::OptimizationContainer, ::NetworkModel, d_class::Type{PSY.TwoTerminalHVDC}, device_types::Set, pm_map::Dict, pm_variable_map::Dict, pm_variable_types::Base.KeySet, time_steps::UnitRange{Int}, ) for d_type in Set(device_types) devices = [d for d in pm_map if typeof(d[2]) == d_type] for (pm_v, ps_v) in pm_variable_map[d_class] if pm_v in pm_variable_types for dir in fieldnames(typeof(ps_v)) var_type = getfield(ps_v, dir) var_type === nothing && continue var_container = add_variable_container!( container, var_type, d_type, [PSY.get_name(d[2]) for d in devices], time_steps, ) for t in time_steps, (pm_d, d) in devices var = PM.var(container.pm, t, pm_v, getfield(pm_d, dir)) var_container[PSY.get_name(d), t] = var end end end end end return end function add_pm_constraint_refs!( container::OptimizationContainer, system_formulation::Type{S}, ::PSY.System, ) where {S <: PM.AbstractPowerModel} time_steps = get_time_steps(container) bus_dict = container.pm.ext[:PMmap].bus pm_constraint_names = [k for k in keys(PM.con(container.pm, 1)) if !isempty(PM.con(container.pm, 1, k))] pm_constraint_map = PMconmap(system_formulation) for (pm_v, ps_v) in pm_constraint_map[PSY.ACBus] if pm_v in pm_constraint_names cons_container = add_constraints_container!( container, ps_v, PSY.ACBus, [PSY.get_name(b) for b in values(bus_dict)], time_steps, ) for t in time_steps, (pm_bus, bus) in bus_dict name = PSY.get_name(bus) cons_container[name, t] = PM.con(container.pm, t, pm_v)[pm_bus] end end end return end ================================================ FILE: src/operation/decision_model.jl ================================================ mutable struct DecisionModel{M <: DecisionProblem} <: OperationModel name::Symbol template::AbstractProblemTemplate sys::PSY.System internal::Union{Nothing, ISOPT.ModelInternal} simulation_info::SimulationInfo store::DecisionModelStore ext::Dict{String, Any} end """ DecisionModel{M}( template::AbstractProblemTemplate, sys::PSY.System, jump_model::Union{Nothing, JuMP.Model}=nothing; kwargs...) where {M<:DecisionProblem} Build the optimization problem of type M with the specific system and template. # Arguments - `::Type{M} where M<:DecisionProblem`: The abstract operation model type - `template::AbstractProblemTemplate`: The model reference made up of transmission, devices, branches, and services. - `sys::PSY.System`: the system created using Power Systems - `jump_model::Union{Nothing, JuMP.Model}`: Enables passing a custom JuMP model. Use with care - `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::Dates.Period = UNSET_HORIZON`: Manually specify the length of the forecast Horizon - `resolution::Dates.Period = UNSET_RESOLUTION`: Manually specify the model's resolution - `interval::Dates.Period = UNSET_INTERVAL`: Specify the forecast interval to use. Required when the system contains multiple forecast intervals (e.g., from multiple calls to `transform_single_time_series!`). Enables multiple models to share the same system with different time series conversions. - `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 - `check_components::Bool = true`: True to check the components valid fields when building - `initialize_model::Bool = true`: Option to decide to initialize the model or not. - `initialization_file::String = ""`: This allows to pass pre-existing initialization values to avoid the solution of an optimization problem to find feasible initial conditions. - `deserialize_initial_conditions::Bool = false`: Option to deserialize conditions - `export_pwl_vars::Bool = false`: True to export all the pwl intermediate variables. It can slow down significantly the build and solve time. - `allow_fails::Bool = false`: True to allow the simulation to continue even if the optimization step fails. Use with care. - `optimizer_solve_log_print::Bool = false`: Uses JuMP.unset_silent() to print the optimizer's log. By default all solvers are set to MOI.Silent() - `detailed_optimizer_stats::Bool = false`: True to save detailed optimizer stats log. - `calculate_conflict::Bool = false`: True to use solver to calculate conflicts for infeasible problems. Only specific solvers are able to calculate conflicts. - `direct_mode_optimizer::Bool = false`: True to use the solver in direct mode. Creates a [JuMP.direct_model](https://jump.dev/JuMP.jl/dev/reference/models/#JuMP.direct_model). - `store_variable_names::Bool = false`: to store variable names in optimization model. Decreases the build times. - `rebuild_model::Bool = false`: It will force the rebuild of the underlying JuMP model with each call to update the model. It increases solution times, use only if the model can't be updated in memory. - `initial_time::Dates.DateTime = UNSET_INI_TIME`: Initial Time for the model solve. - `time_series_cache_size::Int = IS.TIME_SERIES_CACHE_SIZE_BYTES`: Size in bytes to cache for each time array. Default is 1 MiB. Set to 0 to disable. # Example ```julia template = ProblemTemplate(CopperPlatePowerModel, devices, branches, services) OpModel = DecisionModel(MockOperationProblem, template, system) ``` """ function DecisionModel{M}( template::AbstractProblemTemplate, sys::PSY.System, settings::Settings, jump_model::Union{Nothing, JuMP.Model} = nothing; name = nothing, ) where {M <: DecisionProblem} if name === nothing name = nameof(M) elseif name isa String name = Symbol(name) end auto_transform_time_series!(sys, settings) ts_type = get_deterministic_time_series_type(sys) internal = ISOPT.ModelInternal( OptimizationContainer(sys, settings, jump_model, ts_type), ) template_ = deepcopy(template) finalize_template!(template_, sys) model = DecisionModel{M}( name, template_, sys, internal, SimulationInfo(), DecisionModelStore(), Dict{String, Any}(), ) validate_time_series!(model) return model end function DecisionModel{M}( template::AbstractProblemTemplate, sys::PSY.System, jump_model::Union{Nothing, JuMP.Model} = nothing; name = nothing, optimizer = nothing, horizon = UNSET_HORIZON, resolution = UNSET_RESOLUTION, interval = UNSET_INTERVAL, warm_start = true, check_components = true, initialize_model = true, initialization_file = "", deserialize_initial_conditions = false, export_pwl_vars = false, allow_fails = false, optimizer_solve_log_print = false, detailed_optimizer_stats = false, calculate_conflict = false, direct_mode_optimizer = false, store_variable_names = false, rebuild_model = false, export_optimization_model = false, check_numerical_bounds = true, initial_time = UNSET_INI_TIME, time_series_cache_size::Int = IS.TIME_SERIES_CACHE_SIZE_BYTES, ) where {M <: DecisionProblem} settings = Settings( sys; horizon = horizon, resolution = resolution, interval = interval, initial_time = initial_time, optimizer = optimizer, time_series_cache_size = time_series_cache_size, warm_start = warm_start, check_components = check_components, initialize_model = initialize_model, initialization_file = initialization_file, deserialize_initial_conditions = deserialize_initial_conditions, export_pwl_vars = export_pwl_vars, allow_fails = allow_fails, calculate_conflict = calculate_conflict, optimizer_solve_log_print = optimizer_solve_log_print, detailed_optimizer_stats = detailed_optimizer_stats, direct_mode_optimizer = direct_mode_optimizer, check_numerical_bounds = check_numerical_bounds, store_variable_names = store_variable_names, rebuild_model = rebuild_model, export_optimization_model = export_optimization_model, ) return DecisionModel{M}(template, sys, settings, jump_model; name = name) end """ Build the optimization problem of type M with the specific system and template # Arguments - `::Type{M} where M<:DecisionProblem`: The abstract operation model type - `template::AbstractProblemTemplate`: The model reference made up of transmission, devices, branches, and services. - `sys::PSY.System`: the system created using Power Systems - `jump_model::Union{Nothing, JuMP.Model}` = nothing: Enables passing a custom JuMP model. Use with care. # Example ```julia template = ProblemTemplate(CopperPlatePowerModel, devices, branches, services) problem = DecisionModel(MyOpProblemType, template, system, optimizer) ``` """ function DecisionModel( ::Type{M}, template::AbstractProblemTemplate, sys::PSY.System, jump_model::Union{Nothing, JuMP.Model} = nothing; kwargs..., ) where {M <: DecisionProblem} return DecisionModel{M}(template, sys, jump_model; kwargs...) end function DecisionModel( template::AbstractProblemTemplate, sys::PSY.System, jump_model::Union{Nothing, JuMP.Model} = nothing; kwargs..., ) return DecisionModel{GenericOpProblem}(template, sys, jump_model; kwargs...) end """ Builds an empty decision model. This constructor is used for the implementation of custom decision models that do not require a template. # Arguments - `::Type{M} where M<:DecisionProblem`: The abstract operation model type - `sys::PSY.System`: the system created using Power Systems - `jump_model::Union{Nothing, JuMP.Model}` = nothing: Enables passing a custom JuMP model. Use with care. # Example ```julia problem = DecisionModel(system, optimizer) ``` """ function DecisionModel{M}( sys::PSY.System, jump_model::Union{Nothing, JuMP.Model} = nothing; kwargs..., ) where {M <: DecisionProblem} return DecisionModel{M}(ProblemTemplate(), sys, jump_model; kwargs...) end function DecisionModel{M}( sys::PSY.System, jump_model::Union{Nothing, JuMP.Model} = nothing; kwargs..., ) where {M <: DefaultDecisionProblem} IS.ArgumentError( "DefaultDecisionProblem subtypes require a template. Use DecisionModel subtyping instead.", ) end get_problem_type(::DecisionModel{M}) where {M <: DecisionProblem} = M function validate_template(::DecisionModel{M}) where {M <: DecisionProblem} error("validate_template is not implemented for DecisionModel{$M}") end function validate_template(model::DecisionModel{<:DefaultDecisionProblem}) validate_template_impl!(model) return end # Probably could be more efficient by storing the info in the internal function get_current_time(model::DecisionModel) execution_count = get_execution_count(model) initial_time = get_initial_time(model) interval = get_interval(model) return initial_time + interval * execution_count end function init_model_store_params!(model::DecisionModel) num_executions = get_executions(model) horizon = get_horizon(model) system = get_system(model) settings = get_settings(model) model_interval = get_interval(settings) if model_interval != UNSET_INTERVAL interval = model_interval else interval = PSY.get_forecast_interval(system) end resolution = get_resolution(model) base_power = PSY.get_base_power(system) sys_uuid = IS.get_uuid(system) store_params = ModelStoreParams( num_executions, horizon, iszero(interval) ? resolution : interval, resolution, base_power, sys_uuid, get_metadata(get_optimization_container(model)), ) ISOPT.set_store_params!(get_internal(model), store_params) return end 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 model_interval = get_interval(settings) available_intervals = get_forecast_intervals(sys) if model_interval == UNSET_INTERVAL && length(available_intervals) > 1 throw( IS.ConflictingInputsError( "The system contains multiple forecast intervals $(available_intervals). " * "The `interval` keyword argument must be provided to the DecisionModel constructor " * "to select which interval to use.", ), ) elseif model_interval != UNSET_INTERVAL && !isempty(available_intervals) if model_interval ∉ available_intervals throw( IS.ConflictingInputsError( "Interval $(Dates.canonicalize(model_interval)) is not available in the system data. " * "Available forecast intervals: $(available_intervals)", ), ) end end interval_kwarg = model_interval == UNSET_INTERVAL ? (;) : (; interval = model_interval) if get_horizon(settings) == UNSET_HORIZON set_horizon!(settings, PSY.get_forecast_horizon(sys; interval_kwarg...)) end counts = PSY.get_time_series_counts(sys) if counts.forecast_count < 1 error( "The system does not contain forecast data. A DecisionModel can't be built.", ) end return end function build_pre_step!(model::DecisionModel{<:DecisionProblem}) TimerOutputs.@timeit BUILD_PROBLEMS_TIMER "Build pre-step" begin validate_template(model) if !isempty(model) @info "OptimizationProblem status not ModelBuildStatus.EMPTY. Resetting" reset!(model) end # Initial time are set here because the information is specified in the # Simulation Sequence object and not at the problem creation. @info "Initializing Optimization Container For a DecisionModel" init_optimization_container!( get_optimization_container(model), get_network_model(get_template(model)), get_system(model), ) @info "Initializing ModelStoreParams" init_model_store_params!(model) set_status!(model, ModelBuildStatus.IN_PROGRESS) end return end function build_impl!(model::DecisionModel{<:DecisionProblem}) build_pre_step!(model) @info "Instantiating Network Model" instantiate_network_model!(model) handle_initial_conditions!(model) build_model!(model) serialize_metadata!(get_optimization_container(model), get_output_dir(model)) log_values(get_settings(model)) return end get_horizon(model::DecisionModel) = get_horizon(get_settings(model)) """ Build the Decision Model based on the specified DecisionProblem. # Arguments - `model::DecisionModel{<:DecisionProblem}`: DecisionModel object - `output_dir::String`: Output directory for results - `recorders::Vector{Symbol} = []`: recorder names to register - `console_level = Logging.Error`: - `file_level = Logging.Info`: - `disable_timer_outputs = false` : Enable/Disable timing outputs """ function build!( model::DecisionModel{<:DecisionProblem}; output_dir::String, recorders = [], console_level = Logging.Error, file_level = Logging.Info, disable_timer_outputs = false, ) mkpath(output_dir) set_output_dir!(model, output_dir) set_console_level!(model, console_level) set_file_level!(model, file_level) TimerOutputs.reset_timer!(BUILD_PROBLEMS_TIMER) disable_timer_outputs && TimerOutputs.disable_timer!(BUILD_PROBLEMS_TIMER) file_mode = "w" add_recorders!(model, recorders) register_recorders!(model, 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, ModelBuildStatus.BUILT) @info "\n$(BUILD_PROBLEMS_TIMER)\n" catch e set_status!(model, ModelBuildStatus.FAILED) bt = catch_backtrace() @error "DecisionModel Build Failed" exception = e, bt end end finally unregister_recorders!(model) close(logger) end return get_status(model) end """ Default implementation of build method for Operational Problems for models conforming with DecisionProblem specification. Overload this function to implement a custom build method """ function build_model!(model::DecisionModel{<:DefaultDecisionProblem}) build_impl!(get_optimization_container(model), get_template(model), get_system(model)) return end function reset!(model::DecisionModel{<:DefaultDecisionProblem}) was_built_for_recurrent_solves = built_for_recurrent_solves(model) if was_built_for_recurrent_solves set_execution_count!(model, 0) end sys = get_system(model) ts_type = get_deterministic_time_series_type(sys) ISOPT.set_container!( get_internal(model), OptimizationContainer( get_system(model), get_settings(model), nothing, ts_type, ), ) get_optimization_container(model).built_for_recurrent_solves = was_built_for_recurrent_solves internal = get_internal(model) ISOPT.set_initial_conditions_model_container!(internal, nothing) empty_time_series_cache!(model) empty!(get_store(model)) set_status!(model, ModelBuildStatus.EMPTY) return end """ Default solve method for models that conform to the requirements of DecisionModel{<: DecisionProblem}. This will call `build!` on the model if it is not already built. It will forward all keyword arguments to that function. # Arguments - `model::OperationModel = model`: operation model - `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 - `export_optimization_problem::Bool = true`: If true, serialize the model to a file to allow re-execution later. # Examples ```julia results = solve!(OpModel) results = solve!(OpModel, export_problem_results = true) ``` """ function solve!( model::DecisionModel{<:DecisionProblem}; export_problem_results = false, console_level = Logging.Error, file_level = Logging.Info, disable_timer_outputs = false, export_optimization_problem = true, kwargs..., ) build_if_not_already_built!( model; console_level = console_level, file_level = file_level, disable_timer_outputs = disable_timer_outputs, kwargs..., ) set_console_level!(model, console_level) set_file_level!(model, file_level) TimerOutputs.reset_timer!(RUN_OPERATION_MODEL_TIMER) disable_timer_outputs && TimerOutputs.disable_timer!(RUN_OPERATION_MODEL_TIMER) file_mode = "a" register_recorders!(model, file_mode) logger = ISOPT.configure_logging( get_internal(model), PROBLEM_LOG_FILENAME, file_mode, ) optimizer = get(kwargs, :optimizer, nothing) try Logging.with_logger(logger) do try initialize_storage!( get_store(model), get_optimization_container(model), get_store_params(model), ) TimerOutputs.@timeit RUN_OPERATION_MODEL_TIMER "Solve" begin _pre_solve_model_checks(model, optimizer) solve_impl!(model) current_time = get_initial_time(model) write_results!(get_store(model), model, current_time, current_time) write_optimizer_stats!( get_store(model), get_optimizer_stats(model), current_time, ) end if export_optimization_problem TimerOutputs.@timeit RUN_OPERATION_MODEL_TIMER "Serialize" begin serialize_optimization_model(model) end end TimerOutputs.@timeit RUN_OPERATION_MODEL_TIMER "Results processing" begin # TODO: This could be more complicated than it needs to be results = OptimizationProblemResults(model) serialize_results(results, get_output_dir(model)) export_problem_results && export_results(results) end @info "\n$(RUN_OPERATION_MODEL_TIMER)\n" catch e @error "Decision Problem solve failed" exception = (e, catch_backtrace()) set_run_status!(model, RunStatus.FAILED) end end finally unregister_recorders!(model) close(logger) end return get_run_status(model) end """ Default solve method for a DecisionModel used inside of a Simulation. Solves problems that conform to the requirements of DecisionModel{<: DecisionProblem} # Arguments - `step::Int`: Simulation Step - `model::OperationModel`: operation model - `start_time::Dates.DateTime`: Initial Time of the simulation step in Simulation time. - `store::SimulationStore`: Simulation output store # Accepted Key Words - `exports`: realtime export of output. Use wisely, it can have negative impacts in the simulation times """ function solve!( step::Int, model::DecisionModel{<:DecisionProblem}, start_time::Dates.DateTime, store::SimulationStore; exports = nothing, ) # Note, we don't call solve!(decision_model) here because the solve call includes a lot of # 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.SUCCESSFULLY_FINALIZED write_results!(store, model, start_time, start_time; exports = exports) write_optimizer_stats!(store, model, start_time) advance_execution_count!(model) end return get_run_status(model) end function handle_initial_conditions!(model::DecisionModel{<:DecisionProblem}) TimerOutputs.@timeit BUILD_PROBLEMS_TIMER "Model Initialization" begin if isempty(get_template(model)) return end settings = get_settings(model) initialize_model = get_initialize_model(settings) deserialize_initial_conditions = get_deserialize_initial_conditions(settings) serialized_initial_conditions_file = get_initial_conditions_file(model) custom_init_file = get_initialization_file(settings) if !initialize_model && deserialize_initial_conditions throw( IS.ConflictingInputsError( "!initialize_model && deserialize_initial_conditions", ), ) elseif !initialize_model && !isempty(custom_init_file) throw(IS.ConflictingInputsError("!initialize_model && initialization_file")) end if !initialize_model @info "Skip build of initial conditions" return end if !isempty(custom_init_file) if !isfile(custom_init_file) error("initialization_file = $custom_init_file does not exist") end if abspath(custom_init_file) != abspath(serialized_initial_conditions_file) cp(custom_init_file, serialized_initial_conditions_file; force = true) end end if deserialize_initial_conditions && isfile(serialized_initial_conditions_file) set_initial_conditions_data!( get_optimization_container(model), Serialization.deserialize(serialized_initial_conditions_file), ) @info "Deserialized initial_conditions_data" else @info "Make Initial Conditions Model" build_initial_conditions!(model) initialize!(model) end ISOPT.set_initial_conditions_model_container!( get_internal(model), nothing, ) end return end function _make_device_cache( filter_function::Function, devices::IS.FlattenIteratorWrapper{T}, check_components::Bool, sys::PSY.System, ) where {T <: PSY.Device} device_cache = sizehint!(Vector{T}(), length(devices)) for device in devices if PSY.get_available(device) && filter_function(device) check_components && PSY.check_component(sys, device) push!(device_cache, device) end end return device_cache end function _make_device_cache( ::Nothing, devices::IS.FlattenIteratorWrapper{T}, check_components::Bool, sys::PSY.System, ) where {T <: PSY.Device} device_cache = sizehint!(Vector{T}(), length(devices)) for device in devices if PSY.get_available(device) check_components && PSY.check_component(sys, device) push!(device_cache, device) end end return device_cache end function make_device_cache!( model::DeviceModel{T, <:AbstractDeviceFormulation}, system::PSY.System, check_components::Bool, ) where {T <: PSY.Device} subsystem = get_subsystem(model) !PSY.has_components(system, T) && return false devices = PSY.get_components(T, system; subsystem_name = subsystem) filt_func = get_attribute(model, "filter_function") model.device_cache = _make_device_cache(filt_func, devices, check_components, system) return end ================================================ FILE: src/operation/decision_model_store.jl ================================================ """ Stores results data for one DecisionModel """ mutable struct DecisionModelStore <: ISOPT.AbstractModelStore # All DenseAxisArrays have axes (column names, row indexes) duals::Dict{ConstraintKey, OrderedDict{Dates.DateTime, DenseAxisArray{Float64}}} parameters::Dict{ParameterKey, OrderedDict{Dates.DateTime, DenseAxisArray{Float64}}} variables::Dict{VariableKey, OrderedDict{Dates.DateTime, DenseAxisArray{Float64}}} aux_variables::Dict{AuxVarKey, OrderedDict{Dates.DateTime, DenseAxisArray{Float64}}} expressions::Dict{ExpressionKey, OrderedDict{Dates.DateTime, DenseAxisArray{Float64}}} optimizer_stats::OrderedDict{Dates.DateTime, OptimizerStats} end function DecisionModelStore() return DecisionModelStore( Dict{ConstraintKey, OrderedDict{Dates.DateTime, DenseAxisArray{Float64}}}(), Dict{ParameterKey, OrderedDict{Dates.DateTime, DenseAxisArray{Float64}}}(), Dict{VariableKey, OrderedDict{Dates.DateTime, DenseAxisArray{Float64}}}(), Dict{AuxVarKey, OrderedDict{Dates.DateTime, DenseAxisArray{Float64}}}(), Dict{ExpressionKey, OrderedDict{Dates.DateTime, DenseAxisArray{Float64}}}(), OrderedDict{Dates.DateTime, OptimizerStats}(), ) end function initialize_storage!( store::DecisionModelStore, container::ISOPT.AbstractOptimizationContainer, params::ModelStoreParams, ) num_of_executions = get_num_executions(params) if length(get_time_steps(container)) < 1 error("The time step count in the optimization container is not defined") end time_steps_count = get_time_steps(container)[end] initial_time = get_initial_time(container) model_interval = get_interval(params) for type in STORE_CONTAINERS field_containers = getfield(container, type) results_container = getfield(store, type) for (key, field_container) in field_containers !should_write_resulting_value(key) && continue @debug "Adding $(encode_key_as_string(key)) to DecisionModelStore" _group = LOG_GROUP_MODEL_STORE column_names = get_column_names(container, type, field_container, key) data = OrderedDict{ Dates.DateTime, DenseAxisArray{Float64, length(column_names) + 1}, }() for timestamp in range(initial_time; step = model_interval, length = num_of_executions) data[timestamp] = fill!( DenseAxisArray{Float64}(undef, column_names..., 1:time_steps_count), NaN, ) end results_container[key] = data end end return end function write_result!( store::DecisionModelStore, name::Symbol, key::OptimizationContainerKey, index::DecisionModelIndexType, update_timestamp::Dates.DateTime, array::DenseAxisArray{T, 2, <:Tuple{Vector{String}, UnitRange}}, ) where {T} container = getfield(store, get_store_container_type(key)) container[key][index] = array return end function write_result!( store::DecisionModelStore, name::Symbol, key::OptimizationContainerKey, index::DecisionModelIndexType, update_timestamp::Dates.DateTime, array::DenseAxisArray{T, 2, <:Tuple{Vector{Int}, UnitRange}}, ) where {T} columns = get_column_names_from_axis_array(array) container = getfield(store, get_store_container_type(key)) container[key][index] = DenseAxisArray(array.data, columns..., 1:size(array, 2)) return end function write_result!( store::DecisionModelStore, name::Symbol, key::OptimizationContainerKey, index::DecisionModelIndexType, update_timestamp::Dates.DateTime, array::DenseAxisArray{T, 1, <:Tuple{Vector{String}}}, ) where {T} container = getfield(store, get_store_container_type(key)) container[key][index] = array return end function write_result!( store::DecisionModelStore, name::Symbol, key::OptimizationContainerKey, index::DecisionModelIndexType, update_timestamp::Dates.DateTime, array::DenseAxisArray{T, 1, <:Tuple{UnitRange}}, ) where {T} container = getfield(store, get_store_container_type(key)) container[key][index] = array return end function write_result!( store::DecisionModelStore, name::Symbol, key::OptimizationContainerKey, index::DecisionModelIndexType, update_timestamp::Dates.DateTime, array::DenseAxisArray{T, 3, <:Tuple{Vector{String}, Vector{String}, UnitRange{Int}}}, ) where {T} container = getfield(store, get_store_container_type(key)) container[key][index] = array return end function read_results( store::DecisionModelStore, key::OptimizationContainerKey; index::Union{DecisionModelIndexType, Nothing} = nothing, ) container = getfield(store, get_store_container_type(key)) data = container[key] if isnothing(index) @assert_op length(data) == 1 index = first(keys(data)) end # Return a copy because callers may mutate it. return deepcopy(data[index]) end function write_optimizer_stats!( store::DecisionModelStore, stats::OptimizerStats, index::DecisionModelIndexType, ) # TODO: This check is incompatible with test calls to psi_checksolve_test # Overwriting should not be allowed in normal operation. # if index in keys(store.optimizer_stats) # error("Bug: Overwriting optimizer stats for index = $index") # end store.optimizer_stats[index] = stats return end function read_optimizer_stats(store::DecisionModelStore) 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 end function get_column_names(store::DecisionModelStore, key::OptimizationContainerKey) container = getfield(store, get_store_container_type(key)) return get_column_names_from_axis_array(key, first(values(container[key]))) end ================================================ FILE: src/operation/emulation_model.jl ================================================ """ EmulationModel{M}( template::AbstractProblemTemplate, sys::PSY.System, jump_model::Union{Nothing, JuMP.Model}=nothing; kwargs...) where {M<:EmulationProblem} Build the optimization problem of type M with the specific system and template. # Arguments - `::Type{M} where M<:EmulationProblem`: The abstract Emulation model type - `template::AbstractProblemTemplate`: The model reference made up of transmission, devices, branches, and services. - `sys::PSY.System`: the system created using Power Systems - `jump_model::Union{Nothing, JuMP.Model}`: Enables passing a custom JuMP model. Use with care - `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. - `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 - `initialize_model::Bool = true`: Option to decide to initialize the model or not. - `initialization_file::String = ""`: This allows to pass pre-existing initialization values to avoid the solution of an optimization problem to find feasible initial conditions. - `deserialize_initial_conditions::Bool = false`: Option to deserialize conditions - `export_pwl_vars::Bool = false`: True to export all the pwl intermediate variables. It can slow down significantly the build and solve time. - `allow_fails::Bool = false`: True to allow the simulation to continue even if the optimization step fails. Use with care. - `calculate_conflict::Bool = false`: True to use solver to calculate conflicts for infeasible problems. Only specific solvers are able to calculate conflicts. - `optimizer_solve_log_print::Bool = false`: Uses JuMP.unset_silent() to print the optimizer's log. By default all solvers are set to MOI.Silent() - `detailed_optimizer_stats::Bool = false`: True to save detailed optimizer stats log. - `direct_mode_optimizer::Bool = false`: True to use the solver in direct mode. Creates a [JuMP.direct_model](https://jump.dev/JuMP.jl/dev/reference/models/#JuMP.direct_model). - `store_variable_names::Bool = false`: True to store variable names in optimization model. - `rebuild_model::Bool = false`: It will force the rebuild of the underlying JuMP model with each call to update the model. It increases solution times, use only if the model can't be updated in memory. - `initial_time::Dates.DateTime = UNSET_INI_TIME`: Initial Time for the model solve. - `time_series_cache_size::Int = IS.TIME_SERIES_CACHE_SIZE_BYTES`: Size in bytes to cache for each time array. Default is 1 MiB. Set to 0 to disable. # Example ```julia template = ProblemTemplate(CopperPlatePowerModel, devices, branches, services) OpModel = EmulationModel(MockEmulationProblem, template, system) ``` """ mutable struct EmulationModel{M <: EmulationProblem} <: OperationModel name::Symbol template::AbstractProblemTemplate sys::PSY.System internal::ISOPT.ModelInternal simulation_info::SimulationInfo store::EmulationModelStore # might be extended to other stores for simulation ext::Dict{String, Any} function EmulationModel{M}( template::AbstractProblemTemplate, sys::PSY.System, settings::Settings, jump_model::Union{Nothing, JuMP.Model} = nothing; name = nothing, ) where {M <: EmulationProblem} if name === nothing name = nameof(M) elseif name isa String name = Symbol(name) end finalize_template!(template, sys) internal = ISOPT.ModelInternal( OptimizationContainer(sys, settings, jump_model, PSY.SingleTimeSeries), ) new{M}( name, template, sys, internal, SimulationInfo(), EmulationModelStore(), Dict{String, Any}(), ) end end 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, initialize_model = true, initialization_file = "", deserialize_initial_conditions = false, export_pwl_vars = false, allow_fails = false, calculate_conflict = false, optimizer_solve_log_print = false, detailed_optimizer_stats = false, direct_mode_optimizer = false, check_numerical_bounds = true, store_variable_names = false, rebuild_model = false, initial_time = UNSET_INI_TIME, time_series_cache_size::Int = IS.TIME_SERIES_CACHE_SIZE_BYTES, ) where {M <: EmulationProblem} settings = Settings( sys; initial_time = initial_time, optimizer = optimizer, time_series_cache_size = time_series_cache_size, warm_start = warm_start, initialize_model = initialize_model, initialization_file = initialization_file, deserialize_initial_conditions = deserialize_initial_conditions, export_pwl_vars = export_pwl_vars, allow_fails = allow_fails, calculate_conflict = calculate_conflict, optimizer_solve_log_print = optimizer_solve_log_print, detailed_optimizer_stats = detailed_optimizer_stats, direct_mode_optimizer = direct_mode_optimizer, check_numerical_bounds = check_numerical_bounds, store_variable_names = store_variable_names, rebuild_model = rebuild_model, horizon = resolution, resolution = resolution, ) model = EmulationModel{M}(template, sys, settings, jump_model; name = name) validate_time_series!(model) return model end """ Build the optimization problem of type M with the specific system and template # Arguments - `::Type{M} where M<:EmulationProblem`: The abstract Emulation model type - `template::AbstractProblemTemplate`: The model reference made up of transmission, devices, branches, and services. - `sys::PSY.System`: the system created using Power Systems - `jump_model::Union{Nothing, JuMP.Model}`: Enables passing a custom JuMP model. Use with care # Example ```julia template = ProblemTemplate(CopperPlatePowerModel, devices, branches, services) problem = EmulationModel(MyEmProblemType, template, system, optimizer) ``` """ function EmulationModel( ::Type{M}, template::AbstractProblemTemplate, sys::PSY.System, jump_model::Union{Nothing, JuMP.Model} = nothing; kwargs..., ) where {M <: EmulationProblem} return EmulationModel{M}(template, sys, jump_model; kwargs...) end function EmulationModel( template::AbstractProblemTemplate, sys::PSY.System, jump_model::Union{Nothing, JuMP.Model} = nothing; kwargs..., ) return EmulationModel{GenericEmulationProblem}(template, sys, jump_model; kwargs...) end """ Builds an empty emulation model. This constructor is used for the implementation of custom emulation models that do not require a template. # Arguments - `::Type{M} where M<:EmulationProblem`: The abstract operation model type - `sys::PSY.System`: the system created using Power Systems - `jump_model::Union{Nothing, JuMP.Model}` = nothing: Enables passing a custom JuMP model. Use with care. # Example ```julia problem = EmulationModel(system, optimizer) ``` """ function EmulationModel{M}( sys::PSY.System, jump_model::Union{Nothing, JuMP.Model} = nothing; kwargs..., ) where {M <: EmulationProblem} return EmulationModel{M}(template, sys, jump_model; kwargs...) end get_problem_type(::EmulationModel{M}) where {M <: EmulationProblem} = M function validate_template(::EmulationModel{M}) where {M <: EmulationProblem} error("validate_template is not implemented for EmulationModel{$M}") end function validate_template(model::EmulationModel{<:DefaultEmulationProblem}) validate_template_impl!(model) return end 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_execution_count(model) initial_time = get_initial_time(model) 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) settings = get_settings(model) horizon = interval = resolution = get_resolution(settings) base_power = PSY.get_base_power(system) sys_uuid = IS.get_uuid(system) ISOPT.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 build_pre_step!(model::EmulationModel) TimerOutputs.@timeit BUILD_PROBLEMS_TIMER "Build pre-step" begin validate_template(model) if !isempty(model) @info "EmulationProblem status not ModelBuildStatus.EMPTY. Resetting" reset!(model) end container = get_optimization_container(model) container.built_for_recurrent_solves = true @info "Initializing Optimization Container For an EmulationModel" init_optimization_container!( get_optimization_container(model), get_network_model(get_template(model)), get_system(model), ) @info "Initializing ModelStoreParams" init_model_store_params!(model) set_status!(model, ModelBuildStatus.IN_PROGRESS) end return end function build_impl!(model::EmulationModel{<:EmulationProblem}) build_pre_step!(model) @info "Instantiating Network Model" instantiate_network_model!(model) handle_initial_conditions!(model) build_model!(model) serialize_metadata!(get_optimization_container(model), get_output_dir(model)) log_values(get_settings(model)) return end """ Implementation of build for any EmulationProblem """ function build!( model::EmulationModel{<:EmulationProblem}; executions = 1, output_dir::String, recorders = [], console_level = Logging.Error, file_level = Logging.Info, disable_timer_outputs = false, ) mkpath(output_dir) set_output_dir!(model, output_dir) set_console_level!(model, console_level) set_file_level!(model, file_level) TimerOutputs.reset_timer!(BUILD_PROBLEMS_TIMER) disable_timer_outputs && TimerOutputs.disable_timer!(BUILD_PROBLEMS_TIMER) file_mode = "w" add_recorders!(model, recorders) register_recorders!(model, file_mode) logger = ISOPT.configure_logging( get_internal(model), PROBLEM_LOG_FILENAME, file_mode, ) try Logging.with_logger(logger) do try set_executions!(model, executions) TimerOutputs.@timeit BUILD_PROBLEMS_TIMER "Problem $(get_name(model))" begin build_impl!(model) end set_status!(model, ModelBuildStatus.BUILT) @info "\n$(BUILD_PROBLEMS_TIMER)\n" catch e set_status!(model, ModelBuildStatus.FAILED) bt = catch_backtrace() @error "EmulationModel Build Failed" exception = e, bt end end finally unregister_recorders!(model) close(logger) end return get_status(model) end """ Default implementation of build method for Emulation Problems for models conforming with DecisionProblem specification. Overload this function to implement a custom build method """ function build_model!(model::EmulationModel{<:EmulationProblem}) container = get_optimization_container(model) system = get_system(model) build_impl!(container, get_template(model), system) return end function reset!(model::EmulationModel{<:EmulationProblem}) if built_for_recurrent_solves(model) set_execution_count!(model, 0) end ISOPT.set_container!( get_internal(model), OptimizationContainer( get_system(model), get_settings(model), nothing, PSY.SingleTimeSeries, ), ) ISOPT.set_initial_conditions_model_container!(get_internal(model), nothing) empty_time_series_cache!(model) empty!(get_store(model)) set_status!(model, ModelBuildStatus.EMPTY) return end function update_parameters!(model::EmulationModel, store::EmulationModelStore) update_parameters!(model, store.data_container) return end function update_parameters!(model::EmulationModel, data::DatasetContainer{InMemoryDataset}) cost_function_unsynch(get_optimization_container(model)) for key in keys(get_parameters(model)) update_parameter_values!(model, key, data) end 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) end return end function update_initial_conditions!( model::EmulationModel, source::EmulationModelStore, ::InterProblemChronology, ) for key in keys(get_initial_conditions(model)) update_initial_conditions!(model, key, source) end return end function update_model!( model::EmulationModel, source::EmulationModelStore, ini_cond_chronology, ) TimerOutputs.@timeit RUN_SIMULATION_TIMER "Parameter Updates" begin update_parameters!(model, source) end TimerOutputs.@timeit RUN_SIMULATION_TIMER "Ini Cond Updates" begin update_initial_conditions!(model, source, ini_cond_chronology) end return end """ Update parameter function an OperationModel """ function update_parameter_values!( model::EmulationModel, 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) update_container_parameter_values!(optimization_container, model, key, input) parameter_attributes = get_parameter_attributes(optimization_container, key) IS.@record :execution ParameterUpdateEvent( T, U, "event", # parameter_attributes, get_current_timestamp(model), get_name(model), ) #end return end function update_model!(model::EmulationModel) update_model!(model, get_store(model), InterProblemChronology()) return end function run_impl!( model::EmulationModel; optimizer = nothing, enable_progress_bar = progress_meter_enabled(), kwargs..., ) _pre_solve_model_checks(model, optimizer) internal = get_internal(model) executions = ISOPT.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(executions; enabled = enable_progress_bar) initial_time = get_initial_time(model) 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) write_results!(get_store(model), model, execution, current_time) write_optimizer_stats!(get_store(model), get_optimizer_stats(model), execution) advance_execution_count!(model) update_model!(model) ProgressMeter.update!( prog_bar, get_execution_count(model); showvalues = [(:Execution, execution)], ) end end return end """ Default run method for problems that conform to the requirements of EmulationModel{<: EmulationProblem} This will call `build!` on the model if it is not already built. It will forward all keyword arguments to that function. # Arguments - `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 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 - `export_optimization_model::Bool`: If true, serialize the model to a file to allow re-execution later. # Examples ```julia status = run!(model; optimizer = HiGHS.Optimizer, executions = 10) status = run!(model; output_dir = ./model_output, optimizer = HiGHS.Optimizer, executions = 10) ``` """ function run!( model::EmulationModel{<:EmulationProblem}; export_problem_results = false, console_level = Logging.Error, file_level = Logging.Info, disable_timer_outputs = false, export_optimization_model = true, kwargs..., ) build_if_not_already_built!( model; console_level = console_level, file_level = file_level, disable_timer_outputs = disable_timer_outputs, kwargs..., ) set_console_level!(model, console_level) set_file_level!(model, file_level) TimerOutputs.reset_timer!(RUN_OPERATION_MODEL_TIMER) disable_timer_outputs && TimerOutputs.disable_timer!(RUN_OPERATION_MODEL_TIMER) file_mode = "a" register_recorders!(model, file_mode) logger = ISOPT.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), get_store_params(model), ) TimerOutputs.@timeit RUN_OPERATION_MODEL_TIMER "Run" begin run_impl!(model; kwargs...) set_run_status!(model, RunStatus.SUCCESSFULLY_FINALIZED) end if export_optimization_model TimerOutputs.@timeit RUN_OPERATION_MODEL_TIMER "Serialize" begin serialize_optimization_model(model) end end TimerOutputs.@timeit RUN_OPERATION_MODEL_TIMER "Results processing" begin results = OptimizationProblemResults(model) serialize_results(results, get_output_dir(model)) export_problem_results && export_results(results) end @info "\n$(RUN_OPERATION_MODEL_TIMER)\n" catch e @error "Emulation Problem Run failed" exception = (e, catch_backtrace()) set_run_status!(model, RunStatus.FAILED) end end finally unregister_recorders!(model) close(logger) end return get_run_status(model) end """ Default solve method for an EmulationModel used inside of a Simulation. Solves problems that conform to the requirements of DecisionModel{<: DecisionProblem} # Arguments - `step::Int`: Simulation Step - `model::OperationModel`: operation model - `start_time::Dates.DateTime`: Initial Time of the simulation step in Simulation time. - `store::SimulationStore`: Simulation output store - `exports = nothing`: realtime export of output. Use wisely, it can have negative impacts in the simulation times """ function solve!( step::Int, model::EmulationModel{<:EmulationProblem}, start_time::Dates.DateTime, store::SimulationStore; exports = nothing, ) # Note, we don't call solve!(decision_model) here because the solve call includes a lot of # 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.SUCCESSFULLY_FINALIZED advance_execution_count!(model) write_results!( store, model, get_execution_count(model), start_time; exports = exports, ) write_optimizer_stats!(store, model, get_execution_count(model)) end return get_run_status(model) end function handle_initial_conditions!(model::EmulationModel{<:EmulationProblem}) # This code is a duplicate of DecisionModel initial conditions handling. # It should be refactored to better handle AGC emulator initial conditions TimerOutputs.@timeit BUILD_PROBLEMS_TIMER "Model Initialization" begin if isempty(get_template(model)) return end settings = get_settings(model) initialize_model = get_initialize_model(settings) deserialize_initial_conditions = get_deserialize_initial_conditions(settings) serialized_initial_conditions_file = get_initial_conditions_file(model) custom_init_file = get_initialization_file(settings) if !initialize_model && deserialize_initial_conditions throw( IS.ConflictingInputsError( "!initialize_model && deserialize_initial_conditions", ), ) elseif !initialize_model && !isempty(custom_init_file) throw(IS.ConflictingInputsError("!initialize_model && initialization_file")) end if !initialize_model @info "Skip build of initial conditions" return end if !isempty(custom_init_file) if !isfile(custom_init_file) error("initialization_file = $custom_init_file does not exist") end if abspath(custom_init_file) != abspath(serialized_initial_conditions_file) cp(custom_init_file, serialized_initial_conditions_file; force = true) end end if deserialize_initial_conditions && isfile(serialized_initial_conditions_file) set_initial_conditions_data!( get_optimization_container(model), Serialization.deserialize(serialized_initial_conditions_file), ) @info "Deserialized initial_conditions_data" else @info "Make Initial Conditions Model" build_initial_conditions!(model) initialize!(model) end ISOPT.set_initial_conditions_model_container!( get_internal(model), nothing, ) end return end ================================================ FILE: src/operation/emulation_model_store.jl ================================================ """ Stores results data for one EmulationModel """ mutable struct EmulationModelStore <: ISOPT.AbstractModelStore data_container::DatasetContainer{InMemoryDataset} optimizer_stats::OrderedDict{Int, OptimizerStats} end get_data_field(store::EmulationModelStore, type::Symbol) = getfield(store.data_container, type) function EmulationModelStore() return EmulationModelStore( DatasetContainer{InMemoryDataset}(), OrderedDict{Int, OptimizerStats}(), ) end """ Base.empty!(store::EmulationModelStore) Empty the [`EmulationModelStore`](@ref) """ function Base.empty!(store::EmulationModelStore) stype = DatasetContainer for (name, _) in zip(fieldnames(stype), fieldtypes(stype)) if name ∉ [:values, :timestamps] val = get_data_field(store, name) try empty!(val) catch @error "Base.empty! must be customized for type $stype or skipped" rethrow() end elseif name == :update_timestamp store.update_timestamp = UNSET_INI_TIME else setfield!( store.data_container, name, zero(fieldtype(store.data_container, name)), ) end end empty!(store.optimizer_stats) return end function Base.isempty(store::EmulationModelStore) stype = DatasetContainer for (name, type) in zip(fieldnames(stype), fieldtypes(stype)) if name ∉ [:values, :timestamps] 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 elseif name == :update_timestamp store.update_timestamp != UNSET_INI_TIME && return false else val = get_data_field(store, name) iszero(val) && return false end end return isempty(store.optimizer_stats) end function initialize_storage!( store::EmulationModelStore, container::OptimizationContainer, params::ModelStoreParams, ) num_of_executions = get_num_executions(params) for type in STORE_CONTAINERS field_containers = getfield(container, type) results_container = get_data_field(store, type) for (key, field_container) in field_containers @debug "Adding $(encode_key_as_string(key)) to EmulationModelStore" _group = LOG_GROUP_MODEL_STORE column_names = get_column_names(container, type, field_container, key) results_container[key] = InMemoryDataset( fill!( DenseAxisArray{Float64}(undef, column_names..., 1:num_of_executions), NaN, ), ) end end return end function write_result!( store::EmulationModelStore, name::Symbol, key::OptimizationContainerKey, index::EmulationModelIndexType, update_timestamp::Dates.DateTime, array::DenseAxisArray{Float64, 2}, ) if size(array, 2) == 1 write_result!(store, name, key, index, update_timestamp, array[:, 1]) else container = get_data_field(store, get_store_container_type(key)) set_value!( container[key], array, index, ) set_last_recorded_row!(container[key], index) set_update_timestamp!(container[key], update_timestamp) end return end function write_result!( store::EmulationModelStore, ::Symbol, key::OptimizationContainerKey, index::EmulationModelIndexType, update_timestamp::Dates.DateTime, array::DenseAxisArray{Float64, 1}, ) container = get_data_field(store, get_store_container_type(key)) set_value!( container[key], array, index, ) set_last_recorded_row!(container[key], index) set_update_timestamp!(container[key], update_timestamp) return end function write_result!( store::EmulationModelStore, name::Symbol, key::OptimizationContainerKey, index::EmulationModelIndexType, update_timestamp::Dates.DateTime, array::DenseAxisArray{Float64, 3}, ) # Handle 3D arrays by reducing to 2D when the last dimension is 1. # This mirrors the 2D case above where size(array, 2) == 1 triggers a dimension reduction. if size(array, 3) == 1 write_result!(store, name, key, index, update_timestamp, array[:, :, 1]) else container = get_data_field(store, get_store_container_type(key)) set_value!( container[key], array, index, ) set_last_recorded_row!(container[key], index) set_update_timestamp!(container[key], update_timestamp) end return end function read_results( store::EmulationModelStore, key::OptimizationContainerKey; index::Union{Int, Nothing} = nothing, len::Union{Int, Nothing} = nothing, ) container = get_data_field(store, get_store_container_type(key)) data = container[key].values num_dims = ndims(data) # Return a copy because callers may mutate it. if num_dims == 2 if isnothing(index) @assert_op len === nothing return data[:, :] elseif isnothing(len) return data[:, index:end] else return data[:, index:(index + len - 1)] end elseif num_dims == 3 if isnothing(index) @assert_op len === nothing return data[:, :, :] elseif isnothing(len) return data[:, :, index:end] else return data[:, :, index:(index + len - 1)] end else error("Unsupported number of dimensions for emulation dataset: $num_dims") end end function get_column_names(store::EmulationModelStore, key::OptimizationContainerKey) container = get_data_field(store, get_store_container_type(key)) return get_column_names_from_axis_array(key, container[key].values) end function get_dataset_size(store::EmulationModelStore, key::OptimizationContainerKey) container = get_data_field(store, get_store_container_type(key)) return size(container[key].values) end function get_last_updated_timestamp( store::EmulationModelStore, key::OptimizationContainerKey, ) container = get_data_field(store, get_store_container_type(key)) return get_update_timestamp(container[key]) end function write_optimizer_stats!( store::EmulationModelStore, stats::OptimizerStats, index::EmulationModelIndexType, ) @assert !(index in keys(store.optimizer_stats)) store.optimizer_stats[index] = stats return end function read_optimizer_stats(store::EmulationModelStore) return DataFrames.DataFrame([ IS.to_namedtuple(x) for x in values(store.optimizer_stats) ]) end function get_last_recorded_row(x::EmulationModelStore, key::OptimizationContainerKey) return get_last_recorded_row(x.data_container, key) end ================================================ FILE: src/operation/initial_conditions_update_in_memory_store.jl ================================================ ################## ic updates from store for emulation problems simulation ################# function update_initial_conditions!( ics::T, store::EmulationModelStore, ::Dates.Millisecond, ) where { T <: Union{ Vector{ Union{ InitialCondition{InitialTimeDurationOn, Nothing}, InitialCondition{InitialTimeDurationOn, Float64}, }, }, Vector{ Union{ InitialCondition{InitialTimeDurationOn, Nothing}, InitialCondition{InitialTimeDurationOn, JuMP.VariableRef}, }, }, }, } for ic in ics 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 end function update_initial_conditions!( ics::T, store::EmulationModelStore, ::Dates.Millisecond, ) where { T <: Union{ Vector{ Union{ InitialCondition{InitialTimeDurationOff, Nothing}, InitialCondition{InitialTimeDurationOff, Float64}, }, }, Vector{ Union{ InitialCondition{InitialTimeDurationOff, Nothing}, InitialCondition{InitialTimeDurationOff, JuMP.VariableRef}, }, }, }, } for ic in ics 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 end function update_initial_conditions!( ics::T, store::EmulationModelStore, ::Dates.Millisecond, ) where { T <: Union{ Vector{ Union{ InitialCondition{DevicePower, Nothing}, InitialCondition{DevicePower, Float64}, }, }, Vector{ Union{ InitialCondition{DevicePower, Nothing}, InitialCondition{DevicePower, JuMP.VariableRef}, }, }, }, } for ic in ics 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 end function update_initial_conditions!( ics::T, store::EmulationModelStore, ::Dates.Millisecond, ) where { T <: Union{ Vector{ Union{ InitialCondition{DeviceStatus, Nothing}, InitialCondition{DeviceStatus, Float64}, }, }, Vector{ Union{ InitialCondition{DeviceStatus, Nothing}, InitialCondition{DeviceStatus, JuMP.VariableRef}, }, }, }, } for ic in ics 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 end function update_initial_conditions!( ics::T, store::EmulationModelStore, ::Dates.Millisecond, ) where { T <: Union{ Vector{ Union{ InitialCondition{DeviceAboveMinPower, Nothing}, InitialCondition{DeviceAboveMinPower, Float64}, }, }, Vector{ Union{ InitialCondition{DeviceAboveMinPower, Nothing}, InitialCondition{DeviceAboveMinPower, JuMP.VariableRef}, }, }, }, } for ic in ics var_val = get_value(store, PowerAboveMinimumVariable(), get_component_type(ic)) set_ic_quantity!(ic, get_last_recorded_value(var_val)[get_component_name(ic)]) end return end #= Unused without the AGC model enabled function update_initial_conditions!( ics::Vector{T}, store::EmulationModelStore, ::Dates.Millisecond, ) where { T <: InitialCondition{AreaControlError, S}, } where {S <: Union{Float64, JuMP.VariableRef}} for ic in ics 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 end =# function update_initial_conditions!( ics::T, store::EmulationModelStore, ::Dates.Millisecond, ) where { T <: Union{ Vector{ Union{ InitialCondition{InitialEnergyLevel, Nothing}, InitialCondition{InitialEnergyLevel, Float64}, }, }, Vector{ Union{ InitialCondition{InitialEnergyLevel, Nothing}, InitialCondition{InitialEnergyLevel, JuMP.VariableRef}, }, }, }, } for ic in ics 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 end ================================================ FILE: src/operation/model_numerical_analysis_utils.jl ================================================ # The Numerical stability checks code in this file is based on the code from the SDDP.jl package, # from the below mentioned commit and file. # commit :8cd305188caffc50a1734913053fc81bba613778 # link to file :https://github.com/odow/SDDP.jl/blob/d353fe5a2903421e7fed6d609eb9377c35d715a1/src/print.jl#L190 mutable struct NumericalBounds min::Float64 max::Float64 min_index::Any max_index::Any end NumericalBounds() = NumericalBounds(Inf, -Inf, nothing, nothing) set_min!(v::NumericalBounds, value::Real) = v.min = value set_max!(v::NumericalBounds, value::Real) = v.max = value set_min_index!(v::NumericalBounds, idx) = v.min_index = idx set_max_index!(v::NumericalBounds, idx) = v.max_index = idx mutable struct ConstraintBounds coefficient::NumericalBounds rhs::NumericalBounds function ConstraintBounds() return new(NumericalBounds(), NumericalBounds()) end end function update_coefficient_bounds( v::ConstraintBounds, constraint::JuMP.ScalarConstraint, idx, ) update_numerical_bounds(v.coefficient, constraint.func, idx) return end function update_rhs_bounds(v::ConstraintBounds, constraint::JuMP.ScalarConstraint, idx) update_numerical_bounds(v.rhs, constraint.set, idx) return end mutable struct VariableBounds bounds::NumericalBounds function VariableBounds() return new(NumericalBounds()) end end function update_variable_bounds(v::VariableBounds, variable::JuMP.VariableRef, idx) if JuMP.is_binary(variable) set_min!(v.bounds, 0.0) update_numerical_bounds(v.bounds, 1.0, idx) else if JuMP.has_lower_bound(variable) update_numerical_bounds(v.bounds, JuMP.lower_bound(variable), idx) end if JuMP.has_upper_bound(variable) update_numerical_bounds(v.bounds, JuMP.upper_bound(variable), idx) end end return end function update_numerical_bounds(v::NumericalBounds, value::Real, idx) if !isapprox(value, 0.0) if v.min > abs(value) set_min!(v, value) set_min_index!(v, idx) elseif v.max < abs(value) set_max!(v, value) set_max_index!(v, idx) end end return end function update_numerical_bounds(bonuds::NumericalBounds, func::JuMP.GenericAffExpr, idx) for coefficient in values(func.terms) update_numerical_bounds(bonuds, coefficient, idx) end return end function update_numerical_bounds(bonuds::NumericalBounds, func::MOI.LessThan, idx) return update_numerical_bounds(bonuds, func.upper, idx) end function update_numerical_bounds(bonuds::NumericalBounds, func::MOI.GreaterThan, idx) return update_numerical_bounds(bonuds, func.lower, idx) end function update_numerical_bounds(bonuds::NumericalBounds, func::MOI.EqualTo, idx) return update_numerical_bounds(bonuds, func.value, idx) end function update_numerical_bounds(bonuds::NumericalBounds, func::MOI.Interval, idx) update_numerical_bounds(bonuds, func.upper, idx) return update_numerical_bounds(bonuds, func.lower, idx) end # Default fallback for unsupported constraints. update_numerical_bounds(::NumericalBounds, func, idx) = nothing function get_constraint_numerical_bounds(model::OperationModel) if !is_built(model) error("Model not built, can't calculate constraint numerical bounds") end bounds = ConstraintBounds() for (const_key, constraint_array) in get_constraints(get_optimization_container(model)) # TODO: handle this at compile and not at run time if isa(constraint_array, SparseAxisArray) for idx in eachindex(constraint_array) constraint_array[idx] == 0.0 && continue con_obj = JuMP.constraint_object(constraint_array[idx]) update_coefficient_bounds(bounds, con_obj, (const_key, idx)) update_rhs_bounds(bounds, con_obj, (const_key, idx)) end else for idx in Iterators.product(constraint_array.axes...) !isassigned(constraint_array, idx...) && continue con_obj = JuMP.constraint_object(constraint_array[idx...]) update_coefficient_bounds(bounds, con_obj, (const_key, idx)) update_rhs_bounds(bounds, con_obj, (const_key, idx)) end end end return bounds end function get_variable_numerical_bounds(model::OperationModel) if !is_built(model) error("Model not built, can't calculate variable numerical bounds") end bounds = VariableBounds() for (variable_key, variable_array) in get_variables(get_optimization_container(model)) if isa(variable_array, SparseAxisArray) for idx in eachindex(variable_array) var = variable_array[idx] var == 0.0 && continue update_variable_bounds(bounds, var, (variable_key, idx)) end else for idx in Iterators.product(variable_array.axes...) var = variable_array[idx...] update_variable_bounds(bounds, var, (variable_key, idx)) end end end return bounds end ================================================ FILE: src/operation/operation_model_interface.jl ================================================ # Default implementations of getter/setter functions for OperationModel. is_built(model::OperationModel) = ISOPT.get_status(get_internal(model)) == ModelBuildStatus.BUILT isempty(model::OperationModel) = ISOPT.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) = ISOPT.get_constraints(get_internal(model)) get_execution_count(model::OperationModel) = ISOPT.get_execution_count(get_internal(model)) get_executions(model::OperationModel) = ISOPT.get_executions(get_internal(model)) get_initial_time(model::OperationModel) = get_initial_time(get_settings(model)) get_internal(model::OperationModel) = model.internal function get_jump_model(model::OperationModel) return get_jump_model(ISOPT.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 = 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 function get_optimization_container(model::OperationModel) return ISOPT.get_optimization_container(get_internal(model)) end function get_resolution(model::OperationModel) 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) = # This deepcopy is important because the optimization container is overwritten # at each solve in a simulation. deepcopy(get_optimizer_stats(get_optimization_container(model))) 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) = ISOPT.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_store_params(model::OperationModel) = ISOPT.get_store_params(get_internal(model)) get_output_dir(model::OperationModel) = ISOPT.get_output_dir(get_internal(model)) get_initial_conditions_file(model::OperationModel) = 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) = 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) = ISOPT.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) # For EmulationModel interval and resolution are the same. return get_initial_time(model) + get_execution_count(model) * get_interval(model) end function get_timestamps(model::OperationModel) optimization_container = get_optimization_container(model) start_time = get_initial_time(optimization_container) resolution = get_resolution(model) 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...) write_data(get_optimization_container(model), output_dir; kwargs...) return end function get_initial_conditions( model::OperationModel, ::T, ::U, ) where {T <: InitialConditionType, U <: PSY.Device} return get_initial_conditions(get_optimization_container(model), T, U) end function solve_impl!(model::OperationModel) container = get_optimization_container(model) model_name = get_name(model) ts = get_current_timestamp(model) output_dir = get_output_dir(model) if get_export_optimization_model(get_settings(model)) model_output_dir = joinpath(output_dir, "optimization_model_exports") mkpath(model_output_dir) tss = replace("$(ts)", ":" => "_") model_export_path = joinpath(model_output_dir, "exported_$(model_name)_$(tss).json") serialize_optimization_model(container, model_export_path) write_lp_file( get_jump_model(container), replace(model_export_path, ".json" => ".lp"), ) end status = solve_impl!(container, get_system(model)) set_run_status!(model, status) if status != RunStatus.SUCCESSFULLY_FINALIZED settings = get_settings(model) 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) error("Solving model $(model_name) failed at $(ts)") else @error "Solving model $(model_name) failed at $(ts). Failure Allowed" end end return end set_console_level!(model::OperationModel, val) = ISOPT.set_console_level!(get_internal(model), val) set_file_level!(model::OperationModel, val) = ISOPT.set_file_level!(get_internal(model), val) function set_executions!(model::OperationModel, val::Int) ISOPT.set_executions!(get_internal(model), val) return end function set_execution_count!(model::OperationModel, val::Int) ISOPT.set_execution_count!(get_internal(model), val) return end set_initial_time!(model::OperationModel, val::Dates.DateTime) = set_initial_time!(get_settings(model), val) get_simulation_info(model::OperationModel, val) = model.simulation_info = val function set_status!(model::OperationModel, status::ModelBuildStatus) ISOPT.set_status!(get_internal(model), status) return end function set_output_dir!(model::OperationModel, path::AbstractString) ISOPT.set_output_dir!(get_internal(model), path) return end function advance_execution_count!(model::OperationModel) internal = get_internal(model) internal.execution_count += 1 return end function build_initial_conditions!(model::OperationModel) @assert ISOPT.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)()) if requires_init @debug "initial_conditions required for $device_type" _group = LOG_GROUP_BUILD_INITIAL_CONDITIONS build_initial_conditions_model!(model) break end end if !requires_init @info "No initial conditions in the model" end return end function write_initial_conditions_data!(model::OperationModel) write_initial_conditions_data!( get_optimization_container(model), ISOPT.get_initial_conditions_model_container(get_internal(model)), ) return end function initialize!(model::OperationModel) container = get_optimization_container(model) if ISOPT.get_initial_conditions_model_container(get_internal(model)) === nothing return end @info "Solving Initialization Model for $(get_name(model))" status = solve_impl!( ISOPT.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, ISOPT.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" return end # TODO: Document requirements for solve_impl # function solve_impl!(model::OperationModel) # end const _TEMPLATE_VALIDATION_EXCLUSIONS = [PSY.Arc, PSY.Area, PSY.ACBus, PSY.LoadZone] function build_if_not_already_built!(model::OperationModel; kwargs...) status = get_status(model) 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", ) else new_kwargs = Dict(k => v for (k, v) in kwargs if k != :optimizer) status = build!(model; new_kwargs...) end end if status != ModelBuildStatus.BUILT error("build! of the $(typeof(model)) $(get_name(model)) failed: $status") end return end function _check_numerical_bounds(model::OperationModel) variable_bounds = get_variable_numerical_bounds(model) if variable_bounds.bounds.max - variable_bounds.bounds.min > 1e9 @warn "Variable bounds range is $(variable_bounds.bounds.max - variable_bounds.bounds.min) and can result in numerical problems for the solver. \\ max_bound_variable = $(encode_key_as_string(variable_bounds.bounds.max_index)) \\ min_bound_variable = $(encode_key_as_string(variable_bounds.bounds.min_index)) \\ Run get_detailed_variable_numerical_bounds on the model for a deeper analysis" else @info "Variable bounds range is [$(variable_bounds.bounds.min) $(variable_bounds.bounds.max)]" end constraint_bounds = get_constraint_numerical_bounds(model) if constraint_bounds.coefficient.max - constraint_bounds.coefficient.min > 1e9 @warn "Constraint coefficient bounds range is $(constraint_bounds.coefficient.max - constraint_bounds.coefficient.min) and can result in numerical problems for the solver. \\ max_bound_constraint = $(encode_key_as_string(constraint_bounds.coefficient.max_index)) \\ min_bound_constraint = $(encode_key_as_string(constraint_bounds.coefficient.min_index)) \\ Run get_detailed_constraint_numerical_bounds on the model for a deeper analysis" else @info "Constraint coefficient bounds range is [$(constraint_bounds.coefficient.min) $(constraint_bounds.coefficient.max)]" end if constraint_bounds.rhs.max - constraint_bounds.rhs.min > 1e9 @warn "Constraint right-hand-side bounds range is $(constraint_bounds.rhs.max - constraint_bounds.rhs.min) and can result in numerical problems for the solver. \\ max_bound_constraint = $(encode_key_as_string(constraint_bounds.rhs.max_index)) \\ min_bound_constraint = $(encode_key_as_string(constraint_bounds.rhs.min_index)) \\ Run get_detailed_constraint_numerical_bounds on the model for a deeper analysis" else @info "Constraint right-hand-side bounds [$(constraint_bounds.rhs.min) $(constraint_bounds.rhs.max)]" end return end function _pre_solve_model_checks(model::OperationModel, optimizer = nothing) jump_model = get_jump_model(model) if optimizer !== nothing JuMP.set_optimizer(jump_model, optimizer) end if JuMP.mode(jump_model) != JuMP.DIRECT if JuMP.backend(jump_model).state == MOIU.NO_OPTIMIZER error("No Optimizer has been defined, can't solve the operational problem") end else @assert get_direct_mode_optimizer(get_settings(model)) end optimizer_name = JuMP.solver_name(jump_model) @info "$(get_name(model)) optimizer set to: $optimizer_name" settings = get_settings(model) if get_check_numerical_bounds(settings) @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( ISOPT.list_keys(get_store(model), container_type), ) end read_dual(model::OperationModel, key::ConstraintKey) = _read_results(model, key) read_parameter(model::OperationModel, key::ParameterKey) = _read_results(model, key) read_aux_variable(model::OperationModel, key::AuxVarKey) = _read_results(model, key) read_variable(model::OperationModel, key::VariableKey) = _read_results(model, key) read_expression(model::OperationModel, key::ExpressionKey) = _read_results(model, key) function _read_results(model::OperationModel, key::OptimizationContainerKey) array = read_results(get_store(model), key) return to_results_dataframe(array, nothing, Val(TableFormat.LONG)) end read_optimizer_stats(model::OperationModel) = read_optimizer_stats(get_store(model)) function add_recorders!(model::OperationModel, recorders) internal = get_internal(model) for name in union(REQUIRED_RECORDERS, recorders) ISOPT.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 ISOPT.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 ISOPT.get_recorders(get_internal(model)) IS.unregister_recorder!(name) end end const _JUMP_MODEL_FILENAME = "jump_model.json" function serialize_optimization_model(model::OperationModel) serialize_optimization_model( get_optimization_container(model), joinpath(get_output_dir(model), _JUMP_MODEL_FILENAME), ) return end function instantiate_network_model!(model::OperationModel) template = get_template(model) network_model = get_network_model(template) branch_models = get_branch_models(template) number_of_steps = get_time_steps(get_optimization_container(model))[end] instantiate_network_model!( network_model, branch_models, number_of_steps, get_system(model), ) return end list_aux_variable_keys(x::OperationModel) = ISOPT.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) = ISOPT.list_keys(get_store(x), STORE_CONTAINER_VARIABLES) list_variable_names(x::OperationModel) = _list_names(x, STORE_CONTAINER_VARIABLES) list_parameter_keys(x::OperationModel) = ISOPT.list_keys(get_store(x), STORE_CONTAINER_PARAMETERS) list_parameter_names(x::OperationModel) = _list_names(x, STORE_CONTAINER_PARAMETERS) list_dual_keys(x::OperationModel) = ISOPT.list_keys(get_store(x), STORE_CONTAINER_DUALS) list_dual_names(x::OperationModel) = _list_names(x, STORE_CONTAINER_DUALS) list_expression_keys(x::OperationModel) = ISOPT.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) return Iterators.flatten( keys(get_data_field(get_store(x), f)) for f in STORE_CONTAINERS ) end function serialize_optimization_model(model::OperationModel, save_path::String) serialize_jump_optimization_model( get_jump_model(get_optimization_container(model)), save_path, ) return end ================================================ FILE: src/operation/operation_model_simulation_interface.jl ================================================ function update_model!(model::OperationModel, source::SimulationState, ini_cond_chronology) TimerOutputs.@timeit RUN_SIMULATION_TIMER "Parameter Updates" begin update_parameters!(model, source) end TimerOutputs.@timeit RUN_SIMULATION_TIMER "Ini Cond Updates" begin update_initial_conditions!(model, source, ini_cond_chronology) end return end function update_parameters!(model::EmulationModel, state::SimulationState) data = get_decision_states(state) update_parameters!(model, data) return end function update_parameters!( model::DecisionModel, simulation_state::SimulationState, ) cost_function_unsynch(get_optimization_container(model)) for key in keys(get_parameters(model)) update_parameter_values!(model, key, simulation_state) end 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) end return end ================================================ FILE: src/operation/operation_model_types.jl ================================================ """ Abstract type for models than employ PowerSimulations methods. For custom decision problems use DecisionProblem as the super type. """ abstract type DefaultDecisionProblem <: DecisionProblem end """ Generic PowerSimulations Operation Problem Type for unspecified models """ struct GenericOpProblem <: DefaultDecisionProblem end """ Abstract type for models than employ PowerSimulations methods. For custom emulation problems use EmulationProblem as the super type. """ abstract type DefaultEmulationProblem <: EmulationProblem end """ Default PowerSimulations Emulation Problem Type for unspecified problems """ struct GenericEmulationProblem <: DefaultEmulationProblem end ================================================ FILE: src/operation/operation_problem_templates.jl ================================================ struct EconomicDispatchProblem <: DefaultDecisionProblem end struct UnitCommitmentProblem <: DefaultDecisionProblem end struct AGCReserveDeployment <: DefaultDecisionProblem end function _default_devices_uc() return [ DeviceModel(PSY.ThermalStandard, ThermalBasicUnitCommitment), DeviceModel(PSY.RenewableDispatch, RenewableFullDispatch), DeviceModel(PSY.RenewableNonDispatch, FixedOutput), DeviceModel(PSY.PowerLoad, StaticPowerLoad), DeviceModel(PSY.InterruptiblePowerLoad, PowerLoadInterruption), DeviceModel(PSY.Line, StaticBranch), DeviceModel(PSY.Transformer2W, StaticBranch), DeviceModel(PSY.TapTransformer, StaticBranch), DeviceModel(PSY.TwoTerminalGenericHVDCLine, HVDCTwoTerminalDispatch), ] end function _default_devices_dispatch() default = _default_devices_uc() default[1] = DeviceModel(PSY.ThermalStandard, ThermalBasicDispatch) return default end function _default_services() return [ ServiceModel(PSY.VariableReserve{PSY.ReserveUp}, RangeReserve), ServiceModel(PSY.VariableReserve{PSY.ReserveDown}, RangeReserve), ] end """ template_unit_commitment(; kwargs...) Creates a `ProblemTemplate` with default DeviceModels for a Unit Commitment problem. # Example template = template_unit_commitment() ``` # Accepted Key Words - `network::Type{<:PM.AbstractPowerModel}` : override default network model settings - `devices::Vector{DeviceModel}` : override default `DeviceModel` settings - `services::Vector{ServiceModel}` : override default `ServiceModel` settings ``` """ function template_unit_commitment(; kwargs...) network = get(kwargs, :network, CopperPlatePowerModel) template = ProblemTemplate(network) for model in get(kwargs, :devices, _default_devices_uc()) set_device_model!(template, model) end for model in get(kwargs, :services, _default_services()) set_service_model!(template, model) end return template end """ template_economic_dispatch(; kwargs...) Creates a `ProblemTemplate` with default DeviceModels for an Economic Dispatch problem. # Example template = template_economic_dispatch() ``` # Accepted Key Words - `network::Type{<:PM.AbstractPowerModel}` : override default network model settings - `devices::Vector{DeviceModel}` : override default `DeviceModel` settings - `services::Vector{ServiceModel}` : override default `ServiceModel` settings ``` """ function template_economic_dispatch(; kwargs...) network = get(kwargs, :network, CopperPlatePowerModel) template = ProblemTemplate(network) for model in get(kwargs, :devices, _default_devices_dispatch()) set_device_model!(template, model) end for model in get(kwargs, :services, _default_services()) set_service_model!(template, model) end return template end #= """ template_agc_reserve_deployment(; kwargs...) Creates a `ProblemTemplate` with default DeviceModels for an AGC Reserve Deployment Problem. This model doesn't support customization # Example template = agc_reserve_deployment() """ function template_agc_reserve_deployment(; kwargs...) if !isempty(kwargs) throw(ArgumentError("AGC Template doesn't currently support customization")) end template = ProblemTemplate(AreaBalancePowerModel) set_device_model!(template, PSY.ThermalStandard, FixedOutput) set_device_model!(template, PSY.RenewableDispatch, FixedOutput) 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.RenewableNonDispatch, FixedOutput) set_device_model!( template, DeviceModel(PSY.RegulationDevice{PSY.ThermalStandard}, DeviceLimitedRegulation), ) set_device_model!( template, DeviceModel(PSY.RegulationDevice{PSY.HydroDispatch}, ReserveLimitedRegulation), ) set_device_model!( template, DeviceModel( PSY.RegulationDevice{PSY.HydroEnergyReservoir}, ReserveLimitedRegulation, ), ) set_service_model!(template, ServiceModel(PSY.AGC, PIDSmoothACE)) return template end =# ================================================ FILE: src/operation/optimization_debugging.jl ================================================ """ Each Tuple corresponds to (con_name, internal_index, moi_index) """ function get_all_constraint_index(model::OperationModel) con_index = Vector{Tuple{ConstraintKey, Int, Int}}() container = get_optimization_container(model) for (key, value) in get_constraints(container) for (idx, constraint) in enumerate(value) moi_index = JuMP.optimizer_index(constraint) push!(con_index, (key, idx, moi_index.value)) end end return con_index end """ 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 [(ISOPT.encode_key(v[1]), v[2], v[3]) for v in var_keys] end function get_all_variable_keys(model::OperationModel) var_index = Vector{Tuple{VariableKey, Int, Int}}() container = get_optimization_container(model) for (key, value) in get_variables(container) for (idx, variable) in enumerate(value) moi_index = JuMP.optimizer_index(variable) push!(var_index, (key, idx, moi_index.value)) end end return var_index end function get_constraint_index(model::OperationModel, index::Int) container = get_optimization_container(model) constraints = get_constraints(container) for i in get_all_constraint_index(model) if i[3] == index return constraints[i[1]].data[i[2]] end end @info "Index not found" return end function get_variable_index(model::OperationModel, index::Int) container = get_optimization_container(model) variables = get_variables(container) for i in get_all_variable_keys(model) if i[3] == index return variables[i[1]].data[i[2]] end end @info "Index not found" return end function get_detailed_constraint_numerical_bounds(model::OperationModel) if !is_built(model) error("Model not built, can't calculate constraint numerical bounds") end constraint_bounds = Dict() for (const_key, constraint_array) in get_constraints(get_optimization_container(model)) if isa(constraint_array, SparseAxisArray) bounds = ConstraintBounds() for idx in eachindex(constraint_array) constraint_array[idx] == 0.0 && continue con_obj = JuMP.constraint_object(constraint_array[idx]) update_coefficient_bounds(bounds, con_obj, idx) update_rhs_bounds(bounds, con_obj, idx) end constraint_bounds[const_key] = bounds else bounds = ConstraintBounds() for idx in Iterators.product(constraint_array.axes...) con_obj = JuMP.constraint_object(constraint_array[idx...]) update_coefficient_bounds(bounds, con_obj, idx) update_rhs_bounds(bounds, con_obj, idx) end constraint_bounds[const_key] = bounds end end return constraint_bounds end function get_detailed_variable_numerical_bounds(model::OperationModel) if !is_built(model) error("Model not built, can't calculate variable numerical bounds") end variable_bounds = Dict() for (variable_key, variable_array) in get_variables(get_optimization_container(model)) bounds = VariableBounds() if isa(variable_array, SparseAxisArray) for idx in eachindex(variable_array) var = variable_array[idx] var == 0.0 && continue update_variable_bounds(bounds, var, idx) end else for idx in Iterators.product(variable_array.axes...) var = variable_array[idx...] update_variable_bounds(bounds, var, idx) end end variable_bounds[variable_key] = bounds end return variable_bounds end ================================================ FILE: src/operation/problem_results.jl ================================================ """ Construct OptimizationProblemResults from a solved DecisionModel. """ function OptimizationProblemResults(model::DecisionModel) status = get_run_status(model) status != RunStatus.SUCCESSFULLY_FINALIZED && error("problem was not solved successfully: $status") model_store = get_store(model) if isempty(model_store) error("Model Solved as part of a Simulation.") end timestamps = get_timestamps(model) optimizer_stats = ISOPT.to_dataframe(get_optimizer_stats(model)) aux_variable_values = Dict(x => read_aux_variable(model, x) for x in list_aux_variable_keys(model)) variable_values = Dict(x => read_variable(model, x) for x in list_variable_keys(model)) dual_values = Dict(x => read_dual(model, x) for x in list_dual_keys(model)) parameter_values = Dict(x => read_parameter(model, x) for x in list_parameter_keys(model)) expression_values = Dict(x => read_expression(model, x) for x in list_expression_keys(model)) sys = get_system(model) return OptimizationProblemResults( get_problem_base_power(model), timestamps, sys, IS.get_uuid(sys), aux_variable_values, variable_values, dual_values, parameter_values, expression_values, 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 OptimizationProblemResults from a solved EmulationModel. """ function OptimizationProblemResults(model::EmulationModel) status = get_run_status(model) status != RunStatus.SUCCESSFULLY_FINALIZED && error("problem was not solved successfully: $status") model_store = get_store(model) if isempty(model_store) error("Model Solved as part of a Simulation.") end aux_variables = Dict(x => read_aux_variable(model, x) for x in list_aux_variable_keys(model)) variables = Dict(x => read_variable(model, x) for x in list_variable_keys(model)) duals = Dict(x => read_dual(model, x) for x in list_dual_keys(model)) parameters = Dict(x => read_parameter(model, x) for x in list_parameter_keys(model)) expression = Dict(x => read_expression(model, x) for x in list_expression_keys(model)) optimizer_stats = read_optimizer_stats(model) initial_time = get_initial_time(model) container = get_optimization_container(model) sys = get_system(model) return OptimizationProblemResults( get_problem_base_power(model), StepRange(initial_time, get_resolution(model), initial_time), sys, IS.get_uuid(sys), aux_variables, variables, duals, parameters, expression, optimizer_stats, get_metadata(container), IS.strip_module_name(typeof(model)), get_output_dir(model), mkpath(joinpath(get_output_dir(model), "results")), ) end ================================================ FILE: src/operation/problem_template.jl ================================================ const DevicesModelContainer = Dict{Symbol, DeviceModel} const ServicesModelContainer = Dict{Tuple{String, Symbol}, ServiceModel} abstract type AbstractProblemTemplate end """ ProblemTemplate(::Type{T}) where {T<:PM.AbstractPowerFormulation} Creates a model reference of the PowerSimulations Optimization Problem. # Arguments - `model::Type{T<:PM.AbstractPowerFormulation}`: # Example template = ProblemTemplate(CopperPlatePowerModel) """ mutable struct ProblemTemplate <: AbstractProblemTemplate network_model::NetworkModel{<:PM.AbstractPowerModel} devices::DevicesModelContainer branches::BranchModelContainer services::ServicesModelContainer function ProblemTemplate(network::NetworkModel{T}) where {T <: PM.AbstractPowerModel} new( network, DevicesModelContainer(), BranchModelContainer(), ServicesModelContainer(), ) end end function Base.isempty(template::ProblemTemplate) if !isempty(template.devices) return false elseif !isempty(template.branches) return false elseif !isempty(template.services) return false else return true end end ProblemTemplate(::Type{T}) where {T <: PM.AbstractPowerModel} = ProblemTemplate(NetworkModel(T)) ProblemTemplate() = ProblemTemplate(CopperPlatePowerModel) get_device_models(template::ProblemTemplate) = template.devices get_branch_models(template::ProblemTemplate) = template.branches get_service_models(template::ProblemTemplate) = template.services get_network_model(template::ProblemTemplate) = template.network_model get_network_formulation(template::ProblemTemplate) = get_network_formulation(get_network_model(template)) get_hvdc_network_model(template::ProblemTemplate) = template.network_model.hvdc_network_model function get_component_types(template::ProblemTemplate)::Vector{DataType} return vcat( get_component_type.(values(get_device_models(template))), get_component_type.(values(get_branch_models(template))), get_component_type.(values(get_service_models(template))), ) end function get_model(template::ProblemTemplate, ::Type{T}) where {T <: PSY.Device} if T <: PSY.Branch return get(template.branches, Symbol(T), nothing) elseif T <: PSY.Device return get(template.devices, Symbol(T), nothing) else error("Component $T not present in the template") end end function get_model( template::ProblemTemplate, ::Type{T}, name::String = NO_SERVICE_NAME_PROVIDED, ) where {T <: PSY.Service} if haskey(template.services, (name, Symbol(T))) return template.services[(name, Symbol(T))] else error("Service $T $name not present in the template") end end # Note to devs. PSY exports set_model! these names are chosen to avoid name clashes """ Sets the network model in a template. """ function set_network_model!( template::ProblemTemplate, model::NetworkModel{<:PM.AbstractPowerModel}, ) template.network_model = model return end """ Sets the network model in a template. """ function set_hvdc_network_model!( template::ProblemTemplate, model::Union{Nothing, AbstractHVDCNetworkModel}, ) set_hvdc_network_model!(template.network_model, model) return end """ Sets the network model in a template. """ function set_hvdc_network_model!( template::ProblemTemplate, model::Type{U}, ) where {U <: AbstractHVDCNetworkModel} set_hvdc_network_model!(template.network_model, model()) return end """ Sets the device model in a template using the component type and formulation. Builds a default DeviceModel """ function set_device_model!( template::ProblemTemplate, component_type::Type{<:PSY.Device}, formulation::Type{<:AbstractDeviceFormulation}, ) set_device_model!(template, DeviceModel(component_type, formulation)) return end """ Sets the device model in a template using a DeviceModel instance """ function set_device_model!( template::ProblemTemplate, model::DeviceModel{<:PSY.Device, <:AbstractDeviceFormulation}, ) _set_model!(template.devices, model) return end function set_device_model!( template::ProblemTemplate, model::DeviceModel{<:PSY.Branch, <:AbstractDeviceFormulation}, ) _set_model!(template.branches, model) return end """ Sets the service model in a template using a name and the service type and formulation. Builds a default ServiceModel with use_service_name set to true. """ function set_service_model!( template::ProblemTemplate, service_name::String, service_type::Type{<:PSY.Service}, formulation::Type{<:AbstractServiceFormulation}, ) set_service_model!( template, service_name, ServiceModel(service_type, formulation; use_service_name = true), ) return end """ Sets the service model in a template using a ServiceModel instance. """ function set_service_model!( template::ProblemTemplate, service_type::Type{<:PSY.Service}, formulation::Type{<:AbstractServiceFormulation}, ) set_service_model!(template, ServiceModel(service_type, formulation)) return end function set_service_model!( template::ProblemTemplate, service_name::String, model::ServiceModel{T, <:AbstractServiceFormulation}, ) where {T <: PSY.Service} _set_model!(template.services, (service_name, Symbol(T)), model) return end function set_service_model!( template::ProblemTemplate, model::ServiceModel{<:PSY.Service, <:AbstractServiceFormulation}, ) _set_model!(template.services, model) return end function _add_contributing_device_by_type!( service_model::ServiceModel, contributing_device::T, incompatible_device_types::Set{DataType}, modeled_devices::Set{DataType}, ) where {T <: PSY.Device} !PSY.get_available(contributing_device) && return if T ∈ incompatible_device_types || T ∉ modeled_devices return end push!(get!(get_contributing_devices_map(service_model), T, T[]), contributing_device) return end function _populate_contributing_devices!(template::ProblemTemplate, sys::PSY.System) service_models = get_service_models(template) isempty(service_models) && return device_models = get_device_models(template) branch_models = get_branch_models(template) # Type stability: explicitly type the Set to avoid widening to Set{Type} modeled_devices = Set{DataType}(get_component_type(m) for m in values(device_models)) union!(modeled_devices, (get_component_type(m) for m in values(branch_models))) incompatible_device_types = get_incompatible_devices(device_models) services_mapping = PSY.get_contributing_device_mapping(sys) if isempty(keys(services_mapping)) @warn "The system doesn't include any services. No services will be modeled, consider removing the service models from the template." _group = LOG_GROUP_SERVICE_CONSTUCTORS empty!(service_models) return end for (service_key, service_model) in service_models @debug "Populating service $(service_key)" empty!(get_contributing_devices_map(service_model)) S = get_component_type(service_model) service = PSY.get_component(S, sys, get_service_name(service_model)) if service === nothing @info "The data doesn't include services of type $(S) and name $(get_service_name(service_model)), consider changing the service models" _group = LOG_GROUP_SERVICE_CONSTUCTORS continue end service_devices_key = (type = S, name = PSY.get_name(service)) contributing_devices_ = services_mapping[service_devices_key].contributing_devices for d in contributing_devices_ _add_contributing_device_by_type!( service_model, d, incompatible_device_types, modeled_devices, ) end if isempty(get_contributing_devices_map(service_model)) error( "The contributing devices for service $(PSY.get_name(service)) is empty. Add contributing devices to the service in the data to continue.", ) end end return end function _modify_device_model!( devices_template::Dict{Symbol, DeviceModel}, service_model::ServiceModel{<:PSY.Reserve, <:AbstractReservesFormulation}, contributing_devices::Vector{<:PSY.Component}, ) # Type stability: explicitly type the Set to avoid widening for dt in Set{DataType}(typeof.(contributing_devices)) for device_model in values(devices_template) # add message here when it exists get_component_type(device_model) != dt && continue service_model in device_model.services && continue # type instability: pushing to vector of abstract type push!(device_model.services, service_model) end end return end function _modify_device_model!( ::Dict{Symbol, DeviceModel}, ::ServiceModel{<:PSY.ReserveNonSpinning, <:AbstractReservesFormulation}, ::Vector{<:PSY.Component}, ) return end function _modify_device_model!( ::Dict{Symbol, DeviceModel}, ::ServiceModel{PSY.TransmissionInterface, ConstantMaxInterfaceFlow}, ::Vector, ) 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.ConstantReserveGroup) && continue contributing_devices = get_contributing_devices(service_model) isempty(contributing_devices) && continue _modify_device_model!(devices_template, service_model, contributing_devices) end return end function _populate_aggregated_service_model!(template::ProblemTemplate, sys::PSY.System) services_template = get_service_models(template) for (key, service_model) in services_template attributes = get_attributes(service_model) use_slacks = service_model.use_slacks duals = service_model.duals if pop!(attributes, "aggregated_service_model", false) delete!(services_template, key) D = get_component_type(service_model) B = get_formulation(service_model) for service in get_available_components(service_model, sys) new_key = (PSY.get_name(service), Symbol(D)) if !haskey(services_template, new_key) template.services[new_key] = ServiceModel( D, B, PSY.get_name(service); use_slacks = use_slacks, duals = duals, attributes = attributes, ) else error("Key $new_key already assigned in ServiceModel") end end end end return end function finalize_template!(template::ProblemTemplate, sys::PSY.System) _populate_aggregated_service_model!(template, sys) _populate_contributing_devices!(template, sys) _add_services_to_device_model!(template) return end ================================================ FILE: src/operation/template_validation.jl ================================================ function _check_branch_network_compatibility( ::NetworkModel{T}, unmodeled_branch_types::Vector{DataType}, ) where {T <: PM.AbstractPowerModel} if requires_all_branch_models(T) && !isempty(unmodeled_branch_types) for d in unmodeled_branch_types @error "The system has a branch branch type $(d) but the DeviceModel is not included in the Template." end throw( IS.ConflictingInputsError( "Network model $(T) requires all AC Transmission devices have a model", ), ) end return end function _validate_branch_models( ::Type{T}, model_has_branch_filters::Bool, ) where {T <: PM.AbstractPowerModel} if supports_branch_filtering(T) || !model_has_branch_filters return elseif model_has_branch_filters if ignores_branch_filtering(T) @warn "Branch filtering is ignored for network model $(T)" else throw( IS.ConflictingInputsError( "Branch filtering is not supported for network model $(T). Remove branch \\ filter functions from branch models or use a different network model.", ), ) end else throw( IS.ConflictingInputsError( "Network model $(T) can't be validated against branch models", ), ) end return end function validate_network_model(network_model::NetworkModel{T}, unmodeled_branch_types::Vector{DataType}, model_has_branch_filters::Bool, ) where {T <: PM.AbstractPowerModel} _check_branch_network_compatibility(network_model, unmodeled_branch_types) _validate_branch_models(T, model_has_branch_filters) return end function validate_template_impl!(model::OperationModel) template = get_template(model) settings = get_settings(model) if isempty(template) error("Template can't be empty for models $(get_problem_type(model))") end system = get_system(model) modeled_types = get_component_types(template) system_component_types = PSY.get_existing_component_types(system) network_model = get_network_model(template) valid_device_types = union(modeled_types, _TEMPLATE_VALIDATION_EXCLUSIONS) unmodeled_branch_types = DataType[] for m in setdiff(system_component_types, valid_device_types) @warn "The template doesn't include models for components of type $(m), consider changing the template" _group = LOG_GROUP_MODELS_VALIDATION if m <: PSY.ACTransmission push!(unmodeled_branch_types, m) end end device_keys_to_delete = Symbol[] for (k, device_model) in model.template.devices make_device_cache!(device_model, system, get_check_components(settings)) if isempty(get_device_cache(device_model)) @info "The system data doesn't include devices of type $(k), consider changing the models in the template" _group = LOG_GROUP_MODELS_VALIDATION push!(device_keys_to_delete, k) end end for k in device_keys_to_delete delete!(model.template.devices, k) end model_has_branch_filters = false branch_keys_to_delete = Symbol[] for (k, device_model) in model.template.branches make_device_cache!(device_model, system, get_check_components(settings)) if isempty(get_device_cache(device_model)) @info "The system data doesn't include Branches of type $(k), consider changing the models in the template" _group = LOG_GROUP_MODELS_VALIDATION push!(branch_keys_to_delete, k) else push!(network_model.modeled_ac_branch_types, get_component_type(device_model)) end if get_attribute(device_model, "filter_function") !== nothing model_has_branch_filters = true end end for k in branch_keys_to_delete delete!(model.template.branches, k) end validate_network_model(network_model, unmodeled_branch_types, model_has_branch_filters) return end ================================================ FILE: src/operation/time_series_interface.jl ================================================ function get_time_series_values!( time_series_type::Type{T}, model::DecisionModel, component, name::String, initial_time::Dates.DateTime, horizon::Int; ignore_scaling_factors = true, interval::Dates.Millisecond = UNSET_INTERVAL, ) where {T <: PSY.Forecast} is_interval = _to_is_interval(interval) settings = get_settings(model) resolution = get_resolution(settings) if !use_time_series_cache(settings) return IS.get_time_series_values( T, component, name; start_time = initial_time, len = horizon, ignore_scaling_factors = ignore_scaling_factors, interval = is_interval, ) end cache = get_time_series_cache(model) key = IS.TimeSeriesCacheKey(IS.get_uuid(component), T, name, resolution, is_interval) if haskey(cache, key) ts_cache = cache[key] else ts_cache = IS.make_time_series_cache( time_series_type, component, name, initial_time, horizon; ignore_scaling_factors = ignore_scaling_factors, interval = is_interval, resolution = resolution, ) cache[key] = ts_cache end ts = IS.get_time_series_array!(ts_cache, initial_time) return TimeSeries.values(ts) end function get_time_series_values!( ::Type{T}, model::EmulationModel, component::U, name::String, initial_time::Dates.DateTime, len::Int = 1; ignore_scaling_factors = true, resolution::Dates.Millisecond = UNSET_RESOLUTION, ) where {T <: PSY.StaticTimeSeries, U <: PSY.Component} settings = get_settings(model) key_resolution = resolution == UNSET_RESOLUTION ? get_resolution(settings) : resolution is_resolution = _to_is_resolution(key_resolution) if !use_time_series_cache(settings) return IS.get_time_series_values( T, component, name; start_time = initial_time, len = len, ignore_scaling_factors = ignore_scaling_factors, resolution = is_resolution, ) end cache = get_time_series_cache(model) key = IS.TimeSeriesCacheKey(IS.get_uuid(component), T, name, key_resolution, nothing) if haskey(cache, key) ts_cache = cache[key] else ts_cache = IS.make_time_series_cache( T, component, name, initial_time, len; ignore_scaling_factors = ignore_scaling_factors, resolution = is_resolution, ) cache[key] = ts_cache end ts = IS.get_time_series_array!(ts_cache, initial_time) return TimeSeries.values(ts) end ================================================ FILE: src/parameters/add_parameters.jl ================================================ function add_parameters!( container::OptimizationContainer, ::Type{T}, devices::U, model::DeviceModel{D, W}, ) where { T <: ParameterType, U <: Union{Vector{D}, IS.FlattenIteratorWrapper{D}}, W <: AbstractDeviceFormulation, } where {D <: PSY.Component} if get_rebuild_model(get_settings(container)) && has_container_key(container, T, D) return end _add_parameters!(container, T(), devices, model) return end function add_branch_parameters!( container::OptimizationContainer, ::Type{T}, devices::U, model::DeviceModel{D, W}, network_model::NetworkModel{<:AbstractPTDFModel}, ) where { T <: ParameterType, U <: Union{Vector{D}, IS.FlattenIteratorWrapper{D}}, W <: AbstractDeviceFormulation, } where {D <: PSY.ACTransmission} if get_rebuild_model(get_settings(container)) && has_container_key(container, T, D) return end _add_time_series_parameters!(container, T(), network_model, devices, model) return end function add_parameters!( container::OptimizationContainer, ::Type{T}, devices::U, device_model::DeviceModel{D, W}, event_model::EventModel{V, X}, ) where { T <: ParameterType, U <: Vector{D}, V <: PSY.Contingency, W <: AbstractDeviceFormulation, X <: AbstractEventCondition, } where {D <: PSY.Component} if get_rebuild_model(get_settings(container)) && has_container_key(container, T, D) return end _add_parameters!(container, T(), devices, device_model, event_model) return end function add_parameters!( container::OptimizationContainer, ::Type{T}, ff::LowerBoundFeedforward, model::ServiceModel{S, W}, devices::V, ) where { S <: PSY.AbstractReserve, T <: VariableValueParameter, V <: Union{Vector{D}, IS.FlattenIteratorWrapper{D}}, W <: AbstractReservesFormulation, } where {D <: PSY.Component} if get_rebuild_model(get_settings(container)) && has_container_key(container, T, S) return end source_key = get_optimization_container_key(ff) _add_parameters!(container, T(), source_key, model, devices) return end function add_parameters!( container::OptimizationContainer, ::Type{T}, service::U, model::ServiceModel{U, V}, ) 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 end _add_parameters!(container, T(), service, model) return end function add_parameters!( container::OptimizationContainer, ::Type{T}, ff::AbstractAffectFeedforward, model::DeviceModel{D, W}, devices::V, ) where { T <: VariableValueParameter, V <: Union{Vector{D}, IS.FlattenIteratorWrapper{D}}, W <: AbstractDeviceFormulation, } where {D <: PSY.Component} if get_rebuild_model(get_settings(container)) && has_container_key(container, T, D) return end source_key = get_optimization_container_key(ff) _add_parameters!(container, T(), source_key, model, devices) return end function add_parameters!( container::OptimizationContainer, ::Type{T}, ff::FixValueFeedforward, model::DeviceModel{D, W}, devices::V, ) where { T <: VariableValueParameter, V <: Union{Vector{D}, IS.FlattenIteratorWrapper{D}}, W <: AbstractDeviceFormulation, } where {D <: PSY.Component} if get_rebuild_model(get_settings(container)) && has_container_key(container, T, D) return end source_key = get_optimization_container_key(ff) _add_parameters!(container, T(), source_key, model, devices) _set_affected_variables!(container, T(), D, ff) return end function add_parameters!( container::OptimizationContainer, ::Type{T}, ff::FixValueFeedforward, model::ServiceModel{K, W}, devices::V, ) where { T <: VariableValueParameter, V <: Union{Vector{D}, IS.FlattenIteratorWrapper{D}}, W <: AbstractServiceFormulation, K <: PSY.Reserve, } where {D <: PSY.Component} if get_rebuild_model(get_settings(container)) && has_container_key(container, T, D) return end source_key = get_optimization_container_key(ff) _add_parameters!(container, T(), source_key, model, devices) _set_affected_variables!(container, T(), K, ff) return end function _set_affected_variables!( container::OptimizationContainer, ::T, device_type::Type{U}, ff::FixValueFeedforward, ) where { T <: VariableValueParameter, U <: PSY.Component, } source_key = get_optimization_container_key(ff) var_type = get_entry_type(source_key) parameter_container = get_parameter(container, T(), U, "$var_type") param_attributes = get_attributes(parameter_container) affected_variables = get_affected_values(ff) push!(param_attributes.affected_keys, affected_variables...) return end function _set_affected_variables!( container::OptimizationContainer, ::T, device_type::Type{U}, ff::FixValueFeedforward, ) where { T <: VariableValueParameter, U <: PSY.Service, } meta = ff.optimization_container_key.meta parameter_container = get_parameter(container, T(), U, meta) param_attributes = get_attributes(parameter_container) affected_variables = get_affected_values(ff) push!(param_attributes.affected_keys, affected_variables...) return end function _add_parameters!( container::OptimizationContainer, param::T, devices::U, model::DeviceModel{D, W}, ) where { T <: TimeSeriesParameter, U <: Union{Vector{D}, IS.FlattenIteratorWrapper{D}}, W <: AbstractDeviceFormulation, } where {D <: PSY.Component} _add_time_series_parameters!(container, param, devices, model) return end function _check_dynamic_branch_rating_ts( ts::AbstractArray, ::T, device::PSY.Device, model::DeviceModel{D, W}, ) where {D <: PSY.Component, T <: TimeSeriesParameter, W <: AbstractDeviceFormulation} if !(T <: AbstractDynamicBranchRatingTimeSeriesParameter) return end rating = PSY.get_rating(device) if (T <: PostContingencyDynamicBranchRatingTimeSeriesParameter) if !(PSY.get_rating_b(device) === nothing) rating = PSY.get_rating_b(device) else @warn "Device $(typeof(device)) '$(PSY.get_name(device))' has Parameter $T but it has no static 'rating_b' defined." end end multiplier = get_multiplier_value(T(), device, W()) if !all(x -> x >= rating, multiplier * ts) @warn "There are values of Parameter $T associated with $(typeof(device)) '$(PSY.get_name(device))' lower than the device static rating $(rating)." end return end # Extends `size` to tuples, treating them like scalars _size_wrapper(elem) = size(elem) _size_wrapper(::Tuple) = () function _add_time_series_parameters!( container::OptimizationContainer, param::T, network_model::NetworkModel{<:AbstractPTDFModel}, devices, model::DeviceModel{D, W}, ) where {D <: PSY.ACTransmission, T <: TimeSeriesParameter, W <: AbstractDeviceFormulation} 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") end time_steps = get_time_steps(container) net_reduction_data = network_model.network_reduction reduced_branch_tracker = get_reduced_branch_tracker(network_model) all_branch_maps_by_type = PNM.get_all_branch_maps_by_type(net_reduction_data) # TODO: Temporary workaround to get the name where we assume all the names are the same accross devices. ts_name = _get_time_series_name(T(), first(devices), model) model_interval = get_interval(get_settings(container)) ts_interval = model_interval device_name_axis, ts_uuid_axis = get_branch_argument_parameter_axes( net_reduction_data, devices, ts_type, ts_name; interval = ts_interval, ) if isempty(device_name_axis) @info "No devices with time series $ts_name found for $D devices. Skipping parameter addition." return end # name -> ts_uuid cache built from the axis pair so the per-branch loop below # doesn't re-query IS.get_time_series_uuid for each branch. branch_ts_uuids = Dict{String, String}(zip(device_name_axis, ts_uuid_axis)) additional_axes = () param_container = add_param_container!( container, param, D, ts_type, ts_name, ts_uuid_axis, device_name_axis, additional_axes, time_steps, ) set_subsystem!(get_attributes(param_container), get_subsystem(model)) param_instance = T() jump_model = get_jump_model(container) parent_param = get_parameter_array_data(param_container) parent_mult = get_multiplier_array_data(param_container) # The param array's first axis is `ts_uuid_axis` (UUID-keyed) while the # multiplier array's first axis is `device_name_axis` (name-keyed); we need # two separate row lookups so parallel branches sharing a UUID still write # their multiplier to the correct (per-branch-name) row. param_lookup = get_parameter_array(param_container).lookup[1] mult_lookup = get_multiplier_array(param_container).lookup[1] for (name, (arc, reduction)) in PNM.get_name_to_arc_map(net_reduction_data, D) reduction_entry = all_branch_maps_by_type[reduction][D][arc] if !PNM.has_time_series(reduction_entry, ts_type, ts_name) continue end device_with_time_series = PNM.get_device_with_time_series(reduction_entry, ts_type, ts_name) ts_uuid = branch_ts_uuids[name] i_param = param_lookup[ts_uuid] i_mult = mult_lookup[name] has_entry, tracker_container = search_for_reduced_branch_parameter!( reduced_branch_tracker, arc, T, ) if has_entry @assert !isempty(tracker_container) name arc reduction else raw_ts_vals = get_time_series_initial_values!( container, ts_type, device_with_time_series, ts_name; interval = ts_interval, ) ts_vals = _unwrap_for_param.(Ref(param_instance), raw_ts_vals, Ref(additional_axes)) @assert all(_size_wrapper.(ts_vals) .== Ref(length.(additional_axes))) end multiplier = get_multiplier_value(T(), reduction_entry, W()) _set_multiplier_at!(parent_mult, Float64(multiplier), i_mult) for t in time_steps if !has_entry # Store raw float in tracker for non-recurrent builds. For recurrent # builds (JuMP parameters), read back the VariableRef that the fast-path # setter just created so that parallel branch types share the same # JuMP parameter. _set_parameter_at!(parent_param, jump_model, ts_vals[t], i_param, t) if built_for_recurrent_solves(container) tracker_container[t] = parent_param[i_param, t] else tracker_container[t] = ts_vals[t] end else # Reuse the value (Float64) or VariableRef already stored by the first # branch type that processed this arc. _set_parameter_at!( parent_param, jump_model, tracker_container[t], i_param, t, ) end end add_component_name!(get_attributes(param_container), name, ts_uuid) end return end # NOTE direct equivalent of _add_parameters! on ObjectiveFunctionParameter # PERF: compilation hotspot. Switch to TSC. function _add_time_series_parameters!( container::OptimizationContainer, param::T, devices, model::DeviceModel{D, W}, ) where {D <: PSY.Component, T <: TimeSeriesParameter, W <: AbstractDeviceFormulation} 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") end time_steps = get_time_steps(container) # TODO: Temporary workaround to get the name where we assume all the names are the same accross devices. ts_name = _get_time_series_name(T(), first(devices), model) device_names = String[] devices_with_time_series = D[] initial_values = Dict{String, AbstractArray}() # device name -> ts_uuid cache so the second loop below doesn't re-query IS. device_ts_uuids = Dict{String, String}() model_interval = get_interval(get_settings(container)) is_ts_interval = _to_is_interval(model_interval) model_resolution = get_resolution(get_settings(container)) is_ts_resolution = _to_is_resolution(model_resolution) @debug "adding" T D ts_name ts_type _group = LOG_GROUP_OPTIMIZATION_CONTAINER for device::D in devices if !PSY.has_time_series(device, ts_type, ts_name) @debug "Time series $(ts_type):$(ts_name) for $D, $(PSY.get_name(device)) not found. Skipping parameter addition for this device." continue end device_name = PSY.get_name(device) push!(device_names, device_name) push!(devices_with_time_series, device) ts_uuid = string( IS.get_time_series_uuid( ts_type, device, ts_name; resolution = is_ts_resolution, interval = is_ts_interval, ), ) device_ts_uuids[device_name] = ts_uuid if !(ts_uuid in keys(initial_values)) initial_values[ts_uuid] = get_time_series_initial_values!( container, ts_type, device, ts_name; interval = model_interval, resolution = model_resolution, ) _check_dynamic_branch_rating_ts(initial_values[ts_uuid], param, device, model) end end #= # NOTE this is always the case for "normal" time series, but it is currently not enforced in PSY for MBC time series. # TODO decide whether this is an acceptable restriction or whether we need to support multiple time series names # JD: Yes, the restriction are that the names for this has to be unique as they are specified from the model attributes if isempty(active_devices) return end unique_ts_names = unique(ts_names) if length(unique_ts_names) > 1 throw( ArgumentError( "All time series names must be equal for parameter $T within a given device type. Got $unique_ts_names for device type $D", ), ) end ts_name = only(unique_ts_names) =# if isempty(device_names) @info "No devices with time series $ts_name found for $D devices. Skipping parameter addition." return end additional_axes = calc_additional_axes(container, param, devices_with_time_series, model) param_container = add_param_container!( container, param, D, ts_type, ts_name, collect(keys(initial_values)), device_names, additional_axes, time_steps, ) set_subsystem!(get_attributes(param_container), get_subsystem(model)) jump_model = get_jump_model(container) param_instance = T() parent_param = get_parameter_array_data(param_container) # `param_axs = collect(keys(initial_values))` was passed to `add_param_container!` # above, so `initial_values`'s iteration order matches the container's first axis. for (i, (ts_uuid, raw_ts_vals)) in enumerate(initial_values) ts_vals = _unwrap_for_param.(Ref(param_instance), raw_ts_vals, Ref(additional_axes)) @assert all(_size_wrapper.(ts_vals) .== Ref(length.(additional_axes))) for step in time_steps _set_parameter_at!(parent_param, jump_model, ts_vals[step], i, step) end end parent_mult = get_multiplier_array_data(param_container) # `devices_with_time_series` was built in the same order as `device_names`, which # matches the multiplier array's first axis, so enumeration index `i` is correct. for (i, device) in enumerate(devices_with_time_series) multiplier = get_multiplier_value(T(), device, W()) device_name = PSY.get_name(device) _set_multiplier_at!(parent_mult, Float64(multiplier), i) add_component_name!( get_attributes(param_container), device_name, device_ts_uuids[device_name], ) end return end # Layer of indirection to deal with the fact that some time series names are stored in the component _get_time_series_name(::T, ::PSY.Component, model::DeviceModel) where {T <: ParameterType} = get_time_series_names(model)[T] _get_time_series_name(::StartupCostParameter, device::PSY.Component, ::DeviceModel) = get_name(PSY.get_start_up(PSY.get_operation_cost(device))) _get_time_series_name(::ShutdownCostParameter, device::PSY.Component, ::DeviceModel) = get_name(PSY.get_shut_down(PSY.get_operation_cost(device))) _get_time_series_name( ::IncrementalCostAtMinParameter, device::PSY.Device, ::DeviceModel, ) = get_name(PSY.get_incremental_initial_input(PSY.get_operation_cost(device))) _get_time_series_name( ::DecrementalCostAtMinParameter, device::PSY.Device, ::DeviceModel, ) = get_name(PSY.get_decremental_initial_input(PSY.get_operation_cost(device))) _get_time_series_name( ::Union{ IncrementalPiecewiseLinearSlopeParameter, IncrementalPiecewiseLinearBreakpointParameter, }, device::PSY.Device, ::DeviceModel, ) = get_name(get_output_offer_curves(PSY.get_operation_cost(device))) _get_time_series_name( ::Union{ DecrementalPiecewiseLinearSlopeParameter, DecrementalPiecewiseLinearBreakpointParameter, }, device::PSY.Device, ::DeviceModel, ) = get_name(get_input_offer_curves(PSY.get_operation_cost(device))) # Layer of indirection to figure out what eltype we expect to find in various time series # (we could just read the time series and figure it out dynamically if this becomes too brittle) _get_expected_time_series_eltype(::T) where {T <: ParameterType} = Float64 _get_expected_time_series_eltype(::StartupCostParameter) = NTuple{3, Float64} # Lookup that defines which variables the ObjectiveFunctionParameter corresponds to _param_to_vars(::FuelCostParameter, ::AbstractDeviceFormulation) = (ActivePowerVariable,) _param_to_vars(::StartupCostParameter, ::AbstractThermalFormulation) = (StartVariable,) _param_to_vars(::StartupCostParameter, ::ThermalMultiStartUnitCommitment) = MULTI_START_VARIABLES _param_to_vars(::ShutdownCostParameter, ::AbstractThermalFormulation) = (StopVariable,) _param_to_vars(::AbstractCostAtMinParameter, ::AbstractDeviceFormulation) = (OnVariable,) _param_to_vars( ::Union{ IncrementalPiecewiseLinearSlopeParameter, IncrementalPiecewiseLinearBreakpointParameter, }, ::AbstractDeviceFormulation, ) = (PiecewiseLinearBlockIncrementalOffer,) _param_to_vars( ::Union{ DecrementalPiecewiseLinearSlopeParameter, DecrementalPiecewiseLinearBreakpointParameter, }, ::AbstractDeviceFormulation, ) = (PiecewiseLinearBlockDecrementalOffer,) # Layer of indirection to handle possible additional axes. Most parameters have just the two # usual axes (device, timestamp), but some have a third (e.g., piecewise tranche) calc_additional_axes( ::OptimizationContainer, ::T, ::U, ::DeviceModel{D, W}, ) where { T <: ParameterType, U <: Union{Vector{D}, IS.FlattenIteratorWrapper{D}}, W <: AbstractDeviceFormulation, } where {D <: PSY.Component} = () calc_additional_axes( ::OptimizationContainer, ::T, ::U, ::ServiceModel{D, W}, ) where { T <: ParameterType, U <: Union{Vector{D}, IS.FlattenIteratorWrapper{D}}, W <: AbstractServiceFormulation, } where {D <: PSY.Service} = () _get_max_tranches(data::Vector{IS.PiecewiseStepData}) = maximum(length.(data)) _get_max_tranches(data::TimeSeries.TimeArray) = _get_max_tranches(values(data)) _get_max_tranches(data::AbstractDict) = maximum(_get_max_tranches.(values(data))) # Iterate through all periods of a piecewise time series and return the maximum number of tranches function get_max_tranches(device::PSY.Device, piecewise_ts::IS.TimeSeriesKey) data = PSY.get_data(PSY.get_time_series(device, piecewise_ts)) max_tranches = _get_max_tranches(data) return max_tranches end # It's nice for debugging purposes to have meaningful labels on the tranche axis. These # labels are never relied upon in the current implementation make_tranche_axis(n_tranches) = "tranche_" .* string.(1:n_tranches) # Find the global maximum number of tranches we'll have to handle and create the parameter with an axis of that length function calc_additional_axes( ::OptimizationContainer, ::P, devices::U, ::DeviceModel{D, W}, ) where { P <: AbstractPiecewiseLinearSlopeParameter, U <: Union{Vector{D}, IS.FlattenIteratorWrapper{D}}, W <: AbstractDeviceFormulation, } where {D <: PSY.Component} curves = _get_parameter_field.((P(),), PSY.get_operation_cost.(devices)) max_tranches = maximum(get_max_tranches.(devices, curves)) return (make_tranche_axis(max_tranches),) end function calc_additional_axes( ::OptimizationContainer, ::P, devices::U, ::DeviceModel{D, W}, ) where { P <: AbstractPiecewiseLinearBreakpointParameter, U <: Union{Vector{D}, IS.FlattenIteratorWrapper{D}}, W <: AbstractDeviceFormulation, } where {D <: PSY.Component} curves = _get_parameter_field.((P(),), PSY.get_operation_cost.(devices)) max_tranches = maximum(get_max_tranches.(devices, curves)) return (make_tranche_axis(max_tranches + 1),) # one more breakpoint than tranches end """ Given a parameter array, get any additional axes, i.e., those that aren't the first (component) or the last (time) """ lookup_additional_axes(parameter_array) = axes(parameter_array)[2:(end - 1)] # Layer of indirection to handle the fact that some parameters come from time series that # represent multiple things (e.g., both slopes and breakpoints come from the same time # series of `FunctionData`). This function is called on every element of the time series # with an expected output axes tuple. _unwrap_for_param(::ParameterType, ts_elem, expected_axs) = ts_elem # For piecewise MarketBidCost-like data, the number of tranches can vary over time, so the # parameter container is sized for the maximum number of tranches and in smaller cases we # have to pad. We do this by creating additional "degenerate" tranches at the top end of the # curve with dx = 0 such that their dispatch variables are constrained to 0. In theory, the # slope shouldn't matter for these degenerate segments. In practice, we'll use slope = 0 so # the term can be more trivially dropped from the objective function. function _unwrap_for_param( ::AbstractPiecewiseLinearSlopeParameter, ts_elem::IS.PiecewiseStepData, expected_axs, ) max_len = length(only(expected_axs)) y_coords = IS.get_y_coords(ts_elem) @assert length(y_coords) <= max_len fill_value = 0.0 # pad with slope = 0 if necessary (see above) padded_y_coords = vcat(y_coords, fill(fill_value, max_len - length(y_coords))) return padded_y_coords end function _unwrap_for_param( ::AbstractPiecewiseLinearBreakpointParameter, ts_elem::IS.PiecewiseStepData, expected_axs, ) max_len = length(only(expected_axs)) x_coords = IS.get_x_coords(ts_elem) @assert length(x_coords) <= max_len fill_value = x_coords[end] # if padding is necessary, repeat the last breakpoint so dx = 0 (see above) padded_x_coords = vcat(x_coords, fill(fill_value, max_len - length(x_coords))) return padded_x_coords end # PERF: compilation hotspot. Switch to TSC. # NOTE direct equivalent of _add_time_series_parameters! for TimeSeriesParameter function _add_parameters!( container::OptimizationContainer, param::T, devices::U, model::DeviceModel{D, W}, ) where { T <: ObjectiveFunctionParameter, U <: Union{Vector{D}, IS.FlattenIteratorWrapper{D}}, W <: AbstractDeviceFormulation, } where {D <: PSY.Component} ts_type = get_default_time_series_type(container) if !(ts_type <: Union{PSY.AbstractDeterministic, PSY.StaticTimeSeries}) error( "add_parameters! for ObjectiveFunctionParameter is not compatible with $ts_type", ) end time_steps = get_time_steps(container) ts_names = String[] device_names = String[] active_devices = D[] for device in devices ts_name = _get_time_series_name(T(), device, model) if PSY.has_time_series(device, ts_type, ts_name) push!(ts_names, ts_name) push!(device_names, PSY.get_name(device)) push!(active_devices, device) else @debug "Skipped time series for $D, $(PSY.get_name(device))" end end if isempty(active_devices) return end jump_model = get_jump_model(container) additional_axes = calc_additional_axes(container, param, active_devices, model) param_container = add_param_container!( container, param, D, _param_to_vars(T(), W()), SOSStatusVariable.NO_VARIABLE, false, _get_expected_time_series_eltype(T()), device_names, additional_axes..., time_steps, ) model_interval = get_interval(get_settings(container)) ts_interval = model_interval param_instance = T() parent_mult = get_multiplier_array_data(param_container) parent_param = get_parameter_array_data(param_container) for (i, (ts_name, device_name, device)) in enumerate(zip(ts_names, device_names, active_devices)) raw_ts_vals = get_time_series_initial_values!( container, ts_type, device, ts_name; interval = ts_interval, ) ts_vals = _unwrap_for_param.(Ref(param_instance), raw_ts_vals, Ref(additional_axes)) @assert all(_size_wrapper.(ts_vals) .== Ref(length.(additional_axes))) # PWL/cost-function path: the parameter values flowing through # `_set_parameter_at!` below are tuples-of-floats from `_unwrap_for_param`; # the multiplier itself is a scalar Float64 (per-device cost weight). _set_multiplier_at!( parent_mult, get_multiplier_value(T(), device, W()), i, ) for step in time_steps _set_parameter_at!(parent_param, jump_model, ts_vals[step], i, step) end end return end function _add_parameters!( container::OptimizationContainer, ::T, service::U, model::ServiceModel{U, V}, ) 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") end ts_name = get_time_series_names(model)[T] time_steps = get_time_steps(container) name = PSY.get_name(service) model_interval = get_interval(get_settings(container)) ts_interval = model_interval ts_uuid = string( IS.get_time_series_uuid( ts_type, service, ts_name; interval = _to_is_interval(ts_interval), ), ) @debug "adding" T U _group = LOG_GROUP_OPTIMIZATION_CONTAINER additional_axes = calc_additional_axes(container, T(), [service], model) parameter_container = add_param_container!( container, T(), U, ts_type, ts_name, [ts_uuid], [name], additional_axes, time_steps; meta = name, ) set_subsystem!(get_attributes(parameter_container), get_subsystem(model)) jump_model = get_jump_model(container) ts_vector = get_time_series(container, service, T(), name; interval = ts_interval) multiplier = get_multiplier_value(T(), service, V()) parent_mult = get_multiplier_array_data(parameter_container) _set_multiplier_at!(parent_mult, Float64(multiplier), 1) parent_param = get_parameter_array_data(parameter_container) for t in time_steps _set_parameter_at!(parent_param, jump_model, ts_vector[t], 1, t) end add_component_name!(get_attributes(parameter_container), name, ts_uuid) return end function _add_parameters!( container::OptimizationContainer, ::T, key::VariableKey{U, D}, model::DeviceModel{D, W}, devices::V, ) where { T <: VariableValueParameter, U <: VariableType, V <: Union{Vector{D}, IS.FlattenIteratorWrapper{D}}, W <: AbstractDeviceFormulation, } where {D <: PSY.Component} @debug "adding" T D U _group = LOG_GROUP_OPTIMIZATION_CONTAINER names = [PSY.get_name(device) for device in devices] time_steps = get_time_steps(container) parameter_container = add_param_container!(container, T(), D, key, names, time_steps) jump_model = get_jump_model(container) parent_mult = get_multiplier_array_data(parameter_container) parent_param = get_parameter_array_data(parameter_container) for (i, d) in enumerate(devices) name = PSY.get_name(d) _set_multiplier_at!( parent_mult, get_parameter_multiplier(T(), d, W()), i, ) if get_variable_warm_start_value(U(), d, W()) === nothing inital_parameter_value = 0.0 else inital_parameter_value = get_variable_warm_start_value(U(), d, W()) end for t in time_steps _set_parameter_at!(parent_param, jump_model, inital_parameter_value, i, t) end end return end function _add_parameters!( container::OptimizationContainer, ::T, key::VariableKey{U, D}, model::DeviceModel{D, W}, devices::V, ) where { T <: OnStatusParameter, U <: OnVariable, V <: Union{Vector{D}, IS.FlattenIteratorWrapper{D}}, W <: AbstractThermalFormulation, } where {D <: PSY.ThermalGen} @debug "adding" T D U _group = LOG_GROUP_OPTIMIZATION_CONTAINER names = [PSY.get_name(device) for device in devices if !PSY.get_must_run(device)] time_steps = get_time_steps(container) parameter_container = add_param_container!(container, T(), D, key, names, time_steps) jump_model = get_jump_model(container) parent_mult = get_multiplier_array_data(parameter_container) parent_param = get_parameter_array_data(parameter_container) # Iterate the same filtered view used to construct `names` so enumeration index # `i` lines up with the parameter container's first axis. for (i, d) in enumerate(Iterators.filter(d -> !PSY.get_must_run(d), devices)) name = PSY.get_name(d) _set_multiplier_at!( parent_mult, get_parameter_multiplier(T(), d, W()), i, ) if get_variable_warm_start_value(U(), d, W()) === nothing inital_parameter_value = 0.0 else inital_parameter_value = get_variable_warm_start_value(U(), d, W()) end for t in time_steps _set_parameter_at!(parent_param, jump_model, inital_parameter_value, i, t) end end return end function _add_parameters!( container::OptimizationContainer, ::T, key::VariableKey{U, D}, model::DeviceModel{D, W}, devices::V, ) where { T <: FixValueParameter, U <: VariableType, V <: Union{Vector{D}, IS.FlattenIteratorWrapper{D}}, W <: AbstractDeviceFormulation, } where {D <: PSY.Component} @debug "adding" T D U _group = LOG_GROUP_OPTIMIZATION_CONTAINER names = [PSY.get_name(device) for device in devices] time_steps = get_time_steps(container) parameter_container = add_param_container!(container, T(), D, key, names, time_steps; meta = "$U") jump_model = get_jump_model(container) parent_mult = get_multiplier_array_data(parameter_container) parent_param = get_parameter_array_data(parameter_container) for (i, d) in enumerate(devices) name = PSY.get_name(d) _set_multiplier_at!( parent_mult, get_parameter_multiplier(T(), d, W()), i, ) if get_variable_warm_start_value(U(), d, W()) === nothing inital_parameter_value = 0.0 else inital_parameter_value = get_variable_warm_start_value(U(), d, W()) end for t in time_steps _set_parameter_at!(parent_param, jump_model, inital_parameter_value, i, t) end end return end function _add_parameters!( container::OptimizationContainer, ::T, key::AuxVarKey{U, D}, model::DeviceModel{D, W}, devices::V, ) where { T <: VariableValueParameter, U <: AuxVariableType, V <: Union{Vector{D}, IS.FlattenIteratorWrapper{D}}, W <: AbstractDeviceFormulation, } where {D <: PSY.Component} @debug "adding" T D U _group = LOG_GROUP_OPTIMIZATION_CONTAINER names = [PSY.get_name(device) for device in devices] time_steps = get_time_steps(container) parameter_container = add_param_container!( container, T(), D, key, names, time_steps, ) jump_model = get_jump_model(container) parent_mult = get_multiplier_array_data(parameter_container) parent_param = get_parameter_array_data(parameter_container) for (i, d) in enumerate(devices) name = PSY.get_name(d) _set_multiplier_at!( parent_mult, get_parameter_multiplier(T(), d, W()), i, ) ini_val = get_initial_parameter_value(T(), d, W()) for t in time_steps _set_parameter_at!(parent_param, jump_model, ini_val, i, t) end end return end function _add_parameters!( container::OptimizationContainer, ::T, devices::V, model::DeviceModel{D, W}, ) where { T <: OnStatusParameter, V <: Union{Vector{D}, IS.FlattenIteratorWrapper{D}}, W <: AbstractDeviceFormulation, } where {D <: PSY.Component} @debug "adding" T D V _group = LOG_GROUP_OPTIMIZATION_CONTAINER # We do this to handle cases where the same parameter is also added as a Feedforward. # When the OnStatusParameter is added without a feedforward it takes a Float value. # This is used to handle the special case of compact formulations. !isempty(get_feedforwards(model)) && return names = [PSY.get_name(device) for device in devices] time_steps = get_time_steps(container) parameter_container = add_param_container!( container, T(), D, VariableKey(OnVariable, D), names, time_steps, ) jump_model = get_jump_model(container) parent_mult = get_multiplier_array_data(parameter_container) parent_param = get_parameter_array_data(parameter_container) for (i, d) in enumerate(devices) name = PSY.get_name(d) _set_multiplier_at!( parent_mult, get_parameter_multiplier(T(), d, W()), i, ) ini_val = get_initial_parameter_value(T(), d, W()) for t in time_steps _set_parameter_at!(parent_param, jump_model, ini_val, i, t) end end return end function _add_parameters!( container::OptimizationContainer, ::T, key::VariableKey{U, S}, model::ServiceModel{S, W}, devices::V, ) where { S <: PSY.AbstractReserve, T <: VariableValueParameter, U <: VariableType, V <: Union{Vector{D}, IS.FlattenIteratorWrapper{D}}, W <: AbstractReservesFormulation, } where {D <: PSY.Component} @debug "adding" T D U _group = LOG_GROUP_OPTIMIZATION_CONTAINER contributing_devices = get_contributing_devices(model) names = [PSY.get_name(device) for device in contributing_devices] time_steps = get_time_steps(container) parameter_container = add_param_container!( container, T(), S, key, names, time_steps; meta = get_service_name(model), ) jump_model = get_jump_model(container) parent_mult = get_multiplier_array_data(parameter_container) parent_param = get_parameter_array_data(parameter_container) multiplier = get_parameter_multiplier(T(), S, W()) ini_val = get_initial_parameter_value(T(), S, W()) for (i, d) in enumerate(contributing_devices) name = PSY.get_name(d) _set_multiplier_at!(parent_mult, multiplier, i) for t in time_steps _set_parameter_at!(parent_param, jump_model, ini_val, i, t) end end return end ================================================ FILE: src/parameters/update_container_parameter_values.jl ================================================ function _update_parameter_values!( ::AbstractArray{T}, ::ParameterType, ::NoAttributes, args..., ) where {T <: Union{Float64, JuMP.VariableRef}} end ######################## Methods to update Parameters from Time Series ##################### function _set_param_value!( param::JuMPVariableTensor, value::Union{T, AbstractVector{T}}, name::String, t::Int, ) where {T <: ValidDataParamEltypes} fix_maybe_broadcast!(param, value, (name, t)) return end function _set_param_value!( param::DenseAxisArray{T, 2}, value::T, name::String, t::Int, ) where {T <: ValidDataParamEltypes} param[name, t] = value return end function _set_param_value!( param::DenseAxisArray{T}, value::Union{T, AbstractVector{T}}, name::String, t::Int, ) where {T <: ValidDataParamEltypes} assign_maybe_broadcast!(param, value, (name, t)) return end function _update_parameter_values!( parameter_array::DenseAxisArray{T}, ::W, attributes::TimeSeriesAttributes{U}, ::Type{V}, model::DecisionModel, ::DatasetContainer{InMemoryDataset}, ) where { T <: Union{JuMP.VariableRef, Float64}, U <: PSY.AbstractDeterministic, V <: PSY.Component, W <: ParameterType, } 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) model_interval = get_interval(get_settings(model)) ts_interval = model_interval subsystem = get_subsystem(attributes) template = get_template(model) if isempty(subsystem) device_model = get_model(template, V) else device_model = get_model(template, V, subsystem) end components = get_available_components(device_model, get_system(model)) # Hoist the underlying dense storage and per-component name lookup once so each # write skips DenseAxisArray's String-keyed axis lookup. `additional_axes` is # invariant for the lifetime of this update call. parent_param = parameter_array.data name_lookup = parameter_array.lookup[1] additional_axes = lookup_additional_axes(parameter_array) ts_uuids = Set{String}() for component in components if !PSY.has_time_series(component, U, ts_name) continue end ts_uuid = _get_ts_uuid(attributes, PSY.get_name(component)) if !(ts_uuid in ts_uuids) ts_vector = get_time_series_values!( U, model, component, ts_name, initial_forecast_time, horizon; interval = ts_interval, ) i_param = name_lookup[ts_uuid] for (t, value) in enumerate(ts_vector) # first two axes of parameter_array are component, time; we care about any additional ones unwrapped_value = _unwrap_for_param(W(), value, additional_axes) if !all(isfinite.(unwrapped_value)) error("The value for the time series $(ts_name) is not finite. \ Check that the data in the time series is valid.") end _set_param_value_at!(parent_param, unwrapped_value, i_param, t) end push!(ts_uuids, ts_uuid) end end return end # Time-series parameter update for reduced ACTransmission branches function _update_parameter_values!( parameter_array::DenseAxisArray{T}, ::W, attributes::TimeSeriesAttributes{U}, ::Type{V}, model::DecisionModel, ::DatasetContainer{InMemoryDataset}, ) where { T <: Union{JuMP.VariableRef, Float64}, U <: PSY.AbstractDeterministic, V <: PSY.ACTransmission, W <: TimeSeriesParameter, } initial_forecast_time = get_current_time(model) horizon = get_time_steps(get_optimization_container(model))[end] ts_name = get_time_series_name(attributes) model_interval = get_interval(get_settings(model)) ts_interval = model_interval network_model = get_network_model(get_template(model)) net_reduction_data = network_model.network_reduction all_branch_maps_by_type = PNM.get_all_branch_maps_by_type(net_reduction_data) if !haskey(net_reduction_data.name_to_arc_map, V) return end # Hoist the underlying dense storage and per-component name lookup once so each # write skips DenseAxisArray's String-keyed axis lookup. parent_param = parameter_array.data name_lookup = parameter_array.lookup[1] ts_uuids_updated = Set{String}() for (name, (arc, reduction)) in PNM.get_name_to_arc_map(net_reduction_data, V) reduction_entry = all_branch_maps_by_type[reduction][V][arc] if !PNM.has_time_series(reduction_entry, U, ts_name) continue end device_with_time_series = PNM.get_device_with_time_series(reduction_entry, U, ts_name) ts_uuid = _get_ts_uuid(attributes, name) if ts_uuid in ts_uuids_updated continue end ts_vector = get_time_series_values!( U, model, device_with_time_series, ts_name, initial_forecast_time, horizon; interval = ts_interval, ) i_param = name_lookup[ts_uuid] for (t, value) in enumerate(ts_vector) if !isfinite(value) error( "The value for the time series $(ts_name) is not finite. \ Check that the data in the time series is valid.", ) end _set_param_value_at!(parent_param, value, i_param, t) end push!(ts_uuids_updated, ts_uuid) end return end function _update_parameter_values!( parameter_array::DenseAxisArray{T}, ::ParameterType, attributes::TimeSeriesAttributes{U}, service::V, model::DecisionModel, ::DatasetContainer{InMemoryDataset}, ) where { T <: Union{JuMP.VariableRef, Float64}, U <: PSY.AbstractDeterministic, V <: PSY.Service, } 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) model_interval = get_interval(get_settings(model)) ts_interval = model_interval ts_uuid = _get_ts_uuid(attributes, PSY.get_name(service)) ts_vector = get_time_series_values!( U, model, service, get_time_series_name(attributes), initial_forecast_time, horizon; interval = ts_interval, ) # Hoist the underlying dense storage and resolve the row index once so the # per-time-step writes skip DenseAxisArray's String-keyed axis lookup. parent_param = parameter_array.data i_param = parameter_array.lookup[1][ts_uuid] for (t, value) in enumerate(ts_vector) if !isfinite(value) error("The value for the time series $(ts_name) is not finite. \ Check that the data in the time series is valid.") end _set_param_value_at!(parent_param, value, i_param, t) end end function _update_parameter_values!( parameter_array::DenseAxisArray{T}, ::ParameterType, attributes::TimeSeriesAttributes{U}, ::Type{V}, model::EmulationModel, ::DatasetContainer{InMemoryDataset}, ) where {T <: Union{JuMP.VariableRef, Float64}, U <: PSY.SingleTimeSeries, V <: PSY.Device} initial_forecast_time = get_current_time(model) template = get_template(model) device_model = get_model(template, V) components = get_available_components(device_model, get_system(model)) ts_name = get_time_series_name(attributes) ts_resolution = get_resolution(get_settings(model)) # Hoist the underlying dense storage and per-component name lookup once. parent_param = parameter_array.data name_lookup = parameter_array.lookup[1] ts_uuids = Set{String}() for component in components ts_uuid = _get_ts_uuid(attributes, PSY.get_name(component)) if !(ts_uuid in ts_uuids) # Note: This interface reads one single value per component at a time. value = get_time_series_values!( U, model, component, get_time_series_name(attributes), initial_forecast_time; resolution = ts_resolution, )[1] if !isfinite(value) error("The value for the time series $(ts_name) is not finite. \ Check that the data in the time series is valid.") end _set_param_value_at!(parent_param, value, name_lookup[ts_uuid], 1) push!(ts_uuids, ts_uuid) end end return end function _update_parameter_values!( parameter_array::DenseAxisArray{T}, ::ParameterType, attributes::TimeSeriesAttributes{U}, service::V, model::EmulationModel, ::DatasetContainer{InMemoryDataset}, ) where {T <: Union{JuMP.VariableRef, Float64}, U <: PSY.SingleTimeSeries, V <: PSY.Service} initial_forecast_time = get_current_time(model) ts_name = get_time_series_name(attributes) ts_uuid = _get_ts_uuid(attributes, PSY.get_name(service)) ts_resolution = get_resolution(get_settings(model)) # Note: This interface reads one single value per component at a time. value = get_time_series_values!( U, model, service, get_time_series_name(attributes), initial_forecast_time; resolution = ts_resolution, )[1] if !isfinite(value) error("The value for the time series $(ts_name) is not finite. \ Check that the data in the time series is valid.") end parent_param = parameter_array.data i_param = parameter_array.lookup[1][ts_uuid] _set_param_value_at!(parent_param, value, i_param, 1) return end function _update_parameter_values!( parameter_array::DenseAxisArray{T}, ::ParameterType, attributes::VariableValueAttributes, ::Type{<:PSY.Device}, model::DecisionModel, state::DatasetContainer{InMemoryDataset}, ) where {T <: Union{JuMP.VariableRef, Float64}} current_time = get_current_time(model) state_values = get_dataset_values(state, get_attribute_key(attributes)) component_names, time = axes(parameter_array) model_resolution = get_resolution(model) state_data = get_dataset(state, get_attribute_key(attributes)) state_timestamps = state_data.timestamps max_state_index = get_num_rows(state_data) if model_resolution < state_data.resolution t_step = 1 else t_step = model_resolution ÷ state_data.resolution end state_data_index = find_timestamp_index(state_timestamps, current_time) sim_timestamps = range(current_time; step = model_resolution, length = time[end]) # Hoist underlying dense storage and per-axis name lookups so the inner loop # can index by integer pair, skipping DenseAxisArray's String-keyed lookup. parent_param = parameter_array.data parent_state = state_values.data param_lookup = parameter_array.lookup[1] state_lookup = state_values.lookup[1] for t in time timestamp_ix = min(max_state_index, state_data_index + t_step) @debug "parameter horizon is over the step" max_state_index > state_data_index + 1 if state_timestamps[timestamp_ix] <= sim_timestamps[t] state_data_index = timestamp_ix end for name in component_names i_state = state_lookup[name] i_param = param_lookup[name] state_value = parent_state[i_state, state_data_index] if !isfinite(state_value) error( "The value for the system state used in $(encode_key_as_string(get_attribute_key(attributes))) is not a finite value $(state_value) \ This is commonly caused by referencing a state value at a time when such decision hasn't been made. \ Consider reviewing your models' horizon and interval definitions", ) end _set_param_value_at!(parent_param, state_value, i_param, t) end end return end function _update_parameter_values!( parameter_array::DenseAxisArray{T}, ::ParameterType, attributes::VariableValueAttributes, ::PSY.Reserve, model::DecisionModel, state::DatasetContainer{InMemoryDataset}, ) where {T <: Union{JuMP.VariableRef, Float64}} current_time = get_current_time(model) state_values = get_dataset_values(state, get_attribute_key(attributes)) component_names, time = axes(parameter_array) model_resolution = get_resolution(model) state_data = get_dataset(state, get_attribute_key(attributes)) state_timestamps = state_data.timestamps max_state_index = get_num_rows(state_data) if model_resolution < state_data.resolution t_step = 1 else t_step = model_resolution ÷ state_data.resolution end state_data_index = find_timestamp_index(state_timestamps, current_time) sim_timestamps = range(current_time; step = model_resolution, length = time[end]) # Hoist underlying dense storage and per-axis name lookups so the inner loop # can index by integer pair, skipping DenseAxisArray's String-keyed lookup. parent_param = parameter_array.data parent_state = state_values.data param_lookup = parameter_array.lookup[1] state_lookup = state_values.lookup[1] for t in time timestamp_ix = min(max_state_index, state_data_index + t_step) @debug "parameter horizon is over the step" max_state_index > state_data_index + 1 if state_timestamps[timestamp_ix] <= sim_timestamps[t] state_data_index = timestamp_ix end for name in component_names i_state = state_lookup[name] i_param = param_lookup[name] state_value = parent_state[i_state, state_data_index] if !isfinite(state_value) error( "The value for the system state used in $(encode_key_as_string(get_attribute_key(attributes))) is not a finite value $(state_value) \ This is commonly caused by referencing a state value at a time when such decision hasn't been made. \ Consider reviewing your models' horizon and interval definitions", ) end _set_param_value_at!(parent_param, state_value, i_param, t) end end return end function _update_parameter_values!( parameter_array::DenseAxisArray{T}, ::ParameterType, attributes::VariableValueAttributes{VariableKey{OnVariable, U}}, ::Type{U}, model::DecisionModel, state::DatasetContainer{InMemoryDataset}, ) where {T <: Union{JuMP.VariableRef, Float64}, U <: PSY.Device} current_time = get_current_time(model) state_values = get_dataset_values(state, get_attribute_key(attributes)) component_names, time = axes(parameter_array) model_resolution = get_resolution(model) state_data = get_dataset(state, get_attribute_key(attributes)) state_timestamps = state_data.timestamps max_state_index = get_num_rows(state_data) if model_resolution < state_data.resolution t_step = 1 else t_step = model_resolution ÷ state_data.resolution end state_data_index = find_timestamp_index(state_timestamps, current_time) sim_timestamps = range(current_time; step = model_resolution, length = time[end]) # Hoist underlying dense storage and per-axis name lookups so the inner loop # can index by integer pair, skipping DenseAxisArray's String-keyed lookup. parent_param = parameter_array.data parent_state = state_values.data param_lookup = parameter_array.lookup[1] state_lookup = state_values.lookup[1] for t in time timestamp_ix = min(max_state_index, state_data_index + t_step) @debug "parameter horizon is over the step" max_state_index > state_data_index + 1 if state_timestamps[timestamp_ix] <= sim_timestamps[t] state_data_index = timestamp_ix end for name in component_names i_state = state_lookup[name] i_param = param_lookup[name] value = round(parent_state[i_state, state_data_index]) if !isfinite(value) error( "The value for the system state used in $(encode_key_as_string(get_attribute_key(attributes))) is not a finite value $(value) \ This is commonly caused by referencing a state value at a time when such decision hasn't been made. \ Consider reviewing your models' horizon and interval definitions", ) end if 0.0 > value || value > 1.0 error( "The value for the system state used in $(encode_key_as_string(get_attribute_key(attributes))): $(value) is out of the [0, 1] range", ) end _set_param_value_at!(parent_param, value, i_param, t) end end return end function _update_parameter_values!( parameter_array::DenseAxisArray{T}, ::ParameterType, attributes::VariableValueAttributes, ::Type{<:PSY.Component}, model::EmulationModel, state::DatasetContainer{InMemoryDataset}, ) where {T <: Union{JuMP.VariableRef, Float64}} current_time = get_current_time(model) state_values = get_dataset_values(state, get_attribute_key(attributes)) component_names, _ = axes(parameter_array) state_data = get_dataset(state, get_attribute_key(attributes)) state_timestamps = state_data.timestamps state_data_index = find_timestamp_index(state_timestamps, current_time) # Hoist underlying dense storage and per-axis name lookups. parent_param = parameter_array.data parent_state = state_values.data param_lookup = parameter_array.lookup[1] state_lookup = state_values.lookup[1] for name in component_names i_state = state_lookup[name] i_param = param_lookup[name] _set_param_value_at!( parent_param, parent_state[i_state, state_data_index], i_param, 1, ) end return end function _update_parameter_values!( parameter_array::DenseAxisArray{T}, ::ParameterType, attributes::VariableValueAttributes{VariableKey{OnVariable, U}}, ::Type{<:PSY.Component}, model::EmulationModel, state::DatasetContainer{InMemoryDataset}, ) where {T <: Union{JuMP.VariableRef, Float64}, U <: PSY.Component} current_time = get_current_time(model) state_values = get_dataset_values(state, get_attribute_key(attributes)) component_names, _ = axes(parameter_array) state_data = get_dataset(state, get_attribute_key(attributes)) state_timestamps = state_data.timestamps state_data_index = find_timestamp_index(state_timestamps, current_time) has_outage = haskey( get_parameters_values(state), ISOPT.ParameterKey{ AvailableStatusParameter, U, }( "", ), ) if has_outage status_values = get_dataset_values( state, ISOPT.ParameterKey{ AvailableStatusParameter, U, }( "", ), ) status_data = get_dataset( state, ISOPT.ParameterKey{ AvailableStatusParameter, U, }( "", ), ) status_timestamps = status_data.timestamps status_data_index = find_timestamp_index(status_timestamps, current_time) parent_status = status_values.data # `_AxisLookup{Dict{String,Int64}}` wraps a `Dict`; reach for `.data` # so we can `haskey` and integer-index without a String-keyed scan. status_lookup_dict = status_values.lookup[1].data end # Hoist underlying dense storage and per-axis name lookups for the inner loop. parent_param = parameter_array.data parent_state = state_values.data param_lookup = parameter_array.lookup[1] state_lookup = state_values.lookup[1] for name in component_names i_state = state_lookup[name] i_param = param_lookup[name] if has_outage && haskey(status_lookup_dict, name) && parent_status[status_lookup_dict[name], status_data_index] == 0.0 && round(parent_state[i_state, state_data_index]) == 1.0 # Override feed forward based on status parameter value = 0.0 else value = round(parent_state[i_state, state_data_index]) end if !isfinite(value) error( "The value for the system state used in $(encode_key_as_string(get_attribute_key(attributes))) is not a finite value $(value) \ This is commonly caused by referencing a state value at a time when such decision hasn't been made. \ Consider reviewing your models' horizon and interval definitions", ) end if 0.0 > value || value > 1.0 error( "The value for the system state used in $(encode_key_as_string(get_attribute_key(attributes))): $(value) is out of the [0, 1] range", ) end _set_param_value_at!(parent_param, value, i_param, 1) end return end function _update_parameter_values!( ::AbstractArray{T}, ::ParameterType, ::VariableValueAttributes, ::Type{<:PSY.Component}, ::EmulationModel, ::EmulationModelStore, ) where {T <: Union{JuMP.VariableRef, Float64}} error("The emulation model has parameters that can't be updated from its results") return end function _update_parameter_values!( parameter_array::DenseAxisArray{T}, attributes::EventParametersAttributes{W, U}, ::Type{V}, model::DecisionModel, state::DatasetContainer{InMemoryDataset}, ) where { T <: Union{JuMP.VariableRef, Float64}, W <: PSY.Contingency, U <: EventParameter, V <: PSY.Component, } current_time = get_current_time(model) # state_values = get_dataset_values(state, get_attribute_key(attributes)) state_values = get_dataset_values(state, U(), V) component_names, time = axes(parameter_array) model_resolution = get_resolution(model) state_data = get_dataset(state, U(), V) state_timestamps = state_data.timestamps max_state_index = get_num_rows(state_data) if model_resolution < state_data.resolution t_step = 1 else t_step = model_resolution ÷ state_data.resolution end state_data_index = find_timestamp_index(state_timestamps, current_time) sim_timestamps = range(current_time; step = model_resolution, length = time[end]) # Hoist underlying dense storage and per-axis name lookups for the inner loop. parent_param = parameter_array.data parent_state = state_values.data param_lookup = parameter_array.lookup[1] state_lookup = state_values.lookup[1] for t in time timestamp_ix = min(max_state_index, state_data_index + t_step) @debug "parameter horizon is over the step" max_state_index > state_data_index + 1 if state_timestamps[timestamp_ix] <= sim_timestamps[t] state_data_index = timestamp_ix end for name in component_names i_state = state_lookup[name] i_param = param_lookup[name] value = parent_state[i_state, state_data_index] if !isfinite(value) error( "The value for the system state used in $(encode_key_as_string(get_attribute_key(attributes))) is not a finite value $(value) \ This is commonly caused by referencing a state value at a time when such decision hasn't been made. \ Consider reviewing your models' horizon and interval definitions", ) end _set_param_value_at!(parent_param, value, i_param, t) end end return end function _update_parameter_values!( parameter_array::DenseAxisArray{T}, ::EventParametersAttributes{W, U}, ::Type{V}, model::EmulationModel, state::DatasetContainer{InMemoryDataset}, ) where { T <: Union{JuMP.VariableRef, Float64}, W <: PSY.Contingency, U <: EventParameter, V <: PSY.Component, } current_time = get_current_time(model) state_values = get_dataset_values(state, U(), V) component_names, _ = axes(parameter_array) state_data = get_dataset(state, U(), V) state_timestamps = state_data.timestamps state_data_index = find_timestamp_index(state_timestamps, current_time) # Hoist underlying dense storage and per-axis name lookups. parent_param = parameter_array.data parent_state = state_values.data param_lookup = parameter_array.lookup[1] state_lookup = state_values.lookup[1] for name in component_names i_state = state_lookup[name] i_param = param_lookup[name] _set_param_value_at!( parent_param, parent_state[i_state, state_data_index], i_param, 1, ) end return end """ Update parameter function an OperationModel """ 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 # 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, T(), parameter_attributes, U, model, input) return end function update_container_parameter_values!( optimization_container::OptimizationContainer, model::OperationModel, key::ParameterKey{T, U}, input::DatasetContainer{InMemoryDataset}, ) where {T <: EventParameter, U <: PSY.Component} # Enable again for detailed debugging # TimerOutputs.@timeit RUN_SIMULATION_TIMER "$T $U Parameter Update" begin # 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) return end function update_container_parameter_values!( optimization_container::OptimizationContainer, model::OperationModel, key::ParameterKey{T, U}, input::DatasetContainer{InMemoryDataset}, ) where {T <: ObjectiveFunctionParameter, 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) # 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, T(), parameter_multiplier, parameter_attributes, U, model, input, ) 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 instantiate 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, T(), parameter_multiplier, parameter_attributes, U, model, input, ) return end function update_container_parameter_values!( optimization_container::OptimizationContainer, model::OperationModel, key::ParameterKey{FixValueParameter, U}, input::DatasetContainer{InMemoryDataset}, ) 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, FixValueParameter(), parameter_attributes, U, 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{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) service = PSY.get_component(U, get_system(model), key.meta) @assert service !== nothing _update_parameter_values!( parameter_array, FixValueParameter(), parameter_attributes, U, 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} # 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) service = PSY.get_component(U, get_system(model), key.meta) @assert service !== nothing _update_parameter_values!( parameter_array, T(), parameter_attributes, service, model, input, ) return end # This method is included to avoid ambiguities function update_container_parameter_values!( optimization_container::OptimizationContainer, model::OperationModel, key::ParameterKey{T, U}, input::DatasetContainer{InMemoryDataset}, ) where {T <: EventParameter, U <: PSY.Service} return end ================================================ FILE: src/parameters/update_cost_parameters.jl ================================================ function _update_parameter_values!( parameter_array::DenseAxisArray, ::T, parameter_multiplier::JuMPFloatArray, attributes::CostFunctionAttributes, ::Type{V}, model::DecisionModel, ::DatasetContainer{InMemoryDataset}, ) where {T <: ObjectiveFunctionParameter, V <: PSY.Component} initial_forecast_time = get_current_time(model) # Function not well defined for DecisionModels time_steps = get_time_steps(get_optimization_container(model)) horizon = time_steps[end] container = get_optimization_container(model) @assert !is_synchronized(container) template = get_template(model) device_model = get_model(template, V) components = get_available_components(device_model, get_system(model)) for component in components name = PSY.get_name(component) op_cost = PSY.get_operation_cost(component) # `handle_variable_cost_parameter` is responsible for figuring out whether there is # actually time variance for this particular component and, if so, performing the update handle_variable_cost_parameter( T(), op_cost, component, name, parameter_array, parameter_multiplier, attributes, container, initial_forecast_time, horizon, ) end return end # We only support certain time series costs for PSY.OfferCurveCost, nothing to do for all the others # We group them this way because we implement them that way: avoids method ambiguity issues handle_variable_cost_parameter( ::Union{StartupCostParameter, ShutdownCostParameter, AbstractCostAtMinParameter}, op_cost::PSY.OperationalCost, args...) = @assert !(op_cost isa PSY.OfferCurveCost) handle_variable_cost_parameter( ::AbstractPiecewiseLinearSlopeParameter, op_cost::PSY.OperationalCost, args...) = @assert !(op_cost isa PSY.OfferCurveCost) # typically used just with 1 arg, _get_parameter_field(T(), operation_cost). _get_parameter_field(::StartupCostParameter, args...; kwargs...) = PSY.get_start_up(args...; kwargs...) _get_parameter_field(::ShutdownCostParameter, args...; kwargs...) = PSY.get_shut_down(args...; kwargs...) _get_parameter_field(::IncrementalCostAtMinParameter, args...; kwargs...) = PSY.get_incremental_initial_input(args...; kwargs...) _get_parameter_field(::DecrementalCostAtMinParameter, args...; kwargs...) = PSY.get_decremental_initial_input(args...; kwargs...) _get_parameter_field( ::Union{ IncrementalPiecewiseLinearSlopeParameter, IncrementalPiecewiseLinearBreakpointParameter, }, args...; kwargs..., ) = get_output_offer_curves(args...; kwargs...) _get_parameter_field( ::Union{ DecrementalPiecewiseLinearSlopeParameter, DecrementalPiecewiseLinearBreakpointParameter, }, args...; kwargs..., ) = get_input_offer_curves(args...; kwargs...) _maybe_tuple(::StartupCostParameter, value) = Tuple(value) _maybe_tuple(::ShutdownCostParameter, value) = value _maybe_tuple(::AbstractCostAtMinParameter, value) = value function handle_variable_cost_parameter( param::Union{StartupCostParameter, ShutdownCostParameter, AbstractCostAtMinParameter}, op_cost::PSY.OfferCurveCost, component, name, parameter_array, parameter_multiplier, attributes, container, initial_forecast_time, horizon, ) is_time_variant(_get_parameter_field(param, op_cost)) || return ts_vector = _get_parameter_field(param, component, op_cost; start_time = initial_forecast_time, len = horizon, ) for (t, value) in enumerate(TimeSeries.values(ts_vector)) # startup needs Tuple(value), rest just value. (slight type instability) _set_param_value!(parameter_array, _maybe_tuple(param, value), name, t) update_variable_cost!( param, container, parameter_array, parameter_multiplier, attributes, component, t, ) end return end function handle_variable_cost_parameter( slope_param::T, op_cost::PSY.OfferCurveCost, component, name, parameter_array, parameter_multiplier, attributes, container, initial_forecast_time, horizon, ) where {T <: AbstractPiecewiseLinearSlopeParameter} is_time_variant(_get_parameter_field(slope_param, op_cost)) || return ts_vector = _get_parameter_field(slope_param, component, op_cost; start_time = initial_forecast_time, len = horizon, ) for (t, value::PSY.PiecewiseStepData) in enumerate(TimeSeries.values(ts_vector)) unwrapped_value = _unwrap_for_param(T(), value, lookup_additional_axes(parameter_array)) _set_param_value!(parameter_array, unwrapped_value, name, t) update_variable_cost!( slope_param, container, value, # intentionally passing the PiecewiseStepData here, not the unwrapped parameter_multiplier, attributes, component, t, ) end return end function handle_variable_cost_parameter( ::FuelCostParameter, op_cost::PSY.ThermalGenerationCost, component, name, parameter_array, parameter_multiplier, attributes, container, initial_forecast_time, horizon, ) fuel_curve = PSY.get_variable(op_cost) # Nothing to update for this component if we don't have a fuel cost time series (fuel_curve isa PSY.FuelCurve && is_time_variant(PSY.get_fuel_cost(fuel_curve))) || return ts_vector = PSY.get_fuel_cost( component; start_time = initial_forecast_time, len = horizon, ) fuel_cost_forecast_values = TimeSeries.values(ts_vector) for (t, value) in enumerate(fuel_cost_forecast_values) # TODO: MBC Is this compact power attribute being used? if attributes.uses_compact_power # TODO implement this value, _ = _convert_variable_cost(value) end _set_param_value!(parameter_array, value, name, t) update_variable_cost!( FuelCostParameter(), container, parameter_array, parameter_multiplier, attributes, component, fuel_curve, t, ) end return end _linear_block_param(::Type{IncrementalPiecewiseLinearSlopeParameter}) = PiecewiseLinearBlockIncrementalOffer() _linear_block_param(::Type{DecrementalPiecewiseLinearSlopeParameter}) = PiecewiseLinearBlockDecrementalOffer() function _update_pwl_cost_expression( ::P, container::OptimizationContainer, ::Type{T}, component_name::String, time_period::Int, cost_data::PSY.PiecewiseStepData, ) where {P <: AbstractPiecewiseLinearSlopeParameter, T <: PSY.Component} pwl_var_container = get_variable(container, _linear_block_param(P), T) resolution = get_resolution(container) dt = Dates.value(resolution) / MILLISECONDS_IN_HOUR gen_cost = JuMP.AffExpr(0.0) slopes = PSY.get_y_coords(cost_data) for i in 1:length(cost_data) JuMP.add_to_expression!( gen_cost, slopes[i] * dt, pwl_var_container[(component_name, i, time_period)], ) end return gen_cost end # For multi-start variables, we need to get a subset of the parameter _index_into_param(cost_data, ::T) where {T <: Union{StartVariable, MultiStartVariable}} = start_up_cost(cost_data, T()) _index_into_param(cost_data, ::VariableType) = cost_data get_update_multiplier(::DecrementalCostAtMinParameter) = -1.0 get_update_multiplier(::IncrementalCostAtMinParameter) = 1.0 get_update_multiplier(::ObjectiveFunctionParameter) = 1.0 # Mirrors the per-component decomposition done at build time, so recurrent solves # update the same constituent expression that contributed to ProductionCostExpression. _constituent_cost_expression(::StartupCostParameter) = StartUpCostExpression _constituent_cost_expression(::ShutdownCostParameter) = ShutDownCostExpression _constituent_cost_expression(::AbstractCostAtMinParameter) = FixedCostExpression # General case function update_variable_cost!( parameter::ObjectiveFunctionParameter, container::OptimizationContainer, parameter_array::DenseAxisArray{T}, parameter_multiplier::JuMPFloatArray, attributes::CostFunctionAttributes{T}, component::U, time_period::Int, ) where {T, U <: PSY.Component} component_name = PSY.get_name(component) cost_data = parameter_array[component_name, time_period] mult_ = parameter_multiplier[component_name, time_period] mult2 = get_update_multiplier(parameter) constituent_type = _constituent_cost_expression(parameter) for MyVariableType in get_variable_types(attributes) variable = get_variable(container, MyVariableType(), U) my_cost_data = _index_into_param(cost_data, MyVariableType()) iszero(my_cost_data) && continue cost_expr = variable[component_name, time_period] * my_cost_data * mult_ * mult2 add_to_objective_variant_expression!(container, cost_expr) set_expression!( container, ProductionCostExpression, cost_expr, component, time_period, ) set_expression!(container, constituent_type, cost_expr, component, time_period) end return end get_update_multiplier(::IncrementalPiecewiseLinearSlopeParameter) = 1.0 get_update_multiplier(::DecrementalPiecewiseLinearSlopeParameter) = -1.0 # Special case for PiecewiseStepData function update_variable_cost!( slope_param::AbstractPiecewiseLinearSlopeParameter, container::OptimizationContainer, function_data::PSY.PiecewiseStepData, parameter_multiplier::JuMPFloatArray, ::CostFunctionAttributes, component::T, time_period::Int, ) where {T <: PSY.Component} component_name = PSY.get_name(component) # TODO handle per-tranche multiplier if necessary mult_ = 1.0 # parameter_multiplier[component_name, time_period, 1] mult2 = get_update_multiplier(slope_param) converted_data = get_piecewise_curve_per_system_unit( function_data, PSY.UnitSystem.NATURAL_UNITS, # PSY's cost_function_timeseries.jl says this will always be natural units get_base_power(container), PSY.get_base_power(component), ) gen_cost = _update_pwl_cost_expression( slope_param, container, T, component_name, time_period, converted_data, ) add_to_objective_variant_expression!(container, mult2 * mult_ * gen_cost) set_expression!(container, ProductionCostExpression, gen_cost, component, time_period) set_expression!(container, FuelCostExpression, gen_cost, component, time_period) return end # Special case for fuel cost function update_variable_cost!( ::FuelCostParameter, container::OptimizationContainer, parameter_array::JuMPFloatArray, parameter_multiplier::JuMPFloatArray, ::CostFunctionAttributes{Float64}, component::T, fuel_curve::PSY.FuelCurve, time_period::Int, ) where {T <: PSY.Component} component_name = PSY.get_name(component) fuel_cost = parameter_array[component_name, time_period] if all(iszero.(last.(fuel_cost))) return end mult_ = parameter_multiplier[component_name, time_period] expression = get_expression(container, FuelConsumptionExpression(), T) cost_expr = expression[component_name, time_period] * fuel_cost * mult_ add_to_objective_variant_expression!(container, cost_expr) set_expression!(container, ProductionCostExpression, cost_expr, component, time_period) set_expression!(container, FuelCostExpression, cost_expr, component, time_period) return end ================================================ FILE: src/parameters/update_parameters.jl ================================================ """ Update parameter function an OperationModel """ function update_parameter_values!( model::OperationModel, key::ParameterKey{T, U}, simulation_state::SimulationState, ) 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) input = get_decision_states(simulation_state) update_container_parameter_values!(optimization_container, model, key, input) parameter_attributes = get_parameter_attributes(optimization_container, key) IS.@record :execution ParameterUpdateEvent( T, U, parameter_attributes, get_current_timestamp(model), get_name(model), ) #end return end function _fix_parameter_value!( container::OptimizationContainer, parameter_array::DenseAxisArray{Float64, 2}, parameter_attributes::VariableValueAttributes, ) affected_variable_keys = parameter_attributes.affected_keys @assert !isempty(affected_variable_keys) # Hoist underlying dense storage for the parameter array once. The variable # array's storage is hoisted per affected key (different arrays per key). parent_param = parameter_array.data component_names, time = axes(parameter_array) param_lookup = parameter_array.lookup[1] for var_key in affected_variable_keys variable = get_variable(container, var_key) parent_var = variable.data var_lookup = variable.lookup[1] for name in component_names i_param = param_lookup[name] i_var = var_lookup[name] for t in time JuMP.fix( parent_var[i_var, t], parent_param[i_param, t]; force = true, ) end end end return end function update_parameter_values!( model::OperationModel, key::ParameterKey{FixValueParameter, T}, simulation_state::SimulationState, ) where {T <: 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) parameter_attributes = get_parameter_attributes(optimization_container, key) service = PSY.get_component(T, get_system(model), key.meta) @assert service !== nothing input = get_decision_states(simulation_state) _update_parameter_values!( parameter_array, FixValueParameter(), parameter_attributes, service, 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 ================================================ FILE: src/services_models/agc.jl ================================================ #! format: off get_variable_multiplier(_, ::Type{<:PSY.AGC}, ::AbstractAGCFormulation) = NaN ########################## ActivePowerVariable, AGC ########################### ########################## SteadyStateFrequencyDeviation ################################## get_variable_binary(::SteadyStateFrequencyDeviation, ::Type{<:PSY.AGC}, ::AbstractAGCFormulation) = false get_variable_binary(::ActivePowerVariable, ::Type{<:PSY.Area}, ::AbstractAGCFormulation) = false ########################## SmoothACE, AggregationTopology ########################### get_variable_binary(::SmoothACE, ::Type{<:PSY.AggregationTopology}, ::AbstractAGCFormulation) = false get_variable_binary(::SmoothACE, ::Type{<:PSY.AGC}, ::AbstractAGCFormulation) = false ########################## DeltaActivePowerUpVariable, AGC ########################### get_variable_binary(::DeltaActivePowerUpVariable, ::Type{<:PSY.AGC}, ::AbstractAGCFormulation) = false get_variable_lower_bound(::DeltaActivePowerUpVariable, ::PSY.AGC, ::AbstractAGCFormulation) = 0.0 ########################## DeltaActivePowerDownVariable, AGC ########################### get_variable_binary(::DeltaActivePowerDownVariable, ::Type{<:PSY.AGC}, ::AbstractAGCFormulation) = false get_variable_lower_bound(::DeltaActivePowerDownVariable, ::PSY.AGC, ::AbstractAGCFormulation) = 0.0 ########################## AdditionalDeltaPowerUpVariable, Area ########################### get_variable_binary(::AdditionalDeltaActivePowerUpVariable, ::Type{<:PSY.Area}, ::AbstractAGCFormulation) = false get_variable_lower_bound(::AdditionalDeltaActivePowerUpVariable, ::PSY.Area, ::AbstractAGCFormulation) = 0.0 ########################## AdditionalDeltaPowerDownVariable, Area ########################### get_variable_binary(::AdditionalDeltaActivePowerDownVariable, ::Type{<:PSY.Area}, ::AbstractAGCFormulation) = false get_variable_lower_bound(::AdditionalDeltaActivePowerDownVariable, ::PSY.Area, ::AbstractAGCFormulation) = 0.0 ########################## AreaMismatchVariable, AGC ########################### get_variable_binary(::AreaMismatchVariable, ::Type{<:PSY.AGC}, ::AbstractAGCFormulation) = false ########################## LiftVariable, Area ########################### get_variable_binary(::LiftVariable, ::Type{<:PSY.AGC}, ::AbstractAGCFormulation) = false get_variable_lower_bound(::LiftVariable, ::PSY.AGC, ::AbstractAGCFormulation) = 0.0 initial_condition_default(::AreaControlError, d::PSY.AGC, ::AbstractAGCFormulation) = PSY.get_initial_ace(d) initial_condition_variable(::AreaControlError, d::PSY.AGC, ::AbstractAGCFormulation) = AreaMismatchVariable() get_variable_multiplier(::SteadyStateFrequencyDeviation, d::PSY.AGC, ::AbstractAGCFormulation) = -10 * PSY.get_bias(d) #! format: on function get_default_time_series_names( ::Type{PSY.AGC}, ::Type{<:AbstractAGCFormulation}, ) return Dict{Type{<:TimeSeriesParameter}, String}() end function get_default_attributes( ::Type{PSY.AGC}, ::Type{<:AbstractAGCFormulation}, ) return Dict{String, Any}("aggregated_service_model" => false) end """ Steady State deviation of the frequency """ function add_variables!( container::OptimizationContainer, ::Type{T}, ) where {T <: SteadyStateFrequencyDeviation} time_steps = get_time_steps(container) variable = add_variable_container!(container, T(), PSY.AGC, time_steps) for t in time_steps variable[t] = JuMP.@variable(container.JuMPmodel, base_name = "ΔF_{$(t)}") end end ########################## Initial Condition ########################### function _get_variable_initial_value( d::PSY.Component, key::InitialConditionKey, ::AbstractAGCFormulation, ::Nothing, ) return _get_ace_error(d, key) end function add_constraints!( container::OptimizationContainer, ::Type{T}, ::Type{LiftVariable}, agcs::IS.FlattenIteratorWrapper{U}, ::ServiceModel{PSY.AGC, V}, ) where {T <: AbsoluteValueConstraint, U <: PSY.AGC, V <: PIDSmoothACE} time_steps = get_time_steps(container) agc_names = PSY.get_name.(agcs) container_lb = add_constraints_container!(container, T(), U, agc_names, time_steps; meta = "lb") container_ub = add_constraints_container!(container, T(), U, agc_names, time_steps; meta = "ub") mismatch = get_variable(container, AreaMismatchVariable(), U) z = get_variable(container, LiftVariable(), U) jump_model = get_jump_model(container) for t in time_steps, a in agc_names container_lb[a, t] = JuMP.@constraint(jump_model, mismatch[a, t] <= z[a, t]) container_ub[a, t] = JuMP.@constraint(jump_model, -1 * mismatch[a, t] <= z[a, t]) end return end """ Expression for the power deviation given deviation in the frequency. This expression allows updating the response of the frequency depending on commitment decisions """ function add_constraints!( container::OptimizationContainer, ::Type{T}, ::Type{SteadyStateFrequencyDeviation}, agcs::IS.FlattenIteratorWrapper{U}, ::ServiceModel{PSY.AGC, V}, sys::PSY.System, ) where {T <: FrequencyResponseConstraint, U <: PSY.AGC, V <: PIDSmoothACE} time_steps = get_time_steps(container) agc_names = PSY.get_name.(agcs) frequency_response = 0.0 for agc in agcs area = PSY.get_area(agc) frequency_response += PSY.get_load_response(area) end for g in PSY.get_components(PSY.get_available, PSY.RegulationDevice, sys) d = PSY.get_droop(g) response = 1 / d frequency_response += response end IS.@assert_op frequency_response >= 0.0 # This value is the one updated later in simulation based on the UC result inv_frequency_response = 1 / frequency_response area_balance = get_variable(container, ActivePowerVariable(), PSY.Area) frequency = get_variable(container, SteadyStateFrequencyDeviation(), U) R_up = get_variable(container, DeltaActivePowerUpVariable(), U) R_dn = get_variable(container, DeltaActivePowerDownVariable(), U) R_up_emergency = get_variable(container, AdditionalDeltaActivePowerUpVariable(), PSY.Area) R_dn_emergency = get_variable(container, AdditionalDeltaActivePowerUpVariable(), PSY.Area) const_container = add_constraints_container!(container, T(), PSY.System, time_steps) for t in time_steps system_balance = sum(area_balance.data[:, t]) for agc in agcs a = PSY.get_name(agc) area_name = PSY.get_name(PSY.get_area(agc)) JuMP.add_to_expression!(system_balance, R_up[a, t]) JuMP.add_to_expression!(system_balance, -1.0, R_dn[a, t]) JuMP.add_to_expression!(system_balance, R_up_emergency[area_name, t]) JuMP.add_to_expression!(system_balance, -1.0, R_dn_emergency[area_name, t]) end const_container[t] = JuMP.@constraint( container.JuMPmodel, frequency[t] == -inv_frequency_response * system_balance ) end return end function add_constraints!( container::OptimizationContainer, ::Type{T}, ::Type{SteadyStateFrequencyDeviation}, agcs::IS.FlattenIteratorWrapper{U}, model::ServiceModel{PSY.AGC, V}, sys::PSY.System, ) where {T <: SACEPIDAreaConstraint, U <: PSY.AGC, V <: PIDSmoothACE} 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] RAW_ACE = get_expression(container, RawACE(), U) SACE = get_variable(container, SmoothACE(), U) SACE_pid = add_constraints_container!( container, SACEPIDAreaConstraint(), U, agc_names, time_steps, ) jump_model = get_jump_model(container) for (ix, service) in enumerate(services) kp = PSY.get_K_p(service) ki = PSY.get_K_i(service) kd = PSY.get_K_d(service) Δt = convert(Dates.Second, get_resolution(container)).value a = PSY.get_name(service) for t in time_steps if t == 1 ACE_ini = get_initial_condition(container, AreaControlError(), PSY.AGC)[ix] ace_exp = get_value(ACE_ini) + kp * ((1 + Δt / (kp / ki)) * (RAW_ACE[a, t])) SACE_pid[a, t] = JuMP.@constraint(jump_model, SACE[a, t] == ace_exp) continue end SACE_pid[a, t] = JuMP.@constraint( jump_model, SACE[a, t] == SACE[a, t - 1] + kp * ( (1 + Δt / (kp / ki) + (kd / kp) / Δt) * (RAW_ACE[a, t]) + (-1 - 2 * (kd / kp) / Δt) * (RAW_ACE[a, t - 1]) ) ) end end return end function add_constraints!( container::OptimizationContainer, ::Type{T}, ::Type{SmoothACE}, agcs::IS.FlattenIteratorWrapper{U}, ::ServiceModel{PSY.AGC, V}, sys::PSY.System, ) where {T <: BalanceAuxConstraint, U <: PSY.AGC, V <: PIDSmoothACE} time_steps = get_time_steps(container) agc_names = PSY.get_name.(agcs) aux_equation = add_constraints_container!( container, BalanceAuxConstraint(), PSY.System, agc_names, time_steps, ) area_mismatch = get_variable(container, AreaMismatchVariable(), PSY.AGC) SACE = get_variable(container, SmoothACE(), PSY.AGC) R_up = get_variable(container, DeltaActivePowerUpVariable(), PSY.AGC) R_dn = get_variable(container, DeltaActivePowerDownVariable(), PSY.AGC) R_up_emergency = get_variable(container, AdditionalDeltaActivePowerUpVariable(), PSY.Area) R_dn_emergency = get_variable(container, AdditionalDeltaActivePowerUpVariable(), PSY.Area) for t in time_steps for agc in agcs a = PSY.get_name(agc) area_name = PSY.get_name(PSY.get_area(agc)) aux_equation[a, t] = JuMP.@constraint( container.JuMPmodel, -1 * SACE[a, t] == (R_up[a, t] - R_dn[a, t]) + (R_up_emergency[area_name, t] - R_dn_emergency[area_name, t]) + area_mismatch[a, t] ) end end return end function objective_function!( container::OptimizationContainer, agcs::IS.FlattenIteratorWrapper{T}, ::ServiceModel{<:PSY.AGC, U}, ) where {T <: PSY.AGC, U <: PIDSmoothACE} add_proportional_cost!(container, LiftVariable(), agcs, U()) return end # Defined here so we can dispatch on PIDSmoothACE function add_feedforward_arguments!( container::OptimizationContainer, model::ServiceModel{PSY.AGC, PIDSmoothACE}, areas::IS.FlattenIteratorWrapper{PSY.AGC}, ) for ff in get_feedforwards(model) @debug "arguments" ff V _group = LOG_GROUP_FEEDFORWARDS_CONSTRUCTION add_feedforward_arguments!(container, model, areas, ff) end return end function add_feedforward_constraints!( container::OptimizationContainer, model::ServiceModel{PSY.AGC, PIDSmoothACE}, areas::IS.FlattenIteratorWrapper{PSY.AGC}, ) for ff in get_feedforwards(model) @debug "arguments" ff V _group = LOG_GROUP_FEEDFORWARDS_CONSTRUCTION add_feedforward_constraints!(container, model, areas, ff) 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 ================================================ FILE: src/services_models/reserve_group.jl ================================================ function get_default_time_series_names( ::Type{PSY.ConstantReserveGroup{T}}, ::Type{GroupReserve}) where {T <: PSY.ReserveDirection} return Dict{String, Any}() end function get_default_attributes( ::Type{PSY.ConstantReserveGroup{T}}, ::Type{GroupReserve}) where {T <: PSY.ReserveDirection} return Dict{String, Any}() end ############################### Reserve Variables` ######################################### """ This function checks if the variables for reserves were created """ function check_activeservice_variables( container::OptimizationContainer, contributing_services::Vector{T}, ) where {T <: PSY.Service} for service in contributing_services get_variable( container, ActivePowerReserveVariable(), typeof(service), PSY.get_name(service), ) end return end ################################## Reserve Requirement Constraint ########################## """ This function creates the requirement constraint that will be attained by the appropriate services """ function add_constraints!( container::OptimizationContainer, ::Type{RequirementConstraint}, service::SR, contributing_services::Vector{<:PSY.Service}, model::ServiceModel{SR, GroupReserve}, ) where {SR <: PSY.ConstantReserveGroup} time_steps = get_time_steps(container) service_name = PSY.get_name(service) add_constraints_container!( container, RequirementConstraint(), SR, [service_name], time_steps; meta = service_name, ) constraint = get_constraint(container, RequirementConstraint(), SR, service_name) use_slacks = get_use_slacks(model) reserve_variables = [ get_variable(container, ActivePowerReserveVariable(), typeof(r), PSY.get_name(r)) for r in contributing_services ] requirement = PSY.get_requirement(service) for t in time_steps resource_expression = JuMP.GenericAffExpr{Float64, JuMP.VariableRef}() for reserve_variable in reserve_variables JuMP.add_to_expression!(resource_expression, sum(@view reserve_variable[:, t])) end if use_slacks resource_expression += slack_vars[t] end constraint[service_name, t] = JuMP.@constraint(container.JuMPmodel, resource_expression >= requirement) end return end ================================================ FILE: src/services_models/reserves.jl ================================================ #! format: off ############################### Reserve Variables ######################################### get_variable_multiplier(_, ::Type{<:PSY.Reserve}, ::AbstractReservesFormulation) = NaN ############################### PostContingencyActivePowerReserveDeploymentVariable, Reserve ######################################### get_variable_binary(::PostContingencyActivePowerReserveDeploymentVariable, ::Type{<:PSY.Reserve}, ::AbstractSecurityConstrainedReservesFormulation) = false function get_variable_upper_bound(::PostContingencyActivePowerReserveDeploymentVariable, r::PSY.Reserve, d::PSY.Device, ::AbstractSecurityConstrainedReservesFormulation) return PSY.get_max_active_power(d) end get_variable_lower_bound(::PostContingencyActivePowerReserveDeploymentVariable, ::PSY.Reserve, ::PSY.Device, _) = 0.0 get_variable_warm_start_value(::PostContingencyActivePowerReserveDeploymentVariable, d::PSY.Reserve, ::AbstractSecurityConstrainedReservesFormulation) = 0.0 get_variable_multiplier(::AbstractContingencyVariableType, ::Type{<:PSY.Reserve{PSY.ReserveDown}}, ::AbstractSecurityConstrainedReservesFormulation) = -1.0 get_variable_multiplier(::AbstractContingencyVariableType, ::Type{<:PSY.Reserve{PSY.ReserveUp}}, ::AbstractSecurityConstrainedReservesFormulation) = 1.0 get_variable_multiplier(::VariableType, ::Type{<:PSY.Generator}, ::AbstractSecurityConstrainedReservesFormulation) = -1.0 ############################### ActivePowerReserveVariable, Reserve ######################################### get_variable_binary(::ActivePowerReserveVariable, ::Type{<:PSY.Reserve}, ::AbstractReservesFormulation) = false function get_variable_upper_bound(::ActivePowerReserveVariable, r::PSY.Reserve, d::PSY.Device, ::AbstractReservesFormulation) return PSY.get_max_output_fraction(r) * PSY.get_max_active_power(d) end get_variable_upper_bound(::ActivePowerReserveVariable, r::PSY.ReserveDemandCurve, d::PSY.Device, ::AbstractReservesFormulation) = PSY.get_max_active_power(d) get_variable_lower_bound(::ActivePowerReserveVariable, ::PSY.Reserve, ::PSY.Device, _) = 0.0 ############################### ActivePowerReserveVariable, ReserveNonSpinning ######################################### get_variable_binary(::ActivePowerReserveVariable, ::Type{<:PSY.ReserveNonSpinning}, ::AbstractReservesFormulation) = false function get_variable_upper_bound(::ActivePowerReserveVariable, r::PSY.ReserveNonSpinning, d::PSY.Device, ::AbstractReservesFormulation) return PSY.get_max_output_fraction(r) * PSY.get_max_active_power(d) end get_variable_lower_bound(::ActivePowerReserveVariable, ::PSY.ReserveNonSpinning, ::PSY.Device, _) = 0.0 ############################### ServiceRequirementVariable, ReserveDemandCurve ################################ get_variable_binary(::ServiceRequirementVariable, ::Type{<:PSY.ReserveDemandCurve}, ::AbstractReservesFormulation) = false get_variable_upper_bound(::ServiceRequirementVariable, ::PSY.ReserveDemandCurve, d::PSY.Component, ::AbstractReservesFormulation) = PSY.get_max_active_power(d) get_variable_lower_bound(::ServiceRequirementVariable, ::PSY.ReserveDemandCurve, ::PSY.Component, ::AbstractReservesFormulation) = 0.0 get_multiplier_value(::RequirementTimeSeriesParameter, d::PSY.Reserve, ::AbstractReservesFormulation) = PSY.get_requirement(d) get_multiplier_value(::RequirementTimeSeriesParameter, d::PSY.ReserveNonSpinning, ::AbstractReservesFormulation) = PSY.get_requirement(d) 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 sos_status(::PSY.ReserveDemandCurve, ::StepwiseCostReserve)=SOSStatusVariable.NO_VARIABLE uses_compact_power(::PSY.ReserveDemandCurve, ::StepwiseCostReserve)=false #! format: on function get_initial_conditions_service_model( ::OperationModel, ::ServiceModel{T, D}, ) where {T <: PSY.Reserve, D <: AbstractReservesFormulation} return ServiceModel(T, D) end function get_initial_conditions_service_model( ::OperationModel, ::ServiceModel{T, D}, ) where {T <: PSY.VariableReserveNonSpinning, D <: AbstractReservesFormulation} return ServiceModel(T, D) end function get_default_time_series_names( ::Type{<:PSY.Reserve}, ::Type{T}, ) where {T <: Union{RangeReserve, RampReserve}} return Dict{Type{<:TimeSeriesParameter}, String}( RequirementTimeSeriesParameter => "requirement", ) end function get_default_time_series_names( ::Type{<:PSY.ReserveNonSpinning}, ::Type{NonSpinningReserve}, ) return Dict{Type{<:TimeSeriesParameter}, String}( RequirementTimeSeriesParameter => "requirement", ) end function get_default_time_series_names( ::Type{T}, ::Type{<:AbstractReservesFormulation}, ) where {T <: PSY.Reserve} return Dict{Type{<:TimeSeriesParameter}, String}() end function get_default_attributes( ::Type{<:PSY.Reserve}, ::Type{<:AbstractReservesFormulation}, ) return Dict{String, Any}() end function get_default_attributes( ::Type{<:PSY.ReserveNonSpinning}, ::Type{<:AbstractReservesFormulation}, ) 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 function _sum_reserve_variables( vars::AbstractArray{<:JuMP.AbstractVariableRef}, extra::Int, ) acc = get_hinted_aff_expr(length(vars) + extra) for v in vars JuMP.add_to_expression!(acc, v) end return acc end ################################## Reserve Requirement Constraint ########################## function add_constraints!( container::OptimizationContainer, T::Type{RequirementConstraint}, service::SR, ::U, model::ServiceModel{SR, V}, ) where { SR <: PSY.AbstractReserve, V <: AbstractReservesFormulation, U <: Union{Vector{D}, IS.FlattenIteratorWrapper{D}}, } where {D <: PSY.Component} parameters = built_for_recurrent_solves(container) time_steps = get_time_steps(container) service_name = PSY.get_name(service) # TODO: Add a method for services that handles this better constraint = add_constraints_container!( container, T(), SR, [service_name], time_steps; meta = service_name, ) reserve_variable = get_variable(container, ActivePowerReserveVariable(), SR, service_name) use_slacks = get_use_slacks(model) ts_vector = get_time_series( container, service, "requirement"; interval = get_interval(get_settings(container)), ) use_slacks && (slack_vars = reserve_slacks!(container, service)) requirement = PSY.get_requirement(service) jump_model = get_jump_model(container) extra = use_slacks ? 1 : 0 if built_for_recurrent_solves(container) param_container = get_parameter(container, RequirementTimeSeriesParameter(), SR, service_name) param = get_parameter_column_refs(param_container, service_name) for t in time_steps resource_expression = _sum_reserve_variables(@view(reserve_variable[:, t]), extra) use_slacks && JuMP.add_to_expression!(resource_expression, slack_vars[t]) constraint[service_name, t] = JuMP.@constraint(jump_model, resource_expression >= param[t] * requirement) end else for t in time_steps resource_expression = _sum_reserve_variables(@view(reserve_variable[:, t]), extra) use_slacks && JuMP.add_to_expression!(resource_expression, slack_vars[t]) constraint[service_name, t] = JuMP.@constraint( jump_model, resource_expression >= ts_vector[t] * requirement ) end end return end function add_constraints!( container::OptimizationContainer, T::Type{ParticipationFractionConstraint}, service::SR, contributing_devices::U, ::ServiceModel{SR, V}, ) where { SR <: PSY.AbstractReserve, V <: AbstractReservesFormulation, U <: Union{Vector{D}, IS.FlattenIteratorWrapper{D}}, } where {D <: PSY.Device} max_participation_factor = PSY.get_max_participation_factor(service) if max_participation_factor >= 1.0 return end time_steps = get_time_steps(container) service_name = PSY.get_name(service) cons = add_constraints_container!( container, T(), SR, [PSY.get_name(d) for d in contributing_devices], time_steps; meta = service_name, ) var_r = get_variable(container, ActivePowerReserveVariable(), SR, service_name) jump_model = get_jump_model(container) requirement = PSY.get_requirement(service) ts_vector = get_time_series( container, service, "requirement"; interval = get_interval(get_settings(container)), ) param_container = get_parameter(container, RequirementTimeSeriesParameter(), SR, service_name) param = get_parameter_column_refs(param_container, service_name) for t in time_steps, d in contributing_devices name = PSY.get_name(d) if built_for_recurrent_solves(container) cons[name, t] = JuMP.@constraint( jump_model, var_r[name, t] <= (requirement * max_participation_factor) * param[t] ) else cons[name, t] = JuMP.@constraint( jump_model, var_r[name, t] <= (requirement * max_participation_factor) * ts_vector[t] ) end end return end function add_constraints!( container::OptimizationContainer, T::Type{RequirementConstraint}, service::SR, ::U, model::ServiceModel{SR, V}, ) where { SR <: PSY.ConstantReserve, V <: AbstractReservesFormulation, U <: Union{Vector{D}, IS.FlattenIteratorWrapper{D}}, } where {D <: PSY.Component} time_steps = get_time_steps(container) service_name = PSY.get_name(service) # TODO: The constraint addition is still not clean enough constraint = add_constraints_container!( container, T(), SR, [service_name], time_steps; meta = service_name, ) reserve_variable = get_variable(container, ActivePowerReserveVariable(), SR, service_name) use_slacks = get_use_slacks(model) use_slacks && (slack_vars = reserve_slacks!(container, service)) requirement = PSY.get_requirement(service) jump_model = get_jump_model(container) extra = use_slacks ? 1 : 0 for t in time_steps resource_expression = _sum_reserve_variables(@view(reserve_variable[:, t]), extra) use_slacks && JuMP.add_to_expression!(resource_expression, slack_vars[t]) constraint[service_name, t] = JuMP.@constraint(jump_model, resource_expression >= requirement) end return end function objective_function!( container::OptimizationContainer, service::SR, ::ServiceModel{SR, T}, ) where {SR <: PSY.AbstractReserve, T <: AbstractReservesFormulation} add_proportional_cost!(container, ActivePowerReserveVariable(), service, T()) return end function add_constraints!( container::OptimizationContainer, T::Type{RequirementConstraint}, service::SR, ::U, ::ServiceModel{SR, StepwiseCostReserve}, ) where { SR <: PSY.ReserveDemandCurve, U <: Union{Vector{D}, IS.FlattenIteratorWrapper{D}}, } where {D <: PSY.Component} time_steps = get_time_steps(container) service_name = PSY.get_name(service) constraint = add_constraints_container!( container, T(), SR, [service_name], time_steps; meta = service_name, ) reserve_variable = get_variable(container, ActivePowerReserveVariable(), SR, service_name) 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( jump_model, sum(@view reserve_variable[:, t]) >= requirement_variable[service_name, t] ) end return end _get_ramp_limits(::PSY.Component) = nothing _get_ramp_limits(d::PSY.ThermalGen) = PSY.get_ramp_limits(d) _get_ramp_limits(d::PSY.HydroGen) = PSY.get_ramp_limits(d) function _get_ramp_constraint_contributing_devices( service::PSY.Reserve, contributing_devices::Union{Vector{D}, IS.FlattenIteratorWrapper{D}}, ) where {D <: PSY.Component} time_frame = PSY.get_time_frame(service) filtered_device = Vector{D}() for d in contributing_devices ramp_limits = _get_ramp_limits(d) if ramp_limits !== nothing p_lims = PSY.get_active_power_limits(d) max_rate = abs(p_lims.min - p_lims.max) / time_frame if (ramp_limits.up >= max_rate) & (ramp_limits.down >= max_rate) @debug "Generator $(name) has a nonbinding ramp limits. Constraints Skipped" continue else push!(filtered_device, d) end end end return filtered_device end function add_constraints!( container::OptimizationContainer, T::Type{RampConstraint}, service::SR, contributing_devices::Union{Vector{D}, IS.FlattenIteratorWrapper{D}}, ::ServiceModel{SR, V}, ) where { SR <: PSY.Reserve{PSY.ReserveUp}, V <: AbstractReservesFormulation, D <: PSY.Component, } ramp_devices = _get_ramp_constraint_contributing_devices(service, contributing_devices) service_name = PSY.get_name(service) if !isempty(ramp_devices) jump_model = get_jump_model(container) time_steps = get_time_steps(container) time_frame = PSY.get_time_frame(service) variable = get_variable(container, ActivePowerReserveVariable(), SR, service_name) device_name_set = [PSY.get_name(d) for d in ramp_devices] con_up = add_constraints_container!( container, T(), SR, device_name_set, time_steps; meta = service_name, ) for d in ramp_devices, t in time_steps name = PSY.get_name(d) ramp_limits = PSY.get_ramp_limits(d) con_up[name, t] = JuMP.@constraint( jump_model, variable[name, t] <= ramp_limits.up * time_frame ) end else @warn "Data doesn't contain contributing devices with ramp limits for service $service_name, consider adjusting your formulation" end return end function add_constraints!( container::OptimizationContainer, T::Type{RampConstraint}, service::SR, contributing_devices::Union{Vector{D}, IS.FlattenIteratorWrapper{D}}, ::ServiceModel{SR, V}, ) where { SR <: PSY.Reserve{PSY.ReserveDown}, V <: AbstractReservesFormulation, D <: PSY.Component, } ramp_devices = _get_ramp_constraint_contributing_devices(service, contributing_devices) service_name = PSY.get_name(service) if !isempty(ramp_devices) jump_model = get_jump_model(container) time_steps = get_time_steps(container) time_frame = PSY.get_time_frame(service) variable = get_variable(container, ActivePowerReserveVariable(), SR, service_name) device_name_set = [PSY.get_name(d) for d in ramp_devices] con_down = add_constraints_container!( container, T(), SR, device_name_set, time_steps; meta = service_name, ) for d in ramp_devices, t in time_steps name = PSY.get_name(d) ramp_limits = PSY.get_ramp_limits(d) con_down[name, t] = JuMP.@constraint( jump_model, variable[name, t] <= ramp_limits.down * time_frame ) end else @warn "Data doesn't contain contributing devices with ramp limits for service $service_name, consider adjusting your formulation" end return end function add_constraints!( container::OptimizationContainer, T::Type{ReservePowerConstraint}, service::SR, contributing_devices::U, ::ServiceModel{SR, V}, ) where { SR <: PSY.VariableReserveNonSpinning, V <: AbstractReservesFormulation, U <: Union{Vector{D}, IS.FlattenIteratorWrapper{D}}, } where {D <: PSY.Component} time_steps = get_time_steps(container) resolution = get_resolution(container) if resolution > Dates.Minute(1) minutes_per_period = Dates.value(Dates.Minute(resolution)) else @warn("Not all formulations support under 1-minute resolutions. Exercise caution.") minutes_per_period = Dates.value(Dates.Second(resolution)) / 60 end service_name = PSY.get_name(service) cons = add_constraints_container!( container, T(), SR, [PSY.get_name(d) for d in contributing_devices], time_steps; meta = service_name, ) var_r = get_variable(container, ActivePowerReserveVariable(), SR, service_name) reserve_response_time = PSY.get_time_frame(service) jump_model = get_jump_model(container) for d in contributing_devices component_type = typeof(d) name = PSY.get_name(d) varstatus = get_variable(container, OnVariable(), component_type) startup_time = PSY.get_time_limits(d).up ramp_limits = _get_ramp_limits(d) if reserve_response_time > startup_time reserve_limit = PSY.get_active_power_limits(d).min + (reserve_response_time - startup_time) * minutes_per_period * ramp_limits.up else reserve_limit = 0.0 end for t in time_steps cons[name, t] = JuMP.@constraint( jump_model, var_r[name, t] <= (1 - varstatus[name, t]) * reserve_limit ) end end return end function objective_function!( container::OptimizationContainer, service::PSY.ReserveDemandCurve{T}, ::ServiceModel{PSY.ReserveDemandCurve{T}, SR}, ) where {T <: PSY.ReserveDirection, SR <: StepwiseCostReserve} 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, # possibly decouple DEFAULT_RESERVE_COST / base_p * reserve_variable[index...], ) end return end ================================================ FILE: src/services_models/service_slacks.jl ================================================ function reserve_slacks!( container::OptimizationContainer, service::T, ) where {T <: Union{PSY.Reserve, PSY.ReserveNonSpinning}} time_steps = get_time_steps(container) variable = add_variable_container!( container, ReserveRequirementSlack(), T, PSY.get_name(service), time_steps, ) for t in time_steps variable[t] = JuMP.@variable( get_jump_model(container), base_name = "slack_{$(PSY.get_name(service)), $(t)}", lower_bound = 0.0 ) add_to_objective_invariant_expression!(container, variable[t] * SERVICES_SLACK_COST) end return variable end function transmission_interface_slacks!( container::OptimizationContainer, service::T, ) where {T <: PSY.TransmissionInterface} time_steps = get_time_steps(container) for variable_type in [InterfaceFlowSlackUp, InterfaceFlowSlackDown] variable = add_variable_container!( container, variable_type(), T, PSY.get_name(service), time_steps, ) penalty = PSY.get_violation_penalty(service) name = PSY.get_name(service) for t in time_steps variable[t] = JuMP.@variable( get_jump_model(container), base_name = "$(T)_$(variable_type)_{$(name), $(t)}", ) JuMP.set_lower_bound(variable[t], 0.0) add_to_objective_invariant_expression!( container, variable[t] * penalty, ) end end return end ================================================ FILE: src/services_models/services_constructor.jl ================================================ function get_incompatible_devices(devices_template::Dict) incompatible_device_types = Set{DataType}() for model in values(devices_template) formulation = get_formulation(model) if formulation == FixedOutput if !isempty(get_services(model)) @info "$(formulation) for $(get_component_type(model)) is not compatible with the provision of reserve services" end push!(incompatible_device_types, get_component_type(model)) end end return incompatible_device_types end function construct_services!( container::OptimizationContainer, sys::PSY.System, 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) groupservice = nothing for (key, service_model) in services_template if get_formulation(service_model) === GroupReserve # group service needs to be constructed last groupservice = key continue end isempty(get_contributing_devices(service_model)) && continue construct_service!( container, sys, stage, service_model, devices_template, incompatible_device_types, network_model, ) end groupservice === nothing || construct_service!( container, sys, stage, services_template[groupservice], devices_template, incompatible_device_types, network_model, ) return end function construct_services!( container::OptimizationContainer, sys::PSY.System, 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) groupservice = nothing for (key, service_model) in services_template if get_formulation(service_model) === GroupReserve # group service needs to be constructed last groupservice = key continue end isempty(get_contributing_devices_map(service_model)) && continue construct_service!( container, sys, stage, service_model, devices_template, incompatible_device_types, network_model, ) end groupservice === nothing || construct_service!( container, sys, stage, services_template[groupservice], devices_template, incompatible_device_types, network_model, ) return end function construct_service!( container::OptimizationContainer, sys::PSY.System, ::ArgumentConstructStage, 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) !PSY.get_available(service) && return add_parameters!(container, RequirementTimeSeriesParameter, service, model) contributing_devices = get_contributing_devices(model) add_variables!( container, ActivePowerReserveVariable, service, contributing_devices, RangeReserve(), ) add_to_expression!(container, ActivePowerReserveVariable, model, devices_template) add_feedforward_arguments!(container, model, service) return end function construct_service!( container::OptimizationContainer, sys::PSY.System, ::ModelConstructStage, 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) !PSY.get_available(service) && return contributing_devices = get_contributing_devices(model) add_constraints!(container, RequirementConstraint, service, contributing_devices, model) add_constraints!( container, ParticipationFractionConstraint, service, contributing_devices, model, ) objective_function!(container, service, model) add_feedforward_constraints!(container, model, service) add_constraint_dual!(container, sys, model) return end function construct_service!( container::OptimizationContainer, sys::PSY.System, ::ArgumentConstructStage, model::ServiceModel{SR, RangeReserve}, devices_template::Dict{Symbol, DeviceModel}, incompatible_device_types::Set{<:DataType}, ::NetworkModel{<:PM.AbstractPowerModel}, ) where {SR <: PSY.ConstantReserve} name = get_service_name(model) service = PSY.get_component(SR, sys, name) !PSY.get_available(service) && return contributing_devices = get_contributing_devices(model) add_variables!( container, ActivePowerReserveVariable, service, contributing_devices, RangeReserve(), ) add_to_expression!(container, ActivePowerReserveVariable, model, devices_template) add_feedforward_arguments!(container, model, service) return end function construct_service!( container::OptimizationContainer, sys::PSY.System, ::ModelConstructStage, model::ServiceModel{SR, RangeReserve}, devices_template::Dict{Symbol, DeviceModel}, incompatible_device_types::Set{<:DataType}, ::NetworkModel{<:PM.AbstractPowerModel}, ) where {SR <: PSY.ConstantReserve} name = get_service_name(model) service = PSY.get_component(SR, sys, name) !PSY.get_available(service) && return contributing_devices = get_contributing_devices(model) add_constraints!(container, RequirementConstraint, service, contributing_devices, model) add_constraints!( container, ParticipationFractionConstraint, service, contributing_devices, model, ) objective_function!(container, service, model) add_feedforward_constraints!(container, model, service) add_constraint_dual!(container, sys, model) return end function construct_service!( container::OptimizationContainer, sys::PSY.System, ::ArgumentConstructStage, 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) !PSY.get_available(service) && return contributing_devices = get_contributing_devices(model) add_variable!(container, ServiceRequirementVariable(), service, StepwiseCostReserve()) add_variables!( container, ActivePowerReserveVariable, service, contributing_devices, StepwiseCostReserve(), ) add_to_expression!(container, ActivePowerReserveVariable, model, devices_template) add_expressions!(container, ProductionCostExpression, [service], model) return end function construct_service!( container::OptimizationContainer, sys::PSY.System, ::ModelConstructStage, 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) !PSY.get_available(service) && return contributing_devices = get_contributing_devices(model) add_constraints!(container, RequirementConstraint, service, contributing_devices, model) objective_function!(container, service, model) add_feedforward_constraints!(container, model, service) add_constraint_dual!(container, sys, model) return end #= function construct_service!( container::OptimizationContainer, sys::PSY.System, ::ArgumentConstructStage, model::ServiceModel{S, T}, devices_template::Dict{Symbol, DeviceModel}, ::Set{<:DataType}, ::NetworkModel{<:PM.AbstractPowerModel}, ) where {S <: PSY.AGC, T <: AbstractAGCFormulation} 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)) throw( IS.ConflictingInputsError( "All area must have an AGC service assigned in order to model the System's Frequency regulation", ), ) end add_variables!(container, SteadyStateFrequencyDeviation) add_variables!(container, AreaMismatchVariable, services, T()) add_variables!(container, SmoothACE, services, T()) add_variables!(container, LiftVariable, services, T()) add_variables!(container, ActivePowerVariable, areas, T()) add_variables!(container, DeltaActivePowerUpVariable, services, T()) add_variables!(container, DeltaActivePowerDownVariable, services, T()) add_variables!(container, AdditionalDeltaActivePowerUpVariable, areas, T()) add_variables!(container, AdditionalDeltaActivePowerDownVariable, areas, T()) add_initial_condition!(container, services, T(), AreaControlError()) add_to_expression!( container, EmergencyUp, AdditionalDeltaActivePowerUpVariable, areas, model, ) add_to_expression!( container, EmergencyDown, AdditionalDeltaActivePowerDownVariable, areas, model, ) add_to_expression!(container, RawACE, SteadyStateFrequencyDeviation, services, model) add_feedforward_arguments!(container, model, services) return end function construct_service!( container::OptimizationContainer, sys::PSY.System, ::ModelConstructStage, 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(model, sys) add_constraints!(container, AbsoluteValueConstraint, LiftVariable, services, model) add_constraints!( container, FrequencyResponseConstraint, SteadyStateFrequencyDeviation, services, model, sys, ) add_constraints!( container, SACEPIDAreaConstraint, SteadyStateFrequencyDeviation, services, model, sys, ) add_constraints!(container, BalanceAuxConstraint, SmoothACE, services, model, sys) add_feedforward_constraints!(container, model, services) add_constraint_dual!(container, sys, model) objective_function!(container, services, model) return end =# """ Constructs a service for ConstantReserveGroup. """ function construct_service!( container::OptimizationContainer, sys::PSY.System, ::ArgumentConstructStage, model::ServiceModel{SR, GroupReserve}, ::Dict{Symbol, DeviceModel}, ::Set{<:DataType}, ::NetworkModel{<:PM.AbstractPowerModel}, ) where {SR <: PSY.ConstantReserveGroup} name = get_service_name(model) service = PSY.get_component(SR, sys, name) !PSY.get_available(service) && return contributing_services = PSY.get_contributing_services(service) # check if variables exist check_activeservice_variables(container, contributing_services) return end function construct_service!( container::OptimizationContainer, sys::PSY.System, ::ModelConstructStage, model::ServiceModel{SR, GroupReserve}, ::Dict{Symbol, DeviceModel}, ::Set{<:DataType}, ::NetworkModel{<:PM.AbstractPowerModel}, ) where {SR <: PSY.ConstantReserveGroup} name = get_service_name(model) service = PSY.get_component(SR, sys, name) !PSY.get_available(service) && return contributing_services = PSY.get_contributing_services(service) add_constraints!( container, RequirementConstraint, service, contributing_services, model, ) add_constraint_dual!(container, sys, model) return end function construct_service!( container::OptimizationContainer, sys::PSY.System, ::ArgumentConstructStage, 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) !PSY.get_available(service) && return contributing_devices = get_contributing_devices(model) add_parameters!(container, RequirementTimeSeriesParameter, service, model) add_variables!( container, ActivePowerReserveVariable, service, contributing_devices, RampReserve(), ) add_to_expression!(container, ActivePowerReserveVariable, model, devices_template) add_feedforward_arguments!(container, model, service) return end function construct_service!( container::OptimizationContainer, sys::PSY.System, ::ModelConstructStage, 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) !PSY.get_available(service) && return contributing_devices = get_contributing_devices(model) add_constraints!(container, RequirementConstraint, service, contributing_devices, model) add_constraints!(container, RampConstraint, service, contributing_devices, model) add_constraints!( container, ParticipationFractionConstraint, service, contributing_devices, model, ) objective_function!(container, service, model) add_feedforward_constraints!(container, model, service) add_constraint_dual!(container, sys, model) return end function construct_service!( container::OptimizationContainer, sys::PSY.System, ::ArgumentConstructStage, 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) !PSY.get_available(service) && return contributing_devices = get_contributing_devices(model) add_parameters!(container, RequirementTimeSeriesParameter, service, model) add_variables!( container, ActivePowerReserveVariable, service, contributing_devices, NonSpinningReserve(), ) add_feedforward_arguments!(container, model, service) return end function construct_service!( container::OptimizationContainer, sys::PSY.System, ::ModelConstructStage, 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) !PSY.get_available(service) && return contributing_devices = get_contributing_devices(model) add_constraints!(container, RequirementConstraint, service, contributing_devices, model) add_constraints!( container, ReservePowerConstraint, service, contributing_devices, model, ) add_constraints!( container, ParticipationFractionConstraint, service, contributing_devices, model, ) objective_function!(container, service, model) add_feedforward_constraints!(container, model, service) add_constraint_dual!(container, sys, model) return end function construct_service!( container::OptimizationContainer, sys::PSY.System, ::ArgumentConstructStage, 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(model, sys) interface = PSY.get_component(T, sys, get_service_name(model)) if get_use_slacks(model) # Adding the slacks can be done in a cleaner fashion @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), ) add_feedforward_arguments!(container, model, interface) return end function construct_service!( container::OptimizationContainer, sys::PSY.System, ::ArgumentConstructStage, model::ServiceModel{PSY.TransmissionInterface, ConstantMaxInterfaceFlow}, devices_template::Dict{Symbol, DeviceModel}, incompatible_device_types::Set{<:DataType}, network_model::NetworkModel{AreaBalancePowerModel}, ) interfaces = get_available_components(model, sys) interface = PSY.get_component(PSY.TransmissionInterface, sys, get_service_name(model)) if get_use_slacks(model) # Adding the slacks can be done in a cleaner fashion @assert PSY.get_available(interface) transmission_interface_slacks!(container, interface) end # Lazy container addition for the expressions. lazy_container_addition!( container, InterfaceTotalFlow(), PSY.TransmissionInterface, PSY.get_name.(interfaces), get_time_steps(container), ) @warn "AreaBalancePowerModel doesn't model individual line flows and it ignores the flows on AC Transmission Devices" add_feedforward_arguments!(container, model, interface) return end function construct_service!( container::OptimizationContainer, sys::PSY.System, ::ModelConstructStage, model::ServiceModel{PSY.TransmissionInterface, ConstantMaxInterfaceFlow}, devices_template::Dict{Symbol, DeviceModel}, incompatible_device_types::Set{<:DataType}, network_model::NetworkModel{<:PM.AbstractActivePowerModel}, ) name = get_service_name(model) service = PSY.get_component(PSY.TransmissionInterface, sys, name) !PSY.get_available(service) && return add_to_expression!( container, InterfaceTotalFlow, FlowActivePowerVariable, service, model, network_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, ::ModelConstructStage, model::ServiceModel{PSY.TransmissionInterface, ConstantMaxInterfaceFlow}, devices_template::Dict{Symbol, DeviceModel}, incompatible_device_types::Set{<:DataType}, network_model::NetworkModel{PTDFPowerModel}, ) name = get_service_name(model) service = PSY.get_component(PSY.TransmissionInterface, sys, name) !PSY.get_available(service) && return add_to_expression!( container, InterfaceTotalFlow, PTDFBranchFlow, service, model, network_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, ::ModelConstructStage, model::ServiceModel{PSY.TransmissionInterface, ConstantMaxInterfaceFlow}, devices_template::Dict{Symbol, DeviceModel}, incompatible_device_types::Set{<:DataType}, network_model::NetworkModel{AreaPTDFPowerModel}, ) name = get_service_name(model) service = PSY.get_component(PSY.TransmissionInterface, sys, name) !PSY.get_available(service) && return # This function makes interfaces for the AC Branches add_to_expression!( container, InterfaceTotalFlow, PTDFBranchFlow, service, model, network_model, ) # This function makes interfaces for the interchanges add_to_expression!( container, InterfaceTotalFlow, FlowActivePowerVariable, service, model, network_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, ::ModelConstructStage, model::ServiceModel{PSY.TransmissionInterface, VariableMaxInterfaceFlow}, devices_template::Dict{Symbol, DeviceModel}, incompatible_device_types::Set{<:DataType}, network_model::NetworkModel{<:AbstractPTDFModel}, ) name = get_service_name(model) service = PSY.get_component(PSY.TransmissionInterface, sys, name) !PSY.get_available(service) && return # This function makes interfaces for the AC Branches add_to_expression!( container, InterfaceTotalFlow, PTDFBranchFlow, service, model, network_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, ::ModelConstructStage, model::ServiceModel{PSY.TransmissionInterface, U}, devices_template::Dict{Symbol, DeviceModel}, incompatible_device_types::Set{<:DataType}, network_model::NetworkModel{T}, ) where { T <: PM.AbstractPowerModel, U <: Union{ConstantMaxInterfaceFlow, VariableMaxInterfaceFlow}, } error("TransmissionInterface models not implemented for PowerModel of type $T") return end function construct_service!( container::OptimizationContainer, sys::PSY.System, ::ArgumentConstructStage, model::ServiceModel{PSY.TransmissionInterface, VariableMaxInterfaceFlow}, devices_template::Dict{Symbol, DeviceModel}, incompatible_device_types::Set{<:DataType}, network_model::NetworkModel{<:PM.AbstractPowerModel}, ) 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(PSY.TransmissionInterface, 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(), PSY.TransmissionInterface, 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 interface = PSY.get_component(PSY.TransmissionInterface, sys, get_service_name(model)) add_feedforward_arguments!(container, model, interface) return end function construct_service!( container::OptimizationContainer, sys::PSY.System, ::ModelConstructStage, model::ServiceModel{PSY.TransmissionInterface, U}, devices_template::Dict{Symbol, DeviceModel}, incompatible_device_types::Set{<:DataType}, network_model::NetworkModel{<:PM.AbstractActivePowerModel}, ) where {U <: Union{ConstantMaxInterfaceFlow, VariableMaxInterfaceFlow}} name = get_service_name(model) service = PSY.get_component(PSY.TransmissionInterface, sys, name) !PSY.get_available(service) && return add_to_expression!( container, InterfaceTotalFlow, FlowActivePowerVariable, service, model, network_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 ================================================ FILE: src/services_models/transmission_interface.jl ================================================ #! format: off get_variable_binary(_, ::Type{PSY.TransmissionInterface}, ::ConstantMaxInterfaceFlow) = false get_variable_lower_bound(::InterfaceFlowSlackUp, ::PSY.TransmissionInterface, ::ConstantMaxInterfaceFlow) = 0.0 get_variable_lower_bound(::InterfaceFlowSlackDown, ::PSY.TransmissionInterface, ::ConstantMaxInterfaceFlow) = 0.0 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}, ::Type{ConstantMaxInterfaceFlow}, ) 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}, ) where {T <: PSY.TransmissionInterface, D <: ConstantMaxInterfaceFlow} return ServiceModel(T, D) end function add_constraints!( container::OptimizationContainer, ::Type{InterfaceFlowLimit}, interface::T, model::ServiceModel{T, ConstantMaxInterfaceFlow}, ) where {T <: PSY.TransmissionInterface} expr = get_expression(container, InterfaceTotalFlow(), T) interfaces, time_steps = axes(expr) constraint_container_ub = lazy_container_addition!( container, InterfaceFlowLimit(), T, interfaces, time_steps; meta = "ub", ) constraint_container_lb = lazy_container_addition!( container, InterfaceFlowLimit(), T, interfaces, time_steps; meta = "lb", ) int_name = PSY.get_name(interface) min_flow, max_flow = PSY.get_active_power_flow_limits(interface) for t in time_steps constraint_container_ub[int_name, t] = JuMP.@constraint(get_jump_model(container), expr[int_name, t] <= max_flow) constraint_container_lb[int_name, t] = JuMP.@constraint(get_jump_model(container), expr[int_name, t] >= min_flow) end 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 <: Union{ConstantMaxInterfaceFlow, VariableMaxInterfaceFlow}} # At the moment the interfaces have no costs associated with them return end ================================================ FILE: src/simulation/decision_model_simulation_results.jl ================================================ struct DecisionModelSimulationResults <: OperationModelSimulationResults variables::ResultsByKeyAndTime duals::ResultsByKeyAndTime parameters::ResultsByKeyAndTime aux_variables::ResultsByKeyAndTime expressions::ResultsByKeyAndTime forecast_horizon::Int container_key_lookup::Dict{String, OptimizationContainerKey} end function SimulationProblemResults( ::Type{DecisionModel}, store::SimulationStore, model_name::AbstractString, problem_params::ModelStoreParams, sim_params::SimulationStoreParams, path, container_key_lookup; kwargs..., ) name = Symbol(model_name) return SimulationProblemResults{DecisionModelSimulationResults}( store, model_name, problem_params, sim_params, path, DecisionModelSimulationResults( ResultsByKeyAndTime( list_decision_model_keys(store, name, STORE_CONTAINER_VARIABLES), ), ResultsByKeyAndTime( list_decision_model_keys(store, name, STORE_CONTAINER_DUALS), ), ResultsByKeyAndTime( list_decision_model_keys(store, name, STORE_CONTAINER_PARAMETERS), ), ResultsByKeyAndTime( list_decision_model_keys(store, name, STORE_CONTAINER_AUX_VARIABLES), ), ResultsByKeyAndTime( list_decision_model_keys(store, name, STORE_CONTAINER_EXPRESSIONS), ), get_horizon_count(problem_params), container_key_lookup, ); kwargs..., ) end function _list_containers(res::SimulationProblemResults{DecisionModelSimulationResults}) return (getfield(res.values, x).cached_results for x in get_container_fields(res)) end function Base.empty!(res::SimulationProblemResults{DecisionModelSimulationResults}) foreach(empty!, _list_containers(res)) empty!(get_results_timestamps(res)) end function Base.isempty(res::SimulationProblemResults{DecisionModelSimulationResults}) all(isempty, _list_containers(res)) end # This returns the number of timestamps stored in all containers. function Base.length(res::SimulationProblemResults{DecisionModelSimulationResults}) return mapreduce(length, +, (y for x in _list_containers(res) for y in values(x))) end list_aux_variable_keys(res::SimulationProblemResults{DecisionModelSimulationResults}) = res.values.aux_variables.result_keys[:] list_dual_keys(res::SimulationProblemResults{DecisionModelSimulationResults}) = res.values.duals.result_keys[:] list_expression_keys(res::SimulationProblemResults{DecisionModelSimulationResults}) = res.values.expressions.result_keys[:] list_parameter_keys(res::SimulationProblemResults{DecisionModelSimulationResults}) = res.values.parameters.result_keys[:] list_variable_keys(res::SimulationProblemResults{DecisionModelSimulationResults}) = res.values.variables.result_keys[:] get_cached_aux_variables(res::SimulationProblemResults{DecisionModelSimulationResults}) = res.values.aux_variables.cached_results get_cached_duals(res::SimulationProblemResults{DecisionModelSimulationResults}) = res.values.duals.cached_results get_cached_expressions(res::SimulationProblemResults{DecisionModelSimulationResults}) = res.values.expressions.cached_results get_cached_parameters(res::SimulationProblemResults{DecisionModelSimulationResults}) = res.values.parameters.cached_results 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 function _get_store_value( res::SimulationProblemResults{DecisionModelSimulationResults}, container_keys::Vector{<:OptimizationContainerKey}, timestamps, ::Nothing, ) simulation_store_path = joinpath(get_execution_path(res), "data_store") return open_store(HdfSimulationStore, simulation_store_path, "r") do store _get_store_value(res, container_keys, timestamps, store) end end function _get_store_value( sim_results::SimulationProblemResults{DecisionModelSimulationResults}, container_keys::Vector{<:OptimizationContainerKey}, timestamps::Vector{Dates.DateTime}, store::SimulationStore, ) results_by_key = Dict{OptimizationContainerKey, ResultsByTime}() model_name = Symbol(get_model_name(sim_results)) for ckey in container_keys n_dims = get_number_of_dimensions(store, DecisionModelIndexType, model_name, ckey) container_type = DenseAxisArray{Float64, n_dims + 1} results_by_key[ckey] = _get_store_value(container_type, sim_results, ckey, timestamps, store) end return results_by_key end function _get_store_value( ::Type{T}, sim_results::SimulationProblemResults{DecisionModelSimulationResults}, key::OptimizationContainerKey, timestamps::Vector{Dates.DateTime}, store::SimulationStore, ) where {T <: DenseAxisArray{Float64, 2}} resolution = get_resolution(sim_results) horizon = get_forecast_horizon(sim_results) base_power = get_model_base_power(sim_results) model_name = Symbol(get_model_name(sim_results)) results_by_time = ResultsByTime( key, SortedDict{Dates.DateTime, T}(), resolution, get_column_names(store, DecisionModelIndexType, model_name, key), ) array_size::Union{Nothing, Tuple{Int, Int}} = nothing for ts in timestamps array = read_result(DenseAxisArray, store, model_name, key, ts) if isnothing(array_size) array_size = size(array) elseif size(array) != array_size error( "Arrays for $(encode_key_as_string(key)) at different timestamps have different sizes", ) end if convert_result_to_natural_units(key) array.data .*= base_power end if array_size[2] != horizon @warn "$(encode_key_as_string(key)) has a different horizon than the " * "problem specification. Can't assign timestamps to the resulting DataFrame." results_by_time.resolution = Dates.Period(Dates.Millisecond(0)) end results_by_time[ts] = array end return results_by_time end function _get_store_value( ::Type{T}, sim_results::SimulationProblemResults{DecisionModelSimulationResults}, key::OptimizationContainerKey, timestamps::Vector{Dates.DateTime}, store::SimulationStore, ) where {T <: DenseAxisArray{Float64, 3}} resolution = get_resolution(sim_results) horizon = get_forecast_horizon(sim_results) base_power = get_model_base_power(sim_results) model_name = Symbol(get_model_name(sim_results)) results_by_time = ResultsByTime( key, SortedDict{Dates.DateTime, T}(), resolution, get_column_names(store, DecisionModelIndexType, model_name, key), ) array_size::Union{Nothing, Tuple{Int, Int, Int}} = nothing for ts in timestamps array = read_result(DenseAxisArray, store, model_name, key, ts) if isnothing(array_size) array_size = size(array) elseif size(array) != array_size error( "Arrays for $(encode_key_as_string(key)) at different timestamps have different sizes", ) end if convert_result_to_natural_units(key) array.data .*= base_power end if array_size[3] != horizon @warn "$(encode_key_as_string(key)) has a different horizon than the " * "problem specification. Can't assign timestamps to the resulting DataFrame." results_by_time.resolution = Dates.Period(Dates.Millisecond(0)) end results_by_time[ts] = array end return results_by_time end function _process_timestamps( res::SimulationProblemResults, initial_time::Union{Nothing, Dates.DateTime}, count::Union{Nothing, Int}, ) if initial_time === nothing initial_time = first(get_timestamps(res)) end if initial_time ∉ res.timestamps invalid_timestamps = [initial_time] else if count === nothing requested_range = [v for v in res.timestamps if v >= initial_time] else requested_range = collect(range(initial_time; length = count, step = get_interval(res))) end invalid_timestamps = [v for v in requested_range if v ∉ res.timestamps] end if !isempty(invalid_timestamps) @error "Timestamps $(invalid_timestamps) not stored" get_timestamps(res) throw(IS.InvalidValue("Timestamps not stored")) end return requested_range end function _read_results( ::Type{DataFrame}, res::SimulationProblemResults{DecisionModelSimulationResults}, result_keys, timestamps::Vector{Dates.DateTime}, store::Union{Nothing, <:SimulationStore}; cols::Union{Colon, Vector{String}} = (:), table_format::TableFormat = TableFormat.LONG, ) vals = _read_results(res, result_keys, timestamps, store) converted_vals = Dict{OptimizationContainerKey, ResultsByTime{DataFrame}}() for (result_key, result_data) in vals inner_converted = SortedDict{Dates.DateTime, DataFrame}() for (date_key, inner_data) in result_data extra = ntuple(_ -> (:), ndims(inner_data) - 1) inner_converted[date_key] = to_results_dataframe(inner_data[cols, extra...], nothing, Val(table_format)) end _cols = (cols isa Vector) ? (cols,) : result_data.column_names num_dims = length(_cols) converted_vals[result_key] = ResultsByTime{DataFrame, num_dims}( result_data.key, inner_converted, result_data.resolution, _cols) end return converted_vals end function _read_results( res::SimulationProblemResults{DecisionModelSimulationResults}, result_keys, timestamps::Vector{Dates.DateTime}, store::Union{Nothing, <:SimulationStore}, ) isempty(result_keys) && return Dict{OptimizationContainerKey, ResultsByTime{DenseAxisArray{Float64, 2}}}() _store = try_resolve_store(store, res.store) existing_keys = list_result_keys(res, first(result_keys)) ISOPT._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 vals = Dict(k => cached_results[k] for k in result_keys) # Cached data may contain more timestamps than we need, remove these if so (timestamps == get_results_timestamps(res)) && return vals filtered_vals = Dict{keytype(vals), valtype(vals)}() for (result_key, result_data) in vals inner_converted = filter((((k, v),) -> k in timestamps), result_data.data) filtered_vals[result_key] = ResultsByTime{valtype(inner_converted), 1}( result_data.key, inner_converted, result_data.resolution, result_data.column_names) end return filtered_vals else @debug "reading results from data store" # NOTE tests match on this vals = _get_store_value(res, result_keys, timestamps, _store) end return vals end """ Return the values for the requested variable. It keeps requests when performing multiple retrievals. # Arguments - `args`: Can be a string returned from [`list_variable_names`](@ref) or args that can be splatted into a VariableKey. - `initial_time::Dates.DateTime` : initial of the requested results - `count::Int`: Number of results - `store::SimulationStore`: a store that has been opened for reading - `table_format::TableFormat`: Format of the table to be returned. Default is `TableFormat.LONG` where the columns are `DateTime`, `name`, and `value` when the data has two dimensions and `DateTime`, `name`, `name2`, and `value` when the data has three dimensions. Set to it `TableFormat.WIDE` to pivot the names as columns. Note: `TableFormat.WIDE` is not supported when the data has three dimensions. # Examples ```julia read_variable(results, ActivePowerVariable, ThermalStandard) read_variable(results, "ActivePowerVariable__ThermalStandard") read_variable(results, "ActivePowerVariable__ThermalStandard", table_format = TableFormat.WIDE) ``` """ function read_variable( res::SimulationProblemResults{DecisionModelSimulationResults}, args...; initial_time::Union{Nothing, Dates.DateTime} = nothing, count::Union{Int, Nothing} = nothing, store = nothing, table_format::TableFormat = TableFormat.LONG, ) key = _deserialize_key(VariableKey, res, args...) timestamps = _process_timestamps(res, initial_time, count) return make_dataframes( _read_results(res, [key], timestamps, store)[key]; table_format = table_format, ) end """ Return the values for the requested dual. It keeps requests when performing multiple retrievals. # Arguments - `args`: Can be a string returned from [`list_dual_names`](@ref) or args that can be splatted into a ConstraintKey. - `initial_time::Dates.DateTime` : initial of the requested results - `count::Int`: Number of results - `store::SimulationStore`: a store that has been opened for reading - `table_format::TableFormat`: Format of the table to be returned. Default is `TableFormat.LONG` where the columns are `DateTime`, `name`, and `value` when the data has two dimensions and `DateTime`, `name`, `name2`, and `value` when the data has three dimensions. Set to it `TableFormat.WIDE` to pivot the names as columns. Note: `TableFormat.WIDE` is not supported when the data has three dimensions. """ function read_dual( res::SimulationProblemResults{DecisionModelSimulationResults}, args...; initial_time::Union{Nothing, Dates.DateTime} = nothing, count::Union{Int, Nothing} = nothing, store = nothing, table_format::TableFormat = TableFormat.LONG, ) key = _deserialize_key(ConstraintKey, res, args...) timestamps = _process_timestamps(res, initial_time, count) return make_dataframes( _read_results(res, [key], timestamps, store)[key]; table_format = table_format, ) end """ Return the values for the requested parameter. It keeps requests when performing multiple retrievals. # Arguments - `args`: Can be a string returned from [`list_parameter_names`](@ref) or args that can be splatted into a ParameterKey. - `initial_time::Dates.DateTime` : initial of the requested results - `count::Int`: Number of results - `table_format::TableFormat`: Format of the table to be returned. Default is `TableFormat.LONG` where the columns are `DateTime`, `name`, and `value` when the data has two dimensions and `DateTime`, `name`, `name2`, and `value` when the data has three dimensions. Set to it `TableFormat.WIDE` to pivot the names as columns. Note: `TableFormat.WIDE` is not supported when the data has three dimensions. """ function read_parameter( res::SimulationProblemResults{DecisionModelSimulationResults}, args...; initial_time::Union{Nothing, Dates.DateTime} = nothing, count::Union{Int, Nothing} = nothing, store = nothing, table_format::TableFormat = TableFormat.LONG, ) key = _deserialize_key(ParameterKey, res, args...) timestamps = _process_timestamps(res, initial_time, count) return make_dataframes( _read_results(res, [key], timestamps, store)[key]; table_format = table_format, ) end """ Return the values for the requested auxillary variables. It keeps requests when performing multiple retrievals. # Arguments - `args`: Can be a string returned from [`list_aux_variable_names`](@ref) or args that can be splatted into a AuxVarKey. - `initial_time::Dates.DateTime` : initial of the requested results - `count::Int`: Number of results """ function read_aux_variable( res::SimulationProblemResults{DecisionModelSimulationResults}, args...; initial_time::Union{Nothing, Dates.DateTime} = nothing, count::Union{Int, Nothing} = nothing, store = nothing, table_format::TableFormat = TableFormat.LONG, ) key = _deserialize_key(AuxVarKey, res, args...) timestamps = _process_timestamps(res, initial_time, count) return make_dataframes( _read_results(res, [key], timestamps, store)[key]; table_format = table_format, ) end """ Return the values for the requested auxillary variables. It keeps requests when performing multiple retrievals. # Arguments - `args`: Can be a string returned from [`list_expression_names`](@ref) or args that can be splatted into a ExpressionKey. - `initial_time::Dates.DateTime` : initial of the requested results - `count::Int`: Number of results """ function read_expression( res::SimulationProblemResults{DecisionModelSimulationResults}, args...; initial_time::Union{Nothing, Dates.DateTime} = nothing, count::Union{Int, Nothing} = nothing, store = nothing, table_format::TableFormat = TableFormat.LONG, ) key = _deserialize_key(ExpressionKey, res, args...) timestamps = _process_timestamps(res, initial_time, count) return make_dataframes( _read_results(res, [key], timestamps, store)[key]; table_format = table_format, ) end function get_realized_timestamps( res::IS.Results; start_time::Union{Nothing, Dates.DateTime} = nothing, len::Union{Int, Nothing} = nothing, ) timestamps = get_timestamps(res) resolution = get_resolution(res) intervals = diff(timestamps) if isempty(intervals) && isnothing(resolution) # If Single Interval Step and single time step: use dummy resolution/interval interval = Dates.Millisecond(1) resolution = Dates.Millisecond(1) elseif !isempty(intervals) && isnothing(resolution) # Multiple simulation steps but single time step: Set resolution = interval interval = first(intervals) resolution = interval elseif isempty(intervals) && !isnothing(resolution) # There is multiple time steps but single simulation step: Set interval = resolution interval = resolution else # Both data are available: Use existing resolution and grab first interval interval = first(intervals) end horizon = get_forecast_horizon(res) start_time = isnothing(start_time) ? first(timestamps) : start_time end_time = if isnothing(len) last(timestamps) + interval - resolution else start_time + (len - 1) * resolution end requested_range = start_time:resolution:end_time available_range = first(timestamps):resolution:(last(timestamps) + (horizon - 1) * resolution) invalid_timestamps = setdiff(requested_range, available_range) if !isempty(invalid_timestamps) msg = "Requested time does not match available results" @error msg throw(IS.InvalidValue(msg)) end return requested_range end function get_realized_timestamps( res::SimulationProblemResults; start_time::Union{Nothing, Dates.DateTime} = nothing, len::Union{Int, Nothing} = nothing, ) timestamps = get_timestamps(res) resolution = get_resolution(res) interval = get_interval(res) horizon = get_forecast_horizon(res) start_time = isnothing(start_time) ? first(timestamps) : start_time end_time = if isnothing(len) last(timestamps) + interval - resolution else start_time + (len - 1) * resolution end requested_range = start_time:resolution:end_time available_range = first(timestamps):resolution:(last(timestamps) + (horizon - 1) * resolution) invalid_timestamps = setdiff(requested_range, available_range) if !isempty(invalid_timestamps) msg = "Requested time does not match available results" @error msg throw(IS.InvalidValue(msg)) end 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}} = (:), table_format = TableFormat.LONG, ) meta = RealizedMeta(res; start_time = start_time, len = len) timestamps = _process_timestamps(res, meta.start_time, meta.len) result_values = _read_results( DataFrame, res, result_keys, timestamps, nothing; cols = cols, table_format = table_format, ) return get_realization(result_values, meta; table_format = table_format) end function _are_results_cached( res::SimulationProblemResults{DecisionModelSimulationResults}, output_keys::Vector{<:OptimizationContainerKey}, timestamps::Vector{Dates.DateTime}, cached_keys, ) return isempty(setdiff(timestamps, get_results_timestamps(res))) && isempty(setdiff(output_keys, cached_keys)) end """ Load the simulation results into memory for repeated reads. This is useful when loading results from remote locations over network connections, when reading the same data very many times, etc. Multiple calls augment the cache according to these rules, where "variable" means "variable, expression, etc.": - Requests for an already cached variable at a lesser `count` than already cached do *not* decrease the `count` of the cached variable - Requests for an already cached variable at a greater `count` than already cached *do* increase the `count` of the cached variable - Requests for new variables are fulfilled without evicting existing variables Note that `count` is global across all variables, so increasing the `count` re-reads already cached variables. For each variable, each element must be the name encoded as a string, like `"ActivePowerVariable__ThermalStandard"` or a Tuple with its constituent types, like `(ActivePowerVariable, ThermalStandard)`. To clear the cache, use [`Base.empty!`](@ref). # Arguments - `count::Int`: Number of windows to load. - `initial_time::Dates.DateTime` : Initial time of first window to load. Defaults to first. - `aux_variables::Vector{Union{String, Tuple}}`: Optional list of aux variables to load. - `duals::Vector{Union{String, Tuple}}`: Optional list of duals to load. - `expressions::Vector{Union{String, Tuple}}`: Optional list of expressions to load. - `parameters::Vector{Union{String, Tuple}}`: Optional list of parameters to load. - `variables::Vector{Union{String, Tuple}}`: Optional list of variables to load. """ function load_results!( res::SimulationProblemResults{DecisionModelSimulationResults}, count::Int; initial_time::Union{Dates.DateTime, Nothing} = nothing, variables = Vector{Tuple}(), duals = Vector{Tuple}(), 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) 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) return nothing end function _read_optimizer_stats( res::SimulationProblemResults{DecisionModelSimulationResults}, store::SimulationStore, ) return read_optimizer_stats(store, Symbol(res.problem)) end ================================================ FILE: src/simulation/emulation_model_simulation_results.jl ================================================ struct EmulationModelSimulationResults <: OperationModelSimulationResults variables::Dict{OptimizationContainerKey, DataFrames.DataFrame} duals::Dict{OptimizationContainerKey, DataFrames.DataFrame} parameters::Dict{OptimizationContainerKey, DataFrames.DataFrame} aux_variables::Dict{OptimizationContainerKey, DataFrames.DataFrame} expressions::Dict{OptimizationContainerKey, DataFrames.DataFrame} container_key_lookup::Dict{String, OptimizationContainerKey} end function SimulationProblemResults( ::Type{EmulationModel}, store::SimulationStore, model_name::AbstractString, problem_params::ModelStoreParams, sim_params::SimulationStoreParams, path, container_key_lookup; kwargs..., ) return SimulationProblemResults{EmulationModelSimulationResults}( store, model_name, problem_params, sim_params, path, EmulationModelSimulationResults( Dict( x => DataFrames.DataFrame() for x in list_emulation_model_keys(store, STORE_CONTAINER_VARIABLES) ), Dict( x => DataFrames.DataFrame() for x in list_emulation_model_keys(store, STORE_CONTAINER_DUALS) ), Dict( x => DataFrames.DataFrame() for x in list_emulation_model_keys(store, STORE_CONTAINER_PARAMETERS) ), Dict( x => DataFrames.DataFrame() for x in list_emulation_model_keys(store, STORE_CONTAINER_AUX_VARIABLES) ), Dict( x => DataFrames.DataFrame() for x in list_emulation_model_keys(store, STORE_CONTAINER_EXPRESSIONS) ), container_key_lookup, ); kwargs..., ) end list_aux_variable_keys(res::SimulationProblemResults{EmulationModelSimulationResults}) = collect(keys(res.values.aux_variables)) list_dual_keys(res::SimulationProblemResults{EmulationModelSimulationResults}) = collect(keys(res.values.duals)) list_expression_keys(res::SimulationProblemResults{EmulationModelSimulationResults}) = collect(keys(res.values.expressions)) list_parameter_keys(res::SimulationProblemResults{EmulationModelSimulationResults}) = collect(keys(res.values.parameters)) list_variable_keys(res::SimulationProblemResults{EmulationModelSimulationResults}) = collect(keys(res.values.variables)) get_cached_aux_variables(res::SimulationProblemResults{EmulationModelSimulationResults}) = res.values.aux_variables get_cached_duals(res::SimulationProblemResults{EmulationModelSimulationResults}) = res.values.duals get_cached_expressions(res::SimulationProblemResults{EmulationModelSimulationResults}) = res.values.expressions get_cached_parameters(res::SimulationProblemResults{EmulationModelSimulationResults}) = res.values.parameters get_cached_variables(res::SimulationProblemResults{EmulationModelSimulationResults}) = res.values.variables function _list_containers(res::SimulationProblemResults) return (getfield(res.values, x) for x in get_container_fields(res)) end function Base.empty!(res::SimulationProblemResults{EmulationModelSimulationResults}) for container in _list_containers(res) for df in values(container) empty!(df) end end end function Base.isempty(res::SimulationProblemResults{EmulationModelSimulationResults}) for container in _list_containers(res) for df in values(container) if !isempty(df) return false end end end return true end function Base.length(res::SimulationProblemResults{EmulationModelSimulationResults}) count_not_empty = 0 for container in _list_containers(res) for df in values(container) if !isempty(df) count_not_empty += 1 end end end return count_not_empty end function _get_store_value( res::SimulationProblemResults{EmulationModelSimulationResults}, container_keys::Vector{<:OptimizationContainerKey}, ::Nothing; start_time = nothing, len = nothing, table_format = TableFormat.LONG, ) simulation_store_path = joinpath(get_execution_path(res), "data_store") return open_store(HdfSimulationStore, simulation_store_path, "r") do store _get_store_value( res, container_keys, store; start_time = start_time, len = len, table_format = table_format, ) end end function _get_store_value( res::SimulationProblemResults{EmulationModelSimulationResults}, container_keys::Vector{<:OptimizationContainerKey}, store::SimulationStore; start_time::Union{Nothing, Dates.DateTime} = nothing, len::Union{Nothing, Int} = nothing, table_format = TableFormat.LONG, ) base_power = res.base_power results = Dict{OptimizationContainerKey, DataFrames.DataFrame}() for key in container_keys start_time, _len, resolution = _check_offsets(res, key, store, start_time, len) start_index = (start_time - first(res.timestamps)) ÷ resolution + 1 array = read_results(store, key; index = start_index, len = _len) if convert_result_to_natural_units(key) array.data .*= base_power end # PERF: this is a double-permutedims with HDF # We could make an optimized version of this that reads Arrays # like decision_model_simulation_results timestamps = range(start_time; length = _len, step = res.resolution) results[key] = to_results_dataframe(array, timestamps, Val(table_format)) end return results end function _check_offsets( res::SimulationProblemResults{EmulationModelSimulationResults}, key, store, start_time, len, ) dataset_size = get_emulation_model_dataset_size(store, key) resolution = (last(res.timestamps) - first(res.timestamps) + res.resolution) ÷ dataset_size if isnothing(start_time) start_time = first(res.timestamps) elseif start_time < first(res.timestamps) || start_time > last(res.timestamps) throw( IS.InvalidValue( "start_time = $start_time is not in the results range $(res.timestamps)", ), ) elseif (start_time - first(res.timestamps)) % resolution != Dates.Millisecond(0) throw( IS.InvalidValue( "start_time = $start_time is not a multiple of resolution = $resolution", ), ) end if isnothing(len) len = (last(res.timestamps) + resolution - start_time) ÷ resolution elseif start_time + resolution * len > last(res.timestamps) + res.resolution throw( IS.InvalidValue( "len = $len resolution = $resolution exceeds the results range $(res.timestamps)", ), ) end return start_time, len, resolution end function _read_results( res::SimulationProblemResults{EmulationModelSimulationResults}, result_keys, store; start_time = nothing, len = nothing, table_format = TableFormat.LONG, ) isempty(result_keys) && return Dict{OptimizationContainerKey, DataFrames.DataFrame}() _store = try_resolve_store(store, res.store) existing_keys = list_result_keys(res, first(result_keys)) ISOPT._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) ) if isempty(setdiff(result_keys, keys(cached_results))) @debug "reading aux_variables from SimulationsResults" vals = Dict(k => cached_results[k] for k in result_keys) if table_format == TableFormat.WIDE for (k, v) in vals if :name2 in DataFrames.propertynames(v) error( "TableFormat.WIDE is not supported when the data has three dimensions.", ) end end vals = Dict( k => DataFrames.unstack(v, :DateTime, :name, :value) for (k, v) in vals ) end else @debug "reading aux_variables from data store" vals = _get_store_value( res, result_keys, _store; start_time = start_time, len = len, table_format = table_format, ) end return vals end function read_results_with_keys( res::SimulationProblemResults{EmulationModelSimulationResults}, result_keys::Vector{<:OptimizationContainerKey}; start_time::Union{Nothing, Dates.DateTime} = nothing, len::Union{Nothing, Int} = nothing, table_format = TableFormat.LONG, ) return _read_results( res, result_keys, nothing; start_time = start_time, len = len, table_format = table_format, ) end """ Load the simulation results into memory for repeated reads. This is useful when loading results from remote locations over network connections. For each variable/parameter/dual, etc., each element must be the name encoded as a string, like `"ActivePowerVariable__ThermalStandard"`` or a Tuple with its constituent types, like `(ActivePowerVariable, ThermalStandard)`. # Arguments - `aux_variables::Vector{Union{String, Tuple}}`: Optional list of aux variables to load. - `duals::Vector{Union{String, Tuple}}`: Optional list of duals to load. - `expressions::Vector{Union{String, Tuple}}`: Optional list of expressions to load. - `parameters::Vector{Union{String, Tuple}}`: Optional list of parameters to load. - `variables::Vector{Union{String, Tuple}}`: Optional list of variables to load. """ function load_results!( res::SimulationProblemResults{EmulationModelSimulationResults}; aux_variables = Vector{Tuple}(), duals = Vector{Tuple}(), expressions = Vector{Tuple}(), parameters = Vector{Tuple}(), variables = Vector{Tuple}(), ) # TODO: consider extending this to support start_time and len aux_variable_keys = [_deserialize_key(AuxVarKey, res, x...) for x in aux_variables] dual_keys = [_deserialize_key(ConstraintKey, res, x...) for x in duals] expression_keys = [_deserialize_key(ExpressionKey, res, x...) for x in expressions] parameter_keys = [_deserialize_key(ParameterKey, res, x...) for x in parameters] variable_keys = [_deserialize_key(VariableKey, res, x...) for x in variables] function merge_results(store) merge!(get_cached_aux_variables(res), _read_results(res, aux_variable_keys, store)) merge!(get_cached_duals(res), _read_results(res, dual_keys, store)) merge!(get_cached_expressions(res), _read_results(res, expression_keys, store)) merge!(get_cached_parameters(res), _read_results(res, parameter_keys, store)) merge!(get_cached_variables(res), _read_results(res, variable_keys, store)) 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 end return end # TODO: These aren't being written to the store. function _read_optimizer_stats( res::SimulationProblemResults{EmulationModelSimulationResults}, store::SimulationStore, ) return end ================================================ FILE: src/simulation/get_components_interface.jl ================================================ # Analogous to `src/get_components_interface.jl` in PowerSystems.jl, see comments there. # get_components """ Calling `get_components` on a `Results` is the same as calling [`get_available_components`] on the system attached to the results. """ PSY.get_components( ::Type{T}, res::IS.Results; subsystem_name = nothing, ) where {T <: IS.InfrastructureSystemsComponent} = IS.get_components(T, res; subsystem_name = subsystem_name) PSY.get_components(res::IS.Results, attribute::IS.SupplementalAttribute) = IS.get_components(res, attribute) PSY.get_components( filter_func::Function, ::Type{T}, res::IS.Results; subsystem_name = nothing, ) where {T <: IS.InfrastructureSystemsComponent} = IS.get_components(filter_func, T, res; subsystem_name = subsystem_name) PSY.get_components( scope_limiter::Union{Function, Nothing}, selector::IS.ComponentSelector, res::IS.Results, ) = IS.get_components(scope_limiter, selector, res) PSY.get_components(selector::IS.ComponentSelector, res::IS.Results) = IS.get_components(selector, res) # get_component """ Calling `get_component` on a `Results` is the same as calling [`get_available_component`] on the system attached to the results. """ PSY.get_component(res::IS.Results, uuid::Base.UUID) = IS.get_component(res, uuid) PSY.get_component(res::IS.Results, uuid::String) = IS.get_component(res, uuid) PSY.get_component( ::Type{T}, res::IS.Results, name::AbstractString, ) where {T <: IS.InfrastructureSystemsComponent} = IS.get_component(T, res, name) PSY.get_component( scope_limiter::Union{Function, Nothing}, selector::IS.SingularComponentSelector, res::IS.Results, ) = IS.get_component(scope_limiter, selector, res) PSY.get_component(selector::IS.SingularComponentSelector, res::IS.Results) = IS.get_component(selector, res) # get_groups """ Calling `get_groups` on a `Results` is the same as calling [`get_available_groups`] on the system attached to the results. """ PSY.get_groups( scope_limiter::Union{Function, Nothing}, selector::IS.ComponentSelector, res::IS.Results, ) = IS.get_groups(scope_limiter, selector, res) PSY.get_groups(selector::IS.ComponentSelector, res::IS.Results) = IS.get_groups(selector, res) ================================================ FILE: src/simulation/hdf_simulation_store.jl ================================================ const HDF_FILENAME = "simulation_store.h5" const HDF_SIMULATION_ROOT_PATH = "simulation" const EMULATION_MODEL_PATH = "$HDF_SIMULATION_ROOT_PATH/emulation_model" const OPTIMIZER_STATS_PATH = "optimizer_stats" const SERIALIZED_KEYS_PATH = "serialized_keys" # This only applies if chunks are enabled, and that will only likely happen if we enable # compression. # The optimal number of chunks to store in memory will vary widely. # The HDF docs recommend keeping chunk byte sizes between 10 KiB - 1 MiB. # We want to make it big enough to compress duplicate values. # The downside to making this larger is that any read causes the # entire chunk to be read. # If one variable has 10,000 components and each value is a Float64 then one row would # consume 10,000 * 8 = 78 KiB DEFAULT_MAX_CHUNK_BYTES = 128 * KiB """ Stores simulation data in an HDF file. """ mutable struct HdfSimulationStore <: SimulationStore file::HDF5.File params::SimulationStoreParams # The key order is the problem execution order. dm_data::OrderedDict{Symbol, DatasetContainer{HDF5Dataset}} em_data::DatasetContainer{HDF5Dataset} # The key is the problem name. optimizer_stats_datasets::Dict{Symbol, HDF5.Dataset} optimizer_stats_write_index::Dict{Symbol, Int} cache::OptimizationOutputCaches end get_initial_time(store::HdfSimulationStore) = get_initial_time(store.params) function HdfSimulationStore(file_path::AbstractString, mode::AbstractString) if !(mode in ("w", "r", "rw")) throw(IS.ConflictingInputsError("mode can only be 'w', 'r', or 'rw'")) end if !isfile(file_path) && mode in ("r", "rw") throw(IS.ConflictingInputsError("$file_path does not exist")) end if isfile(file_path) && mode == "w" throw(IS.ConflictingInputsError("$file_path already exists")) end hdf5_mode = mode == "rw" ? "r+" : mode file = HDF5.h5open(file_path, hdf5_mode) if mode == "w" HDF5.create_group(file, HDF_SIMULATION_ROOT_PATH) @debug "Created store" file_path end store = HdfSimulationStore( file, SimulationStoreParams(), OrderedDict{Symbol, DatasetContainer{HDF5Dataset}}(), DatasetContainer{HDF5Dataset}(), Dict{Symbol, HDF5.Dataset}(), Dict{Symbol, Int}(), OptimizationOutputCaches(), ) mode in ("r", "rw") && _deserialize_attributes!(store) finalizer(_check_state, store) return store end """ Construct and open an HdfSimulationStore. When reading or writing results in a program you should use the method that accepts a function in order to guarantee that the file handle gets closed. # Arguments - `directory::AbstractString`: Directory containing the store file - `mode::AbstractString`: Mode to use to open the store file - `filename::AbstractString`: Base name of the store file # Examples ```julia # Assumes a simulation has been executed in the './rts' directory with these parameters. path = "./rts" problem = :ED var_name = :P__ThermalStandard timestamp = DateTime("2020-01-01T05:00:00") store = open_store(HdfSimulationStore, path) df = PowerSimulations.read_result(DataFrame, store, model, :variables, var_name, timestamp) ``` """ function open_store( ::Type{HdfSimulationStore}, directory::AbstractString, mode = "r"; filename = HDF_FILENAME, ) return HdfSimulationStore(joinpath(directory, filename), mode) end function open_store( func::Function, ::Type{HdfSimulationStore}, directory::AbstractString, mode = "r"; filename = HDF_FILENAME, ) store = nothing try store = HdfSimulationStore(joinpath(directory, filename), mode) return func(store) finally if store !== nothing close(store) end end end function Base.close(store::HdfSimulationStore) flush(store) HDF5.close(store.file) empty!(store.cache) @debug "Close store file handle" store.file end function Base.isopen(store::HdfSimulationStore) return store.file === nothing ? false : HDF5.isopen(store.file) end function Base.flush(store::HdfSimulationStore) for (key, output_cache) in store.cache.data _flush_data!(output_cache, store, key, false) @assert !has_dirty(output_cache) "$key has dirty cache after flushing" end flush(store.file) @debug "Flush store" return end get_params(store::HdfSimulationStore) = store.params function set_cache_flush_rules!(store::HdfSimulationStore, flush_rules::CacheFlushRules) new_cache = OptimizationOutputCaches(flush_rules) for (key, output_cache) in store.cache.data new_cache.data[key] = output_cache end store.cache = new_cache @debug "Updated store cache rules" get_min_flush_size(store.cache) get_max_size( store.cache, ) return end function get_decision_model_params(store::HdfSimulationStore, model_name::Symbol) return get_decision_model_params(get_params(store), model_name) end function get_emulation_model_params(store::HdfSimulationStore) return get_emulation_model_params(get_params(store)) end function get_container_key_lookup(store::HdfSimulationStore) function _get_lookup() root = _get_root(store) buf = IOBuffer(root[SERIALIZED_KEYS_PATH][:]) return Serialization.deserialize(buf) end isopen(store) && return _get_lookup() store.file = HDF5.h5open(store.file.filename, "r") try return _get_lookup() finally HDF5.close(store.file) end end """ Return the problem names in order of execution. """ list_decision_models(store::HdfSimulationStore) = keys(get_dm_data(store)) """ Return the fields stored for the `problem` and `container_type` (duals/parameters/variables). """ function list_decision_model_keys( store::HdfSimulationStore, model::Symbol, container_type::Symbol, ) container = getfield(get_dm_data(store)[model], container_type) return collect(keys(container)) end function list_emulation_model_keys(store::HdfSimulationStore, container_type::Symbol) container = getfield(get_em_data(store), container_type) return collect(keys(container)) end function write_optimizer_stats!( store::HdfSimulationStore, model::OperationModel, ::DecisionModelIndexType, ) stats = get_optimizer_stats(model) model_name = get_name(model) dataset = _get_dataset(OptimizerStats, store, model_name) # Uncomment for performance measures of HDF Store #TimerOutputs.@timeit RUN_SIMULATION_TIMER "Write optimizer stats" begin dataset[:, store.optimizer_stats_write_index[model_name]] = to_matrix(stats) #end store.optimizer_stats_write_index[model_name] += 1 return end function write_optimizer_stats!( store::HdfSimulationStore, model::OperationModel, ::EmulationModelIndexType, ) return end """ Read the optimizer stats for a problem execution. """ function read_optimizer_stats( store::HdfSimulationStore, simulation_step::Int, model_name::Symbol, execution_index::Int, ) optimizer_stats_write_index = (simulation_step - 1) * store.params.decision_models_params[model_name].num_executions + execution_index dataset = _get_dataset(OptimizerStats, store, model_name) return OptimizerStats(dataset[:, optimizer_stats_write_index]) end """ 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 = [IS.to_namedtuple(OptimizerStats(data[i, :])) for i in axes(data)[1]] return DataFrames.DataFrame(stats) end function initialize_problem_storage!( store::HdfSimulationStore, params::SimulationStoreParams, dm_problem_reqs::Dict{Symbol, SimulationModelStoreRequirements}, em_problem_reqs::SimulationModelStoreRequirements, flush_rules::CacheFlushRules, ) store.params = params root = store.file[HDF_SIMULATION_ROOT_PATH] problems_group = _get_group_or_create(root, "decision_models") store.cache = OptimizationOutputCaches(flush_rules) @info "Initialize store cache" get_min_flush_size(store.cache) get_max_size(store.cache) initial_time = get_initial_time(store) container_key_lookup = Dict{String, OptimizationContainerKey}() for (problem, problem_params) in store.params.decision_models_params get_dm_data(store)[problem] = DatasetContainer{HDF5Dataset}() problem_group = _get_group_or_create(problems_group, string(problem)) for type in STORE_CONTAINERS group = _get_group_or_create(problem_group, string(type)) for (key, reqs) in getfield(dm_problem_reqs[problem], type) !should_write_resulting_value(key) && continue name = encode_key_as_string(key) dataset = _create_dataset(group, name, reqs) # Columns can't be stored in attributes because they might be larger than # the max size of 64 KiB. col = _make_column_name(name) if length(reqs["columns"]) == 1 HDF5.write_dataset(group, col, string.(reqs["columns"][1])) else col_vals = vcat(reqs["columns"]...) HDF5.write_dataset(group, col, string.(col_vals)) end column_dataset = group[col] datasets = getfield(get_dm_data(store)[problem], type) column_lengths = reqs["dims"][2:(end - 1)] datasets[key] = HDF5Dataset{length(column_lengths)}( dataset, column_dataset, column_lengths, get_resolution(problem_params), initial_time, ) add_output_cache!( store.cache, problem, key, get_rule(flush_rules, problem, key), ) container_key_lookup[encode_key_as_string(key)] = key end end num_stats = params.num_steps * params.decision_models_params[problem].num_executions columns = fieldnames(OptimizerStats) num_columns = length(columns) dataset = HDF5.create_dataset( problem_group, OPTIMIZER_STATS_PATH, HDF5.datatype(Float64), HDF5.dataspace((num_columns, num_stats)), ) HDF5.attributes(dataset)["columns"] = [string(x) for x in columns] store.optimizer_stats_datasets[problem] = dataset store.optimizer_stats_write_index[problem] = 1 @debug "Initialized optimizer_stats_datasets $problem ($num_columns, $num_stats)" end emulation_group = _get_group_or_create(root, "emulation_model") for emulation_params in values(store.params.emulation_model_params) for type in STORE_CONTAINERS group = _get_group_or_create(emulation_group, string(type)) for (key, reqs) in getfield(em_problem_reqs, type) name = encode_key_as_string(key) dataset = _create_dataset(group, name, reqs) # Columns can't be stored in attributes because they might be larger than # the max size of 64 KiB. col = _make_column_name(name) if length(reqs["columns"]) == 1 HDF5.write_dataset(group, col, string.(reqs["columns"][1])) else col_vals = vcat(reqs["columns"]...) HDF5.write_dataset(group, col, string.(col_vals)) end column_dataset = group[col] datasets = getfield(store.em_data, type) column_lengths = reqs["dims"][2:end] datasets[key] = HDF5Dataset{length(column_lengths)}( dataset, column_dataset, column_lengths, get_resolution(emulation_params), initial_time, ) container_key_lookup[encode_key_as_string(key)] = key end end end buf = IOBuffer() Serialization.serialize(buf, container_key_lookup) seek(buf, 0) root[SERIALIZED_KEYS_PATH] = buf.data # This has to run after problem groups are created. _serialize_attributes(store) return end log_cache_hit_percentages(x::HdfSimulationStore) = log_cache_hit_percentages(x.cache) function _make_dataframe(data::Matrix{Float64}, columns::Tuple{Vector{String}}) return DataFrames.DataFrame(data, columns[1]; copycols = false) end """ Return DataFrame, DenseAxisArray, or Array for a model result at a timestamp. """ function read_result( ::Type{DataFrames.DataFrame}, store::HdfSimulationStore, model_name::Symbol, key::OptimizationContainerKey, index::Union{DecisionModelIndexType, EmulationModelIndexType}, ) data, columns = _read_data_columns(store, model_name, key, index) return _make_dataframe(data, columns) end function _make_denseaxisarray( data::Matrix{Float64}, columns::Tuple{Vector{String}}, ) return DenseAxisArray(permutedims(data), columns[1], 1:size(data)[1]) end function _make_denseaxisarray( data::Matrix{Float64}, columns::NTuple{2, <:Any}, ) # Handle 2D data with 2 column axes (e.g., from reshaped 3D emulation data) return DenseAxisArray( permutedims(data), columns[1], columns[2], ) end function _make_denseaxisarray( data::Array{Float64, 3}, columns::NTuple{2, <:Any}, ) return DenseAxisArray( permutedims(data, (2, 3, 1)), columns[1], columns[2], 1:size(data)[1], ) end function read_result( ::Type{DenseAxisArray}, store::HdfSimulationStore, model_name::Symbol, key::OptimizationContainerKey, index::Union{DecisionModelIndexType, EmulationModelIndexType}, ) if is_cached(store.cache, model_name, key, index) data = read_result(store.cache, model_name, key, index) columns = get_column_names(store, DecisionModelIndexType, model_name, key) else data, columns = _read_result(store, model_name, key, index) end return _make_denseaxisarray(data, columns) end function read_result( ::Type{Array}, store::HdfSimulationStore, model_name::Symbol, key::OptimizationContainerKey, index::Union{DecisionModelIndexType, EmulationModelIndexType}, ) if is_cached(store.cache, model_name, key, index) data = read_result(store.cache, model_name, key, index) else data, _ = _read_result(store, model_name, key, index) end return data end function read_results( store::HdfSimulationStore, key::OptimizationContainerKey; index::Union{Nothing, EmulationModelIndexType} = nothing, len::Union{Nothing, Int} = nothing, ) dataset = _get_em_dataset(store, key) num_dims = ndims(dataset.values) if num_dims == 2 if isnothing(index) @assert_op(isnothing(len)) data = dataset.values[:, :] elseif isnothing(len) data = dataset.values[index:end, :] else data = dataset.values[index:(index + len - 1), :] end columns = get_column_names(key, dataset) return DenseAxisArray(permutedims(data), columns..., 1:size(data)[1]) elseif num_dims == 3 if isnothing(index) @assert_op(isnothing(len)) data = dataset.values[:, :, :] elseif isnothing(len) data = dataset.values[index:end, :, :] else data = dataset.values[index:(index + len - 1), :, :] end columns = get_column_names(key, dataset) return DenseAxisArray(permutedims(data, (2, 3, 1)), columns..., 1:size(data)[1]) else error("Unsupported number of dimensions for emulation dataset: $num_dims") end end function get_column_names( store::HdfSimulationStore, ::Type{DecisionModelIndexType}, model_name::Symbol, key::OptimizationContainerKey, ) !isopen(store) && throw(ArgumentError("store must be opened prior to reading")) dataset = _get_dm_dataset(store, model_name, key) return get_column_names(key, dataset) end function get_column_names( store::HdfSimulationStore, ::Type{EmulationModelIndexType}, key::OptimizationContainerKey, ) !isopen(store) && throw(ArgumentError("store must be opened prior to reading")) dataset = _get_em_dataset(store, key) return get_column_names(key, dataset) end function get_number_of_dimensions( store::HdfSimulationStore, i::Type{DecisionModelIndexType}, model_name::Symbol, key::OptimizationContainerKey, ) return length(get_column_names(store, i, model_name, key)) end function get_number_of_dimensions( store::HdfSimulationStore, i::Type{EmulationModelIndexType}, key::OptimizationContainerKey, ) return length(get_column_names(store, i, key)) end function get_emulation_model_dataset_size( store::HdfSimulationStore, key::OptimizationContainerKey, ) dataset = _get_em_dataset(store, key) return size(dataset.values)[1] end function _read_result( store::HdfSimulationStore, ::Symbol, key::OptimizationContainerKey, index::EmulationModelIndexType, ) !isopen(store) && throw(ArgumentError("store must be opened prior to reading")) model_params = get_emulation_model_params(store) if index > model_params.num_executions throw( ArgumentError( "index = $index cannot be larger than $(model_params.num_executions)", ), ) end dataset = _get_em_dataset(store, key) dset = dataset.values # Uncomment for performance checking #TimerOutputs.@timeit RUN_SIMULATION_TIMER "Read dataset" begin num_dims = ndims(dset) if num_dims == 2 data = dset[index, :] elseif num_dims == 3 data = dset[index, :, :] else error("Unsupported number of dimensions for emulation dataset: $num_dims") end #end columns = get_column_names(key, dataset) data = ndims(data) == 1 ? permutedims(data) : data return data, columns end function _read_result( store::HdfSimulationStore, model_name::Symbol, key::OptimizationContainerKey, index::DecisionModelIndexType, ) simulation_step, execution_index = _get_indices(store, model_name, index) return _read_result(store, model_name, key, simulation_step, execution_index) end function _read_result( store::HdfSimulationStore, model_name::Symbol, key::OptimizationContainerKey, simulation_step::Int, execution_index::Int, ) !isopen(store) && throw(ArgumentError("store must be opened prior to reading")) model_params = get_decision_model_params(store, model_name) num_executions = model_params.num_executions if execution_index > num_executions throw( ArgumentError( "execution_index = $execution_index cannot be larger than $num_executions", ), ) end dataset = _get_dm_dataset(store, model_name, key) dset = dataset.values row_index = (simulation_step - 1) * num_executions + execution_index columns = get_column_names(key, dataset) # Uncomment for performance checking #TimerOutputs.@timeit RUN_SIMULATION_TIMER "Read dataset" begin num_dims = ndims(dset) if num_dims == 3 data = dset[:, :, row_index] elseif num_dims == 4 data = dset[:, :, :, row_index] else error("unsupported dims: $num_dims") end #end return data, columns end """ Write a decision model result for a timestamp to the store. """ function write_result!( store::HdfSimulationStore, model_name::Symbol, key::OptimizationContainerKey, index::DecisionModelIndexType, ::Dates.DateTime, data::DenseAxisArray{Float64, N, <:NTuple{N, Any}}, ) where {N} output_cache = get_output_cache(store.cache, model_name, key) cur_size = get_size(store.cache) add_result!(output_cache, index, to_matrix(data), is_full(store.cache, cur_size)) if get_dirty_size(output_cache) >= get_min_flush_size(store.cache) discard = !should_keep_in_cache(output_cache) # PERF: A potentially significant performance improvement would be to queue several # flushes and submit them in parallel. size_flushed = _flush_data!(output_cache, store, model_name, key, discard) @debug "flushed data" LOG_GROUP_SIMULATION_STORE key size_flushed discard cur_size end # Disabled because this is currently a noop. #if is_full(store.cache) # _flush_data!(store.cache, store) #end @debug "write_result" get_size(store.cache) encode_key_as_string(key) return end """ Write a decision model result for a timestamp to the store. """ function write_result!( store::HdfSimulationStore, model_name::Symbol, key::OptimizationContainerKey, index::DecisionModelIndexType, ::Dates.DateTime, data::DenseAxisArray{Float64, 3, <:NTuple{3, Any}}, ) output_cache = get_output_cache(store.cache, model_name, key) cur_size = get_size(store.cache) add_result!( output_cache, index, permutedims(data.data, (3, 1, 2)), is_full(store.cache, cur_size), ) if get_dirty_size(output_cache) >= get_min_flush_size(store.cache) discard = !should_keep_in_cache(output_cache) # PERF: A potentially significant performance improvement would be to queue several # flushes and submit them in parallel. size_flushed = _flush_data!(output_cache, store, model_name, key, discard) @debug "flushed data" LOG_GROUP_SIMULATION_STORE key size_flushed discard cur_size end # Disabled because this is currently a noop. #if is_full(store.cache) # _flush_data!(store.cache, store) #end @debug "write_result" get_size(store.cache) encode_key_as_string(key) return end """ Write an emulation model result for an execution index value and the timestamp of the update """ function write_result!( store::HdfSimulationStore, ::Symbol, key::OptimizationContainerKey, index::EmulationModelIndexType, simulation_time::Dates.DateTime, array::DenseAxisArray{Float64, 2}, ) # TODO: This is a temporary fix. # Not sure why the special case for this dimension size is needed. # It fails with the key = InfrastructureSystems.Optimization.ParameterKey{OnStatusParameter, ThermalStandard}("") # The array size is 5 x 1 data = size(array, 2) == 1 ? reshape(array.data, length(array.data)) : array.data dataset = _get_em_dataset(store, key) _write_dataset!(dataset.values, data, index) set_last_recorded_row!(dataset, index) set_update_timestamp!(dataset, simulation_time) return end function write_result!( store::HdfSimulationStore, ::Symbol, key::OptimizationContainerKey, index::EmulationModelIndexType, simulation_time::Dates.DateTime, array::DenseAxisArray{Float64, 3}, ) # Handle 3D arrays by reshaping if the last dimension is 1 # This mirrors the 2D case above where size(array, 2) == 1 triggers a reshape if size(array, 3) == 1 data = reshape(array.data, size(array, 1), size(array, 2)) else data = array.data end dataset = _get_em_dataset(store, key) _write_dataset!(dataset.values, data, index) set_last_recorded_row!(dataset, index) set_update_timestamp!(dataset, simulation_time) return end function write_result!( store::HdfSimulationStore, ::Symbol, key::OptimizationContainerKey, index::EmulationModelIndexType, simulation_time::Dates.DateTime, array::DenseAxisArray{Float64}, ) dataset = _get_em_dataset(store, key) _write_dataset!(dataset.values, array.data, index) set_last_recorded_row!(dataset, index) set_update_timestamp!(dataset, simulation_time) 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") uuid = string(IS.get_uuid(sys)) if haskey(systems_group, uuid) @debug "System with UUID = $uuid is already stored" _group = LOG_GROUP_SIMULATION_STORE return end json_text = PSY.to_json(sys) systems_group[uuid] = json_text return end function write_system_json!(store::HdfSimulationStore, uuid::String, json_text::String) root = store.file[HDF_SIMULATION_ROOT_PATH] systems_group = _get_group_or_create(root, "systems") if !haskey(systems_group, uuid) systems_group[uuid] = json_text end return end function has_system(store::HdfSimulationStore, uuid::Base.UUID) root = store.file[HDF_SIMULATION_ROOT_PATH] haskey(root, "systems") || return false return haskey(root["systems"], string(uuid)) end function deserialize_system(store::HdfSimulationStore, uuid::Base.UUID) root = store.file[HDF_SIMULATION_ROOT_PATH] uuid_str = string(uuid) if !haskey(root, "systems") || !haskey(root["systems"], uuid_str) error("No system with UUID $uuid_str is stored") end json_text = HDF5.read(root["systems"][uuid_str]) return PSY.from_json(json_text, PSY.System) end function _check_state(store::HdfSimulationStore) if has_dirty(store.cache) error("BUG!!! dirty cache is present at shutdown: $(store.file)") end end function _compute_chunk_count(dims, dtype; max_chunk_bytes = DEFAULT_MAX_CHUNK_BYTES) bytes_per_element = sizeof(dtype) if length(dims) == 2 size_row = dims[1] * bytes_per_element elseif length(dims) == 3 size_row = dims[1] * dims[2] * bytes_per_element elseif length(dims) == 4 size_row = dims[1] * dims[2] * dims[3] * bytes_per_element else error("unsupported dims = $dims") end chunk_count = minimum((trunc(max_chunk_bytes / size_row), dims[end])) if chunk_count == 0 error( "HDF Max Chunk Bytes is smaller than the size of a row. Please increase it. " * "max_chunk_bytes=$max_chunk_bytes dims=$dims " * "size_row=$size_row", ) end chunk_dims = [x for x in dims] chunk_dims[end] = chunk_count return chunk_dims end function _create_dataset(group, name, reqs) dataset = HDF5.create_dataset( group, name, HDF5.datatype(Float64), HDF5.dataspace(reqs["dims"]), # We are choosing to optimize read performance in the first implementation. # Compression would slow that down. #chunk = _compute_chunk_count(reqs["dims"], Float64), #shuffle = (), #deflate = 3, ) @debug "Created dataset for" group name size(dataset) return dataset end function _deserialize_attributes!(store::HdfSimulationStore) container_key_lookup = get_container_key_lookup(store) group = store.file["simulation"] initial_time = Dates.DateTime(HDF5.read(HDF5.attributes(group)["initial_time"])) step_resolution = Dates.Millisecond(HDF5.read(HDF5.attributes(group)["step_resolution_ms"])) num_steps = HDF5.read(HDF5.attributes(group)["num_steps"]) store.params = SimulationStoreParams(initial_time, step_resolution, num_steps) empty!(get_dm_data(store)) for model in HDF5.read(HDF5.attributes(group)["problem_order"]) problem_group = store.file["simulation/decision_models/$model"] # 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_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"]), Base.UUID(HDF5.read(HDF5.attributes(problem_group)["system_uuid"])), ) get_dm_data(store)[model_name] = DatasetContainer{HDF5Dataset}() for type in STORE_CONTAINERS group = problem_group[string(type)] for name in keys(group) if !endswith(name, "columns") dataset = group[name] column_dataset = group[_make_column_name(name)] resolution = get_resolution(get_decision_model_params(store, model_name)) column_lengths = size(dataset)[2:(end - 1)] item = HDF5Dataset{length(column_lengths)}( dataset, column_dataset, column_lengths, resolution, initial_time, ) container_key = container_key_lookup[name] getfield(get_dm_data(store)[model_name], type)[container_key] = item add_output_cache!( store.cache, model_name, container_key, CacheFlushRule(), ) end end end store.optimizer_stats_datasets[model_name] = problem_group[OPTIMIZER_STATS_PATH] store.optimizer_stats_write_index[model_name] = 1 end em_group = _get_emulation_model_path(store) # 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"]), horizon_count, Dates.Millisecond(HDF5.read(HDF5.attributes(em_group)["interval_ms"])), resolution, HDF5.read(HDF5.attributes(em_group)["base_power"]), Base.UUID(HDF5.read(HDF5.attributes(em_group)["system_uuid"])), ) for type in STORE_CONTAINERS group = em_group[string(type)] for name in keys(group) if !endswith(name, "columns") dataset = group[name] column_dataset = group[_make_column_name(name)] column_lengths = size(dataset)[2:end] item = HDF5Dataset{length(column_lengths)}( dataset, column_dataset, column_lengths, resolution, initial_time, ) container_key = container_key_lookup[name] getfield(store.em_data, type)[container_key] = item add_output_cache!(store.cache, model_name, container_key, CacheFlushRule()) end end end # TODO: optimizer stats are not being written for EM. @debug "deserialized store params and datasets" store.params end function _serialize_attributes(store::HdfSimulationStore) params = store.params group = store.file["simulation"] HDF5.attributes(group)["problem_order"] = [string(k) for k in keys(params.decision_models_params)] HDF5.attributes(group)["initial_time"] = string(params.initial_time) HDF5.attributes(group)["step_resolution_ms"] = Dates.Millisecond(params.step_resolution).value HDF5.attributes(group)["num_steps"] = params.num_steps for problem in keys(params.decision_models_params) 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_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"] = Dates.Millisecond(params.decision_models_params[problem].interval).value HDF5.attributes(problem_group)["base_power"] = params.decision_models_params[problem].base_power HDF5.attributes(problem_group)["system_uuid"] = string(params.decision_models_params[problem].system_uuid) end if !isempty(params.emulation_model_params) em_params = first(values(params.emulation_model_params)) emulation_group = store.file["simulation/emulation_model"] 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_count"] = em_params.horizon_count HDF5.attributes(emulation_group)["resolution_ms"] = Dates.Millisecond(em_params.resolution).value HDF5.attributes(emulation_group)["interval_ms"] = Dates.Millisecond(em_params.interval).value HDF5.attributes(emulation_group)["base_power"] = em_params.base_power HDF5.attributes(emulation_group)["system_uuid"] = string(em_params.system_uuid) end return end function _flush_data!( cache::OptimizationOutputCache, store::HdfSimulationStore, model_name, key::OptimizationContainerKey, discard, ) return _flush_data!(cache, store, OptimizationResultCacheKey(model_name, key), discard) end function _flush_data!( cache::OptimizationOutputCache, store::HdfSimulationStore, cache_key::OptimizationResultCacheKey, discard::Bool, ) !has_dirty(cache) && return 0 dataset = _get_dm_dataset(store, cache_key) timestamps, data = get_dirty_data_to_flush!(cache) num_results = length(timestamps) @assert_op num_results == size(data)[end] end_index = dataset.write_index + length(timestamps) - 1 write_range = (dataset.write_index):end_index # Enable only for development and benchmarking #TimerOutputs.@timeit RUN_SIMULATION_TIMER "Write $(key.key) array to HDF" begin _write_dataset!(dataset.values, data, write_range) #end discard && discard_results!(cache, timestamps) dataset.write_index += num_results size_flushed = cache.size_per_entry * num_results @debug "Flushed cache results to HDF5" LOG_GROUP_SIMULATION_STORE cache_key size_flushed num_results get_size( store.cache, ) return size_flushed end function _get_dataset(::Type{OptimizerStats}, store::HdfSimulationStore, model_name) return store.optimizer_stats_datasets[model_name] end function _get_dataset(::Type{OptimizerStats}, store::HdfSimulationStore) return store.optimizer_stats_datasets end function _get_em_dataset(store::HdfSimulationStore, key::OptimizationContainerKey) return getfield(get_em_data(store), get_store_container_type(key))[key] end function _get_dm_dataset(store::HdfSimulationStore, model_name::Symbol) return get_dm_data(store)[model_name] end function _get_dm_dataset( store::HdfSimulationStore, model_name::Symbol, key::OptimizationContainerKey, ) return getfield(get_dm_data(store)[model_name], get_store_container_type(key))[key] end function _get_dm_dataset(store::HdfSimulationStore, key::OptimizationResultCacheKey) return _get_dm_dataset(store, key.model, key.key) end function _get_group_or_create(parent, group_name) if haskey(parent, group_name) group = parent[group_name] else group = HDF5.create_group(parent, group_name) @debug "Created group" group end return group end _make_column_name(name) = string(name) * "__columns" function _get_indices(store::HdfSimulationStore, model_name::Symbol, timestamp) time_diff = Dates.Millisecond(timestamp - store.params.initial_time) step = time_diff ÷ store.params.step_resolution + 1 if step > store.params.num_steps throw( ArgumentError("timestamp = $timestamp is beyond the simulation: step = $step"), ) end problem_params = store.params.decision_models_params[model_name] initial_time = store.params.initial_time + (step - 1) * store.params.step_resolution time_diff = timestamp - initial_time if time_diff % problem_params.interval != Dates.Millisecond(0) throw(ArgumentError("timestamp = $timestamp is not a valid problem timestamp")) end execution_index = time_diff ÷ problem_params.interval + 1 return step, execution_index end _get_root(store::HdfSimulationStore) = store.file[HDF_SIMULATION_ROOT_PATH] _get_emulation_model_path(store::HdfSimulationStore) = store.file[EMULATION_MODEL_PATH] function _read_column_names(::Type{OptimizerStats}, store::HdfSimulationStore) dataset = _get_dataset(OptimizerStats, store) return HDF5.read(HDF5.attributes(dataset), "columns") end function _read_data_columns( store::HdfSimulationStore, model_name::Symbol, key::OptimizationContainerKey, index::DecisionModelIndexType, ) if is_cached(store.cache, model_name, key, index) data = read_result(store.cache, model_name, key, index) column_dataset = _get_dm_dataset(store, model_name, key).column_dataset if ndims(column_dataset) == 1 columns = (column_dataset[:],) elseif ndims(column_dataset) == 2 columns = (column_dataset[:, 1], column_dataset[:, 2]) else error("Datasets with $(ndims(column_dataset)) columns not supported") end else data, columns = _read_result(store, model_name, key, index) end return data, columns end function _read_data_columns( store::HdfSimulationStore, model_name::Symbol, key::OptimizationContainerKey, index::EmulationModelIndexType, ) # TODO: Enable once the cache is in use for em_data #if is_cached(store.cache, model_name, key, index) # data = read_result(store.cache, model_name, key, index) #columns = _get_em_dataset(store, model_name, key).column_dataset[:] #else # data, columns = _read_result(store, model_name, key, index) #end return _read_result(store, model_name, key, index) end function _read_length(::Type{OptimizerStats}, store::HdfSimulationStore) dataset = _get_dataset(OptimizerStats, store) 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::Array{Float64, 3}, row_range::UnitRange{Int64}, ) dataset[:, :, row_range] = array @debug "wrote dm dataset" dataset row_range return end function _write_dataset!( dataset::HDF5.Dataset, array::Array{Float64, 4}, row_range::UnitRange{Int64}, ) 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::Vector{Float64}, index::EmulationModelIndexType, ) assign_maybe_broadcast!(dataset, array, (index,)) @debug "wrote em dataset" dataset index return end function _write_dataset!( dataset::HDF5.Dataset, array::Matrix{Float64}, index::EmulationModelIndexType, ) dataset[index, :, :] = array @debug "wrote em dataset" dataset index return end function _write_dataset!( dataset::HDF5.Dataset, array::Array{Float64, 3}, index::EmulationModelIndexType, ) dataset[index, :, :, :] = array @debug "wrote em dataset" dataset index return end # TODO DT: this looked wrong. Was it tested? function _write_dataset!( dataset::HDF5.Dataset, array::Array{Float64, 4}, index::EmulationModelIndexType, ) dataset[index, :, :, :] = array @debug "wrote em dataset" dataset index return end ================================================ FILE: src/simulation/in_memory_simulation_store.jl ================================================ """ Stores simulation data in memory """ mutable struct InMemorySimulationStore <: SimulationStore params::SimulationStoreParams dm_data::OrderedDict{Symbol, DecisionModelStore} em_data::EmulationModelStore container_key_lookup::Dict{String, OptimizationContainerKey} end function InMemorySimulationStore() return InMemorySimulationStore( SimulationStoreParams(), OrderedDict{Symbol, DecisionModelStore}(), EmulationModelStore(), Dict{String, OptimizationContainerKey}(), ) end function get_number_of_dimensions( store::InMemorySimulationStore, i::Type{EmulationModelIndexType}, key::OptimizationContainerKey, ) return length(get_column_names(store, i, key)) end function get_number_of_dimensions( store::InMemorySimulationStore, i::Type{DecisionModelIndexType}, model_name::Symbol, key::OptimizationContainerKey, ) return length(get_column_names(store, i, model_name, key)) end function open_store( func::Function, ::Type{InMemorySimulationStore}, directory::AbstractString, # Unused. Need to match the interface. mode = nothing, filename = nothing, ) store = InMemorySimulationStore() return func(store) end function Base.empty!(store::InMemorySimulationStore) for val in values(get_dm_data(store)) empty!(val) end empty!(get_em_data(store)) @debug "Emptied the store" _group = LOG_GROUP_SIMULATION_STORE return end Base.isopen(::InMemorySimulationStore) = true Base.close(::InMemorySimulationStore) = nothing Base.flush(::InMemorySimulationStore) = nothing get_params(store::InMemorySimulationStore) = store.params function get_decision_model_params(store::InMemorySimulationStore, model_name::Symbol) return get_params(store).decision_models_params[model_name] end get_container_key_lookup(store::InMemorySimulationStore) = store.container_key_lookup list_decision_models(x::InMemorySimulationStore) = collect(keys(x.dm_data)) log_cache_hit_percentages(::InMemorySimulationStore) = nothing function list_decision_model_keys( store::InMemorySimulationStore, model_name::Symbol, container_type::Symbol, ) return ISOPT.list_fields( _get_model_results(store, model_name), container_type, ) end function list_emulation_model_keys(store::InMemorySimulationStore, container_type::Symbol) return ISOPT.list_fields(store.em_data, container_type) end function write_result!( store::InMemorySimulationStore, model_name::Symbol, key::OptimizationContainerKey, index::DecisionModelIndexType, update_timestamp::Dates.DateTime, array, ) write_result!( get_dm_data(store)[model_name], model_name, key, index, update_timestamp, array, ) return end function write_result!( store::InMemorySimulationStore, model_name::Symbol, key::OptimizationContainerKey, index::EmulationModelIndexType, update_timestamp::Dates.DateTime, array, ) write_result!(get_em_data(store), model_name, key, index, update_timestamp, array) return end function read_optimizer_stats(store::InMemorySimulationStore, model_name) # TODO EmulationModel: this interface is TBD return read_optimizer_stats(get_dm_data(store)[model_name]) end function initialize_problem_storage!( store::InMemorySimulationStore, params::SimulationStoreParams, dm_problem_reqs::Dict{Symbol, SimulationModelStoreRequirements}, em_problem_reqs::SimulationModelStoreRequirements, ::CacheFlushRules, ) store.params = params for problem in keys(store.params.decision_models_params) get_dm_data(store)[problem] = DecisionModelStore() for type in STORE_CONTAINERS for (key, _) in getfield(dm_problem_reqs[problem], type) container = getfield(get_dm_data(store)[problem], type) container[key] = OrderedDict{Dates.DateTime, DenseAxisArray{Float64}}() store.container_key_lookup[encode_key_as_string(key)] = key @debug "Added $type $key in $problem" _group = LOG_GROUP_SIMULATION_STORE end end end for type in STORE_CONTAINERS for (key, reqs) in getfield(em_problem_reqs, type) container = get_data_field(get_em_data(store), type) container[key] = InMemoryDataset( fill!( DenseAxisArray{Float64}(undef, reqs["columns"]..., 1:reqs["dims"][1]), NaN, ), ) store.container_key_lookup[encode_key_as_string(key)] = key @debug "Added $type $key in emulation store" _group = LOG_GROUP_SIMULATION_STORE end end return end function get_column_names( store::InMemorySimulationStore, ::Type{DecisionModelIndexType}, model_name::Symbol, key::OptimizationContainerKey, ) return get_column_names(get_dm_data(store)[model_name], key) end function get_column_names( store::InMemorySimulationStore, ::Type{EmulationModelIndexType}, model_name::Symbol, key::OptimizationContainerKey, ) return get_column_names(get_em_data(store)[model_name], key) end function read_result( ::Type{DenseAxisArray}, store::InMemorySimulationStore, model_name::Symbol, key::OptimizationContainerKey, index::DecisionModelIndexType, ) return read_results(get_dm_data(store)[model_name], key; index = index) end function read_result( ::Type{Array}, store::InMemorySimulationStore, model_name::Symbol, key::OptimizationContainerKey, index::DecisionModelIndexType, ) return permutedims( read_results(get_dm_data(store)[model_name], key; index = index).data, ) end function read_result( ::Type{DenseAxisArray}, store::InMemorySimulationStore, ::Symbol, key::OptimizationContainerKey, index::EmulationModelIndexType, ) return read_results(get_em_data(store), key; index = index) end function read_results( store::InMemorySimulationStore, key::OptimizationContainerKey; index::EmulationModelIndexType = nothing, len::Int = nothing, ) return read_results(get_em_data(store), key; index = index, len = len) end function get_emulation_model_dataset_size( store::InMemorySimulationStore, key::OptimizationContainerKey, ) return get_dataset_size(get_em_data(store), key)[2] end # Note that this function is not type-stable. function _get_model_results(store::InMemorySimulationStore, model_name::Symbol) if model_name in keys(get_dm_data(store)) results = get_dm_data(store) else # TODO EmulationModel: this interface is TBD error("model name $model_name is not stored") end return results[model_name] end function write_optimizer_stats!( store::InMemorySimulationStore, model::DecisionModel, index::DecisionModelIndexType, ) stats = get_optimizer_stats(model) dm_data = get_dm_data(store) write_optimizer_stats!(dm_data[get_name(model)], stats, index) read_optimizer_stats(dm_data[get_name(model)]) return end function write_optimizer_stats!( store::InMemorySimulationStore, model::EmulationModel, index::EmulationModelIndexType, ) stats = get_optimizer_stats(model) em_data = get_em_data(store) write_optimizer_stats!(em_data, stats, index) return end serialize_system!(::InMemorySimulationStore, ::PSY.System) = nothing write_system_json!(::InMemorySimulationStore, ::String, ::String) = nothing ================================================ FILE: src/simulation/initial_condition_update_simulation.jl ================================================ function update_initial_conditions!( model::OperationModel, state::SimulationState, ::InterProblemChronology, ) for key in keys(get_initial_conditions(model)) update_initial_conditions!(model, key, state) end return end function update_initial_conditions!( ::OperationModel, ::SimulationState, ::IntraProblemChronology, ) #for key in keys(get_initial_conditions(model)) # update_initial_conditions!(model, key, state) #end error("Not Implemented yet") return end function update_initial_conditions!( ics::T, state::SimulationState, model_resolution::Dates.Millisecond, ) where { T <: Union{ Vector{ Union{ InitialCondition{InitialTimeDurationOn, Nothing}, InitialCondition{InitialTimeDurationOn, Float64}, }, }, Vector{ Union{ InitialCondition{InitialTimeDurationOn, Nothing}, InitialCondition{InitialTimeDurationOn, JuMP.VariableRef}, }, }, }, } for ic in ics var_val = get_system_state_value(state, TimeDurationOn(), get_component_type(ic)) state_resolution = get_data_resolution( get_system_state_data(state, TimeDurationOn(), get_component_type(ic)), ) # The state data is stored in the state resolution (i.e. lowest resolution among all models) # so this step scales the data to the model resolution. val = var_val[get_component_name(ic)] / (model_resolution / state_resolution) set_ic_quantity!(ic, val) end return end function update_initial_conditions!( ics::T, state::SimulationState, model_resolution::Dates.Millisecond, ) where { T <: Union{ Vector{ Union{ InitialCondition{InitialTimeDurationOff, Nothing}, InitialCondition{InitialTimeDurationOff, Float64}, }, }, Vector{ Union{ InitialCondition{InitialTimeDurationOff, Nothing}, InitialCondition{InitialTimeDurationOff, JuMP.VariableRef}, }, }, }, } for ic in ics isnothing(get_value(ic)) && continue var_val = get_system_state_value(state, TimeDurationOff(), get_component_type(ic)) state_resolution = get_data_resolution( get_system_state_data(state, TimeDurationOff(), get_component_type(ic)), ) # The state data is stored in the state resolution (i.e. lowest resolution among all models) # so this step scales the data to the model resolution. val = var_val[get_component_name(ic)] / (model_resolution / state_resolution) set_ic_quantity!(ic, val) end return end function update_initial_conditions!( ics::T, state::SimulationState, ::Dates.Millisecond, ) where { T <: Union{ Vector{ Union{ InitialCondition{DevicePower, Nothing}, InitialCondition{DevicePower, Float64}, }, }, Vector{ Union{ InitialCondition{DevicePower, Nothing}, InitialCondition{DevicePower, JuMP.VariableRef}, }, }, }, } for ic in ics comp_name = get_component_name(ic) comp_type = get_component_type(ic) comp = get_component(ic) if hasmethod(PSY.get_must_run, Tuple{comp_type}) && PSY.get_must_run(comp) status_val = 1.0 else status_val = get_system_state_value(state, OnVariable(), comp_type)[comp_name] end var_val = get_system_state_value(state, ActivePowerVariable(), comp_type)[comp_name] if !isapprox(status_val, 0.0; atol = ABSOLUTE_TOLERANCE) min = PSY.get_active_power_limits(comp).min max = PSY.get_active_power_limits(comp).max if var_val <= max && var_val >= min set_ic_quantity!(ic, var_val) elseif isapprox(min - var_val, 0.0; atol = ABSOLUTE_TOLERANCE) set_ic_quantity!(ic, min) elseif isapprox(var_val - max, 0.0; atol = ABSOLUTE_TOLERANCE) set_ic_quantity!(ic, max) else error("Variable value $(var_val) for ActivePowerVariable \\ Status value $(status_val) for OnVariable \\ $(comp_type)-$(comp_name) is out of bounds [$(min), $(max)].") end else if !isapprox(var_val, 0.0; atol = ABSOLUTE_TOLERANCE) error("Status and Power variables don't match for $comp_name. \\ ActivePowerVariable: $(var_val)\\ Status value: $(status_val) for OnVariable") end set_ic_quantity!(ic, 0.0) end end return end function update_initial_conditions!( ics::T, state::SimulationState, ::Dates.Millisecond, ) where { T <: Union{ Vector{ Union{ InitialCondition{DeviceStatus, Nothing}, InitialCondition{DeviceStatus, Float64}, }, }, Vector{ Union{ InitialCondition{DeviceStatus, Nothing}, InitialCondition{DeviceStatus, JuMP.VariableRef}, }, }, }, } for ic in ics isnothing(get_value(ic)) && continue var_val = get_system_state_value(state, OnVariable(), get_component_type(ic)) set_ic_quantity!(ic, var_val[get_component_name(ic)]) end return end function update_initial_conditions!( ics::T, state::SimulationState, ::Dates.Millisecond, ) where { T <: Union{ Vector{ Union{ InitialCondition{DeviceAboveMinPower, Nothing}, InitialCondition{DeviceAboveMinPower, Float64}, }, }, Vector{ Union{ InitialCondition{DeviceAboveMinPower, Nothing}, InitialCondition{DeviceAboveMinPower, JuMP.VariableRef}, }, }, }, } for ic in ics var_val = get_system_state_value( state, PowerAboveMinimumVariable(), get_component_type(ic), ) set_ic_quantity!(ic, var_val[get_component_name(ic)]) end return end function update_initial_conditions!( ics::T, state::SimulationState, ::Dates.Millisecond, ) where { T <: Union{ Vector{ Union{ InitialCondition{InitialEnergyLevel, Nothing}, InitialCondition{InitialEnergyLevel, Float64}, }, }, Vector{ Union{ InitialCondition{InitialEnergyLevel, Nothing}, InitialCondition{InitialEnergyLevel, JuMP.VariableRef}, }, }, }, } for ic in ics var_val = get_system_state_value(state, EnergyVariable(), get_component_type(ic)) set_ic_quantity!(ic, var_val[get_component_name(ic)]) end return end ================================================ FILE: src/simulation/optimization_output_cache.jl ================================================ """ Cache for a single parameter/variable/dual. Stores arrays chronologically by simulation timestamp. """ mutable struct OptimizationOutputCache key::OptimizationResultCacheKey "Contains both clean and dirty entries. Any key in data that is earlier than the first dirty timestamp must be clean." data::OrderedDict{Dates.DateTime, Array} "Oldest entry is first" dirty_timestamps::Deque{Dates.DateTime} stats::CacheStats size_per_entry::Int flush_rule::CacheFlushRule end function OptimizationOutputCache(key, flush_rule) return OptimizationOutputCache( key, OrderedDict{Dates.DateTime, Array}(), Deque{Dates.DateTime}(), CacheStats(), 0, flush_rule, ) end Base.length(x::OptimizationOutputCache) = length(x.data) get_cache_hit_percentage(x::OptimizationOutputCache) = get_cache_hit_percentage(x.stats) get_size(x::OptimizationOutputCache) = length(x) * x.size_per_entry has_clean(x::OptimizationOutputCache) = !isempty(x.data) && !is_dirty(x, first(keys(x.data))) has_dirty(x::OptimizationOutputCache) = !isempty(x.dirty_timestamps) should_keep_in_cache(x::OptimizationOutputCache) = x.flush_rule.keep_in_cache function get_dirty_size(cache::OptimizationOutputCache) return length(cache.dirty_timestamps) * cache.size_per_entry end function is_dirty(cache::OptimizationOutputCache, timestamp) isempty(cache.dirty_timestamps) && return false return timestamp >= first(cache.dirty_timestamps) end """ Base.empty!(cache::OptimizationOutputCache) Empty the [`OptimizationOutputCache`](@ref) """ function Base.empty!(cache::OptimizationOutputCache) @assert isempty(cache.dirty_timestamps) "dirty cache was still present $(cache.key) $(cache.dirty_timestamps)" empty!(cache.data) cache.size_per_entry = 0 return end """ Add result to the cache. """ function add_result!(cache::OptimizationOutputCache, timestamp::Dates.DateTime, array::Array{Float64}, system_cache_is_full::Bool) if cache.size_per_entry == 0 cache.size_per_entry = length(array) * sizeof(first(array)) end @debug "add_result!" cache.key timestamp get_size(cache) if haskey(cache.data, timestamp) throw(IS.InvalidValue("$timestamp is already stored in $(cache.key)")) end # Note that we buffer all writes in cache until we reach the flush size. # The entries using "should_keep_in_cache" can grow quite large for read caching. if system_cache_is_full && should_keep_in_cache(cache) if has_clean(cache) popfirst!(cache.data) @debug "replaced cache entry" LOG_GROUP_SIMULATION_STORE cache.key length( cache.data, ) end end _add_result!(cache, timestamp, array) return cache.size_per_entry end function _add_result!( cache::OptimizationOutputCache, timestamp::Dates.DateTime, data::Array{Float64}, ) cache.data[timestamp] = data push!(cache.dirty_timestamps, timestamp) return end function discard_results!(cache::OptimizationOutputCache, timestamps) for timestamp in timestamps pop!(cache.data, timestamp) end @debug "Removed $(first(timestamps)) - $(last(timestamps)) from cache" cache.key return end """ Return all dirty data from the cache. Mark the timestamps as clean. """ function get_dirty_data_to_flush!(cache::OptimizationOutputCache) timestamps = [x for x in cache.dirty_timestamps] empty!(cache.dirty_timestamps) # Uncomment for performance testing of CacheFlush #TimerOutputs.@timeit RUN_SIMULATION_TIMER "Concatenate arrays for flush" begin temp = cache.data[first(timestamps)] sd = collect(size(temp)) push!(sd, length(timestamps)) arrays = Array{Float64}(undef, sd...) for (ix, x) in enumerate(timestamps) temp_data = cache.data[x] if ndims(temp_data) == 1 arrays[:, ix] = temp_data elseif ndims(temp_data) == 2 arrays[:, :, ix] = temp_data elseif ndims(temp_data) == 3 arrays[:, :, :, ix] = temp_data else error("Arrays of dimensions $(ndims(temp_data)) are not supported") end end return timestamps, arrays end function has_timestamp(cache::OptimizationOutputCache, timestamp) present = haskey(cache.data, timestamp) if present cache.stats.hits += 1 else cache.stats.misses += 1 end return present end ================================================ FILE: src/simulation/optimization_output_caches.jl ================================================ """ Cache for all model results """ struct OptimizationOutputCaches data::Dict{OptimizationResultCacheKey, OptimizationOutputCache} max_size::Int min_flush_size::Int end function OptimizationOutputCaches() return OptimizationOutputCaches( Dict{OptimizationResultCacheKey, OptimizationOutputCache}(), 0, 0, ) end function OptimizationOutputCaches(rules::CacheFlushRules) return OptimizationOutputCaches( Dict{OptimizationResultCacheKey, OptimizationOutputCache}(), rules.max_size, rules.min_flush_size, ) end """ Base.empty!(cache::OptimizationOutputCaches) Empty the [`OptimizationOutputCaches`](@ref) """ function Base.empty!(cache::OptimizationOutputCaches) for output_cache in values(cache.data) empty!(output_cache) end end # PERF: incremental improvement if we manually keep track of the current size. # Could be prone to bugs if we miss a change. # The number of containers is not expected to be more than 100. #decrement_size!(cache::OptimizationOutputCaches, size) = cache.size -= size #increment_size!(cache::OptimizationOutputCaches, size) = cache.size += size get_max_size(cache::OptimizationOutputCaches) = cache.max_size get_min_flush_size(cache::OptimizationOutputCaches) = cache.min_flush_size get_size(cache::OptimizationOutputCaches) = reduce(+, (get_size(x) for x in values(cache.data))) # Leave some buffer because we may slightly exceed the limit. is_full(cache::OptimizationOutputCaches, cur_size) = cur_size >= cache.max_size * 0.95 function add_output_cache!(cache::OptimizationOutputCaches, model_name, key, flush_rule) cache_key = OptimizationResultCacheKey(model_name, key) cache.data[cache_key] = OptimizationOutputCache(cache_key, flush_rule) @debug "Added cache container for" LOG_GROUP_SIMULATION_STORE model_name key flush_rule return end """ Return true if the cache has data that has not been flushed to storage. """ function has_dirty(cache::OptimizationOutputCaches) for output_cache in values(cache.data) if has_dirty(output_cache) return true end end return false end get_output_cache(cache::OptimizationOutputCaches, key::OptimizationResultCacheKey) = cache.data[key] function get_output_cache( cache::OptimizationOutputCaches, model_name, key::OptimizationContainerKey, ) cache_key = OptimizationResultCacheKey(model_name, key) return get_output_cache(cache, cache_key) end """ Return true if the data for `timestamp` is stored in cache. """ function is_cached(cache::OptimizationOutputCaches, model_name, key, index) cache_key = OptimizationResultCacheKey(model_name, key) return is_cached(cache, cache_key, index) end is_cached(cache::OptimizationOutputCaches, key, timestamp::Dates.DateTime) = has_timestamp(cache.data[key], timestamp::Dates.DateTime) is_cached(cache::OptimizationOutputCaches, key, ::Int) = false """ Log the cache hit percentages for all caches. """ function log_cache_hit_percentages(cache::OptimizationOutputCaches) for key in keys(cache.data) output_cache = cache.data[key] cache_hit_pecentage = get_cache_hit_percentage(output_cache) @debug "Cache stats" LOG_GROUP_SIMULATION_STORE key cache_hit_pecentage end return end """ Read the result from cache. Callers must first call [`is_cached`](@ref) to check if the timestamp is present. """ function read_result(cache::OptimizationOutputCaches, model_name, key, timestamp) cache_key = OptimizationResultCacheKey(model_name, key) return read_result(cache, cache_key, timestamp) end read_result(cache::OptimizationOutputCaches, key, timestamp) = cache.data[key].data[timestamp] ================================================ FILE: src/simulation/realized_meta.jl ================================================ struct RealizedMeta start_time::Dates.DateTime resolution::Dates.TimePeriod len::Int start_offset::Int end_offset::Int interval_len::Int realized_timestamps::AbstractVector{Dates.DateTime} end function RealizedMeta( res::SimulationProblemResults; start_time::Union{Nothing, Dates.DateTime} = nothing, len::Union{Int, Nothing} = nothing, ) existing_timestamps = get_timestamps(res) interval = existing_timestamps.step resolution = get_resolution(res) interval_len = Int(interval / resolution) realized_timestamps = get_realized_timestamps(res; start_time = start_time, len = len) result_start_time = existing_timestamps[findlast( x -> x .<= first(realized_timestamps), existing_timestamps, )] result_end_time = existing_timestamps[findlast( x -> x .<= last(realized_timestamps), existing_timestamps, )] len = length(result_start_time:interval:result_end_time) start_offset = length(result_start_time:resolution:first(realized_timestamps)) end_offset = length( (last(realized_timestamps) + resolution):resolution:(result_end_time + interval - resolution), ) return RealizedMeta( result_start_time, resolution, len, start_offset, end_offset, interval_len, realized_timestamps, ) end function _make_dataframe( results_by_time::ResultsByTime{DataFrame, N}, num_timestamps::Int, meta::RealizedMeta, key::OptimizationContainerKey, ::Val{TableFormat.LONG}, ) where {N} @assert !isempty(results_by_time) row_index = 1 dfs = DataFrame[] first_cols = names(first(values(results_by_time.data))) for (step, (_, df)) in enumerate(results_by_time) if step > 1 && names(df) != first_cols error("Mismatched columns. First df = $(first_cols), other df = $(names(df))") end first_id = step > 1 ? 1 : meta.start_offset last_id = step == meta.len ? meta.interval_len - meta.end_offset : meta.interval_len if last_id - first_id > DataFrames.nrow(df) error( "Variable $(encode_key_as_string(key)) has $(DataFrames.nrow(df)) number of steps, that is different than the default problem horizon. \ Can't calculate the realized variables. Use `read_variables` instead and write your own concatenation", ) end offset = (step - 1) * meta.interval_len df2 = @chain df begin @subset(first_id .<= :time_index .<= last_id) @transform(:actual_time_index = :time_index .+ offset) @select(Not(:time_index)) @rename(:time_index = :actual_time_index) end push!(dfs, df2) row_index += last_id - first_id + 1 end combined_df = vcat(dfs...) time_df = DataFrame(; DateTime = meta.realized_timestamps, time_index = (meta.start_offset):(meta.start_offset + length( meta.realized_timestamps, ) - 1), ) result_df = @chain begin innerjoin(combined_df, time_df; on = :time_index) @select(:DateTime, Not(:DateTime, :time_index)) @orderby(:DateTime) end actual_num_timestamps = length(unique(result_df.DateTime)) if actual_num_timestamps != num_timestamps error( "Mismatched number of timestamps. Expected $(num_timestamps), got $actual_num_timestamps", ) end return result_df end function _make_dataframe( results_by_time::ResultsByTime{DataFrame, N}, num_timestamps::Int, meta::RealizedMeta, key::OptimizationContainerKey, ::Val{TableFormat.WIDE}, ) where {N} @assert !isempty(results_by_time) row_index = 1 dfs = DataFrame[] first_cols = names(first(values(results_by_time.data))) for (step, (_, df)) in enumerate(results_by_time) if step > 1 && names(df) != first_cols error("Mismatched columns. First df = $(first_cols), other df = $(names(df))") end first_id = step > 1 ? 1 : meta.start_offset last_id = step == meta.len ? meta.interval_len - meta.end_offset : meta.interval_len if last_id - first_id > DataFrames.nrow(df) error( "Variable $(encode_key_as_string(key)) has $(DataFrames.nrow(df)) number of steps, that is different than the default problem horizon. \ Can't calculate the realized variables. Use `read_variables` instead and write your own concatenation", ) end df2 = df[first_id:last_id, :] push!(dfs, df2) row_index += last_id - first_id + 1 end df = vcat(dfs...) DataFrames.insertcols!( df, 1, :DateTime => meta.realized_timestamps, ) DataFrames.select!(df, DataFrames.Not(:time_index)) if DataFrames.nrow(df) != num_timestamps error( "Mismatched number of rows. Expected $(num_timestamps), got $(DataFrames.nrow(df))", ) end return df end function get_realization( results::Dict{OptimizationContainerKey, ResultsByTime{DataFrame}}, meta::RealizedMeta; table_format = TableFormat.LONG, ) realized_values = Dict{OptimizationContainerKey, DataFrames.DataFrame}() lk = ReentrantLock() num_timestamps = length(meta.realized_timestamps) start = time() Threads.@threads for key in collect(keys(results)) results_by_time = results[key] lock(lk) do realized_values[key] = _make_dataframe( results_by_time, num_timestamps, meta, key, Val(table_format), ) end end duration = time() - start if Threads.nthreads() == 1 && duration > 10.0 @info "Time to read results: $duration seconds. You will likely get faster " * "results by starting Julia with multiple threads." end return realized_values end ================================================ FILE: src/simulation/simulation.jl ================================================ """ Simulation( sequence::SimulationSequence, name::String, steps::Int models::SimulationModels, simulation_folder::String, initial_time::Union{Nothing, Dates.DateTime} ) Construct the `Simulation` structure to run the sequence of decision and emulation models specified. # Arguments - `sequence::SimulationSequence`: Simulation sequence that specify how the decision and emulation models will be executed. - `name::String`: Name of the Simulation - `steps::Int`: Number of steps on which the sequence of models will be executed - `models::SimulationModels`: List of Decision and Emulation Models - `simulation_folder::String`: Folder on which results will be stored - `initial_time::Union{Nothing, Dates.DateTime} = nothing`: Initial time of which the simulation starts. If nothing it will default to the first timestamp of time series of the system. # Example ```julia template_uc = template_unit_commitment() template_ed = template_economic_dispatch() my_decision_model_uc = DecisionModel(template_1, sys_uc, optimizer, name = "UC") my_decision_model_ed = DecisionModel(template_ed, sys_ed, optimizer, name = "ED") models = SimulationModels( decision_models = [ my_decision_model_uc, my_decision_model_ed ] ) # The following sequence set the commitment variables (`OnVariable`) for `ThermalStandard` units from UC to ED. sequence = SimulationSequence(; models = models, feedforwards = Dict( "ED" => [ SemiContinuousFeedforward(; component_type = ThermalStandard, source = OnVariable, affected_values = [ActivePowerVariable], ), ], ), ) sim = Simulation( sequence = sequence, name = "Sim", steps = 5, models = models, simulation_folder = mktempdir(cleanup=true), ) ``` """ mutable struct Simulation steps::Int models::SimulationModels initial_time::Union{Nothing, Dates.DateTime} sequence::SimulationSequence simulation_folder::String name::String internal::Union{Nothing, SimulationInternal} function Simulation(; sequence::SimulationSequence, name::String, steps::Int, models::SimulationModels, simulation_folder::AbstractString, initial_time = nothing, ) for model in get_decision_models(models) if get_sequence_uuid(model) != sequence.uuid model_name = get_name(model) throw( IS.ConflictingInputsError( "The decision model definition for $model_name doesn't correspond to the simulation sequence", ), ) end end em = get_emulation_model(models) if em !== nothing if get_sequence_uuid(em) != sequence.uuid model_name = get_name(em) throw( IS.ConflictingInputsError( "The emulation model definition for $model_name doesn't correspond to the simulation sequence", ), ) end end new(steps, models, initial_time, sequence, simulation_folder, name, nothing) end end ###################### Simulation Accessor Functions #################### function get_base_powers(sim::Simulation) base_powers = Dict() for model in get_models(sim) base_powers[get_name(model)] = PSY.get_base_power(get_system(model)) end return base_powers end get_initial_time(sim::Simulation) = sim.initial_time get_sequence(sim::Simulation) = sim.sequence get_steps(sim::Simulation) = sim.steps get_current_time(sim::Simulation) = get_current_time(get_simulation_state(sim)) get_simulation_model(s::Simulation, name) = get_simulation_model(get_models(s), name) get_models(sim::Simulation) = sim.models get_simulation_dir(sim::Simulation) = dirname(sim.internal.logs_dir) get_simulation_files_dir(sim::Simulation) = sim.internal.sim_files_dir get_simulation_partitions_dir(sim::Simulation) = joinpath(get_simulation_dir(sim), "simulation_partitions") get_store_dir(sim::Simulation) = sim.internal.store_dir get_simulation_status(sim::Simulation) = sim.internal.status get_simulation_build_status(sim::Simulation) = sim.internal.build_status get_simulation_state(sim::Simulation) = sim.internal.simulation_state set_simulation_store!(sim::Simulation, store) = sim.internal.store = store get_simulation_store(sim::Simulation) = sim.internal.store get_results_dir(sim::Simulation) = sim.internal.results_dir get_models_dir(sim::Simulation) = sim.internal.models_dir get_interval(sim::Simulation, name::Symbol) = get_interval(sim.sequence, name) function get_simulation_time(sim::Simulation, problem_number::Int) return sim.internal.date_ref[problem_number] end get_ini_cond_chronology(sim::Simulation) = get_sequence(sim).ini_cond_chronology get_name(sim::Simulation) = sim.name get_simulation_folder(sim::Simulation) = sim.simulation_folder get_execution_order(sim::Simulation) = get_sequence(sim).execution_order get_current_execution_index(sim::Simulation) = get_sequence(sim).current_execution_index get_logs_folder(sim::Simulation) = sim.internal.logs_dir get_recorder_folder(sim::Simulation) = sim.internal.recorder_dir get_console_level(sim::Simulation) = sim.internal.console_level get_file_level(sim::Simulation) = sim.internal.file_level get_rng(sim::Simulation) = sim.internal.rng set_simulation_status!(sim::Simulation, status) = sim.internal.status = status set_simulation_build_status!(sim::Simulation, status::SimulationBuildStatus) = sim.internal.build_status = status function set_current_time!(sim::Simulation, val::Dates.DateTime) set_current_time!(get_simulation_state(sim), val) return end function _get_simulation_initial_times!(sim::Simulation) model_initial_times = OrderedDict{Int, Vector{Dates.DateTime}}() sim_ini_time = get_initial_time(sim) for (model_number, model) in enumerate(get_models(sim).decision_models) system = get_system(model) model_interval = get_interval(get_settings(model)) interval_kwarg = model_interval == UNSET_INTERVAL ? (;) : (; interval = model_interval) model_horizon = get_horizon(model) system_horizon = PSY.get_forecast_horizon(system; interval_kwarg...) system_interval = PSY.get_forecast_interval(system; interval_kwarg...) if model_horizon > system_horizon throw( IS.ConflictingInputsError( "$(get_name(model)) model horizon: $(Dates.canonicalize(model_horizon)) and forecast horizon: $(Dates.canonicalize(system_horizon)) are not compatible", ), ) end model_initial_times[model_number] = PSY.get_forecast_initial_times(system; interval_kwarg...) for (ix, element) in enumerate(model_initial_times[model_number][1:(end - 1)]) if !(element + system_interval == model_initial_times[model_number][ix + 1]) throw( IS.ConflictingInputsError( "The sequence of forecasts in the model's systems are invalid", ), ) end end if sim_ini_time !== nothing && !mapreduce(x -> x == sim_ini_time, |, model_initial_times[model_number]) throw( IS.ConflictingInputsError( "The specified simulation initial_time $sim_ini_time isn't contained in model $(get_name(model)). Manually provided initial times have to be compatible with the specified interval and horizon in the models.", ), ) end end if get_initial_time(sim) === nothing sim.initial_time = model_initial_times[1][1] @debug("Initial Simulation timestamp will be infered from the data. \\ Initial Simulation timestamp set to $(sim.initial_time)") end if get_models(sim).emulation_model !== nothing em = get_models(sim).emulation_model system = get_system(em) resolution = get_resolution(em) ini_time, ts_length = get_single_time_series_consistency(system, resolution) em_available_times = range(ini_time; step = resolution, length = ts_length) if get_initial_time(sim) ∉ em_available_times throw( IS.ConflictingInputsError( "The simulation initial_time $sim_ini_time isn't contained in the emulation model $(get_name(em)).", ), ) else model_initial_times[length(model_initial_times) + 1] = [sim.initial_time] end end set_current_time!(sim, sim.initial_time) return model_initial_times end function _check_steps( sim::Simulation, model_initial_times::OrderedDict{Int, Vector{Dates.DateTime}}, ) sequence = get_sequence(sim) execution_order = get_execution_order(sequence) for (model_number, model) in enumerate(get_models(sim).decision_models) execution_counts = get_executions(model) # Checks the consistency between two methods of calculating the number of executions total_model_executions = length(findall(x -> x == model_number, execution_order)) @assert_op total_model_executions == execution_counts forecast_count = length(model_initial_times[model_number]) if get_steps(sim) * execution_counts > forecast_count throw( IS.ConflictingInputsError( "The number of available time series ($(forecast_count)) is not enough to perform the desired amount of simulation steps ($(sim.steps*execution_counts)).", ), ) end end return end function _check_folder(sim::Simulation) folder = get_simulation_folder(sim) !isdir(folder) && throw(IS.ConflictingInputsError("Specified folder = $folder is not valid")) try mkdir(joinpath(folder, "fake")) rm(joinpath(folder, "fake")) catch e throw(IS.ConflictingInputsError("Specified folder does not have write access [$e]")) 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) 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 end end _initial_conditions_reconciliation!(get_decision_models(get_models(sim))) return end function _build_emulation_model!(sim::Simulation) model = get_emulation_model(get_models(sim)) if model === nothing return end try 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) 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, ModelBuildStatus.BUILT) catch set_status!(model, ModelBuildStatus.FAILED) rethrow() end return end function _initialize_simulation_state!(sim::Simulation) step_resolution = get_step_resolution(get_sequence(sim)) simulation_models = get_models(sim) initialize_simulation_state!( get_simulation_state(sim), simulation_models, step_resolution, get_initial_time(sim), ) return end function _get_model_store_requirements!( rules::CacheFlushRules, model::OperationModel, num_rows::Int, ) 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_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_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_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_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_count) add_rule!(rules, model_name, key, false) end return reqs end function _get_emulation_store_requirements(sim::Simulation) sim_state = get_simulation_state(sim) system_state = get_system_states(sim_state) sim_time = get_steps(sim) * get_step_resolution(get_sequence(sim)) reqs = SimulationModelStoreRequirements() for (key, state_values) in get_duals_values(system_state) !should_write_resulting_value(key) && continue num_time_rows = sim_time ÷ get_data_resolution(state_values) cols = get_column_names(key, state_values) # Use actual data dimensions (excluding last axis which is time) for proper HDF storage data_dims = size(state_values.values)[1:(end - 1)] reqs.duals[key] = Dict("columns" => cols, "dims" => (num_time_rows, data_dims...)) end for (key, state_values) in get_parameters_values(system_state) !should_write_resulting_value(key) && continue num_time_rows = sim_time ÷ get_data_resolution(state_values) cols = get_column_names(key, state_values) # Use actual data dimensions (excluding last axis which is time) for proper HDF storage data_dims = size(state_values.values)[1:(end - 1)] reqs.parameters[key] = Dict("columns" => cols, "dims" => (num_time_rows, data_dims...)) end for (key, state_values) in get_variables_values(system_state) !should_write_resulting_value(key) && continue num_time_rows = sim_time ÷ get_data_resolution(state_values) cols = get_column_names(key, state_values) # Use actual data dimensions (excluding last axis which is time) for proper HDF storage data_dims = size(state_values.values)[1:(end - 1)] reqs.variables[key] = Dict("columns" => cols, "dims" => (num_time_rows, data_dims...)) end for (key, state_values) in get_aux_variables_values(system_state) !should_write_resulting_value(key) && continue num_time_rows = sim_time ÷ get_data_resolution(state_values) cols = get_column_names(key, state_values) # Use actual data dimensions (excluding last axis which is time) for proper HDF storage data_dims = size(state_values.values)[1:(end - 1)] reqs.aux_variables[key] = Dict("columns" => cols, "dims" => (num_time_rows, data_dims...)) end for (key, state_values) in get_expression_values(system_state) !should_write_resulting_value(key) && continue num_time_rows = sim_time ÷ get_data_resolution(state_values) cols = get_column_names(key, state_values) # Use actual data dimensions (excluding last axis which is time) for proper HDF storage data_dims = size(state_values.values)[1:(end - 1)] reqs.expressions[key] = Dict("columns" => cols, "dims" => (num_time_rows, data_dims...)) end return reqs end function _initialize_problem_storage!( sim::Simulation, cache_size_mib::Int = DEFAULT_SIMULATION_STORE_CACHE_SIZE_MiB, min_cache_flush_size_mib::Int = MIN_CACHE_FLUSH_SIZE_MiB, ) sequence = get_sequence(sim) executions_by_model = sequence.executions_by_model models = get_models(sim) decision_model_store_params = OrderedDict{Symbol, ModelStoreParams}() dm_model_req = Dict{Symbol, SimulationModelStoreRequirements}() rules = CacheFlushRules(; max_size = cache_size_mib * MiB, min_flush_size = trunc(min_cache_flush_size_mib * MiB), ) for model in get_decision_models(models) model_name = get_name(model) 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) end em = get_emulation_model(models) if em === nothing base_params = last(collect(values(decision_model_store_params))) resolution = minimum([v.resolution for v in values(decision_model_store_params)]) emulation_model_store_params = OrderedDict( :Emulator => ModelStoreParams( get_step_resolution(sequence) ÷ resolution, # Num Executions resolution, # Horizon resolution, # Interval resolution, # Resolution get_base_power(base_params), get_system_uuid(base_params), ), ) else emulation_model_store_params = OrderedDict(Symbol(get_name(em)) => get_store_params(em)) end em_model_req = _get_emulation_store_requirements(sim) simulation_store_params = SimulationStoreParams( get_initial_time(sim), get_step_resolution(sequence), get_steps(sim), decision_model_store_params, emulation_model_store_params, ) @debug "initialized problem requirements" simulation_store_params store = get_simulation_store(sim) initialize_problem_storage!( store, simulation_store_params, dm_model_req, em_model_req, rules, ) return simulation_store_params end function _build!( sim::Simulation; store_systems_in_results = true, setup_simulation_partitions = false, partitions = nothing, index = nothing, ) 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) simulation_models = get_models(sim) if !isnothing(partitions) && !isnothing(index) step_range = get_absolute_step_range(partitions, index) sim.initial_time += step_resolution * (step_range.start - 1) set_current_time!(sim.internal.simulation_state, sim.initial_time) sim.steps = length(step_range) @info "Set parameters for simulation partition" index sim.initial_time sim.steps end for (ix, model) in enumerate(get_decision_models(simulation_models)) problem_interval = get_interval(sequence, model) # Note to devs: Here we are setting the number of operations problem executions we # will see for every step of the simulation. The step of the simulation is determined # by the first decision problem interval if ix == 1 set_executions!(model, 1) else if step_resolution % problem_interval != Dates.Millisecond(0) error( "The $(get_name(model)) problem interval is not an integer fraction of the simulation step", ) end set_executions!(model, Int(step_resolution / problem_interval)) end end em = get_emulation_model(simulation_models) if em !== nothing em_resolution = get_resolution(em) set_executions!(em, get_steps(sim) * Int(step_resolution / em_resolution)) end TimerOutputs.@timeit BUILD_PROBLEMS_TIMER "Check Steps" begin _check_steps(sim, problem_initial_times) end if store_systems_in_results # Spawn system serialization (JSON conversion) in parallel with model builds. # Systems are read-only during building, so this is safe. serialization_task = Threads.@spawn _serialize_systems_to_json(sim) end _build_decision_models!(sim) _build_emulation_model!(sim) TimerOutputs.@timeit BUILD_PROBLEMS_TIMER "Initialize Simulation State" begin _initialize_simulation_state!(sim) end TimerOutputs.@timeit BUILD_PROBLEMS_TIMER "Initialize Problem Storage" begin open_store(HdfSimulationStore, get_store_dir(sim), "w") do store set_simulation_store!(sim, store) try _initialize_problem_storage!(sim) if store_systems_in_results # Fetch pre-computed JSON from the parallel task and write to HDF5 store. serialized = fetch(serialization_task) for (uuid, json_text) in serialized write_system_json!(store, uuid, json_text) end end finally set_simulation_store!(sim, nothing) end end end if setup_simulation_partitions TimerOutputs.@timeit BUILD_PROBLEMS_TIMER "Setup Simulation Partition" begin _setup_simulation_partitions(sim) end end return end function _set_simulation_internal!( sim::Simulation, partitions::Union{Nothing, SimulationPartitions}, recorders, console_level, file_level, ) sim.internal = SimulationInternal( sim.steps, get_models(sim), get_simulation_folder(sim), get_name(sim), recorders, console_level, file_level; partitions = partitions, ) return end function _setup_simulation_partitions(sim::Simulation) mkdir(sim.internal.partitions_dir) filename = joinpath(sim.internal.partitions_dir, "config.json") IS.to_json(sim.internal.partitions, filename; pretty = true) for i in 1:get_num_partitions(sim.internal.partitions) mkdir(joinpath(sim.internal.partitions_dir, string(i))) end end """ Build the Simulation, problems and the related folder structure. # Arguments - `sim::Simulation`: simulation object - `recorders::Vector{Symbol} = []`: recorder names to register - `store_systems_in_results::Bool = true`: stores the systems as JSON in the results HDF5 file - `console_level = Logging.Error`: - `file_level = Logging.Info`: """ function build!( sim::Simulation; recorders = [], console_level = Logging.Error, file_level = Logging.Info, store_systems_in_results = true, partitions::Union{Nothing, SimulationPartitions} = nothing, index = nothing, ) TimerOutputs.reset_timer!(BUILD_PROBLEMS_TIMER) TimerOutputs.@timeit BUILD_PROBLEMS_TIMER "Build Simulation" begin _check_folder(sim) _set_simulation_internal!(sim, partitions, recorders, console_level, file_level) make_dirs(sim.internal) if !isnothing(partitions) && isnothing(index) setup_simulation_partitions = true else setup_simulation_partitions = false end file_mode = "w" logger = configure_logging(sim.internal, file_mode) register_recorders!(sim.internal, file_mode) try Logging.with_logger(logger) do try _build!( sim; store_systems_in_results = store_systems_in_results, setup_simulation_partitions = setup_simulation_partitions, partitions = partitions, index = index, ) 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, SimulationBuildStatus.FAILED) set_simulation_status!(sim, RunStatus.NOT_READY) rethrow(e) end end finally unregister_recorders!(sim.internal) close(logger) end end @info "\n$(BUILD_PROBLEMS_TIMER)\n" return get_simulation_build_status(sim) end function _apply_warm_start!(model::OperationModel) container = get_optimization_container(model) # If the model was used to retrieve duals from an MILP the logic has to be different and # the results need to be read from the primal cache if isempty(container.primal_values_cache) jump_model = get_jump_model(container) all_vars = JuMP.all_variables(jump_model) all_vars_value = jump_value.(all_vars) JuMP.set_start_value.(all_vars, all_vars_value) else for (var_key, variable_value) in container.primal_values_cache.variables_cache variable = get_variable(container, var_key) JuMP.set_start_value.(variable, variable_value) end end return end function _get_next_problem_initial_time(sim::Simulation, model_name::Symbol) current_time = get_current_time(sim) sequence = get_sequence(sim) current_exec_index = sequence.current_execution_index exec_order = get_execution_order(sequence) if length(exec_order) > 1 && (current_exec_index + 1 > length(exec_order)) # Moving to the next step next_initial_time = get_simulation_time(sim, exec_order[1]) elseif length(exec_order) == 1 || exec_order[current_exec_index + 1] == exec_order[current_exec_index] # Solving the same problem again current_model_interval = get_interval(sim.sequence, model_name) next_initial_time = current_time + current_model_interval else # Solving another problem next next_initial_time = get_simulation_time(sim, exec_order[current_exec_index + 1]) end return next_initial_time end function _update_system_state!(sim::Simulation, model_name::Symbol) sim_state = get_simulation_state(sim) system_state = get_system_states(sim_state) decision_state = get_decision_states(sim_state) simulation_time = get_current_time(sim_state) next_stage_initial_time = _get_next_problem_initial_time(sim, model_name) for key in get_dataset_keys(decision_state) state_data = get_dataset(decision_state, key) last_update = get_update_timestamp(decision_state, key) if last_update > simulation_time error("Something went really wrong. Please report this error. \\ last_update: $(last_update) \\ simulation_time: $(simulation_time) \\ key: $(encode_key_as_string(key))") end resolution = get_data_resolution(state_data) update_timestamp = max(next_stage_initial_time - resolution, simulation_time) if update_timestamp < get_update_timestamp(system_state, key) error("The update overwrites more recent data with past data") elseif update_timestamp > get_update_timestamp(system_state, key) update_system_state!(system_state, key, decision_state, update_timestamp) else @assert_op update_timestamp == get_update_timestamp(system_state, key) end end IS.@record :execution StateUpdateEvent(simulation_time, model_name, "SystemState") return end function _update_system_state!(sim::Simulation, model::DecisionModel) _update_system_state!(sim, get_name(model)) return end function _update_system_state!(sim::Simulation, model::EmulationModel) sim_state = get_simulation_state(sim) simulation_time = get_current_time(sim) system_state = get_system_states(sim_state) store = get_simulation_store(sim) em_model_name = get_name(model) for key in get_container_keys(get_optimization_container(model)) !should_write_resulting_value(key) && continue update_system_state!(system_state, key, store, em_model_name, simulation_time) end IS.@record :execution StateUpdateEvent(simulation_time, em_model_name, "SystemState") return end function _update_simulation_state!(sim::Simulation, model::EmulationModel) # Order of these operations matters. Do not reverse. # This will update the state with the results of the store first and then fill # the remaning values with the decision state. _update_system_state!(sim, model) _update_system_state!(sim, get_name(model)) return end function _update_simulation_state!(sim::Simulation, model::DecisionModel) #Order matters; update parameters first to ensure event parameters are updated first _update_simulation_state_parameters!(sim, model) _update_simulation_state_others!(sim, model) model_name = get_name(model) simulation_time = get_current_time(sim) IS.@record :execution StateUpdateEvent(simulation_time, model_name, "DecisionState") return end function _update_simulation_state_parameters!(sim::Simulation, model::DecisionModel) model_name = get_name(model) store = get_simulation_store(sim) simulation_time = get_current_time(sim) state = get_simulation_state(sim) model_params = get_decision_model_params(store, model_name) all_parameter_keys = list_decision_model_keys(store, model_name, :parameters) countdown_parameter_keys = filter(_is_event_countdown_parameter_key, all_parameter_keys) other_parameter_keys = filter(!_is_event_countdown_parameter_key, all_parameter_keys) # Order matters; AvailableStatusChangeCountdownParameter must be updated first if it exists for key in countdown_parameter_keys !has_dataset(get_decision_states(state), key) && continue res = read_result(DenseAxisArray, store, model_name, key, simulation_time) update_decision_state!(state, key, res, simulation_time, model_params) end for key in other_parameter_keys !has_dataset(get_decision_states(state), key) && continue res = read_result(DenseAxisArray, store, model_name, key, simulation_time) update_decision_state!(state, key, res, simulation_time, model_params) end end function _is_event_countdown_parameter_key( ::ParameterKey{T, U}, ) where {T <: ParameterType, U <: PSY.Component} return false end function _is_event_countdown_parameter_key( ::ParameterKey{AvailableStatusChangeCountdownParameter, U}, ) where {U <: PSY.Component} return true end function _update_simulation_state_others!(sim::Simulation, model::DecisionModel) model_name = get_name(model) store = get_simulation_store(sim) simulation_time = get_current_time(sim) state = get_simulation_state(sim) model_params = get_decision_model_params(store, model_name) for field in [:duals, :aux_variables, :variables, :expressions] for key in list_decision_model_keys(store, model_name, field) !has_dataset(get_decision_states(state), key) && continue res = read_result(DenseAxisArray, store, model_name, key, simulation_time) update_decision_state!(state, key, res, simulation_time, model_params) end end return end function _write_state_to_store!(store::SimulationStore, sim::Simulation) sim_state = get_simulation_state(sim) system_state = get_system_states(sim_state) model_name = get_last_decision_model(sim_state) em_store = get_em_data(store) simulation_time = get_current_time(sim) sim_ini_time = get_initial_time(sim) state_resolution = get_system_states_resolution(sim_state) for key in get_dataset_keys(system_state) store_update_time = get_last_updated_timestamp(em_store, key) state_update_time = get_update_timestamp(system_state, key) # If the store is outdated w.r.t to the state @assert store_update_time <= simulation_time 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, ) _update_timestamp += state_resolution end end end return end """ Default problem update function for most problems with no customization """ function update_model!(model::OperationModel, sim::Simulation) update_model!(model, get_simulation_state(sim), get_ini_cond_chronology(sim)) if get_rebuild_model(model) container = get_optimization_container(model) reset_optimization_model!(container) build_impl!(container, get_template(model), get_system(model)) end return end function _execute!( sim::Simulation; cache_size_mib::Union{Nothing, Int} = nothing, min_cache_flush_size_mib::Union{Nothing, Int} = nothing, exports = nothing, enable_progress_bar = progress_meter_enabled(), disable_timer_outputs = false, results_channel = nothing, ) @assert sim.internal !== nothing set_simulation_status!(sim, RunStatus.RUNNING) execution_order = get_execution_order(sim) steps = get_steps(sim) num_executions = steps * length(execution_order) store = get_simulation_store(sim) # For InMemorySimulationStore, initialize storage here since it doesn't persist from build. if store isa InMemorySimulationStore _initialize_problem_storage!( sim, something(cache_size_mib, DEFAULT_SIMULATION_STORE_CACHE_SIZE_MiB), something(min_cache_flush_size_mib, MIN_CACHE_FLUSH_SIZE_MiB), ) else # Override cache flush rules from build phase if user passes kwargs at execution time. rules = CacheFlushRules(; max_size = something(cache_size_mib, DEFAULT_SIMULATION_STORE_CACHE_SIZE_MiB) * MiB, min_flush_size = trunc( something(min_cache_flush_size_mib, MIN_CACHE_FLUSH_SIZE_MiB) * MiB, ), ) set_cache_flush_rules!(store, rules) end store_params = get_params(store) if exports !== nothing if !(exports isa SimulationResultsExport) exports = SimulationResultsExport(exports, store_params) end if exports.path === nothing exports.path = get_results_dir(sim) end end sequence = get_sequence(sim) models = get_models(sim) prog_bar = ProgressMeter.Progress(num_executions; enabled = enable_progress_bar) disable_timer_outputs && TimerOutputs.disable_timer!(RUN_SIMULATION_TIMER) store = get_simulation_store(sim) for step in 1:steps IS.@record :simulation_status SimulationStepEvent( get_current_time(sim), step, "start", ) # This progress print is required to show the progress bar upfront ProgressMeter.update!( prog_bar, 1; showvalues = [ (:Step, 1), (:Problem, get_name(get_simulation_model(models, 1))), (:("Simulation Timestamp"), get_current_time(sim)), ], ) for (ix, model_number) in enumerate(execution_order) model = get_simulation_model(models, model_number) model_name = get_name(model) set_current_time!(sim, sim.internal.date_ref[model_number]) sequence.current_execution_index = ix current_time = get_current_time(sim) IS.@record :simulation_status ProblemExecutionEvent( current_time, step, model_name, "start", ) progress_event = SimulationProgressEvent(; model_name = string(model_name), step = step, index = (step - 1) * length(execution_order) + ix, timestamp = get_current_time(sim), wall_time = Dates.now(), exec_time_s = 0.0, ) ProgressMeter.update!( prog_bar, progress_event.index; showvalues = [ (:Step, progress_event.step), (:Problem, model_name), (:("Simulation Timestamp"), progress_event.timestamp), ], ) start_time = time() TimerOutputs.@timeit RUN_SIMULATION_TIMER "Execute $(model_name)" begin if !is_built(model) error("$(model_name) status is not ModelBuildStatus.BUILT") end # Is first run of first problem? Yes -> don't update problem TimerOutputs.@timeit RUN_SIMULATION_TIMER "Update $(model_name)" begin !(step == 1 && ix == 1) && update_model!(model, sim) end TimerOutputs.@timeit RUN_SIMULATION_TIMER "Solve $(model_name)" begin status = solve!(step, model, current_time, store; exports = exports) end # Run problem Timer TimerOutputs.@timeit RUN_SIMULATION_TIMER "Update State" begin if status == RunStatus.SUCCESSFULLY_FINALIZED # TODO: _update_simulation_state! can use performance improvements _update_simulation_state!(sim, model) if model_number == execution_order[end] _update_system_state!(sim, model) _write_state_to_store!(store, sim) # This function needs to be called last so make sure that the update to the # state get written AFTER the models run. apply_simulation_events!(sim) end end end sim.internal.run_count[step][model_number] += 1 sim.internal.date_ref[model_number] += get_interval(sequence, model_name) # _apply_warm_start! can only be called once all the operations that read solutions # from the optimization container have been called. # See https://github.com/Sienna-Platform/PowerSimulations.jl/pull/793#discussion_r761545526 # for reference if warm_start_enabled(model) _apply_warm_start!(model) end IS.@record :simulation_status ProblemExecutionEvent( get_current_time(sim), step, model_name, "done", ) end #execution problem timer progress_event.exec_time_s = time() - start_time if !isnothing(results_channel) put!(results_channel, SimulationIntermediateResult(progress_event)) end end # execution order for loop IS.@record :simulation_status SimulationStepEvent( get_current_time(sim), step, "done", ) end # Steps for loop return end """ Solves the simulation model for sequential Simulations. # Arguments - `sim::Simulation=sim`: simulation object created by Simulation() The optional keyword argument `exports` controls exporting of results to CSV files as the simulation runs. # Example ```julia sim = Simulation("Test", 7, problems, "/Users/folder") execute!(sim::Simulation; kwargs...) ``` """ function execute!(sim::Simulation; kwargs...) file_mode = "a" logger = configure_logging(sim.internal, file_mode) register_recorders!(sim.internal, file_mode) # Undocumented option for test & dev only. in_memory = get(kwargs, :in_memory, false) store_type = in_memory ? InMemorySimulationStore : HdfSimulationStore 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 file_mode = in_memory ? "w" : "rw" try Logging.with_logger(logger) do open_store(store_type, get_store_dir(sim), file_mode) do store set_simulation_store!(sim, store) try TimerOutputs.reset_timer!(RUN_SIMULATION_TIMER) TimerOutputs.@timeit RUN_SIMULATION_TIMER "Execute Simulation" begin _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.SUCCESSFULLY_FINALIZED) log_cache_hit_percentages(store) catch e set_simulation_status!(sim, RunStatus.FAILED) @error "simulation failed" exception = (e, catch_backtrace()) end end end finally _empty_problem_caches!(sim) unregister_recorders!(sim.internal) close(logger) end if !in_memory && get_simulation_status(sim) == RunStatus.SUCCESSFULLY_FINALIZED IS.compute_file_hash(get_store_dir(sim), HDF_FILENAME) end serialize_status(sim) return get_simulation_status(sim) end function _empty_problem_caches!(sim::Simulation) models = get_models(sim) for model in get_decision_models(models) empty_time_series_cache!(model) end return end function _serialize_systems_to_json(sim::Simulation) simulation_models = get_models(sim) results = Dict{String, String}() @debug Threads.threadid() "Serializing systems to JSON in parallel with model building" for dm in get_decision_models(simulation_models) sys = get_system(dm) uuid = string(IS.get_uuid(sys)) if !haskey(results, uuid) results[uuid] = PSY.to_json(sys) end end em = get_emulation_model(simulation_models) if !isnothing(em) sys = get_system(em) uuid = string(IS.get_uuid(sys)) if !haskey(results, uuid) results[uuid] = PSY.to_json(sys) end end return results end function serialize_status(sim::Simulation) serialize_status(get_simulation_status(sim), get_results_dir(sim)) end function serialize_status(status::RunStatus, results_dir::AbstractString) data = Dict("run_status" => string(status)) filename = joinpath(results_dir, "status.json") open(filename, "w") do io JSON3.write(io, data) end return end function deserialize_status(sim::Simulation) return deserialize_status(get_results_dir(sim)) end function deserialize_status(results_path::AbstractString) filename = joinpath(results_path, "status.json") if !isfile(filename) error("run status file $filename does not exist") end data = open(filename, "r") do io JSON3.read(io, Dict) end return get_enum_value(RunStatus, data["run_status"]) end # The next two structs allow a parent process to monitor the simulation progress. # They may eventually be extended to pass result data back to the parent. Base.@kwdef mutable struct SimulationProgressEvent model_name::String step::Int index::Int timestamp::Dates.DateTime wall_time::Dates.DateTime exec_time_s::Float64 end struct SimulationIntermediateResult progress_event::SimulationProgressEvent end ================================================ FILE: src/simulation/simulation_events.jl ================================================ function apply_simulation_events!(simulation::Simulation) sequence = get_sequence(simulation) events = get_events(sequence) simulation_state = get_simulation_state(simulation) for event_model in events extend_event_parameters!(simulation, event_model) if check_condition(simulation_state, event_model) # TODO: Events for other event categories we need to do something else em_model = get_emulation_model(get_models(simulation)) sys = get_system(em_model) model_name = get_name(em_model) for (event_uuid, device_type_maps) in event_model.attribute_device_map[model_name] event = PSY.get_supplemental_attribute(sys, event_uuid) apply_affect!(simulation, event_model, event, device_type_maps) end end end end function extend_event_parameters!(simulation::Simulation, event_model) sequence = get_sequence(simulation) sim_state = get_simulation_state(simulation) em_model = get_emulation_model(get_models(simulation)) model_name = get_name(em_model) for (event_uuid, device_type_maps) in event_model.attribute_device_map[model_name] sim_time = get_current_time(simulation) for (dtype, device_names) in device_type_maps if dtype == PSY.RenewableDispatch continue end em_model = get_emulation_model(get_models(simulation)) status_change_countdown_data = get_decision_state_data( sim_state, ParameterKey(AvailableStatusChangeCountdownParameter, dtype), ) status_data = get_decision_state_data( sim_state, ParameterKey(AvailableStatusParameter, dtype), ) state_timestamps = status_data.timestamps state_data_index = find_timestamp_index(state_timestamps, sim_time) if state_data_index == 1 for name in device_names if status_change_countdown_data.values[name, 1] > 1.0 starting_count = status_change_countdown_data.values[name, 1] for i in 1:length(status_change_countdown_data.values[name, :]) countdown_val = max(starting_count + 1 - i, 0.0) if countdown_val == 0.0 status_val = 1.0 else status_val = 0.0 end status_change_countdown_data.values[name, i] = countdown_val status_data.values[name, i] = status_val end end end end end end end function check_condition( ::SimulationState, ::EventModel{<:PSY.Contingency, ContinuousCondition}, ) return true end function check_condition( simulation_state::SimulationState, event_model::EventModel{<:PSY.Contingency, PresetTimeCondition}, ) condition = get_event_condition(event_model) event_times = get_time_stamps(condition) current_time = get_current_time(simulation_state) if current_time in event_times return true else return false end end function check_condition( simulation_state::SimulationState, event_model::EventModel{<:PSY.Contingency, StateVariableValueCondition}, ) condition = get_event_condition(event_model) variable_type = get_variable_type(condition) device_type = get_device_type(condition) device_name = get_device_name(condition) event_value = get_value(condition) system_value = get_system_state_data(simulation_state, variable_type, device_type).values[ device_name, 1, ] if isapprox(system_value, event_value; atol = ABSOLUTE_TOLERANCE) return true else return false end end function check_condition( simulation_state::SimulationState, event_model::EventModel{<:PSY.Contingency, DiscreteEventCondition}, ) condition = get_event_condition(event_model) f = condition.condition_function if f(simulation_state) return true else return false end end function apply_affect!( simulation::Simulation, event_model::EventModel{ T, <:AbstractEventCondition, }, event::T, device_type_maps::Dict{DataType, Set{String}}, ) where {T <: PSY.Contingency} sim_state = get_simulation_state(simulation) sim_time = get_current_time(simulation) rng = get_rng(simulation) for (dtype, device_names) in device_type_maps if !supports_outages(dtype) continue end em_model = get_emulation_model(get_models(simulation)) em_model_store = get_store_params(em_model) # Order required: event parameters must be updated first to indicate a change in other parameters/variables. update_system_state!( sim_state, ParameterKey(AvailableStatusChangeCountdownParameter, dtype), device_names, event, event_model, sim_time, rng, ) if haskey( sim_state.system_states.parameters, ParameterKey(ActivePowerOffsetParameter, dtype), ) update_system_state!( sim_state, ParameterKey(ActivePowerOffsetParameter, dtype), device_names, event, event_model, sim_time, rng, ) end if haskey( sim_state.system_states.parameters, ParameterKey(ReactivePowerOffsetParameter, dtype), ) update_system_state!( sim_state, ParameterKey(ReactivePowerOffsetParameter, dtype), device_names, event, event_model, sim_time, rng, ) end update_system_state!( sim_state, ParameterKey(AvailableStatusParameter, dtype), device_names, event, event_model, sim_time, rng, ) for k in keys(sim_state.system_states.variables) #Not an OrderedDict if typeof(k).parameters[2] != dtype continue end update_system_state!( sim_state, k, device_names, event, event_model, sim_time, rng, ) end for k in keys(sim_state.system_states.aux_variables) #Not an OrderedDict if typeof(k).parameters[2] != dtype continue end update_system_state!( sim_state, k, device_names, event, event_model, sim_time, rng, ) end # Order is required here too AvailableStatusChangeCountdownParameter needs to # go first to indicate that there is a change in the other values update_decision_state!( sim_state, ParameterKey(AvailableStatusChangeCountdownParameter, dtype), device_names, event, event_model, sim_time, em_model_store, ) if haskey( sim_state.decision_states.parameters, ParameterKey(ActivePowerOffsetParameter, dtype), ) update_decision_state!( sim_state, ParameterKey(ActivePowerOffsetParameter, dtype), device_names, event, event_model, sim_time, em_model_store, ) end if haskey( sim_state.decision_states.parameters, ParameterKey(ReactivePowerOffsetParameter, dtype), ) update_decision_state!( sim_state, ParameterKey(ReactivePowerOffsetParameter, dtype), device_names, event, event_model, sim_time, em_model_store, ) end update_decision_state!( sim_state, ParameterKey(AvailableStatusParameter, dtype), device_names, event, event_model, sim_time, em_model_store, ) for k in keys(sim_state.decision_states.variables) #Not an OrderedDict if typeof(k).parameters[2] != dtype continue end update_decision_state!( sim_state, k, device_names, event, event_model, sim_time, em_model_store, ) end for k in keys(sim_state.decision_states.aux_variables) #Not an OrderedDict if typeof(k).parameters[2] != dtype continue end update_decision_state!( sim_state, k, device_names, event, event_model, sim_time, em_model_store, ) end end IS.@record :execution StateUpdateEvent(sim_time, "Emulator", "SystemState") end ================================================ FILE: src/simulation/simulation_info.jl ================================================ 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 ================================================ FILE: src/simulation/simulation_internal.jl ================================================ mutable struct SimulationInternal sim_files_dir::String partitions::Union{Nothing, SimulationPartitions} store_dir::String logs_dir::String models_dir::String recorder_dir::String results_dir::String partitions_dir::String run_count::OrderedDict{Int, OrderedDict{Int, Int}} date_ref::OrderedDict{Int, Dates.DateTime} status::RunStatus build_status::SimulationBuildStatus simulation_state::SimulationState store::Union{Nothing, SimulationStore} recorders::Vector{Symbol} console_level::Base.CoreLogging.LogLevel file_level::Base.CoreLogging.LogLevel cache_size_mib::Int min_cache_flush_size_mib::Int rng::AbstractRNG end function SimulationInternal( steps::Int, models::SimulationModels, base_dir::String, name::String, recorders, console_level::Logging.LogLevel, file_level::Logging.LogLevel; partitions::Union{Nothing, SimulationPartitions} = nothing, cache_size_mib = 1024, min_cache_flush_size_mib = MIN_CACHE_FLUSH_SIZE_MiB, ) count_dict = OrderedDict{Int, OrderedDict{Int, Int}}() for s in 1:steps count_dict[s] = OrderedDict{Int, Int}() model_count = length(get_decision_models(models)) for st in 1:model_count count_dict[s][st] = 0 end if get_emulation_model(models) !== nothing count_dict[s][model_count + 1] = 0 end end simulation_dir = joinpath(base_dir, name) if isdir(simulation_dir) simulation_dir = _get_output_dir_name(base_dir, name) end sim_files_dir = joinpath(simulation_dir, "simulation_files") store_dir = joinpath(simulation_dir, "data_store") logs_dir = joinpath(simulation_dir, "logs") models_dir = joinpath(simulation_dir, "problems") recorder_dir = joinpath(simulation_dir, "recorder") results_dir = joinpath(simulation_dir, RESULTS_DIR) partitions_dir = joinpath(simulation_dir, "simulation_partitions") unique_recorders = Set(REQUIRED_RECORDERS) foreach(x -> push!(unique_recorders, x), recorders) return SimulationInternal( sim_files_dir, partitions, store_dir, logs_dir, models_dir, recorder_dir, results_dir, partitions_dir, count_dict, OrderedDict{Int, Dates.DateTime}(), RunStatus.NOT_READY, SimulationBuildStatus.EMPTY, SimulationState(), nothing, collect(unique_recorders), console_level, file_level, cache_size_mib, min_cache_flush_size_mib, Random.Xoshiro(IS.get_random_seed()), ) end function make_dirs(internal::SimulationInternal) mkdir(dirname(internal.sim_files_dir)) for field in (:sim_files_dir, :store_dir, :logs_dir, :models_dir, :recorder_dir, :results_dir) mkdir(getproperty(internal, field)) end end function _get_output_dir_name(path, sim_name) index = _get_most_recent_execution(path, sim_name) + 1 return joinpath(path, "$sim_name-$index") end function _get_most_recent_execution(path, sim_name) sim_dirs = readdir(path) if isempty(sim_dirs) fail = true elseif length(sim_dirs) == 1 fail = sim_dirs[1] != sim_name else fail = false end fail && error("No simulation directories with name=$sim_name are in $path") executions = [1] for path_name in sim_dirs regex = Regex("\\Q$sim_name\\E-(\\d+)\$") m = match(regex, path_name) if !isnothing(m) push!(executions, parse(Int, m.captures[1])) end end return maximum(executions) end function configure_logging(internal::SimulationInternal, file_mode) return IS.configure_logging(; console = true, console_stream = stderr, console_level = internal.console_level, file = true, filename = joinpath(internal.logs_dir, SIMULATION_LOG_FILENAME), file_level = internal.file_level, file_mode = file_mode, tracker = nothing, set_global = false, ) end function register_recorders!(internal::SimulationInternal, file_mode) for name in internal.recorders IS.register_recorder!(name; mode = file_mode, directory = internal.recorder_dir) end end function unregister_recorders!(internal::SimulationInternal) for name in internal.recorders IS.unregister_recorder!(name) end end ================================================ FILE: src/simulation/simulation_models.jl ================================================ """ SimulationModels( decision_models::Vector{<:DecisionModel}, emulation_models::Union{Nothing, EmulationModel} ) Stores the OperationProblem definitions to be used in the simulation. When creating the SimulationModels, the order in which the models are created determines the order on which the simulation is executed. # Arguments - `decision_models::Vector{<:DecisionModel}`: Vector of decision models. - `emulation_models::Union{Nothing, EmulationModel}`: Optional argument to include an EmulationModel in the Simulation # Example ```julia template_uc = template_unit_commitment() template_ed = template_economic_dispatch() my_decision_model_uc = DecisionModel(template_1, sys_uc, optimizer, name = "UC") my_decision_model_ed = DecisionModel(template_ed, sys_ed, optimizer, name = "ED") models = SimulationModels( decision_models = [ my_decision_model_uc, my_decision_model_ed ] ) ``` """ mutable struct SimulationModels decision_models::Vector{<:DecisionModel} emulation_model::Union{Nothing, EmulationModel} function SimulationModels( decision_models::Vector, emulation_model::Union{Nothing, EmulationModel} = nothing, ) all_names = [get_name(x) for x in decision_models] emulation_model !== nothing && push!(all_names, get_name(emulation_model)) model_count = if emulation_model === nothing length(decision_models) else length(decision_models) + 1 end if length(Set(all_names)) != model_count error("All model names must be unique: $all_names") end return new(decision_models, emulation_model) end end function SimulationModels( decision_models::DecisionModel, emulation_model::Union{Nothing, EmulationModel} = nothing, ) return SimulationModels([decision_models], emulation_model) end function SimulationModels(; decision_models, emulation_model::Union{Nothing, EmulationModel} = nothing, ) return SimulationModels(decision_models, emulation_model) end function get_simulation_model(models::SimulationModels, name::Symbol) for model in models.decision_models if get_name(model) == name return model end end em = models.emulation_model if em !== nothing if get_name(em) == name return em end end error("Model $name not stored in SimulationModels") end function get_simulation_model(models::SimulationModels, index::Int) n_decision_models = length(get_decision_models(models)) if index == n_decision_models + 1 return models.emulation_model elseif index <= n_decision_models return get_decision_models(models)[index] else error("Model number $index is invalid") end end get_decision_models(models::SimulationModels) = models.decision_models get_emulation_model(models::SimulationModels) = models.emulation_model function determine_horizons!(models::SimulationModels) horizons = OrderedDict{Symbol, Dates.Millisecond}() for model in models.decision_models container = get_optimization_container(model) settings = get_settings(container) horizon = get_horizon(settings) if horizon == UNSET_HORIZON 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 end em = models.emulation_model if em !== nothing resolution = get_resolution(em) horizons[get_name(em)] = resolution end return horizons end function determine_intervals(models::SimulationModels) intervals = OrderedDict{Symbol, Dates.Millisecond}() for model in models.decision_models model_interval = get_interval(get_settings(model)) if model_interval != UNSET_INTERVAL interval = model_interval else system = get_system(model) interval = PSY.get_forecast_interval(system) end if interval == Dates.Millisecond(0) 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_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 end function determine_resolutions(models::SimulationModels) resolutions = OrderedDict{Symbol, Dates.Millisecond}() for model in models.decision_models resolution = get_resolution(model) if resolution == UNSET_RESOLUTION throw( IS.InvalidValue("Resolution of model $(get_name(model)) not set correctly"), ) end resolutions[get_name(model)] = IS.time_period_conversion(resolution) end em = models.emulation_model if em !== nothing emulator_resolution = get_resolution(em) resolutions[get_name(em)] = IS.time_period_conversion(emulator_resolution) end return resolutions end function initialize_simulation_internals!(models::SimulationModels, uuid::Base.UUID) for (ix, model) in enumerate(get_decision_models(models)) 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 set_simulation_number!(em, ix) set_sequence_uuid!(em, uuid) end return end function get_model_names(models::SimulationModels) all_names = get_name.(get_decision_models(models)) em = get_emulation_model(models) if em !== nothing push!(all_names, get_name(em)) end return all_names end ================================================ FILE: src/simulation/simulation_partition_results.jl ================================================ const _TEMP_WRITE_POSITION = "__write_position__" """ Handles merging of simulation partitions """ struct SimulationPartitionResults "Directory of main simulation" path::String "User-defined simulation name" simulation_name::String "Defines how the simulation is split into partitions" partitions::SimulationPartitions "Cache of datasets" datasets::Dict{String, HDF5.Dataset} end function SimulationPartitionResults(path::AbstractString) config_file = joinpath(path, "simulation_partitions", "config.json") config = open(config_file, "r") do io JSON3.read(io, Dict) end partitions = IS.deserialize(SimulationPartitions, config) return SimulationPartitionResults( path, basename(path), partitions, Dict{String, HDF5.Dataset}(), ) end """ Combine all partition simulation files. """ function join_simulation(path::AbstractString) results = SimulationPartitionResults(path) join_simulation(results) return end function join_simulation(results::SimulationPartitionResults) status = _check_jobs(results) _merge_store_files!(results) _complete(results, status) return end function _partition_path(x::SimulationPartitionResults, i) partition_path = joinpath(x.path, "simulation_partitions", string(i)) execution_no = _get_most_recent_execution(partition_path, x.simulation_name) if execution_no == 1 execution_path = joinpath(partition_path, x.simulation_name) else execution_path = joinpath(partition_path, "$(x.simulation_name)-$execution_no") end return execution_path end _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.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.SUCCESSFULLY_FINALIZED @warn "partition job index = $i was not successful: $status" overall_status = status end end return overall_status end function _merge_store_files!(results::SimulationPartitionResults) HDF5.h5open(_store_path(results), "r+") do dst for i in 1:get_num_partitions(results.partitions) HDF5.h5open(joinpath(_partition_path(results, i), _store_subpath()), "r") do src _copy_datasets!(results, i, src, dst) end end for dataset in values(results.datasets) if occursin("decision_models", HDF5.name(dataset)) IS.@assert_op HDF5.attrs(dataset)[_TEMP_WRITE_POSITION] == size(dataset)[end] + 1 else IS.@assert_op HDF5.attrs(dataset)[_TEMP_WRITE_POSITION] == size(dataset)[1] + 1 end delete!(HDF5.attrs(dataset), _TEMP_WRITE_POSITION) end end end function _copy_datasets!( results::SimulationPartitionResults, index::Int, src::HDF5.File, dst::HDF5.File, ) output_types = string.(STORE_CONTAINERS) function process_dataset(src_dataset, merge_func) if !endswith(HDF5.name(src_dataset), "__columns") name = HDF5.name(src_dataset) dst_dataset = dst[name] if !haskey(results.datasets, name) results.datasets[name] = dst_dataset HDF5.attrs(dst_dataset)[_TEMP_WRITE_POSITION] = 1 end merge_func(results, index, src_dataset, dst_dataset) end end for src_group in src["simulation/decision_models"] for output_type in output_types for src_dataset in src_group[output_type] process_dataset(src_dataset, _merge_dataset_rows!) end end process_dataset(src_group["optimizer_stats"], _merge_dataset_rows!) end for output_type in output_types for src_dataset in src["simulation/emulation_model/$output_type"] process_dataset(src_dataset, _merge_dataset_columns!) end end end function _merge_dataset_columns!(results::SimulationPartitionResults, index, src, dst) num_columns = size(src)[1] step_range = get_absolute_step_range(results.partitions, index) IS.@assert_op num_columns % length(step_range) == 0 num_columns_per_step = num_columns ÷ length(step_range) skip_offset = get_valid_step_offset(results.partitions, index) - 1 src_start = 1 + num_columns_per_step * skip_offset len = get_valid_step_length(results.partitions, index) * num_columns_per_step src_end = src_start + len - 1 IS.@assert_op ndims(src) == ndims(dst) dst_start = HDF5.attrs(dst)[_TEMP_WRITE_POSITION] if ndims(src) == 2 IS.@assert_op size(src)[2] == size(dst)[2] dst_end = dst_start + len - 1 dst[dst_start:dst_end, :] = src[src_start:src_end, :] else error("Unsupported dataset ndims: $(ndims(src))") end HDF5.attrs(dst)[_TEMP_WRITE_POSITION] = dst_end + 1 return end function _merge_dataset_rows!(results::SimulationPartitionResults, index, src, dst) num_rows = size(src)[end] step_range = get_absolute_step_range(results.partitions, index) IS.@assert_op num_rows % length(step_range) == 0 num_rows_per_step = num_rows ÷ length(step_range) skip_offset = get_valid_step_offset(results.partitions, index) - 1 src_start = 1 + num_rows_per_step * skip_offset len = get_valid_step_length(results.partitions, index) * num_rows_per_step src_end = src_start + len - 1 IS.@assert_op ndims(src) == ndims(dst) dst_start = HDF5.attrs(dst)[_TEMP_WRITE_POSITION] if ndims(src) == 2 IS.@assert_op size(src)[1] == size(dst)[1] dst_end = dst_start + len - 1 dst[:, dst_start:dst_end] = src[:, src_start:src_end] elseif ndims(src) == 3 IS.@assert_op size(src)[1] == size(dst)[1] IS.@assert_op size(src)[2] == size(dst)[2] dst_end = dst_start + len - 1 IS.@assert_op dst_end <= size(dst)[3] dst[:, :, dst_start:dst_end] = src[:, :, src_start:src_end] else error("Unsupported dataset ndims: $(ndims(src))") end HDF5.attrs(dst)[_TEMP_WRITE_POSITION] = dst_end + 1 return end function _complete(results::SimulationPartitionResults, status) serialize_status(status, joinpath(results.path, "results")) store_path = _store_path(results) IS.compute_file_hash(dirname(store_path), basename(store_path)) return end ================================================ FILE: src/simulation/simulation_partitions.jl ================================================ """ Defines how a simulation can be partition into partitions and run in parallel. """ struct SimulationPartitions <: IS.InfrastructureSystemsType "Number of steps in the simulation" num_steps::Int "Number of steps in each partition" period::Int "Number of steps that a partition overlaps with the previous partition" num_overlap_steps::Int function SimulationPartitions(num_steps, period, num_overlap_steps = 1) if num_overlap_steps > period error( "period=$period must be greater than num_overlap_steps=$num_overlap_steps", ) end if period >= num_steps error("period=$period must be less than simulation steps=$num_steps") end return new(num_steps, period, num_overlap_steps) end end function SimulationPartitions(; num_steps, period, num_overlap_steps) return SimulationPartitions(num_steps, period, num_overlap_steps) end """ Return the number of partitions in the simulation. """ get_num_partitions(x::SimulationPartitions) = Int(ceil(x.num_steps / x.period)) """ Return a UnitRange for the steps in the partition with the given index. Includes overlap. """ function get_absolute_step_range(partitions::SimulationPartitions, index::Int) num_partitions = _check_partition_index(partitions, index) start_index = partitions.period * (index - 1) + 1 if index < num_partitions end_index = start_index + partitions.period - 1 else end_index = partitions.num_steps end if index > 1 start_index -= partitions.num_overlap_steps end return start_index:end_index end """ Return the step offset for valid data at the given index. """ function get_valid_step_offset(partitions::SimulationPartitions, index::Int) _check_partition_index(partitions, index) return index == 1 ? 1 : partitions.num_overlap_steps + 1 end """ Return the length of valid data at the given index. """ function get_valid_step_length(partitions::SimulationPartitions, index::Int) num_partitions = _check_partition_index(partitions, index) if index < num_partitions return partitions.period end remainder = partitions.num_steps % partitions.period return remainder == 0 ? partitions.period : remainder end function _check_partition_index(partitions::SimulationPartitions, index::Int) num_partitions = get_num_partitions(partitions) if index <= 0 || index > num_partitions error("index=$index=inde must be > 0 and <= $num_partitions") end return num_partitions end function IS.serialize(partitions::SimulationPartitions) return IS.serialize_struct(partitions) end function process_simulation_partition_cli_args(build_function, execute_function, args...) length(args) < 2 && error("Usage: setup|execute|join [options]") function config_logging(filename) return IS.configure_logging(; console = true, console_stream = stderr, console_level = Logging.Warn, file = true, filename = filename, file_level = Logging.Info, file_mode = "w", tracker = nothing, set_global = true, ) end function throw_if_missing(actual, required, label) diff = setdiff(required, actual) !isempty(diff) && error("Missing required options for $label: $diff") end operation = args[1] options = Dict{String, String}() for opt in args[2:end] !startswith(opt, "--") && error("All options must start with '--': $opt") fields = split(opt[3:end], "=") length(fields) != 2 && error("All options must use the format --name=value: $opt") options[fields[1]] = fields[2] end if haskey(options, "output-dir") output_dir = options["output-dir"] elseif haskey(ENV, "JADE_RUNTIME_OUTPUT") output_dir = joinpath(ENV["JADE_RUNTIME_OUTPUT"], "job-outputs") else error("output-dir must be specified as a CLI option or environment variable") end if operation == "setup" required = Set(("simulation-name", "num-steps", "num-period-steps")) throw_if_missing(keys(options), required, operation) if !haskey(options, "num-overlap-steps") options["num-overlap-steps"] = "0" end num_steps = parse(Int, options["num-steps"]) num_period_steps = parse(Int, options["num-period-steps"]) num_overlap_steps = parse(Int, options["num-overlap-steps"]) partitions = SimulationPartitions(num_steps, num_period_steps, num_overlap_steps) config_logging(joinpath(output_dir, "setup_partition_simulation.log")) build_function(output_dir, options["simulation-name"], partitions) elseif operation == "execute" throw_if_missing(keys(options), Set(("simulation-name", "index")), operation) index = parse(Int, options["index"]) base_dir = joinpath(output_dir, options["simulation-name"]) partition_output_dir = joinpath(base_dir, "simulation_partitions", string(index)) config_file = joinpath(base_dir, "simulation_partitions", "config.json") config = open(config_file, "r") do io JSON3.read(io, Dict) end partitions = IS.deserialize(SimulationPartitions, config) config_logging(joinpath(partition_output_dir, "run_partition_simulation.log")) sim = build_function( partition_output_dir, options["simulation-name"], partitions, index, ) execute_function(sim) elseif operation == "join" throw_if_missing(keys(options), Set(("simulation-name",)), operation) base_dir = joinpath(output_dir, options["simulation-name"]) config_file = joinpath(base_dir, "simulation_partitions", "config.json") config = open(config_file, "r") do io JSON3.read(io, Dict) end partitions = IS.deserialize(SimulationPartitions, config) config_logging(joinpath(base_dir, "logs", "join_partitioned_simulation.log")) join_simulation(base_dir) else error("Unsupported operation=$operation") end return end """ Run a partitioned simulation in parallel on a local computer. # Arguments - `build_function`: Function reference that returns a built Simulation. - `execute_function`: Function reference that executes a Simulation. - `script::AbstractString`: Path to script that includes ``build_function`` and ``execute_function``. - `output_dir::AbstractString`: Path for simulation outputs - `name::AbstractString`: Simulation name - `num_steps::Integer`: Total number of steps in the simulation - `period::Integer`: Number of steps in each simulation partition - `num_overlap_steps::Integer`: Number of steps that each partition overlaps with the previous partition - `num_parallel_processes`: Number of partitions to run in parallel. If nothing, use the number of cores. - `exeflags`: Path to Julia project. Forwarded to Distributed.addprocs. - `force`: Overwrite the output directory if it already exists. """ function run_parallel_simulation( build_function, execute_function; script::AbstractString, output_dir::AbstractString, name::AbstractString, num_steps::Integer, period::Integer, num_overlap_steps::Integer = 1, num_parallel_processes = nothing, exeflags = nothing, force = false, ) if isnothing(num_parallel_processes) num_parallel_processes = Sys.CPU_THREADS end partitions = SimulationPartitions(num_steps, period, num_overlap_steps) num_partitions = get_num_partitions(partitions) if isdir(output_dir) if !force error( "output_dir=$output_dir already exists. Choose a different name or set force=true.", ) end rm(output_dir; recursive = true) end mkdir(output_dir) @info "Run parallel simulation" name script output_dir num_steps num_partitions num_parallel_processes args = [ "setup", "--simulation-name=$name", "--num-steps=$(partitions.num_steps)", "--num-period-steps=$(partitions.period)", "--num-overlap-steps=$(partitions.num_overlap_steps)", "--output-dir=$output_dir", ] parent_module_name = nameof(parentmodule(build_function)) build_func_name = nameof(build_function) execute_func_name = nameof(execute_function) process_simulation_partition_cli_args(build_function, execute_function, args...) jobs = Vector{Dict}(undef, num_partitions) for i in 1:num_partitions args = Dict( "parent_module" => parent_module_name, "build_function" => build_func_name, "execute_function" => execute_func_name, "args" => [ "execute", "--simulation-name=$name", "--index=$i", "--output-dir=$output_dir", ], ) jobs[i] = args end if isnothing(exeflags) Distributed.addprocs(num_parallel_processes) else Distributed.addprocs(num_parallel_processes; exeflags = exeflags) end Distributed.@everywhere include($script) try Distributed.pmap(PowerSimulations._run_parallel_simulation, jobs) finally Distributed.rmprocs(Distributed.workers()...) end args = ["join", "--simulation-name=$name", "--output-dir=$output_dir"] process_simulation_partition_cli_args(build_function, execute_function, args...) end function _run_parallel_simulation(params) start = time() if params["parent_module"] == :Main parent_module = Main else # TODO: not tested parent_module = Base.root_module(Base.__toplevel__, Symbol(params["parent_module"])) end result = process_simulation_partition_cli_args( getproperty(parent_module, params["build_function"]), getproperty(parent_module, params["execute_function"]), params["args"]..., ) duration = time() - start args = params["args"] @info "Completed partition" args duration return result end ================================================ FILE: src/simulation/simulation_problem_results.jl ================================================ abstract type OperationModelSimulationResults end # Subtypes need to implement the following methods for SimulationProblemResults{T} # - read_results_with_keys # - list_aux_variable_keys # - list_dual_keys # - list_expression_keys # - list_parameter_keys # - list_variable_keys # - load_results! """ Holds the results of a simulation problem for plotting or exporting. """ mutable struct SimulationProblemResults{T} <: IS.Results where {T <: OperationModelSimulationResults} problem::String base_power::Float64 execution_path::String results_output_folder::String timestamps::StepRange{Dates.DateTime, Dates.Millisecond} results_timestamps::Vector{Dates.DateTime} values::T system::Union{Nothing, PSY.System} system_uuid::Base.UUID resolution::Dates.TimePeriod store::Union{Nothing, SimulationStore} end function SimulationProblemResults{T}( store::SimulationStore, model_name::AbstractString, problem_params::ModelStoreParams, sim_params::SimulationStoreParams, path, vals::T; results_output_path = nothing, system = nothing, ) where {T <: OperationModelSimulationResults} if results_output_path === nothing results_output_path = joinpath(path, "results") end time_steps = range( sim_params.initial_time; length = problem_params.num_executions * sim_params.num_steps, step = problem_params.interval, ) return SimulationProblemResults{T}( model_name, problem_params.base_power, path, results_output_path, time_steps, Vector{Dates.DateTime}(), vals, system, problem_params.system_uuid, get_resolution(problem_params), store isa HdfSimulationStore ? nothing : store, ) end get_model_name(res::SimulationProblemResults) = res.problem get_system(res::SimulationProblemResults) = res.system get_source_data(res::SimulationProblemResults) = get_system(res) # Needed for compatibility with the IS.Results interface get_resolution(res::SimulationProblemResults) = res.resolution get_execution_path(res::SimulationProblemResults) = res.execution_path get_model_base_power(res::SimulationProblemResults) = res.base_power 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!( result::SimulationProblemResults, results_timestamps::Vector{Dates.DateTime}, ) result.results_timestamps = results_timestamps 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, ::VariableKey) = list_variable_keys(res) get_cached_results(res::SimulationProblemResults, ::Type{<:AuxVarKey}) = get_cached_aux_variables(res) get_cached_results(res::SimulationProblemResults, ::Type{<:ConstraintKey}) = get_cached_duals(res) get_cached_results(res::SimulationProblemResults, ::Type{<:ExpressionKey}) = get_cached_expressions(res) get_cached_results(res::SimulationProblemResults, ::Type{<:ParameterKey}) = get_cached_parameters(res) get_cached_results(res::SimulationProblemResults, ::Type{<:VariableKey}) = get_cached_variables(res) get_cached_results( res::SimulationProblemResults, ::Type{<:OptimizationContainerKey} = OptimizationContainerKey, ) = merge( # PERF: could be done lazily get_cached_aux_variables(res), get_cached_duals(res), get_cached_expressions(res), get_cached_parameters(res), get_cached_variables(res), ) """ Return an array of variable names (strings) that are available for reads. """ list_variable_names(res::SimulationProblemResults) = encode_keys_as_strings(list_variable_keys(res)) """ Return an array of dual names (strings) that are available for reads. """ list_dual_names(res::SimulationProblemResults) = encode_keys_as_strings(list_dual_keys(res)) """ Return an array of parmater names (strings) that are available for reads. """ list_parameter_names(res::SimulationProblemResults) = encode_keys_as_strings(list_parameter_keys(res)) """ Return an array of auxillary variable names (strings) that are available for reads. """ list_aux_variable_names(res::SimulationProblemResults) = encode_keys_as_strings(list_aux_variable_keys(res)) """ Return an array of expression names (strings) that are available for reads. """ list_expression_names(res::SimulationProblemResults) = encode_keys_as_strings(list_expression_keys(res)) """ Return a reference to a StepRange of available timestamps. """ get_timestamps(result::SimulationProblemResults) = result.timestamps """ Return the system used for the problem. If the system hasn't already been deserialized or set with [`set_system!`](@ref) then deserialize and store it. If the simulation was configured to serialize all systems to file then the returned system 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::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_system_fallback, false) && isfile(file) system = PSY.System(file; time_series_read_only = true) @info "De-serialized the system from files." else system = get_system_fallback(results) end 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( ISOPT.get_results_dir(results), make_system_filename(ISOPT.get_source_data_uuid(results)), ) get_system(results::OptimizationProblemResults) = ISOPT.get_source_data(results) set_system!(results::OptimizationProblemResults, system) = ISOPT.set_source_data!(results, system) function _deserialize_system(results::SimulationProblemResults, ::Nothing) open_store( HdfSimulationStore, joinpath(get_execution_path(results), "data_store"), "r", ) do store system = deserialize_system(store, results.system_uuid) @info "De-serialized the system from the simulation store. The system does " * "not include time series data." return system end end function _deserialize_system(::SimulationProblemResults, ::InMemorySimulationStore) # This should never be necessary because the system is guaranteed to be in memory. error("Deserializing a system from the InMemorySimulationStore is not supported.") end """ Set the system in the results instance. Throws InvalidValue if the system UUID is incorrect. # Arguments - `results::SimulationProblemResults`: Results object - `system::AbstractString`: Path to the system json file # Examples ```julia julia > set_system!(res, "my_path/system_data.json") ``` """ function set_system!(results::SimulationProblemResults, system::AbstractString) set_system!(results, System(system)) end function set_system!(results::SimulationProblemResults, system::PSY.System) sys_uuid = IS.get_uuid(system) if sys_uuid != results.system_uuid throw( IS.InvalidValue( "System mismatch. $sys_uuid does not match the stored value of $(results.system_uuid)", ), ) end results.system = system return end function _deserialize_key( ::Type{<:OptimizationContainerKey}, results::SimulationProblemResults, name::AbstractString, ) !haskey(results.values.container_key_lookup, name) && error("$name is not stored") return results.values.container_key_lookup[name] end function _deserialize_key( ::Type{T}, results::SimulationProblemResults, args..., ) where {T <: OptimizationContainerKey} return ISOPT.make_key(T, args...) end get_container_fields(x::SimulationProblemResults) = (:aux_variables, :duals, :expressions, :parameters, :variables) """ Return the final values for the requested variables for each time step for a problem. Decision problem results are returned in a Dict{String, Dict{DateTime, DataFrame}}. Emulation problem results are returned in a Dict{String, DataFrame}. Limit the data sizes returned by specifying `initial_time` and `count` for decision problems or `start_time` and `len` for emulation problems. If the Julia process is started with multiple threads, the code will read the variables in parallel. See also [`load_results!`](@ref) to preload data into memory. # Arguments - `variables::Vector{Union{String, Tuple}}`: Variable name as a string or a Tuple with variable type and device type. If not provided then return all variables. - `initial_time::Dates.DateTime`: Initial time of the requested results. Decision problems only. - `count::Int`: Number of results. Decision problems only. - `start_time::Dates.DateTime`: Start time of the requested results. Emulation problems only. - `len::Int`: Number of rows in each DataFrame. Emulation problems only. - `table_format::TableFormat`: Format of the table to be returned. Default is `TableFormat.LONG` where the columns are `DateTime`, `name`, and `value` when the data has two dimensions and `DateTime`, `name`, `name2`, and `value` when the data has three dimensions. Set to it `TableFormat.WIDE` to pivot the names as columns, matching earlier versions of PowerSimulations.jl. Note: `TableFormat.WIDE` is not supported when the data has three dimensions. # Examples ```julia julia> variables_as_strings = ["ActivePowerVariable__ThermalStandard", "ActivePowerVariable__RenewableDispatch"] julia> variables_as_types = [(ActivePowerVariable, ThermalStandard), (ActivePowerVariable, RenewableDispatch)] julia> df_long =read_realized_variables(results, variables_as_strings) julia> df_long = read_realized_variables(results, variables_as_types) julia> df_wide = read_realized_variables(results, variables_as_types, table_format = TableFormat.WIDE) julia> using DataFramesMeta julia> df_agg_generators = @chain df_long begin @groupby(:DateTime) @combine(:value = sum(:value)) end ``` """ function read_realized_variables(res::SimulationProblemResults; kwargs...) return read_realized_variables(res, list_variable_keys(res); kwargs...) end function read_realized_variables( res::SimulationProblemResults, variables::Vector{Tuple{DataType, DataType}}; kwargs..., ) return read_realized_variables( res, [VariableKey(x...) for x in variables]; kwargs..., ) end function read_realized_variables( res::SimulationProblemResults, variables::Vector{<:AbstractString}; kwargs..., ) return read_realized_variables( res, [_deserialize_key(VariableKey, res, x) for x in variables]; kwargs..., ) end function read_realized_variables( res::SimulationProblemResults, variables::Vector{<:OptimizationContainerKey}; kwargs..., ) result_values = read_results_with_keys(res, variables; kwargs...) return Dict(encode_key_as_string(k) => v for (k, v) in result_values) end """ Return the final values for the requested variable for each time step for a problem. Decision problem results are returned in a Dict{DateTime, DataFrame}. Emulation problem results are returned in a DataFrame. Limit the data sizes returned by specifying `initial_time` and `count` for decision problems or `start_time` and `len` for emulation problems. See also [`load_results!`](@ref) to preload data into memory. # Arguments - `variable::Union{String, Tuple}`: Variable name as a string or a Tuple with variable type and device type. - `initial_time::Dates.DateTime`: Initial time of the requested results. Decision problems only. - `count::Int`: Number of results. Decision problems only. - `start_time::Dates.DateTime`: Start time of the requested results. Emulation problems only. - `len::Int`: Number of rows in each DataFrame. Emulation problems only. - `table_format::TableFormat`: Format of the table to be returned. Default is `TableFormat.LONG` where the columns are `DateTime`, `name`, and `value` when the data has two dimensions and `DateTime`, `name`, `name2`, and `value` when the data has three dimensions. Set to it `TableFormat.WIDE` to pivot the names as columns. Note: `TableFormat.WIDE` is not supported when the data has three dimensions. # Examples ```julia julia > read_realized_variable(results, "ActivePowerVariable__ThermalStandard") julia > read_realized_variable(results, (ActivePowerVariable, ThermalStandard)) julia > read_realized_variable(results, (ActivePowerVariable, ThermalStandard), table_format = TableFormat.WIDE) ``` """ function read_realized_variable( res::SimulationProblemResults, variable::AbstractString; kwargs..., ) return first( values( read_realized_variables( res, [_deserialize_key(VariableKey, res, variable)]; kwargs..., ), ), ) end function read_realized_variable(res::SimulationProblemResults, variable...; kwargs...) return first( values(read_realized_variables(res, [VariableKey(variable...)]; kwargs...)), ) end """ Return the final values for the requested auxiliary variables for each time step for a problem. Refer to [`read_realized_aux_variables`](@ref) for help and examples. """ function read_realized_aux_variables(res::SimulationProblemResults; kwargs...) return read_realized_aux_variables( res, list_aux_variable_keys(res); kwargs..., ) end function read_realized_aux_variables( res::SimulationProblemResults, aux_variables::Vector{Tuple{DataType, DataType}}; kwargs..., ) return read_realized_aux_variables( res, [AuxVarKey(x...) for x in aux_variables]; kwargs..., ) end function read_realized_aux_variables( res::SimulationProblemResults, aux_variables::Vector{<:AbstractString}; kwargs..., ) return read_realized_aux_variables( res, [_deserialize_key(AuxVarKey, res, x) for x in aux_variables]; kwargs..., ) end function read_realized_aux_variables( res::SimulationProblemResults, aux_variables::Vector{<:OptimizationContainerKey}; kwargs..., ) result_values = read_results_with_keys(res, aux_variables; kwargs...) return Dict(encode_key_as_string(k) => v for (k, v) in result_values) end """ Return the final values for the requested auxiliary variable for each time step for a problem. Refer to [`read_realized_variable`](@ref) for help and examples. """ function read_realized_aux_variable( res::SimulationProblemResults, aux_variable::AbstractString; kwargs..., ) return first( values( read_realized_aux_variables( res, [_deserialize_key(AuxVarKey, res, aux_variable)]; kwargs..., ), ), ) end function read_realized_aux_variable( res::SimulationProblemResults, aux_variable...; kwargs..., ) return first( values( read_realized_aux_variables(res, [AuxVarKey(aux_variable...)]; kwargs...), ), ) end """ Return the final values for the requested parameters for each time step for a problem. Refer to [`read_realized_parameters`](@ref) for help and examples. """ function read_realized_parameters(res::SimulationProblemResults; kwargs...) return read_realized_parameters(res, list_parameter_keys(res); kwargs...) end function read_realized_parameters( res::SimulationProblemResults, parameters::Vector{Tuple{DataType, DataType}}; kwargs..., ) return read_realized_parameters( res, [ParameterKey(x...) for x in parameters]; kwargs..., ) end function read_realized_parameters( res::SimulationProblemResults, parameters::Vector{<:AbstractString}; kwargs..., ) return read_realized_parameters( res, [_deserialize_key(ParameterKey, res, x) for x in parameters]; kwargs..., ) end function read_realized_parameters( res::SimulationProblemResults, parameters::Vector{<:OptimizationContainerKey}; kwargs..., ) result_values = read_results_with_keys(res, parameters; kwargs...) return Dict(encode_key_as_string(k) => v for (k, v) in result_values) end """ Return the final values for the requested parameter for each time step for a problem. Refer to [`read_realized_variable`](@ref) for help and examples. """ function read_realized_parameter( res::SimulationProblemResults, parameter::AbstractString; kwargs..., ) return first( values( read_realized_parameters( res, [_deserialize_key(ParameterKey, res, parameter)]; kwargs..., ), ), ) end function read_realized_parameter(res::SimulationProblemResults, parameter...; kwargs...) return first( values(read_realized_parameters(res, [ParameterKey(parameter...)]; kwargs...)), ) end """ Return the final values for the requested duals for each time step for a problem. Refer to [`read_realized_duals`](@ref) for help and examples. """ function read_realized_duals(res::SimulationProblemResults; kwargs...) return read_realized_duals(res, list_dual_keys(res); kwargs...) end function read_realized_duals( res::SimulationProblemResults, duals::Vector{Tuple{DataType, DataType}}; kwargs..., ) return read_realized_duals(res, [ConstraintKey(x...) for x in duals]; kwargs...) end function read_realized_duals( res::SimulationProblemResults, duals::Vector{<:AbstractString}; kwargs..., ) return read_realized_duals( res, [_deserialize_key(ConstraintKey, res, x) for x in duals]; kwargs..., ) end function read_realized_duals( res::SimulationProblemResults, duals::Vector{<:OptimizationContainerKey}; kwargs..., ) result_values = read_results_with_keys(res, duals; kwargs...) return Dict(encode_key_as_string(k) => v for (k, v) in result_values) end """ Return the final values for the requested dual for each time step for a problem. Refer to [`read_realized_variable`](@ref) for help and examples. """ function read_realized_dual(res::SimulationProblemResults, dual::AbstractString; kwargs...) return first( values( read_realized_duals( res, [_deserialize_key(ConstraintKey, res, dual)]; kwargs..., ), ), ) end function read_realized_dual(res::SimulationProblemResults, dual...; kwargs...) return first(values(read_realized_duals(res, [ConstraintKey(dual...)]; kwargs...))) end """ Return the final values for the requested expressions for each time step for a problem. Refer to [`read_realized_expressions`](@ref) for help and examples. """ function read_realized_expressions(res::SimulationProblemResults; kwargs...) return read_realized_expressions(res, list_expression_keys(res); kwargs...) end function read_realized_expressions( res::SimulationProblemResults, expressions::Vector{Tuple{DataType, DataType}}; kwargs..., ) return read_realized_expressions( res, [ExpressionKey(x...) for x in expressions]; kwargs..., ) end function read_realized_expressions( res::SimulationProblemResults, expressions::Vector{<:AbstractString}; kwargs..., ) return read_realized_expressions( res, [_deserialize_key(ExpressionKey, res, x) for x in expressions]; kwargs..., ) end function read_realized_expressions( res::SimulationProblemResults, expressions::Vector{<:OptimizationContainerKey}; kwargs..., ) result_values = read_results_with_keys(res, expressions; kwargs...) return Dict(encode_key_as_string(k) => v for (k, v) in result_values) end """ Return the final values for the requested expression for each time step for a problem. Refer to [`read_realized_variable`](@ref) for help and examples. """ function read_realized_expression( res::SimulationProblemResults, expression::AbstractString; kwargs..., ) return first( values( read_realized_expressions( res, [_deserialize_key(ExpressionKey, res, expression)]; kwargs..., ), ), ) end function read_realized_expression(res::SimulationProblemResults, expression...; kwargs...) return first( values( read_realized_expressions(res, [ExpressionKey(expression...)]; kwargs...), ), ) end """ Return the optimizer stats for the problem as a DataFrame. # Accepted keywords - `store::SimulationStore`: a store that has been opened for reading """ function read_optimizer_stats(res::SimulationProblemResults; store = nothing) _store = isnothing(store) ? res.store : store return _read_optimizer_stats(res, _store) end function _read_optimizer_stats(res::SimulationProblemResults, ::Nothing) open_store( HdfSimulationStore, joinpath(get_execution_path(res), "data_store"), "r", ) do store _read_optimizer_stats(res, store) 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 ================================================ FILE: src/simulation/simulation_results.jl ================================================ 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 @warn "Unrecognized simulation files: $(sort(alien_files))" end if "data_store" ∉ folder_files error("The file path doesn't contain any data_store folder") end return false end struct SimulationResults path::String params::SimulationStoreParams decision_problem_results::Dict{ String, SimulationProblemResults{DecisionModelSimulationResults}, } emulation_problem_results::SimulationProblemResults{EmulationModelSimulationResults} store::Union{Nothing, SimulationStore} end function SimulationResults(path::AbstractString, execution = nothing; ignore_status = false) # This method maintains compatibility with the old interface as long as there is only # one simulation name. unique_names = Set{String}() for name in readdir(path) m = match(r"(.*)-\d+$", name) if isnothing(m) push!(unique_names, name) else push!(unique_names, m.captures[1]) end end if length(unique_names) == 1 name = first(unique_names) return SimulationResults(path, name, execution; ignore_status = ignore_status) end if "data_store" in readdir(path) return SimulationResults( dirname(path), basename(path), execution; ignore_status = ignore_status, ) end error( "Found more than one simulation name in $path. Please call the constructor that includes 'name.'", ) end """ Construct SimulationResults from a simulation output directory. # Arguments - `path::AbstractString`: Simulation output directory - `name::AbstractString`: Simulation name - `execution::AbstractString`: Execution number. Default is the most recent. - `ignore_status::Bool`: If true, return results even if the simulation failed. """ function SimulationResults( path::AbstractString, name::AbstractString, execution = nothing; ignore_status = false, ) if isnothing(execution) execution = _get_most_recent_execution(path, name) end if execution == 1 execution_path = joinpath(path, name) else execution_path = joinpath(path, "$name-$execution") end if !isdir(execution_path) error("No valid simulation in $execution_path: execution = $execution") end @info "Loading simulation results from $execution_path" status = deserialize_status(joinpath(execution_path, RESULTS_DIR)) _check_status(status, ignore_status) if !check_folder_integrity(execution_path) @warn "The results folder $(execution_path) is not consistent with the default folder structure. " * "This can lead to errors or unwanted results." end simulation_store_path = joinpath(execution_path, "data_store") check_file_integrity(simulation_store_path) return open_store(HdfSimulationStore, simulation_store_path, "r") do store decision_problem_results = Dict{String, SimulationProblemResults{DecisionModelSimulationResults}}() sim_params = get_params(store) container_key_lookup = get_container_key_lookup(store) for (name, problem_params) in sim_params.decision_models_params name = string(name) system = if has_system(store, get_system_uuid(problem_params)) deserialize_system(store, get_system_uuid(problem_params)) else nothing end problem_result = SimulationProblemResults( DecisionModel, store, name, problem_params, sim_params, execution_path, container_key_lookup; system = system, ) decision_problem_results[name] = problem_result end em_params = get_emulation_model_params(sim_params) em_system = if has_system(store, get_system_uuid(em_params)) deserialize_system(store, get_system_uuid(em_params)) else nothing end emulation_result = SimulationProblemResults( EmulationModel, store, string(first(keys(sim_params.emulation_model_params))), em_params, sim_params, execution_path, container_key_lookup; system = em_system, ) return SimulationResults( execution_path, sim_params, decision_problem_results, emulation_result, nothing, ) end end """ Construct SimulationResults from a simulation. """ function SimulationResults(sim::Simulation; ignore_status = false, kwargs...) _check_status(get_simulation_status(sim), ignore_status) store = get_simulation_store(sim) execution_path = get_simulation_dir(sim) decision_problem_results = Dict{String, SimulationProblemResults{DecisionModelSimulationResults}}() sim_params = get_params(store) models = get_models(sim) container_key_lookup = get_container_key_lookup(store) for (name, problem_params) in sim_params.decision_models_params model = get_simulation_model(models, name) name = string(name) problem_result = SimulationProblemResults( DecisionModel, store, name, problem_params, sim_params, execution_path, container_key_lookup; system = get_system(model), ) decision_problem_results[name] = problem_result end emulation_model = get_emulation_model(models) emulation_results = SimulationProblemResults( EmulationModel, store, string(first(keys(sim_params.emulation_model_params))), first(values(sim_params.emulation_model_params)), sim_params, execution_path, container_key_lookup; system = isnothing(emulation_model) ? nothing : get_system(emulation_model), ) return SimulationResults( execution_path, sim_params, decision_problem_results, emulation_results, store, ) end """ Base.empty!(res::SimulationResults) Empty the [`SimulationResults`](@ref) """ function Base.empty!(res::SimulationResults) foreach(empty!, values(res.decision_problem_results)) empty!(res.emulation_problem_results) end Base.isempty(res::SimulationResults) = all(isempty, values(res.decision_problem_results)) Base.length(res::SimulationResults) = mapreduce(length, +, values(res.decision_problem_results)) get_exports_folder(x::SimulationResults) = joinpath(x.path, "exports") """ Return SimulationProblemResults corresponding to a SimulationResults # Arguments - `sim_results::PSI.SimulationResults`: the simulation results to read from - `problem::String`: the name of the problem (e.g., "UC", "ED") - `populate_system::Bool = true`: whether to set the results' system as if using [`get_system!`](@ref) - `populate_units::Union{IS.UnitSystem, String, Nothing} = IS.UnitSystem.NATURAL_UNITS`: the units system with which to populate the results' system, if any (requires `populate_system=true`) """ function get_decision_problem_results( results::SimulationResults, problem::String; populate_system::Bool = false, populate_units::Union{IS.UnitSystem, String, Nothing} = nothing, ) if !haskey(results.decision_problem_results, problem) throw(IS.InvalidValue("$problem is not stored")) end results = results.decision_problem_results[problem] _populate_system_in_results!(results, populate_system, populate_units) return results end """ Return SimulationProblemResults corresponding to a SimulationResults # Arguments - `sim_results::PSI.SimulationResults`: the simulation results to read from - `populate_system::Bool = true`: whether to set the results' system as if using [`get_system!`](@ref) - `populate_units::Union{IS.UnitSystem, String, Nothing} = IS.UnitSystem.NATURAL_UNITS`: the units system with which to populate the results' system, if any (requires `populate_system=true`) """ function get_emulation_problem_results( results::SimulationResults; populate_system::Bool = false, populate_units::Union{IS.UnitSystem, String, Nothing} = nothing, ) results = results.emulation_problem_results _populate_system_in_results!(results, populate_system, populate_units) return results end function _populate_system_in_results!( results::SimulationProblemResults, populate_system::Bool, populate_units::Union{IS.UnitSystem, String, Nothing}, ) if populate_system try get_system!(results) catch e error("Can't find the system file or retrieve the system error=$e") end if populate_units !== nothing PSY.set_units_base_system!(PSI.get_system(results), populate_units) else PSY.set_units_base_system!(PSI.get_system(results), IS.UnitSystem.NATURAL_UNITS) end else (populate_units === nothing) || throw( ArgumentError( "populate_units=$populate_units is unaccepted when populate_system=$populate_system", ), ) end return end """ Return the problem names in the simulation. """ list_decision_problems(results::SimulationResults) = collect(keys(results.decision_problem_results)) """ Export results to files in the results directory. # Arguments - `results::SimulationResults`: simulation results - `exports`: SimulationResultsExport or anything that can be passed to its constructor. (such as Dict or path to JSON file) An example JSON file demonstrating possible options is below. Note that `start_time`, `end_time`, `path`, and `format` are optional. ``` { "decision_models": [ { "name": "ED", "variables": [ "P__ThermalStandard", ], "parameters": [ "all" ] }, { "name": "UC", "variables": [ "On__ThermalStandard" ], "parameters": [ "all" ], "duals": [ "all" ] } ], "start_time": "2020-01-01T04:00:00", "end_time": null, "path": null, "format": "csv" } ``` """ function export_results(results::SimulationResults, exports) if results.store isa InMemorySimulationStore export_results(results, exports, results.store) else simulation_store_path = joinpath(results.path, "data_store") open_store(HdfSimulationStore, simulation_store_path, "r") do store export_results(results, exports, store) end end return end function export_results(results::SimulationResults, exports, store::SimulationStore) if !(exports isa SimulationResultsExport) exports = SimulationResultsExport(exports, results.params) end file_type = get_export_file_type(exports) for problem_results in values(results.decision_problem_results) problem_exports = get_problem_exports(exports, problem_results.problem) path = exports.path === nothing ? problem_results.results_output_folder : exports.path for timestamp in get_timestamps(problem_results) !should_export(exports, timestamp) && continue export_path = mkpath(joinpath(path, problem_results.problem, "variables")) for name in list_variable_names(problem_results) if should_export_variable(problem_exports, name) dfs = read_variable( problem_results, name; initial_time = timestamp, count = 1, store = store, ) ISOPT.export_result( file_type, export_path, name, timestamp, dfs[timestamp], ) end end export_path = mkpath(joinpath(path, problem_results.problem, "aux_variables")) for name in list_aux_variable_names(problem_results) if should_export_aux_variable(problem_exports, name) dfs = read_aux_variable( problem_results, name; initial_time = timestamp, count = 1, store = store, ) ISOPT.export_result( file_type, export_path, name, timestamp, dfs[timestamp], ) end end export_path = mkpath(joinpath(path, problem_results.problem, "parameters")) for name in list_parameter_names(problem_results) if should_export_parameter(problem_exports, name) dfs = read_parameter( problem_results, name; initial_time = timestamp, count = 1, store = store, ) ISOPT.export_result( file_type, export_path, name, timestamp, dfs[timestamp], ) end end export_path = mkpath(joinpath(path, problem_results.problem, "duals")) for name in list_dual_names(problem_results) if should_export_dual(problem_exports, name) dfs = read_dual( problem_results, name; initial_time = timestamp, count = 1, store = store, ) ISOPT.export_result( file_type, export_path, name, timestamp, dfs[timestamp], ) end end end export_path = mkpath(joinpath(path, problem_results.problem, "expression")) for name in list_expression_names(problem_results) if should_export_expression(problem_exports, name) dfs = read_expression( problem_results, name; initial_time = timestamp, count = 1, store = store, ) ISOPT.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) ISOPT.export_result(file_type, export_path, df) end end return end function _check_status(status::RunStatus, ignore_status) status == RunStatus.SUCCESSFULLY_FINALIZED && return if ignore_status @warn "Simulation was not successful: $status. Results may not be valid." else error( "Simulation was not successful: status = $status. Set ignore_status = true to override.", ) end return end ================================================ FILE: src/simulation/simulation_results_export.jl ================================================ const _SUPPORTED_FORMATS = ("csv",) mutable struct SimulationResultsExport models::Dict{Symbol, OptimizationProblemResultsExport} start_time::Dates.DateTime end_time::Dates.DateTime path::Union{Nothing, String} format::String end function SimulationResultsExport( models::Vector{OptimizationProblemResultsExport}, params::SimulationStoreParams; start_time = nothing, end_time = nothing, path = nothing, format = "csv", ) # This end time is outside the bounds of the simulation. sim_end_time = params.initial_time + params.step_resolution * params.num_steps if start_time === nothing start_time = params.initial_time elseif start_time < params.initial_time || start_time >= sim_end_time throw(IS.InvalidValue("invalid start_time: $start_time")) end if end_time === nothing # Reduce the end_time to be within the simulation. end_time = sim_end_time - Dates.Second(1) elseif end_time < params.initial_time || end_time >= sim_end_time throw(IS.InvalidValue("invalid end_time: $end_time")) end if !(format in list_supported_formats(SimulationResultsExport)) throw(IS.InvalidValue("format = $format is not supported")) end return SimulationResultsExport( Dict(x.name => x for x in models), start_time, end_time, path, format, ) end function SimulationResultsExport(filename::AbstractString, params::SimulationStoreParams) if splitext(filename)[2] != ".json" throw(IS.InvalidValue("only JSON files are supported: $filename")) end return SimulationResultsExport(read_json(filename), params) end function SimulationResultsExport(data::AbstractDict, params::SimulationStoreParams) models = Vector{OptimizationProblemResultsExport}() for model in get(data, "models", []) if !haskey(model, "name") throw(IS.InvalidValue("model data does not define 'name'")) end problem_params = params.decision_models_params[Symbol(model["name"])] duals = Set( deserialize_key(problem_params, x) for x in get(model, "duals", Set{ConstraintKey}()) ) parameters = Set( deserialize_key(problem_params, x) for x in get(model, "parameters", Set{ParameterKey}()) ) variables = Set( deserialize_key(problem_params, x) for x in get(model, "variables", Set{VariableKey}()) ) aux_variables = Set( deserialize_key(problem_params, x) for x in get(model, "variables", Set{AuxVarKey}()) ) problem_export = OptimizationProblemResultsExport( model["name"]; duals = duals, parameters = parameters, variables = variables, optimizer_stats = get(model, "optimizer_stats", false), store_all_duals = get(model, "store_all_duals", false), store_all_parameters = get(model, "store_all_parameters", false), store_all_variables = get(model, "store_all_variables", false), store_all_aux_variables = get(model, "store_all_aux_variables", false), ) push!(models, problem_export) end start_time = get(data, "start_time", nothing) if start_time isa AbstractString start_time = Dates.DateTime(start_time) end end_time = get(data, "end_time", nothing) if end_time isa AbstractString end_time = Dates.DateTime(end_time) end return SimulationResultsExport( models, params; start_time = start_time, end_time = end_time, path = get(data, "path", nothing), format = get(data, "format", "csv"), ) end function get_problem_exports(x::SimulationResultsExport, model_name) name = Symbol(model_name) if !haskey(x.models, name) throw(IS.InvalidValue("model $name is not stored. keys = $(keys(x.models))")) end return x.models[name] end function get_export_file_type(exports::SimulationResultsExport) if exports.format == "csv" return CSV.File end throw(IS.InvalidValue("format not supported: $(exports.format)")) end list_supported_formats(::Type{SimulationResultsExport}) = ("csv",) function should_export(exports::SimulationResultsExport, tstamp::Dates.DateTime) return tstamp >= exports.start_time && tstamp <= exports.end_time end function should_export_dual(exports::SimulationResultsExport, tstamp, model, name) return _should_export(exports, tstamp, model, STORE_CONTAINER_DUALS, name) end function should_export_parameter(exports::SimulationResultsExport, tstamp, model, name) return _should_export(exports, tstamp, model, STORE_CONTAINER_PARAMETERS, name) end function should_export_variable(exports::SimulationResultsExport, tstamp, model, name) return _should_export(exports, tstamp, model, STORE_CONTAINER_VARIABLES, name) end function should_export_expression(exports::SimulationResultsExport, tstamp, model, name) return _should_export(exports, tstamp, model, STORE_CONTAINER_EXPRESSIONS, name) end function should_export_aux_variable(exports::SimulationResultsExport, tstamp, model, name) return _should_export(exports, tstamp, model, STORE_CONTAINER_AUX_VARIABLES, name) end function _should_export(exports::SimulationResultsExport, tstamp, model, field_name, name) if tstamp < exports.start_time || tstamp >= exports.end_time return false end problem_exports = get_problem_exports(exports, model) return ISOPT._should_export(problem_exports, field_name, name) end ================================================ FILE: src/simulation/simulation_sequence.jl ================================================ function check_simulation_chronology( horizons::OrderedDict{Symbol, Dates.Millisecond}, intervals::OrderedDict{Symbol, Dates.Millisecond}, resolutions::OrderedDict{Symbol, Dates.Millisecond}, ) models = collect(keys(resolutions)) for (model, horizon_time) in horizons if horizon_time < intervals[model] throw(IS.ConflictingInputsError("horizon ($horizon_time) is shorter than interval ($interval) for $(model)")) end end for i in 2:length(models) upper_level_model = models[i - 1] lower_level_model = models[i] 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 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 end return end """ _calculate_interval_inner_counts(intervals::OrderedDict{String,<:Dates.TimePeriod}) Calculates how many times a problem is executed for every interval of the previous problem """ function _calculate_interval_inner_counts(intervals::OrderedDict{Symbol, Dates.Millisecond}) order = collect(keys(intervals)) reverse_order = length(intervals):-1:1 interval_run_counts = Vector{Int}(undef, length(intervals)) interval_run_counts[1] = 1 for k in reverse_order[1:(end - 1)] model_name = order[k] previous_model_name = order[k - 1] problem_interval = intervals[model_name] previous_problem_interval = intervals[previous_model_name] if Dates.Millisecond(previous_problem_interval % problem_interval) != Dates.Millisecond(0) throw( IS.ConflictingInputsError( "The interval configuration provided results in a fractional number of executions of problem $model_name", ), ) end interval_run_counts[k] = previous_problem_interval / problem_interval @debug "problem $k is executed $(interval_run_counts[k]) time within each interval of problem $(k-1)" end return interval_run_counts end """ Function calculates the total number of problem executions in the simulation and allocates the appropiate vector """ function _allocate_execution_order(interval_run_counts::Vector{Int}) total_size_of_vector = 0 for k in eachindex(interval_run_counts) mult = 1 for i in 1:k mult *= interval_run_counts[i] end total_size_of_vector += mult end return -1 * ones(Int, total_size_of_vector) end function _fill_execution_order( execution_order::Vector{Int}, interval_run_counts::Vector{Int}, ) function _fill_problem(index::Int, problem::Int) last_problem = problems[end] if problem < last_problem next_problem = problem + 1 for i in 1:interval_run_counts[next_problem] index = _fill_problem(index, next_problem) end end execution_order[index] = problem index -= 1 end index = length(execution_order) problems = sort!(collect(keys(interval_run_counts))) _fill_problem(index, problems[1]) return end function _get_execution_order_vector(intervals::OrderedDict{Symbol, Dates.Millisecond}) length(intervals) == 1 && return [1] interval_run_counts = _calculate_interval_inner_counts(intervals) execution_order_vector = _allocate_execution_order(interval_run_counts) _fill_execution_order(execution_order_vector, interval_run_counts) @assert isempty(findall(x -> x == -1, execution_order_vector)) return execution_order_vector end function _get_num_executions_by_model( models::SimulationModels, execution_order::Vector{Int}, ) model_names = get_model_names(models) executions_by_model = OrderedDict(x => 0 for x in model_names) for number in execution_order executions_by_model[model_names[number]] += 1 end return executions_by_model end function _add_feedforward_to_model( sim_model::OperationModel, ff::T, ::Type{U}, ) where {T <: AbstractAffectFeedforward, U <: PSY.Device} device_model = get_model(get_template(sim_model), get_component_type(ff)) if device_model === nothing model_name = get_name(sim_model) throw( IS.ConflictingInputsError( "Device model $(get_component_type(ff)) not found in model $model_name", ), ) end @debug "attaching $T to $(get_component_type(ff))" attach_feedforward!(device_model, ff) return end function _add_feedforward_to_model( sim_model::OperationModel, ff::T, ::Type{U}, ) where {T <: AbstractAffectFeedforward, U <: PSY.Service} if get_feedforward_meta(ff) != NO_SERVICE_NAME_PROVIDED service_model = get_model( get_template(sim_model), get_component_type(ff), get_feedforward_meta(ff), ) if service_model === nothing throw( IS.ConflictingInputsError( "Service model $(get_component_type(ff)) not found in model $(get_name(sim_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 for (key, model) in get_service_models(get_template(sim_model)) if key[2] == Symbol(get_component_type(ff)) service_found = true @debug "attaching $T to $(get_component_type(ff))" attach_feedforward!(model, ff) end end end return end function _attach_feedforwards(models::SimulationModels, feedforwards) names = Set(string.(get_model_names(models))) ff_dict = Dict{Symbol, Vector}() for (model_name, model_feedforwards) in feedforwards if model_name ∈ names model_name_symbol = Symbol(model_name) ff_dict[model_name_symbol] = model_feedforwards for ff in model_feedforwards sim_model = get_simulation_model(models, model_name_symbol) _add_feedforward_to_model(sim_model, ff, get_component_type(ff)) end else error("Model $model_name not present in the SimulationModels") end end return ff_dict end function _add_event_to_model( sim_model::OperationModel, key::EventKey{T, U}, event_model::EventModel, ) where {T <: PSY.Contingency, U <: PSY.Device} device_model = get_model(get_template(sim_model), U) if !haskey(get_events(device_model), key) set_event_model!(device_model, key, event_model) else @debug "Event Model with key $key already in the device model" end return end function _validate_event_timeseries_data( sys::PSY.System, event::PSY.Contingency, event_model::EventModel, ) devices_with_attribute = PSY.get_components(sys, event) for (k, v) in event_model.timeseries_mapping if v !== nothing try PSY.get_time_series( IS.SingleTimeSeries, event, v, ) catch e devices_with_attribute = PSY.get_components(sys, event) device_names_with_attribute = [PSY.get_name(d) for d in devices_with_attribute] error( "Event $event belonging to devices $device_names_with_attribute missing time series with name $v", ) end end if !haskey(get_empty_timeseries_mapping(typeof(event)), k) error( "Key $k passed as part of event time series mapping does not correspond to a parameter.", ) end if k == :outage_status && v === nothing error( "FixedForcedOutage requires a timeseries mapping for :outage_status parameter", ) end end end function _add_model_to_event_map!( model::OperationModel, sys::PSY.System, event_models::Vector{T}, ) where {T <: EventModel} model_name = get_name(model) for event_model in event_models event_type = get_event_type(event_model) if isempty(PSY.get_supplemental_attributes(event_type, sys)) error( "There is no data for $event_type in $(model_name). \ Since events are simulation-wide objects, they need to be added to all models.", ) continue end event_model.attribute_device_map[model_name] = Dict{Base.UUID, Dict{DataType, Set{String}}}() event_model.attribute_device_map[model_name] for event in PSY.get_supplemental_attributes(event_type, sys) _validate_event_timeseries_data(sys, event, event_model) event_uuid = PSY.IS.get_uuid(event) @debug "Attaching $event_uuid to $model_name" devices_with_attribute = PSY.get_components(sys, event) device_types_with_attribute = Set{DataType}() event_model.attribute_device_map[model_name][event_uuid] = Dict{DataType, Set{String}}() for device in devices_with_attribute dtype = typeof(device) push!(device_types_with_attribute, dtype) name_set = get!( event_model.attribute_device_map[model_name][event_uuid], dtype, Set{String}(), ) push!(name_set, PSY.get_name(device)) end for device_type in device_types_with_attribute key = EventKey(event_type, device_type) _add_event_to_model(model, key, event_model) end end event_model.attribute_device_map[model_name] end return end function _attach_events!( models::SimulationModels, event_models::Vector{T}, ) where {T <: EventModel} for model in get_decision_models(models) sys = get_system(model) _add_model_to_event_map!(model, sys, event_models) end em_model = get_emulation_model(models) if !isnothing(em_model) _add_model_to_event_map!( em_model, get_system(em_model), event_models, ) end return end """ SimulationSequence( models::SimulationModels, feedforward::Dict{String, Vector{<:AbstractAffectFeedforward}} ini_cond_chronology::InitialConditionChronology ) Construct the simulation sequence between decision and emulation models. # Arguments - `models::SimulationModels`: Vector of decisions and emulation models. - `feedforward = Dict{String, Vector{<:AbstractAffectFeedforward}}()`: Optional dictionary to specify how information and variables are exchanged between decision and emulation models. - `ini_cond_chronology::InitialConditionChronology = InterProblemChronology()`: Define information sharing model between stages with [`InterProblemChronology`](@ref) # Example ```julia template_uc = template_unit_commitment() template_ed = template_economic_dispatch() my_decision_model_uc = DecisionModel(template_1, sys_uc, optimizer, name = "UC") my_decision_model_ed = DecisionModel(template_ed, sys_ed, optimizer, name = "ED") models = SimulationModels( decision_models = [ my_decision_model_uc, my_decision_model_ed ] ) # The following sequence set the commitment variables (`OnVariable`) for `ThermalStandard` units from UC to ED. sequence = SimulationSequence(; models = models, feedforwards = Dict( "ED" => [ SemiContinuousFeedforward(; component_type = ThermalStandard, source = OnVariable, affected_values = [ActivePowerVariable], ), ], ), ) ``` """ mutable struct SimulationSequence horizons::OrderedDict{Symbol, Dates.Millisecond} intervals::OrderedDict{Symbol, Dates.Millisecond} feedforwards::Dict{Symbol, Vector{<:AbstractAffectFeedforward}} events::Vector{<:EventModel} ini_cond_chronology::InitialConditionChronology execution_order::Vector{Int} executions_by_model::OrderedDict{Symbol, Int} current_execution_index::Int64 uuid::Base.UUID function SimulationSequence(; models::SimulationModels, feedforwards = Dict{String, Vector{<:AbstractAffectFeedforward}}(), events = Vector{EventModel}(), ini_cond_chronology = InterProblemChronology(), ) # Allow strings or symbols as keys; convert to symbols. intervals = determine_intervals(models) horizons = determine_horizons!(models) resolutions = determine_resolutions(models) if length(models.decision_models) > 1 check_simulation_chronology(horizons, intervals, resolutions) end if length(models.decision_models) == 1 # TODO: Not implemented yet # ini_cond_chronology = IntraProblemChronology() end execution_order = _get_execution_order_vector(intervals) executions_by_model = _get_num_executions_by_model(models, execution_order) sequence_uuid = IS.make_uuid() initialize_simulation_internals!(models, sequence_uuid) _attach_events!(models, events) new( horizons, intervals, _attach_feedforwards(models, feedforwards), events, ini_cond_chronology, execution_order, executions_by_model, 0, sequence_uuid, ) end end get_step_resolution(sequence::SimulationSequence) = first(values(sequence.intervals)) function get_interval(sequence::SimulationSequence, problem::Symbol) return sequence.intervals[problem] end function get_interval(sequence::SimulationSequence, model::DecisionModel) return sequence.intervals[get_name(model)] end get_events(sequence::SimulationSequence) = sequence.events get_execution_order(sequence::SimulationSequence) = sequence.execution_order ================================================ FILE: src/simulation/simulation_state.jl ================================================ struct SimulationState current_time::Base.RefValue{Dates.DateTime} last_decision_model::Base.RefValue{Symbol} decision_states::DatasetContainer{InMemoryDataset} system_states::DatasetContainer{InMemoryDataset} end function SimulationState() return SimulationState( Ref(UNSET_INI_TIME), Ref(:None), DatasetContainer{InMemoryDataset}(), DatasetContainer{InMemoryDataset}(), ) end get_current_time(s::SimulationState) = s.current_time[] get_last_decision_model(s::SimulationState) = s.last_decision_model[] get_decision_states(s::SimulationState) = s.decision_states get_system_states(s::SimulationState) = s.system_states # Not to be used in hot loops function get_system_states_resolution(s::SimulationState) system_state = get_system_states(s) # All the system states have the same resolution return get_data_resolution(first(values(system_state.variables))) end function set_current_time!(s::SimulationState, val::Dates.DateTime) s.current_time[] = val return end function set_last_decision_model!(s::SimulationState, val::Symbol) s.last_decision_model[] = val return end const STATE_TIME_PARAMS = NamedTuple{(:horizon, :resolution), NTuple{2, Dates.Millisecond}} function _get_state_params(models::SimulationModels, simulation_step::Dates.Millisecond) params = OrderedDict{OptimizationContainerKey, STATE_TIME_PARAMS}() for model in get_decision_models(models) container = get_optimization_container(model) model_resolution = get_resolution(model) model_interval = get_interval(model) 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) num_runs = simulation_step / model_interval total_time = (num_runs - 1) * model_interval + horizon_length for type in fieldnames(DatasetContainer) field_containers = getfield(container, type) for key in keys(field_containers) !should_write_resulting_value(key) && continue if !haskey(params, key) params[key] = ( horizon = max(simulation_step + time_residual, total_time), resolution = model_resolution, ) else params[key] = ( horizon = max(params[key].horizon, total_time), resolution = min(params[key].resolution, model_resolution), ) end @debug get_name(model) key params[key] end end end model = get_emulation_model(models) if model !== nothing container = get_optimization_container(model) model_resolution = get_resolution(model) for type in fieldnames(DatasetContainer) field_containers = getfield(container, type) for key in keys(field_containers) !should_write_resulting_value(key) && continue if !haskey(params, key) @debug "New parameter $key found in emulator only" else params[key] = ( horizon = params[key].horizon, resolution = min(params[key].resolution, model_resolution), ) end @debug get_name(model) key params[key] end end end return params end function _initialize_model_states!( sim_state::SimulationState, model::OperationModel, simulation_initial_time::Dates.DateTime, simulation_step::Dates.Millisecond, params::OrderedDict{OptimizationContainerKey, STATE_TIME_PARAMS}, ) states = get_decision_states(sim_state) container = get_optimization_container(model) for field in fieldnames(DatasetContainer) field_containers = getfield(container, field) field_states = getfield(states, field) for (key, field_container) in field_containers !should_write_resulting_value(key) && continue value_counts = params[key].horizon ÷ params[key].resolution column_names = get_column_names(container, field, field_container, key) # TODO DT: why would we overwrite a key? Is this a bug? if !haskey(field_states, key) || get_num_rows(field_states[key]) < value_counts field_states[key] = InMemoryDataset( NaN, simulation_initial_time, params[key].resolution, Int(simulation_step / params[key].resolution), value_counts, column_names) end end end return end function _initialize_system_states!( sim_state::SimulationState, ::Nothing, simulation_initial_time::Dates.DateTime, params::OrderedDict{OptimizationContainerKey, STATE_TIME_PARAMS}, ) decision_states = get_decision_states(sim_state) emulator_states = get_system_states(sim_state) min_res = minimum([v.resolution for v in values(params)]) for key in get_dataset_keys(decision_states) cols = get_column_names(key, get_dataset(decision_states, key)) set_dataset!( emulator_states, key, make_system_state( simulation_initial_time, min_res, cols, ), ) end return end function _initialize_system_states!( sim_state::SimulationState, emulation_model::EmulationModel, simulation_initial_time::Dates.DateTime, params::OrderedDict{OptimizationContainerKey, STATE_TIME_PARAMS}, ) decision_states = get_decision_states(sim_state) emulator_states = get_system_states(sim_state) emulation_container = get_optimization_container(emulation_model) min_res = minimum([v.resolution for v in values(params)]) for field in fieldnames(DatasetContainer) field_containers = getfield(emulation_container, field) for (key, value) in field_containers !should_write_resulting_value(key) && continue if field == :parameters column_names = get_column_names(key, value) else column_names = get_column_names_from_axis_array(key, value) end set_dataset!( emulator_states, key, make_system_state( simulation_initial_time, min_res, column_names, ), ) end end for key in get_dataset_keys(decision_states) dm_cols = get_column_names(key, get_dataset(decision_states, key)) if has_dataset(emulator_states, key) em_cols = get_column_names(key, get_dataset(emulator_states, key)) if length(dm_cols) != length(em_cols) error( "The number of dimensions between the decision states and emulator states don't match", ) end if !isempty(symdiff(first(dm_cols), first(em_cols))) error( "Mismatch in column names for dataset $key: $(symdiff(dm_cols, em_cols)) \ This issue is common when filters are applied to the decision model but not to the emulator model.", ) end continue end set_dataset!( emulator_states, key, make_system_state( simulation_initial_time, min_res, dm_cols, ), ) end return end function initialize_simulation_state!( sim_state::SimulationState, models::SimulationModels, simulation_step::Dates.Millisecond, simulation_initial_time::Dates.DateTime, ) params = _get_state_params(models, simulation_step) for model in get_decision_models(models) _initialize_model_states!( sim_state, model, simulation_initial_time, simulation_step, params, ) end set_last_decision_model!(sim_state, get_name(last(get_decision_models(models)))) em = get_emulation_model(models) _initialize_system_states!(sim_state, em, simulation_initial_time, params) return end function update_decision_state!( state::SimulationState, key::ParameterKey{AvailableStatusChangeCountdownParameter, T}, store_data::DenseAxisArray{Float64, 2}, simulation_time::Dates.DateTime, model_params::ModelStoreParams, ) where {T <: PSY.Component} state_data = get_decision_state_data(state, key) column_names = get_column_names(key, state_data)[1] model_resolution = get_resolution(model_params) state_resolution = get_data_resolution(state_data) resolution_ratio = model_resolution ÷ state_resolution state_timestamps = state_data.timestamps @assert_op resolution_ratio >= 1 if simulation_time > get_end_of_step_timestamp(state_data) state_data_index = 1 state_data.timestamps[:] .= range( simulation_time; step = state_resolution, length = get_num_rows(state_data), ) else state_data_index = find_timestamp_index(state_timestamps, simulation_time) end offset = resolution_ratio - 1 result_time_index = axes(store_data)[2] set_update_timestamp!(state_data, simulation_time) for t in result_time_index state_range = state_data_index:(state_data_index + offset) for name in column_names, (ix, i) in enumerate(state_range) state_data.values[name, i] = maximum([0.0, store_data[name, t] - ix + 1]) end set_last_recorded_row!(state_data, state_range[end]) state_data_index += resolution_ratio end return end function update_decision_state!( state::SimulationState, key::ParameterKey{AvailableStatusParameter, T}, store_data::DenseAxisArray{Float64, 2}, simulation_time::Dates.DateTime, model_params::ModelStoreParams, ) where {T <: PSY.Component} state_data = get_decision_state_data(state, key) countdown_data = get_decision_state_data(state, AvailableStatusChangeCountdownParameter(), T) column_names = get_column_names(key, state_data)[1] model_resolution = get_resolution(model_params) state_resolution = get_data_resolution(state_data) resolution_ratio = model_resolution ÷ state_resolution state_timestamps = state_data.timestamps @assert_op resolution_ratio >= 1 if simulation_time > get_end_of_step_timestamp(state_data) state_data_index = 1 state_data.timestamps[:] .= range( simulation_time; step = state_resolution, length = get_num_rows(state_data), ) else state_data_index = find_timestamp_index(state_timestamps, simulation_time) end offset = resolution_ratio - 1 result_time_index = axes(store_data)[2] set_update_timestamp!(state_data, simulation_time) for t in result_time_index state_range = state_data_index:(state_data_index + offset) for name in column_names, i in state_range if countdown_data.values[name, i] > 0.0 state_data.values[name, i] = 0.0 else state_data.values[name, i] = 1.0 end end set_last_recorded_row!(state_data, state_range[end]) state_data_index += resolution_ratio end return end function update_decision_state!( state::SimulationState, key::ParameterKey{ActivePowerOffsetParameter, T}, column_names::Set{String}, event::PSY.Outage, event_model::EventModel, simulation_time::Dates.DateTime, model_params::ModelStoreParams, ) where {T <: PSY.Component} event_occurrence_data = get_decision_state_data(state, AvailableStatusChangeCountdownParameter(), T) activepower_data = get_decision_state_data(state, ActivePowerTimeSeriesParameter(), T) state_data = get_decision_state_data(state, key) model_resolution = get_resolution(model_params) state_resolution = get_data_resolution(state_data) resolution_ratio = model_resolution ÷ state_resolution @assert_op resolution_ratio >= 1 state_timestamps = state_data.timestamps state_data_index = find_timestamp_index(state_timestamps, simulation_time) for name in column_names if event_occurrence_data.values[name, state_data_index] == 1.0 outage_index = state_data_index + 1 #outage occurs at the following timestep subsequent_outage_occurence_data = Vector(event_occurrence_data.values[name, outage_index:end]) n_remaining_indices = findfirst(x -> x == 1.0, subsequent_outage_occurence_data) if n_remaining_indices === nothing n_remaining_indices = length(subsequent_outage_occurence_data) end for ix in outage_index:(state_data_index + n_remaining_indices) # Set the offset parameter to equal the negative of the timeseries parameter state_data.values[name, ix] = -1.0 * activepower_data.values[name, ix] end end end return end function update_decision_state!( state::SimulationState, key::OptimizationContainerKey, store_data::DenseAxisArray{Float64, 2}, simulation_time::Dates.DateTime, model_params::ModelStoreParams, ) state_data = get_decision_state_data(state, key) column_names = get_column_names(key, state_data)[1] model_resolution = get_resolution(model_params) state_resolution = get_data_resolution(state_data) resolution_ratio = model_resolution ÷ state_resolution state_timestamps = state_data.timestamps @assert_op resolution_ratio >= 1 if simulation_time > get_end_of_step_timestamp(state_data) state_data_index = 1 state_data.timestamps[:] .= range( simulation_time; step = state_resolution, length = get_num_rows(state_data), ) else state_data_index = find_timestamp_index(state_timestamps, simulation_time) end offset = resolution_ratio - 1 result_time_index = axes(store_data)[2] set_update_timestamp!(state_data, simulation_time) for t in result_time_index state_range = state_data_index:(state_data_index + offset) for name in column_names, i in state_range # TODO: We could also interpolate here state_data.values[name, i] = store_data[name, t] end set_last_recorded_row!(state_data, state_range[end]) state_data_index += resolution_ratio end return end function _get_time_to_recover( event::PSY.GeometricDistributionForcedOutage, event_model::EventModel, simulation_time, ) timeseries_mapping = event_model.timeseries_mapping if timeseries_mapping[:mean_time_to_recovery] === nothing return PSY.get_mean_time_to_recovery(event) else ts_mttr = PSY.get_time_series( IS.SingleTimeSeries, event, timeseries_mapping[:mean_time_to_recovery]; start_time = simulation_time, len = 1, ) return TimeSeries.values(ts_mttr.data)[1] end end function _get_time_to_recover( event::PSY.FixedForcedOutage, event_model::EventModel, simulation_time, ) timeseries_mapping = event_model.timeseries_mapping ts_outage_status = PSY.get_time_series( IS.SingleTimeSeries, event, timeseries_mapping[:outage_status]; start_time = simulation_time, ) vals = TimeSeries.values(ts_outage_status.data) if length(vals) < 3 || findfirst(isequal(0.0), vals[3:end]) === nothing return length(vals) else return findfirst(isequal(0.0), vals[3:end]) end end function update_decision_state!( state::SimulationState, key::OptimizationContainerKey, column_names::Set{String}, event::PSY.Outage, event_model::EventModel, simulation_time::Dates.DateTime, model_params::ModelStoreParams, ) return end function update_decision_state!( state::SimulationState, key::ParameterKey{AvailableStatusChangeCountdownParameter, T}, column_names::Set{String}, event::PSY.Outage, event_model::EventModel, simulation_time::Dates.DateTime, ::ModelStoreParams, ) where {T <: PSY.Component} event_occurrence_data = get_system_state_data(state, AvailableStatusChangeCountdownParameter(), T) event_occurrence_values = get_last_recorded_value(event_occurrence_data) # This is required since the data for outages (mttr and λ) is always assumed to be on hourly resolution mttr_resolution = Dates.Hour(1) state_data = get_decision_state_data(state, key) state_resolution = get_data_resolution(state_data) resolution_ratio = mttr_resolution ÷ state_resolution state_timestamps = state_data.timestamps @assert_op resolution_ratio >= 1 state_data_index = find_timestamp_index(state_timestamps, simulation_time) for name in column_names state_data.values[name, state_data_index] = event_occurrence_values[name, 1] if event_occurrence_values[name, 1] == 1.0 mttr_hr = _get_time_to_recover(event, event_model, simulation_time) mttr_state_resolution = mttr_hr * resolution_ratio if !isinteger(mttr_state_resolution) @warn "MTTR is not an integer after conversion from hours to $state_resolution resolution MTTR will be rounded up to $(Int(ceil(mttr_state_resolution))) steps of $state_resolution" mttr_state_resolution = ceil(mttr_state_resolution) end off_time_step_count = Int(mttr_state_resolution) set_update_timestamp!(state_data, simulation_time) for (ix, countdown) in enumerate(off_time_step_count:-1.0:1.0) if state_data_index + ix > length(state_timestamps) #outage extends beyond current state break end state_data.values[name, state_data_index + ix] = countdown end end end return end function update_decision_state!( state::SimulationState, key::ParameterKey{AvailableStatusParameter, T}, column_names::Set{String}, event::PSY.Outage, event_model::EventModel, simulation_time::Dates.DateTime, model_params::ModelStoreParams, ) where {T <: PSY.Component} event_occurrence_data = get_decision_state_data(state, AvailableStatusChangeCountdownParameter(), T) state_data = get_decision_state_data(state, key) model_resolution = get_resolution(model_params) state_resolution = get_data_resolution(state_data) resolution_ratio = model_resolution ÷ state_resolution @assert_op resolution_ratio >= 1 state_timestamps = state_data.timestamps state_data_index = find_timestamp_index(state_timestamps, simulation_time) for name in column_names if event_occurrence_data.values[name, state_data_index] == 1.0 outage_index = state_data_index + 1 #outage occurs at the following timestep subsequent_outage_occurence_data = Vector(event_occurrence_data.values[name, outage_index:end]) n_remaining_indices = findfirst(x -> x == 1.0, subsequent_outage_occurence_data) if n_remaining_indices === nothing n_remaining_indices = length(subsequent_outage_occurence_data) end for ix in outage_index:(state_data_index + n_remaining_indices) state_data.values[name, ix] = 0.0 end end end return end function update_decision_state!( state::SimulationState, key::AuxVarKey{TimeDurationOn, T}, column_names::Set{String}, event::PSY.Outage, event_model::EventModel, simulation_time::Dates.DateTime, model_params::ModelStoreParams, ) where {T <: PSY.Component} event_occurrence_data = get_decision_state_data(state, AvailableStatusChangeCountdownParameter(), T) state_data = get_decision_state_data(state, key) state_timestamps = state_data.timestamps state_data_index = find_timestamp_index(state_timestamps, simulation_time) event_occurence_index = find_timestamp_index(event_occurrence_data.timestamps, simulation_time) for name in column_names if event_occurrence_data.values[name, event_occurence_index] == 1.0 state_data.values[name, (state_data_index + 1):end] .= MISSING_INITIAL_CONDITIONS_TIME_COUNT end end return end function update_decision_state!( state::SimulationState, key::AuxVarKey{TimeDurationOff, T}, column_names::Set{String}, event::PSY.Outage, event_model::EventModel, simulation_time::Dates.DateTime, model_params::ModelStoreParams, ) where {T <: PSY.Component} event_occurrence_data = get_decision_state_data(state, AvailableStatusChangeCountdownParameter(), T) state_data = get_decision_state_data(state, key) state_timestamps = state_data.timestamps state_data_index = find_timestamp_index(state_timestamps, simulation_time) event_occurence_index = find_timestamp_index(event_occurrence_data.timestamps, simulation_time) for name in column_names if event_occurrence_data.values[name, event_occurence_index] == 1.0 for (time_off, ix) in enumerate((state_data_index + 1):length(state_data.values[name, :])) state_data.values[name, ix] = time_off end end end return end function update_decision_state!( state::SimulationState, key::VariableKey{T, U}, column_names::Set{String}, event::PSY.Outage, event_model::EventModel, simulation_time::Dates.DateTime, model_params::ModelStoreParams, ) where {T <: Union{ActivePowerVariable, OnVariable}, U <: PSY.Component} event_occurrence_data = get_decision_state_data(state, AvailableStatusChangeCountdownParameter(), U) state_data = get_decision_state_data(state, key) state_timestamps = state_data.timestamps state_data_index = find_timestamp_index(state_timestamps, simulation_time) event_occurence_index = find_timestamp_index(event_occurrence_data.timestamps, simulation_time) for name in column_names if event_occurrence_data.values[name, event_occurence_index] == 1.0 state_data.values[name, (state_data_index + 1):end] .= 0.0 end end return end function update_decision_state!( state::SimulationState, key::OptimizationContainerKey, store_data::DenseAxisArray{Float64, 3}, simulation_time::Dates.DateTime, model_params::ModelStoreParams, ) state_data = get_decision_state_data(state, key) outages = get_column_names(key, state_data)[1] model_resolution = get_resolution(model_params) state_resolution = get_data_resolution(state_data) resolution_ratio = model_resolution ÷ state_resolution state_timestamps = state_data.timestamps @assert_op resolution_ratio >= 1 if simulation_time > get_end_of_step_timestamp(state_data) state_data_index = 1 state_data.timestamps[:] .= range( simulation_time; step = state_resolution, length = get_num_rows(state_data), ) else state_data_index = find_timestamp_index(state_timestamps, simulation_time) end offset = resolution_ratio - 1 result_time_index = axes(store_data)[3] set_update_timestamp!(state_data, simulation_time) for t in result_time_index state_range = state_data_index:(state_data_index + offset) for name in axes(store_data)[2], i in state_range #loop pelo -outages, names t for outage in outages # TODO: We could also interpolate here state_data.values[outage, name, i] = store_data[outage, name, t] end end set_last_recorded_row!(state_data, state_range[end]) state_data_index += resolution_ratio end return end function update_decision_state!( state::SimulationState, key::AuxVarKey{S, T}, store_data::DenseAxisArray{Float64, 2}, simulation_time::Dates.DateTime, model_params::ModelStoreParams, ) where {T <: PSY.Component, S <: Union{TimeDurationOff, TimeDurationOn}} state_data = get_decision_state_data(state, key) model_resolution = get_resolution(model_params) state_resolution = get_data_resolution(state_data) resolution_ratio = model_resolution ÷ state_resolution @assert_op resolution_ratio >= 1 if simulation_time > get_end_of_step_timestamp(state_data) state_data_index = 1 state_data.timestamps[:] .= range( simulation_time; step = state_resolution, length = get_num_rows(state_data), ) else state_data_index = find_timestamp_index(state_data.timestamps, simulation_time) end offset = resolution_ratio - 1 result_time_index = axes(store_data)[2] set_update_timestamp!(state_data, simulation_time) if resolution_ratio == 1.0 increment_per_period = 1.0 elseif state_resolution < Dates.Day(365) && state_resolution > Dates.Minute(1) increment_per_period = Dates.value(Dates.Minute(state_resolution)) else error("Incorrect Problem Resolution specification: $(state_resolution)") end column_names = axes(state_data.values)[1] for t in result_time_index state_range = state_data_index:(state_data_index + offset) @assert_op state_range[end] <= get_num_rows(state_data) for name in column_names, i in state_range if t == 1 && i == 1 state_data.values[name, i] = store_data[name, t] * resolution_ratio else state_data.values[name, i] = if store_data[name, t] > 0 state_data.values[name, i - 1] + increment_per_period else 0 end end end set_last_recorded_row!(state_data, state_range[end]) state_data_index += resolution_ratio end return end function get_decision_state_data(state::SimulationState, key::OptimizationContainerKey) return get_dataset(get_decision_states(state), key) end function get_decision_state_value(state::SimulationState, key::OptimizationContainerKey) return get_dataset_values(get_decision_states(state), key) end function get_decision_state_data( state::SimulationState, ::T, ::Type{U}, ) where {T <: VariableType, U <: Union{PSY.Component, PSY.System}} return get_decision_state_data(state, VariableKey(T, U)) end function get_decision_state_data( state::SimulationState, ::T, ::Type{U}, ) where {T <: AuxVariableType, U <: Union{PSY.Component, PSY.System}} return get_decision_state_data(state, AuxVarKey(T, U)) end function get_decision_state_data( state::SimulationState, ::T, ::Type{U}, ) where {T <: ConstraintType, U <: Union{PSY.Component, PSY.System}} return get_decision_state_data(state, ConstraintKey(T, U)) end function get_decision_state_data( state::SimulationState, ::T, ::Type{U}, ) where {T <: ParameterType, U <: Union{PSY.Component, PSY.System}} return get_decision_state_data(state, ParameterKey(T, U)) end function get_decision_state_value( state::SimulationState, key::OptimizationContainerKey, date::Dates.DateTime, ) return get_dataset_values(get_decision_states(state), key, date) end function get_system_state_data(state::SimulationState, key::OptimizationContainerKey) return get_dataset(get_system_states(state), key) end function get_system_state_value(state::SimulationState, key::OptimizationContainerKey) return get_dataset_values(get_system_states(state), key)[:, 1] end function update_system_state!( state::DatasetContainer{InMemoryDataset}, key::OptimizationContainerKey, store::SimulationStore, model_name::Symbol, simulation_time::Dates.DateTime, ) em_data = get_em_data(store) ix = get_last_recorded_row(em_data, key) res = read_result(DenseAxisArray, store, model_name, key, ix) dataset = get_dataset(state, key) set_update_timestamp!(dataset, simulation_time) if typeof(store) == HdfSimulationStore set_dataset_values!(state, key, 1, res) else # Handle different dimensionality of results num_dims = ndims(res) if num_dims == 2 set_dataset_values!(state, key, 1, res[:, ix]) elseif num_dims == 3 set_dataset_values!(state, key, 1, res[:, :, ix]) else error("Unsupported number of dimensions for emulation result: $num_dims") end end set_last_recorded_row!(dataset, 1) return end function update_system_state!( state::SimulationState, key::OptimizationContainerKey, column_names_::Set{String}, event::PSY.Outage, event_model::EventModel, simulation_time::Dates.DateTime, rng::AbstractRNG, ) return end function update_system_state!( state::SimulationState, key::ParameterKey{AvailableStatusParameter, T}, column_names_::Set{String}, event::PSY.Outage, event_model::EventModel, simulation_time::Dates.DateTime, rng::AbstractRNG, ) where {T <: PSY.Device} available_status_parameter = get_system_state_data(state, key) available_status_parameter_values = get_last_recorded_value(available_status_parameter) available_status_change_parameter = get_system_state_data(state, AvailableStatusChangeCountdownParameter(), T) available_status_change_parameter_values = get_last_recorded_value(available_status_change_parameter) for name in column_names_ current_status = available_status_parameter_values[name] current_status_change = available_status_change_parameter_values[name] if current_status == 1.0 && current_status_change == 1.0 available_status_parameter.values[name, 1] = 0.0 end end return end function _get_outage_occurrence( event::PSY.GeometricDistributionForcedOutage, event_model::EventModel, rng::AbstractRNG, current_time, ) timeseries_mapping = event_model.timeseries_mapping if timeseries_mapping[:outage_transition_probability] === nothing λ = PSY.get_outage_transition_probability(event) else ts_outage_prob = PSY.get_time_series( IS.SingleTimeSeries, event, timeseries_mapping[:outage_transition_probability]; start_time = current_time, len = 1, ) λ = TimeSeries.values(ts_outage_prob.data)[1] end outage_occurrence = Float64(rand(rng, Bernoulli(λ))) return outage_occurrence end function _get_outage_occurrence( event::PSY.FixedForcedOutage, event_model::EventModel, rng::AbstractRNG, current_time, ) timeseries_mapping = event_model.timeseries_mapping ts = PSY.get_time_series( IS.SingleTimeSeries, event, timeseries_mapping[:outage_status]; start_time = current_time, ) val = TimeSeries.values(ts.data) if length(val) == 1 return 0 end return val[2] end function update_system_state!( state::SimulationState, key::ParameterKey{AvailableStatusChangeCountdownParameter, T}, column_names_::Set{String}, event::PSY.Outage, event_model::EventModel, simulation_time::Dates.DateTime, rng::AbstractRNG, ) where {T <: PSY.Component} outage_occurrence = _get_outage_occurrence(event, event_model, rng, simulation_time) sym_state = get_system_states(state) system_dataset = get_dataset(sym_state, key) # Writes the timestamp of the value used for the update available_status_parameter = get_system_state_data(state, AvailableStatusParameter(), T) available_status_parameter_values = get_last_recorded_value(available_status_parameter) available_status_change_parameter = get_system_state_data(state, key) set_update_timestamp!(system_dataset, simulation_time) for name in column_names_ current_status = available_status_parameter_values[name] if current_status == 1.0 && outage_occurrence == 1.0 @warn "Outage occurred at time $simulation_time for devices $column_names_" available_status_change_parameter.values[name, 1] = outage_occurrence else available_status_change_parameter.values[name, 1] = 0.0 end end return end function update_system_state!( state::SimulationState, key::ParameterKey{ActivePowerOffsetParameter, T}, column_names_::Set{String}, event::PSY.Outage, event_model::EventModel, simulation_time::Dates.DateTime, rng::AbstractRNG, ) where {T <: PSY.Component} available_status_parameter = get_system_state_data(state, AvailableStatusParameter(), T) available_status_parameter_values = get_last_recorded_value(available_status_parameter) available_status_change_parameter = get_system_state_data(state, AvailableStatusChangeCountdownParameter(), T) available_status_change_parameter_values = get_last_recorded_value(available_status_change_parameter) active_power_offset_parameter = get_system_state_data(state, key) active_power_offset_parameter_values = get_last_recorded_value(active_power_offset_parameter) active_power_timeseries_parameter = get_system_state_data(state, ActivePowerTimeSeriesParameter(), T) active_power_timeseries_parameter_values = get_last_recorded_value(active_power_timeseries_parameter) for name in column_names_ current_status = available_status_parameter_values[name] current_status_change = available_status_change_parameter_values[name] if current_status == 1.0 && current_status_change == 1.0 active_power_offset_parameter.values[name, 1] = -1.0 * active_power_timeseries_parameter_values[name] end end return end function update_system_state!( state::SimulationState, key::AuxVarKey{TimeDurationOff, T}, column_names::Set{String}, event::PSY.Outage, event_model::EventModel, simulation_time::Dates.DateTime, rng::AbstractRNG, ) where {T <: PSY.Component} sym_state = get_system_states(state) event_occurrence_data = get_system_state_data(state, AvailableStatusChangeCountdownParameter(), T) event_occurrence_values = get_last_recorded_value(event_occurrence_data) system_dataset = get_dataset(sym_state, key) current_status_data = get_system_state_data(state, key) current_status_values = get_last_recorded_value(current_status_data) set_update_timestamp!(system_dataset, simulation_time) for name in column_names if event_occurrence_values[name] == 1.0 current_status_data.values[name, 1] = 0.0 end end return end function update_system_state!( state::SimulationState, key::AuxVarKey{TimeDurationOn, T}, column_names::Set{String}, event::PSY.Outage, event_model::EventModel, simulation_time::Dates.DateTime, rng::AbstractRNG, ) where {T <: PSY.Component} sym_state = get_system_states(state) event_occurrence_data = get_system_state_data(state, AvailableStatusChangeCountdownParameter(), T) event_occurrence_values = get_last_recorded_value(event_occurrence_data) system_dataset = get_dataset(sym_state, key) current_status_data = get_system_state_data(state, key) current_status_values = get_last_recorded_value(current_status_data) set_update_timestamp!(system_dataset, simulation_time) for name in column_names if event_occurrence_values[name] == 1.0 current_status_data.values[name, 1] = MISSING_INITIAL_CONDITIONS_TIME_COUNT end end return end function update_system_state!( state::SimulationState, key::VariableKey{T, U}, column_names::Set{String}, ::PSY.Outage, event_model::EventModel, simulation_time::Dates.DateTime, rng::AbstractRNG, ) where {T <: Union{ActivePowerVariable, OnVariable}, U <: PSY.Component} sym_state = get_system_states(state) event_occurrence_data = get_system_state_data(state, AvailableStatusChangeCountdownParameter(), U) event_occurrence_values = get_last_recorded_value(event_occurrence_data) system_dataset = get_dataset(sym_state, key) current_status_data = get_system_state_data(state, key) current_status_values = get_last_recorded_value(current_status_data) set_update_timestamp!(system_dataset, simulation_time) for name in column_names if event_occurrence_values[name] == 1.0 current_status_data.values[name, 1] = 0.0 end end return end function update_system_state!( state::DatasetContainer{InMemoryDataset}, key::OptimizationContainerKey, decision_state::DatasetContainer{InMemoryDataset}, simulation_time::Dates.DateTime, ) decision_dataset = get_dataset(decision_state, key) # Gets the timestamp of the value used for the update, which might not match exactly the # simulation time since the value might have not been updated yet ts = get_value_timestamp(decision_dataset, simulation_time) system_dataset = get_dataset(state, key) get_update_timestamp(system_dataset) if ts == get_update_timestamp(system_dataset) # Uncomment for debugging #@warn "Skipped overwriting data with the same timestamp \\ # key: $(encode_key_as_string(key)), $(simulation_time), $ts" return end # Note: This protection is disabled because the rate of update of the emulator # is now higher than the decision rate. If the event happens in the middle of an "hourly" # rate decision variable then the whole hour is updated creating a problem. # New logic will be needed to maintain the protection. #if get_update_timestamp(system_dataset) > ts # error("Trying to update with past data a future state timestamp \\ # key: $(encode_key_as_string(key)), $(simulation_time), $ts") #end # Writes the timestamp of the value used for the update set_update_timestamp!(system_dataset, ts) # Keep coordination between fields. System state is an array of size 1 system_dataset.timestamps[1] = ts data_set_value = get_dataset_value(decision_dataset, simulation_time) set_dataset_values!(state, key, 1, data_set_value) # This value shouldn't be other than one and after one execution is no-op. set_last_recorded_row!(system_dataset, 1) return end function update_system_state!( state::DatasetContainer{InMemoryDataset}, key::AuxVarKey{T, PSY.ThermalStandard}, decision_state::DatasetContainer{InMemoryDataset}, simulation_time::Dates.DateTime, ) where {T <: Union{TimeDurationOn, TimeDurationOff}} decision_dataset = get_dataset(decision_state, key) # Gets the timestamp of the value used for the update, which might not match exactly the # simulation time since the value might have not been updated yet ts = get_value_timestamp(decision_dataset, simulation_time) system_dataset = get_dataset(state, key) system_state_resolution = get_data_resolution(system_dataset) decision_state_resolution = get_data_resolution(decision_dataset) decision_state_value = get_dataset_value(decision_dataset, simulation_time) if ts == get_update_timestamp(system_dataset) # Uncomment for debugging #@warn "Skipped overwriting data with the same timestamp \\ # key: $(encode_key_as_string(key)), $(simulation_time), $ts" return end #if get_update_timestamp(system_dataset) > ts # error("Trying to update with past data a future state timestamp \\ # key: $(encode_key_as_string(key)), $(simulation_time), $ts") #end # Writes the timestamp of the value used for the update set_update_timestamp!(system_dataset, ts) # Keep coordination between fields. System state is an array of size 1 system_dataset.timestamps[1] = ts time_ratio = (decision_state_resolution / system_state_resolution) # Don't use set_dataset_values!(state, key, 1, decision_state_value). # For the time variables we need to grab the values to avoid mutation of the # dataframe row set_value!(system_dataset, values(decision_state_value) .* time_ratio, 1) # This value shouldn't be other than one and after one execution is no-op. set_last_recorded_row!(system_dataset, 1) return end function get_system_state_value( state::SimulationState, ::T, ::Type{U}, ) where {T <: VariableType, U <: Union{PSY.Component, PSY.System}} return get_system_state_value(state, VariableKey(T, U)) end function get_system_state_value( state::SimulationState, ::T, ::Type{U}, ) where {T <: AuxVariableType, U <: Union{PSY.Component, PSY.System}} return get_system_state_value(state, AuxVarKey(T, U)) end function get_system_state_value( state::SimulationState, ::T, ::Type{U}, ) where {T <: ConstraintType, U <: Union{PSY.Component, PSY.System}} return get_system_state_value(state, ConstraintKey(T, U)) end function get_system_state_value( state::SimulationState, ::T, ::Type{U}, ) where {T <: ParameterType, U <: Union{PSY.Component, PSY.System}} return get_system_state_value(state, ParameterKey(T, U)) end function get_system_state_data( state::SimulationState, ::T, ::Type{U}, ) where {T <: VariableType, U <: Union{PSY.Component, PSY.System}} return get_system_state_data(state, VariableKey(T, U)) end function get_system_state_data( state::SimulationState, ::T, ::Type{U}, ) where {T <: AuxVariableType, U <: Union{PSY.Component, PSY.System}} return get_system_state_data(state, AuxVarKey(T, U)) end function get_system_state_data( state::SimulationState, ::T, ::Type{U}, ) where {T <: ConstraintType, U <: Union{PSY.Component, PSY.System}} return get_system_state_data(state, ConstraintKey(T, U)) end function get_system_state_data( state::SimulationState, ::T, ::Type{U}, ) where {T <: ParameterType, U <: Union{PSY.Component, PSY.System}} return get_system_state_data(state, ParameterKey(T, U)) end ================================================ FILE: src/simulation/simulation_store_params.jl ================================================ struct SimulationStoreParams initial_time::Dates.DateTime step_resolution::Dates.Millisecond num_steps::Int # The key order is the problem execution order. decision_models_params::OrderedDict{Symbol, ModelStoreParams} emulation_model_params::OrderedDict{Symbol, ModelStoreParams} function SimulationStoreParams( initial_time::Dates.DateTime, step_resolution::Dates.Period, num_steps::Int, decision_models_params::OrderedDict{Symbol, ModelStoreParams}, emulation_model_params::OrderedDict, ) new( initial_time, Dates.Millisecond(step_resolution), num_steps, decision_models_params, emulation_model_params, ) end end function SimulationStoreParams(initial_time, step_resolution, num_steps) return SimulationStoreParams( initial_time, step_resolution, num_steps, OrderedDict{Symbol, ModelStoreParams}(), OrderedDict{Symbol, ModelStoreParams}(), ) end function SimulationStoreParams() return SimulationStoreParams( Dates.DateTime("1970-01-01T00:00:00"), Dates.Millisecond(0), 0, OrderedDict{Symbol, ModelStoreParams}(), OrderedDict{Symbol, ModelStoreParams}(), ) end get_initial_time(store_params::SimulationStoreParams) = store_params.initial_time function get_decision_model_params(store_params::SimulationStoreParams, model_name::Symbol) return store_params.decision_models_params[model_name] end function get_emulation_model_params(store_params::SimulationStoreParams) # We currently only store one em_model dataset in the store @assert_op length(store_params.emulation_model_params) == 1 return first(values(store_params.emulation_model_params)) end ================================================ FILE: src/simulation/simulation_store_requirements.jl ================================================ 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 ================================================ FILE: src/utils/dataframes_utils.jl ================================================ function to_matrix(df::DataFrame) return Matrix{Float64}(df) end function to_matrix(df_row::DataFrameRow{DataFrame, DataFrames.Index}) return reshape(Vector(df_row), 1, size(df_row)[1]) end ================================================ FILE: src/utils/datetime_utils.jl ================================================ """ calculates the index in the time series corresponding to the data. Assumes that the dates vector is sorted. """ function find_timestamp_index( dates::Union{Vector{Dates.DateTime}, StepRange{Dates.DateTime, Dates.Millisecond}}, date::Dates.DateTime, ) if date == first(dates) index = 1 elseif date == last(dates) index = length(dates) else dates_resolution = dates[2] - dates[1] index = 1 + ((date - first(dates)) ÷ dates_resolution) end # Uncomment for debugging. The method below is fool proof but slower # s_index = findlast(dates .<= date) # IS.@assert_op index == s_index if index < 1 || index > length(dates) error("Requested timestamp $date not in the provided dates $dates") end return index end ================================================ FILE: src/utils/file_utils.jl ================================================ """ Return a decoded JSON file. """ function read_json(filename::AbstractString) open(filename, "r") do io JSON3.read(io) end end """ Return a DataFrame from a CSV file. """ function read_dataframe(filename::AbstractString) return CSV.read(filename, DataFrames.DataFrame) end """ Return the key for the given value """ function find_key_with_value(d, value) for (k, v) in d v == value && return k end error("dict does not have value == $value") end function read_file_hashes(path) data = open(joinpath(path, IS.HASH_FILENAME), "r") do io JSON3.read(io) end return data["files"] end # this ensures that the timestamp is not double shortened function find_variable_length(es::Dict, e_list::Array) return size(es[Symbol(splitext(e_list[1])[1])], 1) end """ check_file_integrity(path::String) Checks the hash value for each file made with the file is written with the new hash_value to verify the file hasn't been tampered with since written # Arguments - `path::String`: this is the folder path that contains the results and the check.sha256 file """ function check_file_integrity(path::String) matched = true for file_info in read_file_hashes(path) filename = file_info["filename"] @info "checking integrity of $filename" expected_hash = file_info["hash"] 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 end end if !matched throw( IS.HashMismatchError( "The hash value in the written files does not match the read files, results may have been tampered.", ), ) end end ================================================ FILE: src/utils/generate_valid_formulations.jl ================================================ """ Generate valid combinations of device_type/formulation and service_type/formulation. Return vectors of dictionaries with Julia types. # Arguments - `sys::Union{Nothing, System}`: If set, only include component types present in the system. """ function generate_formulation_combinations(sys = nothing) combos = Dict( "device_formulations" => generate_device_formulation_combinations(), "service_formulations" => generate_service_formulation_combinations(), ) filter_formulation_combinations!(combos, sys) return combos end filter_formulation_combinations!(combos, ::Nothing) = nothing function filter_formulation_combinations!(combos, sys::PSY.System) device_types = Set(PSY.get_existing_device_types(sys)) service_types = Set((x for x in PSY.get_existing_component_types(sys) if x <: PSY.Service)) filter!(x -> x["device_type"] in device_types, combos["device_formulations"]) filter!(x -> x["service_type"] in service_types, combos["service_formulations"]) end """ Generate valid combinations of device_type/formulation and service_type/formulation. Return vectors of dictionaries with Julia types encoded as strings. # Arguments - `sys::Union{Nothing, System}`: If set, only include component types present in the system. """ function serialize_formulation_combinations(sys = nothing) combos = generate_formulation_combinations(sys) for (i, combo) in enumerate(combos["device_formulations"]) for key in keys(combo) combos["device_formulations"][i][key] = string(nameof(combo[key])) end end for (i, combo) in enumerate(combos["service_formulations"]) for key in keys(combo) combos["service_formulations"][i][key] = string(nameof(combo[key])) end end sort!(combos["device_formulations"]; by = x -> x["device_type"]) sort!(combos["service_formulations"]; by = x -> x["service_type"]) return combos end """ Generate valid combinations of device_type/formulation and service_type/formulation and write the result to a JSON file. # Arguments - `sys::Union{Nothing, System}`: If set, only include component types present in the system. """ function write_formulation_combinations(filename::AbstractString, sys = nothing) open(filename, "w") do io JSON3.pretty(io, serialize_formulation_combinations(sys)) end @info(" to $filename") end function generate_device_formulation_combinations() combos = [] for (d, f) in Iterators.product( IS.get_all_concrete_subtypes(PSY.Device), IS.get_all_concrete_subtypes(AbstractDeviceFormulation), ) # DynamicBranches are not supported in PSI but they are still considered <: PSY.Device since in # PSY 1.0 we haven't introduced the notion of AbstractDynamicBranches. if d <: PSY.DynamicBranch continue end if !isempty(methodswith(DeviceModel{d, f}, construct_device!; supertypes = true)) push!(combos, Dict{String, Any}("device_type" => d, "formulation" => f)) end end return combos end function generate_service_formulation_combinations() combos = [] for (d, f) in Iterators.product( IS.get_all_concrete_subtypes(PSY.Service), IS.get_all_concrete_subtypes(AbstractServiceFormulation), ) if !isempty(methodswith(ServiceModel{d, f}, construct_service!; supertypes = true)) push!(combos, Dict{String, Any}("service_type" => d, "formulation" => f)) end end return combos end ================================================ FILE: src/utils/indexing.jl ================================================ # Pad `ixs` with `:` for any unindexed middle dimensions of `dest` (Python `...`-style). # Fast path for AbstractArrays where `N` is known at compile time → allocation-free `Val(N - K)`. @inline function expand_ixs( ixs::NTuple{K, Any}, dest::AbstractArray{<:Any, N}, ) where {K, N} K <= N || throw(ArgumentError("`ixs` must not index more dimensions than `dest` has")) K == N && return ixs # Single-element ixs is the leading axis; multi-element preserves first..last with `:` filling the middle. K == 1 && return (only(ixs), ntuple(_ -> Colon(), Val(N - 1))...) return (Base.front(ixs)..., ntuple(_ -> Colon(), Val(N - K))..., last(ixs)) end # Fallback for non-AbstractArray containers (e.g. `HDF5.Dataset`) — `ndims` resolved at runtime. @inline function expand_ixs(ixs::NTuple{K, Any}, dest) where {K} N = ndims(dest) K <= N || throw(ArgumentError("`ixs` must not index more dimensions than `dest` has")) K == N && return ixs K == 1 && return (only(ixs), ntuple(_ -> Colon(), N - 1)...) return (Base.front(ixs)..., ntuple(_ -> Colon(), N - K)..., last(ixs)) end # Concrete fast path: scalar `src` with a fully-specified index tuple goes through `setindex!`. @inline function assign_maybe_broadcast!( dest::DenseAxisArray{T, N}, src::T, ixs::NTuple{N, Any}, ) where {T, N} dest[ixs...] = src return end # Array `src`: assign a slice of `dest` from `src` (standard assignment handles non-1:n axes). # `dest` is left untyped so non-`AbstractArray` containers (e.g. `HDF5.Dataset`) are also accepted. @inline function assign_maybe_broadcast!(dest, src::AbstractArray, ixs::Tuple) dest[expand_ixs(ixs, dest)...] = src return end # Scalar/tuple `src`: broadcast the value across the indexed slice of `dest`. @inline function assign_maybe_broadcast!(dest, src, ixs::Tuple) expanded = expand_ixs(ixs, dest) @views dest[expanded...] .= Ref(src) return end # Similar to assign_maybe_broadcast! but for fixing JuMP VariableRefs @inline function fix_maybe_broadcast!( dest::DenseAxisArray{JuMP.VariableRef, N}, src::Float64, ixs::NTuple{N, Any}, ) where {N} JuMP.fix(dest[ixs...], src; force = true) return end fix_expand(dest, src, ixs::Tuple) = fix_parameter_value.(dest[expand_ixs(ixs, dest)...], src) fix_maybe_broadcast!(dest, src::AbstractArray, ixs::Tuple) = fix_expand(dest, src, ixs) fix_maybe_broadcast!(dest, src, ixs::Tuple) = fix_expand(dest, Ref(src), ixs) ================================================ FILE: src/utils/jump_utils.jl ================================================ const IntegerAxis = Union{Vector{Int}, UnitRange{Int}} function get_hinted_aff_expr(size::Int) expr = JuMP.AffExpr(0.0) sizehint!(expr.terms, size) return expr end #Given the changes in syntax in ParameterJuMP and the new format to create anonymous parameters function add_jump_parameter(jump_model::JuMP.Model, val::Number) param = JuMP.@variable(jump_model, base_name = "param") JuMP.fix(param, val; force = true) return param end function write_data(base_power::Float64, save_path::String) JSON3.write(joinpath(save_path, "base_power.json"), JSON3.json(base_power)) return end function jump_value(input::JuMP.VariableRef)::Float64 if JuMP.is_fixed(input) return JuMP.fix_value(input) elseif JuMP.has_values(input.model) return JuMP.value(input) else return NaN end end function jump_value(input::T)::Float64 where {T <: JuMP.AbstractJuMPScalar} return JuMP.value(input) end function jump_value(input::JuMP.ConstraintRef)::Float64 return JuMP.dual(input) end function jump_value(input::JumpSupportedLiterals) return input end # Like jump_value but for certain special cases before optimize! is called jump_fixed_value(input::Number) = input jump_fixed_value(input::JuMP.VariableRef) = JuMP.fix_value(input) jump_fixed_value(input::JuMP.AffExpr) = sum([coeff * jump_fixed_value(param) for (coeff, param) in JuMP.linear_terms(input)]) + JuMP.constant(input) function fix_parameter_value(input::JuMP.VariableRef, value::Float64) JuMP.fix(input, value; force = true) return end """ Convert Vectors, DenseAxisArrays, and SparkAxisArrays to a matrix. - If the input is a 1d array or DenseAxisArray, the returned matrix will have a number of rows equal to the length of the input and one column. - If the input is a 2d DenseAxisArray, the dimensions are transposed, due to the way we store outputs in JuMP. """ function to_matrix(vec::Vector) data = vec[:] return reshape(data, length(data), 1) end to_matrix(array::Matrix) = array function to_matrix(array::DenseAxisArray{T, 1}) where {T} data = array.data[:] return reshape(data, length(data), 1) end function to_matrix(array::DenseAxisArray{T, 2}) where {T} return permutedims(array.data) end function to_matrix(array::DenseAxisArray) error("Converting type = $(typeof(array)) to a matrix is not supported.") end function _to_matrix( array::SparseAxisArray{T, N, K}, columns, ) where {T, N, K <: NTuple{N, Any}} time_steps = Set{Int}(k[N] for k in keys(array.data)) data = Matrix{Float64}(undef, length(time_steps), length(columns)) for (ix, col) in enumerate(columns), t in time_steps data[t, ix] = array.data[(col..., t)] end return data end function to_matrix(array::SparseAxisArray{T, N, K}) where {T, N, K <: NTuple{N, Any}} # Don't use get_column_names_from_axis_array to avoid additional string conversion # TODO: I don't understand why we have two mechanisms of creating columns. # Why does get_column_names_from_axis_array rely on encode_tuple_to_column? columns = sort!(unique!([k[1:(N - 1)] for k in keys(array.data)])) return _to_matrix(array, columns) end """ Return column names from the axes of a JuMP DenseAxisArray or SparseAxisArray. The columns are returned as a tuple of vector of strings. 1d and 2d arrays will return a tuple of length 1. 3d arrays will return a tuple of length 2. There are two variants of this function: - get_column_names_from_axis_array(array) - get_column_names_from_axis_array(::OptimizationContainerKey, array) When the variant with the key is called: In cases where the array has one dimension, retrieve the column names from the key. In cases where the array has two or more dimensions, retrieve the column names from the axes. """ # TODO: the docstring describes what the code does. # The behavior of key vs axes seems suspect to me. function get_column_names_from_axis_array( array::DenseAxisArray{T, 1, <:Tuple{Vector{String}}}, ) where {T} return (axes(array, 1),) end function get_column_names_from_axis_array( array::DenseAxisArray{T, 1, <:Tuple{IntegerAxis}}, ) where {T} # This happens because buses are stored by numbers instead of name. return (string.(axes(array, 1)),) end function get_column_names_from_axis_array( array::DenseAxisArray{T, 2, <:Tuple{Vector{String}, IntegerAxis}}, ) where {T} return (axes(array, 1),) end function get_column_names_from_axis_array( array::DenseAxisArray{T, 2, <:Tuple{IntegerAxis, IntegerAxis}}, ) where {T} return (string.(axes(array, 1)),) end function get_column_names_from_axis_array( array::DenseAxisArray{T, 3, <:Tuple{Vector{String}, Vector{String}, IntegerAxis}}, ) where {T} return (axes(array, 1), axes(array, 2)) end function get_column_names_from_axis_array( array::DenseAxisArray{T, 3, <:Tuple{Vector{String}, IntegerAxis, IntegerAxis}}, ) where {T} return (axes(array, 1), string.(axes(array, 2))) end function get_column_names_from_axis_array( key::OptimizationContainerKey, ::DenseAxisArray{T, 1}, ) where {T} return get_column_names_from_key(key) end function get_column_names_from_axis_array(::OptimizationContainerKey, array::DenseAxisArray) return get_column_names_from_axis_array(array) end function get_column_names_from_axis_array( ::OptimizationContainerKey, array::SparseAxisArray, ) return get_column_names_from_axis_array(array) end function get_column_names_from_axis_array( array::SparseAxisArray{T, N, K}, ) where {T, N, K <: NTuple{N, Any}} return ( sort!( collect(Set(encode_tuple_to_column(k[1:(N - 1)]) for k in keys(array.data))), ), ) end """ Return the column names from a key as a tuple of vector of strings. Only useful for 1d DenseAxisArrays. """ function get_column_names_from_key(key::OptimizationContainerKey) return ([encode_key_as_string(key)],) end function encode_tuple_to_column(val::NTuple{N, <:AbstractString}) where {N} return join(val, PSI_NAME_DELIMITER) end function encode_tuple_to_column(val::Tuple{String, Int}) return join(string.(val), PSI_NAME_DELIMITER) end """ Create a DataFrame from a JuMP DenseAxisArray or SparseAxisArray. # Arguments - `array`: JuMP DenseAxisArray or SparseAxisArray to convert - `key::OptimizationContainerKey`: """ function to_dataframe( array::DenseAxisArray{T, 2}, key::OptimizationContainerKey, ) where {T <: JumpSupportedLiterals} return DataFrame(to_matrix(array), get_column_names_from_axis_array(key, array)[1]) end function to_dataframe( array::DenseAxisArray{T, 2, <:Tuple{Vector{String}, UnitRange{Int}}}, ) where {T <: JumpSupportedLiterals} return DataFrame(to_matrix(array), get_column_names_from_axis_array(array)[1]) end function to_dataframe( array::DenseAxisArray{T, 1}, key::OptimizationContainerKey, ) where {T <: JumpSupportedLiterals} cols = get_column_names_from_axis_array(key, array)[1] if length(cols) != 1 error("Expected a single column, got $(length(cols))") end return DataFrame(Symbol(cols[1]) => array.data) end function to_dataframe(array::SparseAxisArray, key::OptimizationContainerKey) return DataFrame(to_matrix(array), get_column_names_from_axis_array(key, array)[1]) end """ Convert a DenseAxisArray containing components to a results DataFrame consumable by users. # Arguments - `array: DenseAxisArray`: JuMP DenseAxisArray to convert - `timestamps`: Iterable of timestamps for each component or nothing if time is not known. The resulting DataFrame will have the column "DateTime" if timestamps is not nothing. Otherwise, it will have the column "time_index", representing the index of the time dimension. - `::Val{TableFormat}`: Format of the table to create. If it is TableFormat.LONG, the DataFrame will have the column "name", and, if the data has three dimensions, "name2." If it is TableFormat.WIDE, the DataFrame will have columns for each component. Wide format does not support arrays with more than two dimensions. """ function to_results_dataframe(array::DenseAxisArray, timestamps) return to_results_dataframe(array, timestamps, Val(TableFormat.LONG))() end function to_results_dataframe( array::DenseAxisArray{Float64, 1, <:Tuple{Vector{String}}}, timestamps, ::Val{TableFormat.LONG}, ) return DataFrames.DataFrame( :DateTime => ones(Int, length(axes(array, 1))), :name => axes(array, 1), :value => array.data, ) end function to_results_dataframe( array::DenseAxisArray{Float64, 2, <:Tuple{Vector{String}, IntegerAxis}}, timestamps, ::Val{TableFormat.LONG}, ) num_timestamps = size(array, 2) if length(timestamps) != num_timestamps error( "The number of timestamps must match the number of rows per component. " * "timestamps = $(length(timestamps)) " * "num_timestamps = $num_timestamps", ) end num_rows = length(array.data) timestamps_arr = _collect_timestamps(timestamps) time_col = Vector{Dates.DateTime}(undef, num_rows) name_col = Vector{String}(undef, num_rows) row_index = 1 for name in axes(array, 1) for time_index in axes(array, 2) time_col[row_index] = timestamps_arr[time_index] name_col[row_index] = name row_index += 1 end end return DataFrame( :DateTime => time_col, :name => name_col, :value => reshape(permutedims(array.data), num_rows), ) end _collect_timestamps(timestamps::Vector{Dates.DateTime}) = timestamps _collect_timestamps(timestamps) = collect(timestamps) function to_results_dataframe( array::DenseAxisArray{Float64, 2, <:Tuple{Vector{String}, IntegerAxis}}, ::Nothing, ::Val{TableFormat.LONG}, ) num_rows = length(array.data) time_col = Vector{Int}(undef, num_rows) name_col = Vector{String}(undef, num_rows) row_index = 1 for name in axes(array, 1) for time_index in axes(array, 2) time_col[row_index] = time_index name_col[row_index] = name row_index += 1 end end return DataFrame( :time_index => time_col, :name => name_col, :value => reshape(permutedims(array.data), num_rows), ) end function to_results_dataframe( array::DenseAxisArray{Float64, 1, <:Tuple{IntegerAxis}}, ::Nothing, ::Val{TableFormat.LONG}, ) num_rows = length(array.data) time_col = Vector{Int}(undef, num_rows) name_col = Vector{String}(undef, num_rows) row_index = 1 name = "Result" for time_index in axes(array, 1) time_col[row_index] = time_index name_col[row_index] = name row_index += 1 end return DataFrame( :time_index => time_col, :name => name_col, :value => reshape(permutedims(array.data), num_rows), ) end function to_results_dataframe( array::DenseAxisArray{Float64, 2, <:Tuple{Vector{String}, IntegerAxis}}, timestamps, ::Val{TableFormat.WIDE}, ) df = DataFrame(to_matrix(array), axes(array, 1)) DataFrames.insertcols!(df, 1, :DateTime => timestamps) return df end function to_results_dataframe( array::DenseAxisArray{Float64, 2, <:Tuple{Vector{String}, IntegerAxis}}, ::Nothing, ::Val{TableFormat.WIDE}, ) df = DataFrame(to_matrix(array), axes(array, 1)) DataFrames.insertcols!(df, 1, :time_index => axes(array, 2)) return df end function to_results_dataframe( array::DenseAxisArray{ Float64, 3, <:Tuple{Vector{String}, Vector{String}, UnitRange{Int}}, }, timestamps, ::Val{TableFormat.LONG}, ) num_timestamps = size(array, 3) if length(timestamps) != num_timestamps error( "The number of timestamps must match the number of rows per component. " * "timestamps = $(length(timestamps)) " * "num_timestamps = $num_timestamps", ) end num_rows = length(array.data) timestamps_arr = _collect_timestamps(timestamps) time_col = Vector{Dates.DateTime}(undef, num_rows) name_col = Vector{String}(undef, num_rows) name2_col = Vector{String}(undef, num_rows) vals = Vector{Float64}(undef, num_rows) row_index = 1 for name in axes(array, 1) for name2 in axes(array, 2) for time_index in axes(array, 3) time_col[row_index] = timestamps_arr[time_index] name_col[row_index] = name name2_col[row_index] = name2 vals[row_index] = array[name, name2, time_index] row_index += 1 end end end return DataFrame( :DateTime => time_col, :name => name_col, :name2 => name2_col, :value => vals, ) end function to_results_dataframe( array::DenseAxisArray{ Float64, 3, <:Tuple{Vector{String}, Vector{String}, UnitRange{Int}}, }, ::Nothing, ::Val{TableFormat.LONG}, ) num_rows = length(array.data) time_col = Vector{Int}(undef, num_rows) name_col = Vector{String}(undef, num_rows) name2_col = Vector{String}(undef, num_rows) vals = Vector{Float64}(undef, num_rows) row_index = 1 for name in axes(array, 1) for name2 in axes(array, 2) for time_index in axes(array, 3) time_col[row_index] = time_index name_col[row_index] = name name2_col[row_index] = name2 vals[row_index] = array[name, name2, time_index] row_index += 1 end end end return DataFrame( :time_index => time_col, :name => name_col, :name2 => name2_col, :value => vals, ) end function to_dataframe(array::SparseAxisArray{T, N, K}) where {T, N, K <: NTuple{N, Any}} columns = get_column_names_from_axis_array(array) return DataFrames.DataFrame(_to_matrix(array, columns), columns) end """ Returns the correct container specification for the selected type of JuMP Model """ function container_spec(::Type{T}, axs...) where {T <: Any} return DenseAxisArray{T}(undef, axs...) end """ Returns the correct container specification for the selected type of JuMP Model """ function container_spec(::Type{Float64}, axs...) cont = DenseAxisArray{Float64}(undef, axs...) cont.data .= fill(NaN, size(cont.data)) return cont end """ Returns the correct container specification for the selected type of JuMP Model """ function sparse_container_spec(::Type{T}, axs...) where {T <: JuMP.AbstractJuMPScalar} indexes = Base.Iterators.product(axs...) contents = Dict{eltype(indexes), T}(indexes .=> zero(T)) return SparseAxisArray(contents) end function sparse_container_spec(::Type{T}, axs...) where {T <: JuMP.VariableRef} indexes = Base.Iterators.product(axs...) contents = Dict{eltype(indexes), Union{Nothing, T}}(indexes .=> nothing) return SparseAxisArray(contents) end function sparse_container_spec(::Type{T}, axs...) where {T <: JuMP.ConstraintRef} indexes = Base.Iterators.product(axs...) contents = Dict{eltype(indexes), Union{Nothing, T}}(indexes .=> nothing) return SparseAxisArray(contents) end function sparse_container_spec(::Type{T}, axs...) where {T <: Number} indexes = Base.Iterators.product(axs...) contents = Dict{eltype(indexes), T}(indexes .=> zero(T)) return SparseAxisArray(contents) end function remove_undef!(expression_array::AbstractArray) # iteration is deliberately unsupported for CartesianIndex # Makes this code a bit hacky to be able to use isassigned with an array of arbitrary size. for i in CartesianIndices(expression_array.data) if !isassigned(expression_array.data, i.I...) expression_array.data[i] = zero(eltype(expression_array)) end end return expression_array end remove_undef!(expression_array::SparseAxisArray) = expression_array function _calc_dimensions( array::DenseAxisArray, key::OptimizationContainerKey, num_rows::Int, horizon::Int, ) ax = axes(array) columns = get_column_names_from_axis_array(key, array) # Two use cases for read: # 1. Read data for one execution for one device. # 2. Read data for one execution for all devices. # This will ensure that data on disk is contiguous in both cases. if length(ax) == 1 if length(ax[1]) != horizon @debug "$(encode_key_as_string(key)) has length $(length(ax[1])). Different than horizon $horizon." end dims = (length(ax[1]), 1, num_rows) elseif length(ax) == 2 if length(ax[2]) != horizon @debug "$(encode_key_as_string(key)) has length $(length(ax[1])). Different than horizon $horizon." end dims = (length(ax[2]), length(columns[1]), num_rows) elseif length(ax) == 3 if length(ax[3]) != horizon @debug "$(encode_key_as_string(key)) has length $(length(ax[1])). Different than horizon $horizon." end dims = (length(ax[3]), length(columns[1]), length(columns[2]), num_rows) else error("unsupported data size $(length(ax))") end return Dict("columns" => columns, "dims" => dims) end function _calc_dimensions( array::SparseAxisArray, key::OptimizationContainerKey, num_rows::Int, horizon::Int, ) columns = get_column_names_from_axis_array(key, array) dims = (horizon, length.(columns)..., num_rows) return Dict("columns" => columns, "dims" => dims) end """ Run this function only when getting detailed solver stats """ function _summary_to_dict!(optimizer_stats::OptimizerStats, jump_model::JuMP.Model) # JuMP.solution_summary uses a lot of try-catch so it has a performance hit and should be opt-in jump_summary = JuMP.solution_summary(jump_model; verbose = false) # Note we don't grab all the fields from the summary because not all can be encoded as Float for HDF store fields = [ :has_values, # Bool :has_duals, # Bool # Candidate solution :objective_bound, # Union{Missing,Float64} :dual_objective_value, # Union{Missing,Float64} # Work counters :relative_gap, # Union{Missing,Int} :barrier_iterations, # Union{Missing,Int} :simplex_iterations, # Union{Missing,Int} :node_count, # Union{Missing,Int} ] for field in fields field_value = getfield(jump_summary, field) if ismissing(field_value) setfield!(optimizer_stats, field, missing) else setfield!(optimizer_stats, field, field_value) end end return end function supports_milp(jump_model::JuMP.Model) optimizer_backend = JuMP.backend(jump_model) return MOI.supports_constraint(optimizer_backend, MOI.VariableIndex, MOI.ZeroOne) end function _get_solver_time(jump_model::JuMP.Model) solver_solve_time = NaN try_s = get!(jump_model.ext, :try_supports_solvetime, (trycatch = true, supports = true)) if try_s.trycatch try solver_solve_time = MOI.get(jump_model, MOI.SolveTimeSec()) jump_model.ext[:try_supports_solvetime] = (trycatch = false, supports = true) catch @debug "SolveTimeSec() property not supported by the Solver" jump_model.ext[:try_supports_solvetime] = (trycatch = false, supports = false) end else if try_s.supports solver_solve_time = MOI.get(jump_model, MOI.SolveTimeSec()) end end return solver_solve_time end function write_optimizer_stats!(optimizer_stats::OptimizerStats, jump_model::JuMP.Model) if JuMP.primal_status(jump_model) == MOI.FEASIBLE_POINT::MOI.ResultStatusCode optimizer_stats.objective_value = JuMP.objective_value(jump_model) else optimizer_stats.objective_value = Inf end optimizer_stats.termination_status = Int(JuMP.termination_status(jump_model)) optimizer_stats.primal_status = Int(JuMP.primal_status(jump_model)) optimizer_stats.dual_status = Int(JuMP.dual_status(jump_model)) optimizer_stats.result_count = JuMP.result_count(jump_model) optimizer_stats.solve_time = _get_solver_time(jump_model) if optimizer_stats.detailed_stats _summary_to_dict!(optimizer_stats, jump_model) end return end """ Exports the JuMP object in MathOptFormat """ function serialize_jump_optimization_model(jump_model::JuMP.Model, save_path::String) MOF_model = MOPFM(; format = MOI.FileFormats.FORMAT_MOF) MOI.copy_to(MOF_model, JuMP.backend(jump_model)) MOI.write_to_file(MOF_model, save_path) return end function write_lp_file(jump_model::JuMP.Model, save_path::String) MOF_model = MOPFM(; format = MOI.FileFormats.FORMAT_LP) MOI.copy_to(MOF_model, JuMP.backend(jump_model)) MOI.write_to_file(MOF_model, save_path) return end # check_conflict_status functions can't be tested on CI because free solvers don't support IIS function check_conflict_status( jump_model::JuMP.Model, constraint_container::DenseAxisArray{JuMP.ConstraintRef}, ) conflict_indices = Vector() dims = axes(constraint_container) for index in Iterators.product(dims...) if isassigned(constraint_container, index...) && MOI.get( jump_model, MOI.ConstraintConflictStatus(), constraint_container[index...], ) != MOI.NOT_IN_CONFLICT push!(conflict_indices, index) end end return conflict_indices end function check_conflict_status( jump_model::JuMP.Model, constraint_container::SparseAxisArray{JuMP.ConstraintRef}, ) conflict_indices = Vector() for (index, constraint) in constraint_container if isassigned(constraint_container, index...) && MOI.get(jump_model, MOI.ConstraintConflictStatus(), constraint) != MOI.NOT_IN_CONFLICT push!(conflict_indices, index) end end return conflict_indices end ================================================ FILE: src/utils/logging.jl ================================================ LOG_GROUP_COST_FUNCTIONS = :CostFunctionsConstructor LOG_GROUP_OPTIMZATION_CONTAINER = :OptimizationContainer LOG_GROUP_TYPE_REGISTRATIONS = :TypeRegistrations LOG_GROUP_FEEDFORWARDS_CONSTRUCTION = :FeedforwardConstructor LOG_GROUP_BRANCH_CONSTRUCTIONS = :BranchConstructor LOG_GROUP_SERVICE_CONSTUCTORS = :ServicesConstructor LOG_GROUP_MODELS_VALIDATION = :ModelValidation LOG_GROUP_OPTIMIZATION_CONTAINER = :OptimizationContainer LOG_GROUP_BUILD_INITIAL_CONDITIONS = :InitialConditionsBuild LOG_GROUP_NETWORK_CONSTRUCTION = :NetworkConstructor LOG_GROUP_MODEL_STORE = :ModelStore LOG_GROUP_RESULTS = :Results LOG_GROUP_SIMULATION_STORE = :SimulationStore ================================================ FILE: src/utils/powersystems_utils.jl ================================================ """ Convert the internal `Dates.Millisecond` interval (where `UNSET_INTERVAL` means unset) to the `Union{Nothing, Dates.Period}` form the IS / PSY time-series API expects. Internal hot paths carry a concrete `Dates.Millisecond` to stay type-stable; this helper performs the boundary conversion. """ _to_is_interval(interval::Dates.Millisecond) = interval == UNSET_INTERVAL ? nothing : interval """ Convert the internal `Dates.Millisecond` resolution (where `UNSET_RESOLUTION` means unset) to the `Union{Nothing, Dates.Period}` form the IS / PSY time-series API expects. """ _to_is_resolution(resolution::Dates.Millisecond) = resolution == UNSET_RESOLUTION ? nothing : resolution function get_available_components( model::DeviceModel{T, <:AbstractDeviceFormulation}, sys::PSY.System, ) where {T <: PSY.Component} subsystem = get_subsystem(model) filter_function = get_attribute(model, "filter_function") if filter_function === nothing return PSY.get_components( PSY.get_available, T, sys; subsystem_name = subsystem, ) else return PSY.get_components( x -> PSY.get_available(x) && filter_function(x), T, sys; subsystem_name = subsystem, ) end end function get_available_components( model::ServiceModel{T, <:AbstractServiceFormulation}, sys::PSY.System, ) where {T <: PSY.Component} subsystem = get_subsystem(model) filter_function = get_attribute(model, "filter_function") if filter_function === nothing return PSY.get_components( PSY.get_available, T, sys; subsystem_name = subsystem, ) else return PSY.get_components( x -> PSY.get_available(x) && filter_function(x), T, sys; subsystem_name = subsystem, ) end end _filter_function(x::PSY.ACBus) = PSY.get_bustype(x) != PSY.ACBusTypes.ISOLATED && PSY.get_available(x) function get_available_components( model::NetworkModel, ::Type{PSY.ACBus}, sys::PSY.System, ) subsystem = get_subsystem(model) return PSY.get_components( _filter_function, PSY.ACBus, sys; subsystem_name = subsystem, ) end function get_available_components( model::NetworkModel, ::Type{T}, sys::PSY.System, ) where {T <: PSY.Component} subsystem = get_subsystem(model) return PSY.get_components( T, sys; subsystem_name = subsystem, ) end #= function get_available_components( ::Type{PSY.RegulationDevice{T}}, sys::PSY.System, ) where {T <: PSY.Component} return PSY.get_components( x -> (PSY.get_available(x) && PSY.has_service(x, PSY.AGC)), PSY.RegulationDevice{T}, sys, ) end =# 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( d::Union{PSY.TwoTerminalHVDC, PSY.TModelHVDCLine}, ) from_min = PSY.get_active_power_limits_from(d).min to_min = PSY.get_active_power_limits_to(d).min from_max = PSY.get_active_power_limits_from(d).max to_max = PSY.get_active_power_limits_to(d).max if from_max < to_min throw( IS.ConflictingInputsError( "From Max $(from_max) can't be a smaller value than To Min $(to_min)", ), ) elseif to_max < from_min throw( IS.ConflictingInputsError( "To Max $(to_max) can't be a smaller value than From Min $(from_min)", ), ) end return end function check_hvdc_line_limits_unidirectional(d::PSY.TwoTerminalHVDC) from_min = PSY.get_active_power_limits_from(d).min to_min = PSY.get_active_power_limits_to(d).min from_max = PSY.get_active_power_limits_from(d).max to_max = PSY.get_active_power_limits_to(d).max if from_min < 0 || to_min < 0 || from_max < 0 || to_max < 0 throw( IS.ConflictingInputsError( "Changing flow direction on HVDC Line $(PSY.get_name(d)) is not compatible with non-linear network formulations. \ Bi-directional models with losses are only compatible with linear network models like DCPPowerModel.", ), ) end return end ################################################## ########### 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, ) 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 _get_piecewise_pointcurve_per_system_unit( cost_component::PSY.PiecewiseLinearData, ::Val{PSY.UnitSystem.NATURAL_UNITS}, 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 / system_base_power, y = point.y) end return PSY.PiecewiseLinearData(points_normalized) end """ Obtain the normalized PiecewiseStepData 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_curve_per_system_unit( cost_component::PSY.PiecewiseStepData, unit_system::PSY.UnitSystem, system_base_power::Float64, device_base_power::Float64, ) return PSY.PiecewiseStepData( get_piecewise_curve_per_system_unit( PSY.get_x_coords(cost_component), PSY.get_y_coords(cost_component), unit_system, system_base_power, device_base_power, )..., ) end function get_piecewise_curve_per_system_unit( x_coords::AbstractVector, y_coords::AbstractVector, unit_system::PSY.UnitSystem, system_base_power::Float64, device_base_power::Float64, ) return _get_piecewise_curve_per_system_unit( x_coords, y_coords, Val{unit_system}(), system_base_power, device_base_power, ) end function _get_piecewise_curve_per_system_unit( x_coords::AbstractVector, y_coords::AbstractVector, ::Val{PSY.UnitSystem.SYSTEM_BASE}, system_base_power::Float64, device_base_power::Float64, ) return x_coords, y_coords end function _get_piecewise_curve_per_system_unit( x_coords::AbstractVector, y_coords::AbstractVector, ::Val{PSY.UnitSystem.DEVICE_BASE}, system_base_power::Float64, device_base_power::Float64, ) ratio = device_base_power / system_base_power x_coords_normalized = x_coords .* ratio y_coords_normalized = y_coords ./ ratio return x_coords_normalized, y_coords_normalized end function _get_piecewise_curve_per_system_unit( x_coords::AbstractVector, y_coords::AbstractVector, ::Val{PSY.UnitSystem.NATURAL_UNITS}, system_base_power::Float64, device_base_power::Float64, ) x_coords_normalized = x_coords ./ system_base_power y_coords_normalized = y_coords .* system_base_power return x_coords_normalized, y_coords_normalized end is_time_variant(::IS.TimeSeriesKey) = true is_time_variant(::Any) = false function create_temporary_cost_function_in_system_per_unit( original_cost_function::PSY.CostCurve, new_data::PSY.PiecewiseLinearData, ) return PSY.CostCurve( PSY.PiecewisePointCurve(new_data), PSY.UnitSystem.SYSTEM_BASE, PSY.get_vom_cost(original_cost_function), ) end function create_temporary_cost_function_in_system_per_unit( original_cost_function::PSY.FuelCurve, new_data::PSY.PiecewiseLinearData, ) return PSY.FuelCurve( PSY.PiecewisePointCurve(new_data), PSY.UnitSystem.SYSTEM_BASE, PSY.get_fuel_cost(original_cost_function), PSY.LinearCurve(0.0), # setting fuel offtake cost to default value of 0 PSY.get_vom_cost(original_cost_function), ) end """ Return the set of distinct forecast intervals present in the system. """ function get_forecast_intervals(sys::PSY.System) table = PSY.get_forecast_summary_table(sys) return Set(row.interval for row in eachrow(table) if row.interval !== nothing) end """ Return `(initial_timestamp, length)` for the `SingleTimeSeries` in `sys` whose resolution matches `resolution`. Throws `IS.InvalidValue` when no match exists or when matching series disagree on either field. Used to validate emulation model inputs when a system carries SingleTimeSeries at multiple resolutions. """ function get_single_time_series_consistency( sys::PSY.System, resolution::Dates.Period, ) table = PSY.get_static_time_series_summary_table(sys) target = Dates.canonicalize(Dates.Millisecond(resolution)) filtered = [row for row in eachrow(table) if row.resolution == target] if isempty(filtered) throw( IS.InvalidValue( "No SingleTimeSeries found at resolution $(target)", ), ) end unique_pairs = unique((row.initial_timestamp, row.time_step_count) for row in filtered) if length(unique_pairs) > 1 throw( IS.InvalidValue( "SingleTimeSeries at resolution $(target) have inconsistent " * "initial times and lengths: $(collect(unique_pairs))", ), ) end ini_time_str, ts_length = first(unique_pairs) return (Dates.DateTime(ini_time_str), ts_length) end """ Automatically transform `SingleTimeSeries` into `DeterministicSingleTimeSeries` for a given (horizon, interval) when a DecisionModel is built with these settings and the system contains only static time series. Uses `delete_existing=false` so multiple models may share the same system with different transforms. Does nothing when: - The model's `horizon` or `interval` are unset. - The system has no `SingleTimeSeries` to transform. - The system has existing forecast data AND the requested interval is already present in those forecasts. """ function auto_transform_time_series!(sys::PSY.System, settings::Settings) model_interval = get_interval(settings) model_horizon = get_horizon(settings) if model_interval == UNSET_INTERVAL || model_horizon == UNSET_HORIZON return end counts = PSY.get_time_series_counts(sys) if counts.static_time_series_count < 1 return end if counts.forecast_count > 0 && model_interval in get_forecast_intervals(sys) return end model_resolution = get_resolution(settings) resolution_kwarg = model_resolution == UNSET_RESOLUTION ? (;) : (; resolution = model_resolution) @info "Auto-transforming SingleTimeSeries to DeterministicSingleTimeSeries" horizon = Dates.canonicalize(model_horizon) interval = Dates.canonicalize(model_interval) PSY.transform_single_time_series!( sys, model_horizon, model_interval; delete_existing = false, resolution_kwarg..., ) return end function get_deterministic_time_series_type(sys::PSY.System) time_series_types = IS.get_time_series_counts_by_type(sys.data) existing_types = Set(d["type"] for d in time_series_types) if Set(["Deterministic", "DeterministicSingleTimeSeries"]) ∈ existing_types error( "The System contains a combination of forecast data and transformed time series data. Currently this is not supported.", ) end if "Deterministic" ∈ existing_types return PSY.Deterministic elseif "DeterministicSingleTimeSeries" ∈ existing_types return PSY.DeterministicSingleTimeSeries else error( "The System does not contain any forecast data or transformed time series data.", ) end end ================================================ FILE: src/utils/print_pt_v2.jl ================================================ function Base.show(io::IO, container::OptimizationContainer) show(io, get_jump_model(container)) end function Base.show(io::IO, ::MIME"text/plain", input::Union{ServiceModel, DeviceModel}) _show_method(io, input, :auto) end function Base.show(io::IO, ::MIME"text/html", input::Union{ServiceModel, DeviceModel}) _show_method(io, input, :html; standalone = false, tf = PrettyTables.tf_html_simple) end function _show_method( io::IO, model::Union{ServiceModel, DeviceModel}, backend::Symbol; kwargs..., ) println(io) header = ["Device Type", "Formulation", "Slacks"] table = Matrix{String}(undef, 1, length(header)) table[1, 1] = string(get_component_type(model)) table[1, 2] = string(get_formulation(model)) table[1, 3] = string(model.use_slacks) PrettyTables.pretty_table( io, table; header = header, backend = Val(backend), title = "Device Model", alignment = :l, kwargs..., ) if !isempty(model.attributes) println(io) header = ["Name", "Value"] table = Matrix{String}(undef, length(model.attributes), length(header)) for (ix, (k, v)) in enumerate(model.attributes) table[ix, 1] = string(k) table[ix, 2] = string(v) end PrettyTables.pretty_table( io, table; header = header, backend = Val(backend), title = "Attributes", alignment = :l, kwargs..., ) end if !isempty(model.time_series_names) println(io) header = ["Parameter Name", "Time Series Name"] table = Matrix{String}(undef, length(model.time_series_names), length(header)) for (ix, (k, v)) in enumerate(model.time_series_names) table[ix, 1] = string(k) table[ix, 2] = string(v) end PrettyTables.pretty_table( io, table; header = header, backend = Val(backend), title = "Time Series Names", alignment = :l, kwargs..., ) end if !isempty(model.duals) println(io) table = string.(model.duals) PrettyTables.pretty_table( io, table; show_header = false, backend = Val(backend), title = "Duals", alignment = :l, kwargs..., ) end if !isempty(model.feedforwards) println(io) header = ["Type", "Source", "Affected Values"] table = Matrix{String}(undef, length(model.feedforwards), length(header)) for (ix, v) in enumerate(model.feedforwards) table[ix, 1] = string(typeof(v)) table[ix, 2] = encode_key_as_string(get_optimization_container_key(v)) table[ix, 3] = first(encode_key_as_string.(get_affected_values(v))) end PrettyTables.pretty_table( io, table; header = header, backend = Val(backend), title = "Feedforwards", alignment = :l, kwargs..., ) else println(io) print(io, "No FeedForwards Assigned") end end function Base.show(io::IO, ::MIME"text/plain", input::NetworkModel) _show_method(io, input, :auto) end function Base.show(io::IO, ::MIME"text/html", input::NetworkModel) _show_method(io, input, :html; standalone = false, tf = PrettyTables.tf_html_simple) end function _show_method(io::IO, network_model::NetworkModel, backend::Symbol; kwargs...) table = [ "Network Model" string(get_network_formulation(network_model)) "Slacks" get_use_slacks(network_model) "PTDF" !isnothing(get_PTDF_matrix(network_model)) "Duals" join(string.(get_duals(network_model)), " ") ] PrettyTables.pretty_table( io, table; backend = Val(backend), header = ["Field", "Value"], title = "Network Model", alignment = :l, kwargs..., ) return end function Base.show(io::IO, ::MIME"text/plain", input::OperationModel) _show_method(io, input, :auto) end function Base.show(io::IO, ::MIME"text/html", input::OperationModel) _show_method(io, input, :html; standalone = false, tf = PrettyTables.tf_html_simple) end function _show_method(io::IO, model::OperationModel, backend::Symbol; kwargs...) _show_method(io, model.template, backend; kwargs...) end function Base.show(io::IO, ::MIME"text/plain", input::ProblemTemplate) _show_method(io, input, :auto) end function Base.show(io::IO, ::MIME"text/html", input::ProblemTemplate) _show_method(io, input, :html; standalone = false, tf = PrettyTables.tf_html_simple) end function _show_method(io::IO, template::ProblemTemplate, backend::Symbol; kwargs...) table = [ "Network Model" string(get_network_formulation(template.network_model)) "Slacks" get_use_slacks(template.network_model) "PTDF" !isnothing(get_PTDF_matrix(template.network_model)) "Duals" isempty(get_duals(template.network_model)) ? "None" : string.(get_duals(template.network_model)) "HVDC Network Model" isnothing(get_hvdc_network_model(template.network_model)) ? "None" : replace(string(get_hvdc_network_model(template.network_model)), r"[()]" => "") ] PrettyTables.pretty_table( io, table; backend = Val(backend), show_header = false, title = "Network Model", alignment = :l, kwargs..., ) println(io) header = ["Device Type", "Formulation", "Slacks"] table = Matrix{String}(undef, length(template.devices), length(header)) for (ix, model) in enumerate(values(template.devices)) table[ix, 1] = string(get_component_type(model)) table[ix, 2] = string(get_formulation(model)) table[ix, 3] = string(model.use_slacks) end PrettyTables.pretty_table( io, table; backend = Val(backend), header = header, title = "Device Models", alignment = :l, ) if !isempty(template.branches) println(io) header = ["Branch Type", "Formulation", "Slacks"] table = Matrix{String}(undef, length(template.branches), length(header)) for (ix, model) in enumerate(values(template.branches)) table[ix, 1] = string(get_component_type(model)) table[ix, 2] = string(get_formulation(model)) table[ix, 3] = string(model.use_slacks) end PrettyTables.pretty_table( io, table; header = header, backend = Val(backend), title = "Branch Models", alignment = :l, kwargs..., ) end if !isempty(template.services) println(io) if isempty(first(keys(template.services))[1]) header = ["Service Type", "Formulation", "Slacks", "Aggregated Model"] else header = ["Name", "Service Type", "Formulation", "Slacks", "Aggregated Model"] end table = Matrix{String}(undef, length(template.services), length(header)) for (ix, (key, model)) in enumerate(template.services) if isempty(key[1]) table[ix, 1] = string(get_component_type(model)) table[ix, 2] = string(get_formulation(model)) table[ix, 3] = string(model.use_slacks) table[ix, 4] = string(get(model.attributes, "aggregated_service_model", "false")) else table[ix, 1] = key[1] table[ix, 2] = string(get_component_type(model)) table[ix, 3] = string(get_formulation(model)) table[ix, 4] = string(model.use_slacks) table[ix, 5] = string(get(model.attributes, "aggregated_service_model", "false")) end end PrettyTables.pretty_table( io, table; header = header, backend = Val(backend), title = "Service Models", alignment = :l, kwargs..., ) end return end function Base.show(io::IO, ::MIME"text/plain", input::SimulationModels) _show_method(io, input, :auto) end function Base.show(io::IO, ::MIME"text/html", input::SimulationModels) _show_method(io, input, :html; standalone = false, tf = PrettyTables.tf_html_simple) end _get_model_type(::DecisionModel{T}) where {T <: DecisionProblem} = T _get_model_type(::EmulationModel{T}) where {T <: EmulationProblem} = T function _show_method(io::IO, sim_models::SimulationModels, backend::Symbol; kwargs...) println(io) header = ["Model Name", "Model Type", "Status", "Output Directory"] table = Matrix{Any}(undef, length(sim_models.decision_models), length(header)) for (ix, model) in enumerate(sim_models.decision_models) table[ix, 1] = string(get_name(model)) table[ix, 2] = IS.strip_module_name(string(_get_model_type(model))) table[ix, 3] = string(get_status(model)) table[ix, 4] = get_output_dir(model) end PrettyTables.pretty_table( io, table; header = header, backend = Val(backend), title = "Decision Models", alignment = :l, kwargs..., ) if !isnothing(sim_models.emulation_model) println(io) table = Matrix{Any}(undef, 1, length(header)) table[1, 1] = string(get_name(sim_models.emulation_model)) table[1, 2] = IS.strip_module_name(string(_get_model_type(sim_models.emulation_model))) table[1, 3] = string(get_status(sim_models.emulation_model)) table[1, 4] = get_output_dir(sim_models.emulation_model) PrettyTables.pretty_table( io, table; header = header, backend = Val(backend), title = "Emulator Models", alignment = :l, kwargs..., ) else println(io) println(io, "No Emulator Model Specified") end end function Base.show(io::IO, ::MIME"text/plain", input::SimulationSequence) _show_method(io, input, :auto) end function Base.show(io::IO, ::MIME"text/html", input::SimulationSequence) _show_method(io, input, :html; standalone = false, tf = PrettyTables.tf_html_simple) end function _show_method(io::IO, sequence::SimulationSequence, backend::Symbol; kwargs...) println(io) table = [ "Simulation Step Interval" Dates.Hour(get_step_resolution(sequence)) "Number of Problems" length(sequence.executions_by_model) ] PrettyTables.pretty_table( io, table; backend = Val(backend), show_header = false, title = "Simulation Sequence", alignment = :l, kwargs..., ) println(io) header = ["Model Name", "Horizon", "Interval", "Executions Per Step"] 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] = Dates.canonicalize(sequence.horizons[model]) table[ix, 3] = Dates.canonicalize(sequence.intervals[model]) table[ix, 4] = executions end PrettyTables.pretty_table( io, table; header = header, backend = Val(backend), title = "Simulation Problems", alignment = :l, ) if !isempty(sequence.feedforwards) println(io) header = ["Model Name", "Feed Forward Type"] table = Matrix{Any}(undef, length(sequence.feedforwards), length(header)) for (ix, (k, ff)) in enumerate(sequence.feedforwards) table[ix, 1] = k table[ix, 2] = join(string.(typeof.(ff)), " ") end PrettyTables.pretty_table( io, table; header = header, backend = Val(backend), title = "Feedforwards", alignment = :l, kwargs..., ) end end function Base.show(io::IO, ::MIME"text/plain", input::Simulation) _show_method(io, input, :auto) end function Base.show(io::IO, ::MIME"text/html", input::Simulation) _show_method(io, input, :html; standalone = false, tf = PrettyTables.tf_html_simple) end function _get_initial_time_for_show(sim::Simulation) ini_time = get_initial_time(sim) if isnothing(ini_time) return "Unset Initial Time" else return string(ini_time) end end function _get_build_status_for_show(sim::Simulation) internal = sim.internal if isnothing(internal) return "EMPTY" else return string(internal.build_status) end end function _get_run_status_for_show(sim::Simulation) internal = sim.internal if isnothing(internal) return "NOT_READY" else return string(internal.status) end end function _show_method(io::IO, sim::Simulation, backend::Symbol; kwargs...) table = [ "Simulation Name" get_name(sim) "Build Status" _get_build_status_for_show(sim) "Run Status" _get_run_status_for_show(sim) "Initial Time" _get_initial_time_for_show(sim) "Steps" get_steps(sim) ] PrettyTables.pretty_table( io, table; backend = Val(backend), show_header = false, title = "Simulation", alignment = :l, kwargs..., ) _show_method(io, sim.models, backend; kwargs...) _show_method(io, sim.sequence, backend; kwargs...) end function Base.show(io::IO, ::MIME"text/plain", input::SimulationResults) _show_method(io, input, :auto) end function Base.show(io::IO, ::MIME"text/html", input::SimulationResults) _show_method(io, input, :html; standalone = false, tf = PrettyTables.tf_html_simple) end function _show_method(io::IO, results::SimulationResults, backend::Symbol; kwargs...) header = ["Problem Name", "Initial Time", "Resolution", "Last Solution Timestamp"] table = Matrix{Any}(undef, length(results.decision_problem_results), length(header)) for (ix, (key, result)) in enumerate(results.decision_problem_results) table[ix, 1] = key table[ix, 2] = first(result.timestamps) table[ix, 3] = Dates.canonicalize(result.resolution) table[ix, 4] = last(result.timestamps) end println(io) PrettyTables.pretty_table( io, table; header = header, backend = Val(backend), title = "Decision Problem Results", alignment = :l, ) println(io) table = [ "Name" results.emulation_problem_results.problem "Resolution" Dates.Minute(results.emulation_problem_results.resolution) "Number of steps" length(results.emulation_problem_results.timestamps) ] PrettyTables.pretty_table( io, table; show_header = false, backend = Val(backend), title = "Emulator Results", alignment = :l, kwargs..., ) end ProblemResultsTypes = Union{OptimizationProblemResults, SimulationProblemResults} function Base.show(io::IO, ::MIME"text/plain", input::ProblemResultsTypes) _show_method(io, input, :auto) end function Base.show(io::IO, ::MIME"text/html", input::ProblemResultsTypes) _show_method(io, input, :html; standalone = false, tf = PrettyTables.tf_html_simple) end function _show_method( io::IO, results::T, backend::Symbol; kwargs..., ) where {T <: ProblemResultsTypes} timestamps = get_timestamps(results) if backend == :html println(io, "

Start: $(first(timestamps))

") println(io, "

End: $(last(timestamps))

") println( io, "

Resolution: $(Dates.Minute(ISOPT.get_resolution(results)))

", ) else println(io, "Start: $(first(timestamps))") println(io, "End: $(last(timestamps))") println(io, "Resolution: $(Dates.Minute(ISOPT.get_resolution(results)))") end values = Dict{String, Vector{String}}( "Variables" => list_variable_names(results), "Auxiliary variables" => list_aux_variable_names(results), "Duals" => list_dual_names(results), "Expressions" => list_expression_names(results), "Parameters" => list_parameter_names(results), ) if hasfield(T, :problem) name = results.problem else name = "PowerSimulations" end for (k, val) in values if !isempty(val) println(io) PrettyTables.pretty_table( io, val; show_header = false, backend = Val(backend), title = "$name Problem $k Results", alignment = :l, kwargs..., ) end end end function Base.show(io::IO, ::MIME"text/plain", bounds::ConstraintBounds) println(io, "ConstraintBounds:") println(io, "Constraint Coefficient") show(io, MIME"text/plain"(), bounds.coefficient) println(io, "Constraint RHS") show(io, MIME"text/plain"(), bounds.rhs) end function Base.show(io::IO, ::MIME"text/plain", bounds::VariableBounds) println(io, "VariableBounds:") show(io, MIME"text/plain"(), bounds.bounds) end function Base.show(io::IO, ::MIME"text/plain", bounds::NumericalBounds) println(io, rpad(" Minimum", 20), "Maximum") println(io, rpad(" $(bounds.min)", 20), "$(bounds.max)") end ================================================ FILE: src/utils/print_pt_v3.jl ================================================ function Base.show(io::IO, container::OptimizationContainer) show(io, get_jump_model(container)) end function Base.show(io::IO, ::MIME"text/plain", input::Union{ServiceModel, DeviceModel}) _show_method(io, input, :auto) end function Base.show(io::IO, ::MIME"text/html", input::Union{ServiceModel, DeviceModel}) # The tf_html_simple format was eliminated from PrettyTables and it was added to PowerSystems _show_method(io, input, :html; stand_alone = false, table_format = PSY.tf_html_simple) end function _show_method( io::IO, model::Union{ServiceModel, DeviceModel}, backend::Symbol; kwargs..., ) println(io) header = ["Device Type", "Formulation", "Slacks"] table = Matrix{String}(undef, 1, length(header)) table[1, 1] = string(get_component_type(model)) table[1, 2] = string(get_formulation(model)) table[1, 3] = string(model.use_slacks) PrettyTables.pretty_table( io, table; column_labels = header, backend = backend, title = "Device Model", alignment = :l, kwargs..., ) if !isempty(model.attributes) println(io) header = ["Name", "Value"] table = Matrix{String}(undef, length(model.attributes), length(header)) for (ix, (k, v)) in enumerate(model.attributes) table[ix, 1] = string(k) table[ix, 2] = string(v) end PrettyTables.pretty_table( io, table; column_labels = header, backend = backend, title = "Attributes", alignment = :l, kwargs..., ) end if !isempty(model.time_series_names) println(io) header = ["Parameter Name", "Time Series Name"] table = Matrix{String}(undef, length(model.time_series_names), length(header)) for (ix, (k, v)) in enumerate(model.time_series_names) table[ix, 1] = string(k) table[ix, 2] = string(v) end PrettyTables.pretty_table( io, table; column_labels = header, backend = backend, title = "Time Series Names", alignment = :l, kwargs..., ) end if !isempty(model.duals) println(io) table = string.(model.duals) PrettyTables.pretty_table( io, table; show_column_labels = false, backend = backend, title = "Duals", alignment = :l, kwargs..., ) end if !isempty(model.feedforwards) println(io) header = ["Type", "Source", "Affected Values"] table = Matrix{String}(undef, length(model.feedforwards), length(header)) for (ix, v) in enumerate(model.feedforwards) table[ix, 1] = string(typeof(v)) table[ix, 2] = encode_key_as_string(get_optimization_container_key(v)) table[ix, 3] = first(encode_key_as_string.(get_affected_values(v))) end PrettyTables.pretty_table( io, table; column_labels = header, backend = backend, title = "Feedforwards", alignment = :l, kwargs..., ) else println(io) print(io, "No FeedForwards Assigned") end end function Base.show(io::IO, ::MIME"text/plain", input::NetworkModel) _show_method(io, input, :auto) end function Base.show(io::IO, ::MIME"text/html", input::NetworkModel) # The tf_html_simple format was eliminated from PrettyTables and it was added to PowerSystems _show_method(io, input, :html; stand_alone = false, table_format = PSY.tf_html_simple) end function _show_method(io::IO, network_model::NetworkModel, backend::Symbol; kwargs...) table = [ "Network Model" string(get_network_formulation(network_model)) "Slacks" get_use_slacks(network_model) "PTDF" !isnothing(get_PTDF_matrix(network_model)) "Duals" join(string.(get_duals(network_model)), " ") ] PrettyTables.pretty_table( io, table; backend = backend, column_labels = ["Field", "Value"], title = "Network Model", alignment = :l, kwargs..., ) return end function Base.show(io::IO, ::MIME"text/plain", input::OperationModel) _show_method(io, input, :auto) end function Base.show(io::IO, ::MIME"text/html", input::OperationModel) # The tf_html_simple format was eliminated from PrettyTables and it was added to PowerSystems _show_method(io, input, :html; stand_alone = false, table_format = PSY.tf_html_simple) end function _show_method(io::IO, model::OperationModel, backend::Symbol; kwargs...) _show_method(io, model.template, backend; kwargs...) end function Base.show(io::IO, ::MIME"text/plain", input::ProblemTemplate) _show_method(io, input, :auto) end function Base.show(io::IO, ::MIME"text/html", input::ProblemTemplate) # The tf_html_simple format was eliminated from PrettyTables and it was added to PowerSystems _show_method(io, input, :html; stand_alone = false, table_format = PSY.tf_html_simple) end function _show_method(io::IO, template::ProblemTemplate, backend::Symbol; kwargs...) table = [ "Network Model" string(get_network_formulation(template.network_model)) "Slacks" get_use_slacks(template.network_model) "PTDF" !isnothing(get_PTDF_matrix(template.network_model)) "Duals" isempty(get_duals(template.network_model)) ? "None" : string.(get_duals(template.network_model)) "HVDC Network Model" isnothing(get_hvdc_network_model(template.network_model)) ? "None" : replace(string(get_hvdc_network_model(template.network_model)), r"[()]" => "") ] PrettyTables.pretty_table( io, table; backend = backend, show_column_labels = false, title = "Network Model", alignment = :l, kwargs..., ) println(io) header = ["Device Type", "Formulation", "Slacks"] table = Matrix{String}(undef, length(template.devices), length(header)) for (ix, model) in enumerate(values(template.devices)) table[ix, 1] = string(get_component_type(model)) table[ix, 2] = string(get_formulation(model)) table[ix, 3] = string(model.use_slacks) end PrettyTables.pretty_table( io, table; backend = backend, column_labels = header, title = "Device Models", alignment = :l, ) if !isempty(template.branches) println(io) header = ["Branch Type", "Formulation", "Slacks"] table = Matrix{String}(undef, length(template.branches), length(header)) for (ix, model) in enumerate(values(template.branches)) table[ix, 1] = string(get_component_type(model)) table[ix, 2] = string(get_formulation(model)) table[ix, 3] = string(model.use_slacks) end PrettyTables.pretty_table( io, table; column_labels = header, backend = backend, title = "Branch Models", alignment = :l, kwargs..., ) end if !isempty(template.services) println(io) if isempty(first(keys(template.services))[1]) header = ["Service Type", "Formulation", "Slacks", "Aggregated Model"] else header = ["Name", "Service Type", "Formulation", "Slacks", "Aggregated Model"] end table = Matrix{String}(undef, length(template.services), length(header)) for (ix, (key, model)) in enumerate(template.services) if isempty(key[1]) table[ix, 1] = string(get_component_type(model)) table[ix, 2] = string(get_formulation(model)) table[ix, 3] = string(model.use_slacks) table[ix, 4] = string(get(model.attributes, "aggregated_service_model", "false")) else table[ix, 1] = key[1] table[ix, 2] = string(get_component_type(model)) table[ix, 3] = string(get_formulation(model)) table[ix, 4] = string(model.use_slacks) table[ix, 5] = string(get(model.attributes, "aggregated_service_model", "false")) end end PrettyTables.pretty_table( io, table; column_labels = header, backend = backend, title = "Service Models", alignment = :l, kwargs..., ) end return end function Base.show(io::IO, ::MIME"text/plain", input::SimulationModels) _show_method(io, input, :auto) end function Base.show(io::IO, ::MIME"text/html", input::SimulationModels) # The tf_html_simple format was eliminated from PrettyTables and it was added to PowerSystems _show_method(io, input, :html; stand_alone = false, table_format = PSY.tf_html_simple) end _get_model_type(::DecisionModel{T}) where {T <: DecisionProblem} = T _get_model_type(::EmulationModel{T}) where {T <: EmulationProblem} = T function _show_method(io::IO, sim_models::SimulationModels, backend::Symbol; kwargs...) println(io) header = ["Model Name", "Model Type", "Status", "Output Directory"] table = Matrix{Any}(undef, length(sim_models.decision_models), length(header)) for (ix, model) in enumerate(sim_models.decision_models) table[ix, 1] = string(get_name(model)) table[ix, 2] = IS.strip_module_name(string(_get_model_type(model))) table[ix, 3] = string(get_status(model)) table[ix, 4] = get_output_dir(model) end PrettyTables.pretty_table( io, table; column_labels = header, backend = backend, title = "Decision Models", alignment = :l, kwargs..., ) if !isnothing(sim_models.emulation_model) println(io) table = Matrix{Any}(undef, 1, length(header)) table[1, 1] = string(get_name(sim_models.emulation_model)) table[1, 2] = IS.strip_module_name(string(_get_model_type(sim_models.emulation_model))) table[1, 3] = string(get_status(sim_models.emulation_model)) table[1, 4] = get_output_dir(sim_models.emulation_model) PrettyTables.pretty_table( io, table; column_labels = header, backend = backend, title = "Emulator Models", alignment = :l, kwargs..., ) else println(io) println(io, "No Emulator Model Specified") end end function Base.show(io::IO, ::MIME"text/plain", input::SimulationSequence) _show_method(io, input, :auto) end function Base.show(io::IO, ::MIME"text/html", input::SimulationSequence) # The tf_html_simple format was eliminated from PrettyTables and it was added to PowerSystems _show_method(io, input, :html; stand_alone = false, table_format = PSY.tf_html_simple) end function _show_method(io::IO, sequence::SimulationSequence, backend::Symbol; kwargs...) println(io) table = [ "Simulation Step Interval" Dates.Hour(get_step_resolution(sequence)) "Number of Problems" length(sequence.executions_by_model) ] PrettyTables.pretty_table( io, table; backend = backend, show_column_labels = false, title = "Simulation Sequence", alignment = :l, kwargs..., ) println(io) header = ["Model Name", "Horizon", "Interval", "Executions Per Step"] 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] = Dates.canonicalize(sequence.horizons[model]) table[ix, 3] = Dates.canonicalize(sequence.intervals[model]) table[ix, 4] = executions end PrettyTables.pretty_table( io, table; column_labels = header, backend = backend, title = "Simulation Problems", alignment = :l, ) if !isempty(sequence.feedforwards) println(io) header = ["Model Name", "Feed Forward Type"] table = Matrix{Any}(undef, length(sequence.feedforwards), length(header)) for (ix, (k, ff)) in enumerate(sequence.feedforwards) table[ix, 1] = k table[ix, 2] = join(string.(typeof.(ff)), " ") end PrettyTables.pretty_table( io, table; column_labels = header, backend = backend, title = "Feedforwards", alignment = :l, kwargs..., ) end end function Base.show(io::IO, ::MIME"text/plain", input::Simulation) _show_method(io, input, :auto) end function Base.show(io::IO, ::MIME"text/html", input::Simulation) # The tf_html_simple format was eliminated from PrettyTables and it was added to PowerSystems _show_method(io, input, :html; stand_alone = false, table_format = PSY.tf_html_simple) end function _get_initial_time_for_show(sim::Simulation) ini_time = get_initial_time(sim) if isnothing(ini_time) return "Unset Initial Time" else return string(ini_time) end end function _get_build_status_for_show(sim::Simulation) internal = sim.internal if isnothing(internal) return "EMPTY" else return string(internal.build_status) end end function _get_run_status_for_show(sim::Simulation) internal = sim.internal if isnothing(internal) return "NOT_READY" else return string(internal.status) end end function _show_method(io::IO, sim::Simulation, backend::Symbol; kwargs...) table = [ "Simulation Name" get_name(sim) "Build Status" _get_build_status_for_show(sim) "Run Status" _get_run_status_for_show(sim) "Initial Time" _get_initial_time_for_show(sim) "Steps" get_steps(sim) ] PrettyTables.pretty_table( io, table; backend = backend, show_column_labels = false, title = "Simulation", alignment = :l, kwargs..., ) _show_method(io, sim.models, backend; kwargs...) _show_method(io, sim.sequence, backend; kwargs...) end function Base.show(io::IO, ::MIME"text/plain", input::SimulationResults) _show_method(io, input, :auto) end function Base.show(io::IO, ::MIME"text/html", input::SimulationResults) # The tf_html_simple format was eliminated from PrettyTables and it was added to PowerSystems _show_method(io, input, :html; stand_alone = false, table_format = PSY.tf_html_simple) end function _show_method(io::IO, results::SimulationResults, backend::Symbol; kwargs...) header = ["Problem Name", "Initial Time", "Resolution", "Last Solution Timestamp"] table = Matrix{Any}(undef, length(results.decision_problem_results), length(header)) for (ix, (key, result)) in enumerate(results.decision_problem_results) table[ix, 1] = key table[ix, 2] = first(result.timestamps) table[ix, 3] = Dates.canonicalize(result.resolution) table[ix, 4] = last(result.timestamps) end println(io) PrettyTables.pretty_table( io, table; column_labels = header, backend = backend, title = "Decision Problem Results", alignment = :l, ) println(io) table = [ "Name" results.emulation_problem_results.problem "Resolution" Dates.Minute(results.emulation_problem_results.resolution) "Number of steps" length(results.emulation_problem_results.timestamps) ] PrettyTables.pretty_table( io, table; show_column_labels = false, backend = backend, title = "Emulator Results", alignment = :l, kwargs..., ) end ProblemResultsTypes = Union{OptimizationProblemResults, SimulationProblemResults} function Base.show(io::IO, ::MIME"text/plain", input::ProblemResultsTypes) _show_method(io, input, :auto) end function Base.show(io::IO, ::MIME"text/html", input::ProblemResultsTypes) # The tf_html_simple format was eliminated from PrettyTables and it was added to PowerSystems _show_method(io, input, :html; stand_alone = false, table_format = PSY.tf_html_simple) end function _show_method( io::IO, results::T, backend::Symbol; kwargs..., ) where {T <: ProblemResultsTypes} timestamps = get_timestamps(results) if backend == :html println(io, "

Start: $(first(timestamps))

") println(io, "

End: $(last(timestamps))

") println( io, "

Resolution: $(Dates.Minute(ISOPT.get_resolution(results)))

", ) else println(io, "Start: $(first(timestamps))") println(io, "End: $(last(timestamps))") println(io, "Resolution: $(Dates.Minute(ISOPT.get_resolution(results)))") end values = Dict{String, Vector{String}}( "Variables" => list_variable_names(results), "Auxiliary variables" => list_aux_variable_names(results), "Duals" => list_dual_names(results), "Expressions" => list_expression_names(results), "Parameters" => list_parameter_names(results), ) if hasfield(T, :problem) name = results.problem else name = "PowerSimulations" end for (k, val) in values if !isempty(val) println(io) PrettyTables.pretty_table( io, val; show_column_labels = false, backend = backend, title = "$name Problem $k Results", alignment = :l, kwargs..., ) end end end function Base.show(io::IO, ::MIME"text/plain", bounds::ConstraintBounds) println(io, "ConstraintBounds:") println(io, "Constraint Coefficient") show(io, MIME"text/plain"(), bounds.coefficient) println(io, "Constraint RHS") show(io, MIME"text/plain"(), bounds.rhs) end function Base.show(io::IO, ::MIME"text/plain", bounds::VariableBounds) println(io, "VariableBounds:") show(io, MIME"text/plain"(), bounds.bounds) end function Base.show(io::IO, ::MIME"text/plain", bounds::NumericalBounds) println(io, rpad(" Minimum", 20), "Maximum") println(io, rpad(" $(bounds.min)", 20), "$(bounds.max)") end ================================================ FILE: src/utils/recorder_events.jl ================================================ """ All events subtyped from this need to be recorded under :simulation_status. """ abstract type AbstractSimulationStatusEvent <: IS.AbstractRecorderEvent end struct SimulationStepEvent <: AbstractSimulationStatusEvent common::IS.RecorderEventCommon simulation_time::Dates.DateTime step::Int status::String end function SimulationStepEvent( simulation_time::Dates.DateTime, step::Int, status::AbstractString, ) return SimulationStepEvent( IS.RecorderEventCommon("SimulationStepEvent"), simulation_time, step, status, ) end struct ProblemExecutionEvent <: AbstractSimulationStatusEvent common::IS.RecorderEventCommon simulation_time::Dates.DateTime step::Int model_name::Symbol status::String end function ProblemExecutionEvent( simulation_time::Dates.DateTime, step::Int, model_name::Symbol, status::AbstractString, ) return ProblemExecutionEvent( IS.RecorderEventCommon("ProblemExecutionEvent"), simulation_time, step, model_name, status, ) end struct InitialConditionUpdateEvent <: IS.AbstractRecorderEvent common::IS.RecorderEventCommon simulation_time::Dates.DateTime initial_condition_type::String component_type::String device_name::String new_value::Float64 previous_value::Float64 model_name::String end function InitialConditionUpdateEvent( simulation_time, ic::InitialCondition, previous_value::Union{Nothing, Float64}, model_name::Symbol, ) return InitialConditionUpdateEvent( IS.RecorderEventCommon("InitialConditionUpdateEvent"), simulation_time, string(get_ic_type(ic)), string(get_component_type(ic)), get_component_name(ic), isnothing(get_condition(ic)) ? 1e8 : get_condition(ic), isnothing(previous_value) ? 1e8 : previous_value, string(model_name), ) end struct ParameterUpdateEvent <: IS.AbstractRecorderEvent common::IS.RecorderEventCommon simulation_time::Dates.DateTime parameter_type::String component_type::String tag::String model_name::String end function ParameterUpdateEvent( parameter_type::Type{<:ParameterType}, component_type::DataType, tag::String, simulation_time::Dates.DateTime, model_name::Symbol, ) return ParameterUpdateEvent( IS.RecorderEventCommon("ParameterUpdateEvent"), simulation_time, string(parameter_type), string(component_type), tag, string(model_name), ) end function ParameterUpdateEvent( parameter_type::Type{<:ParameterType}, component_type::DataType, attributes::TimeSeriesAttributes, simulation_time::Dates.DateTime, model_name::Symbol, ) return ParameterUpdateEvent( parameter_type, component_type, attributes.name, simulation_time, model_name, ) end function ParameterUpdateEvent( parameter_type::Type{<:ParameterType}, component_type::DataType, attributes::EventParametersAttributes, simulation_time::Dates.DateTime, model_name::Symbol, ) return ParameterUpdateEvent( parameter_type, component_type, "outage - event", simulation_time, model_name, ) end function ParameterUpdateEvent( parameter_type::Type{<:ParameterType}, component_type::DataType, attributes::VariableValueAttributes, simulation_time::Dates.DateTime, model_name::Symbol, ) return ParameterUpdateEvent( parameter_type, component_type, # TODO: Store as string in the attributes to avoid interpolations encode_key_as_string(get_attribute_key(attributes)), simulation_time, model_name, ) end function ParameterUpdateEvent( parameter_type::Type{<:ParameterType}, component_type::DataType, attributes::CostFunctionAttributes, simulation_time::Dates.DateTime, model_name::Symbol, ) return ParameterUpdateEvent( parameter_type, component_type, # TODO: Store as string in the attributes to avoid interpolations string(get_variable_types(attributes)), simulation_time, model_name, ) end struct StateUpdateEvent <: IS.AbstractRecorderEvent common::IS.RecorderEventCommon simulation_time::Dates.DateTime model_name::String state_type::String end function StateUpdateEvent(simulation_time::Dates.DateTime, model_name, state_type::String) return StateUpdateEvent( IS.RecorderEventCommon("StateUpdateEvent"), simulation_time, string(model_name), state_type, ) end function get_simulation_step_range(filename::AbstractString, step::Int) events = IS.list_recorder_events(SimulationStepEvent, filename, x -> x.step == step) if length(events) != 2 throw( ArgumentError( "$filename does not have two SimulationStepEvents for step = $step", ), ) end if events[1].status != "start" || events[2].status != "done" throw( ArgumentError( "$filename does not contain start and done events for step = $step", ), ) end return (start = events[1].simulation_time, done = events[2].simulation_time) end function get_simulation_model_range(filename::AbstractString, step::Int, model::String) events = IS.list_recorder_events( ProblemExecutionEvent, filename, x -> x.step == step && x.model_name == Symbol(model), ) if length(events) != 2 throw( ArgumentError( "$filename does not have two ProblemExecutionEvent for step = $step model = $model", ), ) end if events[1].status != "start" || events[2].status != "done" throw( ArgumentError( "$filename does not contain start and done events for step = $step model = $model", ), ) end return (start = events[1].simulation_time, done = events[2].simulation_time) end function _filter_by_type_range!(events::Vector{<:IS.AbstractRecorderEvent}, time_range) return filter!( x -> x.simulation_time >= time_range.start && x.simulation_time <= time_range.done, events, ) end function _get_recorder_filename(output_dir, recorder_name) return joinpath(output_dir, "recorder", recorder_name * ".log") end function _get_simulation_status_recorder_filename(output_dir) return _get_recorder_filename(output_dir, "simulation_status") end function _get_simulation_recorder_filename(output_dir) return _get_recorder_filename(output_dir, "execution") end """ list_simulation_events( ::Type{T}, output_dir::AbstractString, filter_func::Union{Nothing, Function} = nothing; step = nothing, model = nothing, ) where {T <: IS.AbstractRecorderEvent} List simulation events of type T in a simulation output directory. # Arguments - `output_dir::AbstractString`: Simulation output directory - `filter_func::Union{Nothing, Function} = nothing`: Refer to [`show_simulation_events`](@ref). - `step::Int = nothing`: Filter events by step. Required if model is passed. - `model::Int = nothing`: Filter events by model. """ function list_simulation_events( ::Type{T}, output_dir::AbstractString, filter_func::Union{Nothing, Function} = nothing; step = nothing, model_name::Union{String, Nothing} = nothing, ) where {T <: IS.AbstractRecorderEvent} if model_name !== nothing && step === nothing throw(ArgumentError("step is required if model_name is passed")) end recorder_file = _get_simulation_recorder_filename(output_dir) events = IS.list_recorder_events(T, recorder_file, filter_func) if step !== nothing recorder_file = _get_simulation_status_recorder_filename(output_dir) step_range = get_simulation_step_range(recorder_file, step) _filter_by_type_range!(events, step_range) end if model_name !== nothing recorder_file = _get_simulation_status_recorder_filename(output_dir) model_range = get_simulation_model_range(recorder_file, step, model_name) _filter_by_type_range!(events, model_range) end return events end function list_simulation_events( ::Type{T}, output_dir::AbstractString, filter_func::Union{Nothing, Function} = nothing; kwargs..., ) where {T <: AbstractSimulationStatusEvent} recorder_file = _get_simulation_status_recorder_filename(output_dir) return IS.list_recorder_events(T, recorder_file, filter_func) end """ show_simulation_events( ::Type{T}, output_dir::AbstractString, filter_func::Union{Nothing,Function} = nothing; step = nothing, model = nothing, wall_time = false, kwargs..., ) where { T <: IS.AbstractRecorderEvent} Show all simulation events of type T in a simulation output directory. # Arguments - `::Type{T}`: Recorder event type - `output_dir::AbstractString`: Simulation output directory - `filter_func::Union{Nothing, Function} = nothing`: Refer to [`show_recorder_events`](@ref). - `step::Int = nothing`: Filter events by step. Required if model is passed. - `model::Int = nothing`: Filter events by model. - `wall_time = false`: If true, show the wall_time timestamp. """ function show_simulation_events( ::Type{T}, output_dir::AbstractString, filter_func::Union{Nothing, Function} = nothing; step = nothing, model = nothing, wall_time = false, kwargs..., ) where {T <: IS.AbstractRecorderEvent} show_simulation_events( stdout, T, output_dir, filter_func; step = step, model = model, wall_time = wall_time, kwargs..., ) end function show_simulation_events( ::Type{T}, output_dir::AbstractString, filter_func::Union{Nothing, Function} = nothing; wall_time = false, kwargs..., ) where {T <: AbstractSimulationStatusEvent} show_simulation_events( stdout, T, output_dir, filter_func; wall_time = wall_time, kwargs..., ) end function show_simulation_events( io::IO, ::Type{T}, output_dir::AbstractString, filter_func::Union{Nothing, Function} = nothing; wall_time = false, kwargs..., ) where {T <: AbstractSimulationStatusEvent} events = list_simulation_events(T, output_dir, filter_func) show_recorder_events(io, events, filter_func; wall_time = wall_time, kwargs...) end function show_simulation_events( io::IO, ::Type{T}, output_dir::AbstractString, filter_func::Union{Nothing, Function} = nothing; step = nothing, model_name::Union{String, Nothing} = nothing, wall_time = false, kwargs..., ) where {T <: IS.AbstractRecorderEvent} events = list_simulation_events( T, output_dir, filter_func; step = step, model_name = model_name, ) show_recorder_events(io, events, filter_func; wall_time = wall_time, kwargs...) end """ show_recorder_events( ::Type{T}, filename::AbstractString, filter_func::Union{Nothing, Function} = nothing; wall_time = false, kwargs..., ) where {T <: IS.AbstractRecorderEvent} Show the events of type T in a recorder file. # Arguments - `::Type{T}`: Recorder event type - `filename::AbstractString`: recorder filename - `filter_func::Union{Nothing, Function} = nothing`: Optional function that accepts an event of type T and returns a Bool. Apply this function to each event and only return events where the result is true. - `wall_time = false`: If true, show the wall_time timestamp. """ function show_recorder_events( ::Type{T}, filename::AbstractString, filter_func::Union{Nothing, Function} = nothing; wall_time = false, kwargs..., ) where {T <: IS.AbstractRecorderEvent} show_recorder_events(stdout, T, filename, filter_func; wall_time = wall_time, kwargs...) end function show_recorder_events( io::IO, ::Type{T}, filename::AbstractString, filter_func::Union{Nothing, Function} = nothing; wall_time = false, kwargs..., ) where {T <: IS.AbstractRecorderEvent} if wall_time IS.show_recorder_events(io, T, filename, filter_func) else # This will not display the first column, 'timestamp'. # Passign filters_col No longer supported in PrettyTables # f_c(data, i) = i > 1 IS.show_recorder_events(io, T, filename, filter_func; kwargs...) end end function show_recorder_events( io::IO, events::Vector{T}, filter_func::Union{Nothing, Function} = nothing; wall_time = false, kwargs..., ) where {T <: IS.AbstractRecorderEvent} if wall_time IS.show_recorder_events(io, events; kwargs...) else # This will not display the first column, 'timestamp'. # Passign filters_col No longer supported in PrettyTables #f_c(data, i) = i > 1 IS.show_recorder_events(io, events; kwargs...) end end ================================================ FILE: src/utils/time_series_utils.jl ================================================ apply_maybe_across_time_series(fn::Function, ts_data::AbstractVector) = fn.(ts_data) apply_maybe_across_time_series(fn::Function, ts_data::TimeSeries.TimeArray) = fn.(values(ts_data)) apply_maybe_across_time_series(fn::Function, ts_data::AbstractDict) = apply_maybe_across_time_series.(Ref(fn), values(ts_data)) apply_maybe_across_time_series(fn::Function, ts_data::IS.TimeSeriesData) = apply_maybe_across_time_series(fn, PSY.get_data(ts_data)) """ Helper function to look up a time series if necessary then apply a function (typically a validation routine in a `do` block) to every element in it """ apply_maybe_across_time_series( fn::Function, component::PSY.Component, ts_key::IS.TimeSeriesKey, ) = apply_maybe_across_time_series(fn, PSY.get_time_series(component, ts_key)) # case where the element isn't a time series apply_maybe_across_time_series(fn::Function, ::PSY.Component, elem) = fn(elem) # success case _validate_eltype_helper(::Type{T}, element::T) where {T} = true # failure case _validate_eltype_helper(_, _) = false """ Validate that the eltype of the time series, or the field itself if it's not a time series, is of the type given """ _validate_eltype( ::Type{T}, component::PSY.Component, ts_key::IS.TimeSeriesKey, msg = "", ) where {T} = apply_maybe_across_time_series(component, ts_key) do x result = _validate_eltype_helper(T, x) result || throw( ArgumentError( "Expected element type $T but got $(typeof(x)) in time series $(get_name(ts_key)) for $(get_name(component))" * msg, ), ) end function _validate_eltype(::Type{T}, component::PSY.Component, element, msg = "") where {T} component_name = get_name(component) result = _validate_eltype_helper(T, element) result || throw( ArgumentError( "Expected element type $T but got $(typeof(element)) for $(get_name(component))" * msg, ), ) end ================================================ FILE: test/Project.toml ================================================ [deps] Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595" CSV = "336ed68f-0bac-5ca0-87d4-7b16caf5d00b" DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0" DataFramesMeta = "1313f7d8-7da2-5740-9ea0-a2ca25f37964" DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8" Dates = "ade2ca70-3891-5945-98fb-dc099432e06a" HiGHS = "87dc4568-4c63-4d18-b0c0-bb2238e4078b" HydroPowerSimulations = "fc1677e0-6ad7-4515-bf3a-bd6bf20a0b1b" InfrastructureSystems = "2cd47ed4-ca9b-11e9-27f2-ab636a7671f1" Ipopt = "b6b21f68-93f8-5de0-b562-5493be1d77c9" JSON3 = "0f8b85d8-7281-11e9-16c2-39a750bddbf1" JuMP = "4076af6c-e467-56ae-b986-b466b2749572" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" Logging = "56ddb016-857b-54e1-b83d-db4d58db5568" Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" PowerFlows = "94fada2c-fd9a-4e89-8d82-81405f5cb4f6" PowerModels = "c36e90e8-916a-50a6-bd94-075b64ef4655" PowerNetworkMatrices = "bed98974-b02a-5e2f-9fe0-a103f5c450dd" PowerSimulations = "e690365d-45e2-57bb-ac84-44ba829e73c4" PowerSystemCaseBuilder = "f00506e0-b84f-492a-93c2-c0a9afc4364e" PowerSystems = "bcd98974-b02a-5e2f-9ee0-a103f5c450dd" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" Revise = "295af30f-e4ad-537b-8983-00126c2a3abe" SCS = "c946c3f1-0d1f-5ce8-9dea-7daa1f7e2d13" Serialization = "9e88b42a-f829-5b0c-bbe9-9e923198166b" StorageSystemsSimulations = "e2f1a126-19d0-4674-9252-42b2384f8e3c" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" TestSetExtensions = "98d24dd4-01ad-11ea-1b02-c9a08f80db04" TimeSeries = "9e3dc215-6440-5c97-bce1-76c03772f85e" TimerOutputs = "a759f4b9-e2f1-59dc-863e-4aeb61b1ea8f" UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4" [compat] HiGHS = "1" Ipopt = "=1.4.0" julia = "^1.6" ================================================ FILE: test/includes.jl ================================================ # SIIP Packages using PowerSimulations using PowerSystems using PowerSystemCaseBuilder using InfrastructureSystems using PowerNetworkMatrices using HydroPowerSimulations import PowerSystemCaseBuilder: PSITestSystems using PowerNetworkMatrices using StorageSystemsSimulations using PowerFlows using DataFramesMeta # Test Packages using Test using Logging # Dependencies for testing import PowerModels as PM using DataFrames using DataFramesMeta using Dates using JuMP import JuMP.Containers: DenseAxisArray, SparseAxisArray using TimeSeries using CSV import JSON3 using DataStructures import UUIDs using Random import Serialization import LinearAlgebra import PowerSystems as PSY import PowerSimulations as PSI import PowerNetworkMatrices as PNM import InfrastructureSystems as IS const PFS = PowerFlows const PSB = PowerSystemCaseBuilder const ISOPT = IS.Optimization 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") include("test_utils/run_simulation.jl") include("test_utils/add_components_to_system.jl") include("test_utils/add_dlr_ts.jl") include("test_utils/add_market_bid_cost.jl") include("test_utils/mbc_system_utils.jl") include("test_utils/mbc_simulation_utils.jl") include("test_utils/events_simulation_utils.jl") include("test_utils/iec_simulation_utils.jl") ENV["RUNNING_PSI_TESTS"] = "true" ENV["SIENNA_RANDOM_SEED"] = 1234 # Set a fixed seed for reproducibility in tests ================================================ FILE: test/performance/performance_test.jl ================================================ precompile_time = @timed using PowerSimulations using PowerSimulations import PowerSimulations as PSI using PowerSystems import PowerSystems as PSY import InfrastructureSystems as IS using Logging using PowerSystemCaseBuilder using PowerNetworkMatrices using HydroPowerSimulations using HiGHS using Dates using PowerFlows @info pkgdir(PowerSimulations) function is_running_on_ci() return get(ENV, "CI", "false") == "true" || haskey(ENV, "GITHUB_ACTIONS") end open("precompile_time.txt", "a") do io if length(ARGS) == 0 && !is_running_on_ci() push!(ARGS, "Local Test") end write(io, "| $(ARGS[1]) | $(precompile_time.time) |\n") end function set_device_models!(template::ProblemTemplate, uc::Bool = true) if uc set_device_model!(template, ThermalMultiStart, ThermalStandardUnitCommitment) set_device_model!(template, ThermalStandard, ThermalStandardUnitCommitment) set_device_model!(template, HydroDispatch, FixedOutput) else set_device_model!(template, ThermalMultiStart, ThermalBasicDispatch) set_device_model!(template, ThermalStandard, ThermalBasicDispatch) set_device_model!(template, HydroDispatch, HydroDispatchRunOfRiver) end set_device_model!(template, RenewableDispatch, RenewableFullDispatch) set_device_model!(template, PowerLoad, StaticPowerLoad) set_device_model!(template, DeviceModel(Line, StaticBranch)) set_device_model!(template, Transformer2W, StaticBranchUnbounded) set_device_model!(template, TapTransformer, StaticBranchUnbounded) set_service_model!( template, ServiceModel(VariableReserve{ReserveUp}, RangeReserve), ) set_service_model!( template, ServiceModel(VariableReserve{ReserveDown}, RangeReserve), ) return template end # Copy every SingleTimeSeries attached to components of `src_sys` onto the # same-named component in `dst_sys`. Components not present in `dst_sys` are # skipped silently. Requires that `dst_sys` already has its own storage. function copy_single_time_series_across_systems!(dst_sys::System, src_sys::System) for src_component in IS.iterate_components_with_time_series( src_sys.data; time_series_type = PSY.SingleTimeSeries, ) dst_component = PSY.get_component(typeof(src_component), dst_sys, PSY.get_name(src_component)) dst_component === nothing && continue for ts_metadata in IS.get_time_series_metadata( src_component; time_series_type = PSY.SingleTimeSeries, ) ts = PSY.get_time_series( PSY.SingleTimeSeries, src_component, PSY.get_name(ts_metadata); resolution = PSY.get_resolution(ts_metadata), ) PSY.add_time_series!(dst_sys, dst_component, ts) end end return dst_sys end try # Build both systems, then merge the 5-minute SingleTimeSeries from the # realization system onto the DA system so a single System carries raw # 1-hour and 5-minute data that can be transformed separately per model. sys_rts_da = build_system(PSISystems, "modified_RTS_GMLC_DA_sys") sys_rts_realization = build_system(PSISystems, "modified_RTS_GMLC_realization_sys") copy_single_time_series_across_systems!(sys_rts_da, sys_rts_realization) # Drop the transform that PSB pre-baked so we can attach new per-resolution # transforms and leave both static series intact. PSY.transform_single_time_series!( sys_rts_da, Hour(48), Hour(24); resolution = Hour(1), delete_existing = true, ) PSY.transform_single_time_series!( sys_rts_da, Hour(1), Minute(15); resolution = Minute(5), delete_existing = false, ) for g in get_components(ThermalStandard, sys_rts_da) get_name(g) == "121_NUCLEAR_1" && set_must_run!(g, true) end for i in 1:2 template_uc = ProblemTemplate( NetworkModel( PTDFPowerModel; use_slacks = true, duals = [CopperPlateBalanceConstraint], power_flow_evaluation = DCPowerFlow(), ), ) set_device_models!(template_uc) template_ed = ProblemTemplate( NetworkModel( PTDFPowerModel; use_slacks = true, duals = [CopperPlateBalanceConstraint], power_flow_evaluation = DCPowerFlow(), ), ) set_device_models!(template_ed, false) template_em = ProblemTemplate( NetworkModel( PTDFPowerModel; use_slacks = true, duals = [CopperPlateBalanceConstraint], ), ) set_device_models!(template_em, false) empty!(template_em.services) models = SimulationModels(; decision_models = [ DecisionModel( template_uc, sys_rts_da; name = "UC", optimizer = optimizer_with_attributes(HiGHS.Optimizer, "mip_rel_gap" => 0.01), initialize_model = true, optimizer_solve_log_print = false, direct_mode_optimizer = true, check_numerical_bounds = false, horizon = Hour(48), interval = Hour(24), resolution = Hour(1), ), DecisionModel( template_ed, sys_rts_da; name = "ED", optimizer = optimizer_with_attributes(HiGHS.Optimizer, "mip_rel_gap" => 0.01), initialize_model = true, check_numerical_bounds = false, horizon = Hour(1), interval = Minute(15), resolution = Minute(5), ), ], emulation_model = EmulationModel( template_em, sys_rts_da; name = "PF", optimizer = optimizer_with_attributes(HiGHS.Optimizer), resolution = Minute(5), ), ) sequence = SimulationSequence(; models = models, feedforwards = Dict( "ED" => [ SemiContinuousFeedforward(; component_type = ThermalStandard, source = OnVariable, affected_values = [ActivePowerVariable], ), ], "PF" => [ SemiContinuousFeedforward(; component_type = ThermalStandard, source = OnVariable, affected_values = [ActivePowerVariable], ), ], ), ini_cond_chronology = InterProblemChronology(), ) sim = Simulation(; name = "single_system_sim", steps = 3, models = models, sequence = sequence, initial_time = DateTime("2020-01-01T00:00:00"), simulation_folder = mktempdir(; cleanup = true), ) build_out, time_build, _, _ = @timed build!(sim; console_level = Logging.Error) name = i > 1 ? "Postcompile" : "Precompile" if build_out == PSI.SimulationBuildStatus.BUILT open("build_time.txt", "a") do io write(io, "| $(ARGS[1])-Build Time $name | $(time_build) |\n") end else open("build_time.txt", "a") do io 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 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) open("build_time.txt", "a") do io write(io, "| $(ARGS[1])- Build Time | FAILED TO TEST |\n") end end if !is_running_on_ci() for file in ["precompile_time.txt", "build_time.txt", "solve_time.txt"] name = replace(file, "_" => " ")[begin:(end - 4)] println("$name:") for line in eachline(open(file)) println("\t", line) end end end ================================================ FILE: test/run_partitioned_simulation.jl ================================================ using PowerSimulations using PowerSystems using PowerSystemCaseBuilder using InfrastructureSystems using PowerNetworkMatrices using Logging using Test import PowerModels as PM using DataFrames using Dates using JuMP using TimeSeries using CSV using DataFrames using DataStructures import UUIDs using Random import Serialization import PowerSystems as PSY import PowerSimulations as PSI import InfrastructureSystems as IS const PSB = PowerSystemCaseBuilder const BASE_DIR = string(dirname(dirname(pathof(PowerSimulations)))) const DATA_DIR = joinpath(BASE_DIR, "test/test_data") include(joinpath(BASE_DIR, "test/test_utils/solver_definitions.jl")) # avoid redefinition of functions and constants when running on CI if get(ENV, "CI", nothing) != "true" include(joinpath(BASE_DIR, "test/test_utils/common_operation_model.jl")) include(joinpath(BASE_DIR, "test/test_utils/model_checks.jl")) include(joinpath(BASE_DIR, "test/test_utils/mock_operation_models.jl")) include(joinpath(BASE_DIR, "test/test_utils/operations_problem_templates.jl")) end function build_simulation( output_dir::AbstractString, simulation_name::AbstractString, partitions::Union{Nothing, SimulationPartitions} = nothing, index::Union{Nothing, Integer} = nothing; initial_time = nothing, num_steps = nothing, HiGHS_optimizer = HiGHS_optimizer, ) if isnothing(partitions) && isnothing(num_steps) error("num_steps must be set if partitions is nothing") end if !isnothing(partitions) && !isnothing(num_steps) 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, 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, Hour(1), Hour(1)) for sys in [c_sys5_pjm_da, c_sys5_pjm_rt] th = get_component(ThermalStandard, sys, "Park City") set_active_power_limits!(th, (min = 0.1, max = 1.7)) set_status!(th, false) set_active_power!(th, 0.0) c = get_operation_cost(th) 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") set_time_limits!(th, (up = 5, down = 1)) set_active_power_limits!(th, (min = 0.05, max = 0.4)) set_active_power!(th, 0.05) c = get_operation_cost(th) 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) 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) 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") set_active_power_limits!(th, (min = 1.0, max = 5.2)) set_ramp_limits!(th, (up = 0.0052, down = 0.0052)) set_active_power!(th, 2.0) c = get_operation_cost(th) PSY.set_start_up!(c, 3000.0) PSY.set_shut_down!(c, 1500.0) PSY.set_must_run!(th, true) set_status!(th, true) end to_json( c_sys5_pjm_da, joinpath(output_dir, "PSI-5-BUS-UC-ED/c_sys5_pjm_da.json"); force = true, ) to_json( c_sys5_pjm_rt, joinpath(output_dir, "PSI-5-BUS-UC-ED/c_sys5_pjm_rt.json"); force = true, ) template_uc = template_unit_commitment() set_network_model!( template_uc, NetworkModel( PTDFPowerModel; ), ) set_device_model!(template_uc, ThermalStandard, ThermalStandardUnitCommitment) template_ed = deepcopy(template_uc) # template_ed.network_model.use_slacks = true set_device_model!(template_ed, ThermalStandard, ThermalBasicDispatch) models = SimulationModels(; decision_models = [ DecisionModel( template_uc, c_sys5_pjm_da; optimizer = HiGHS_optimizer, name = "UC", initialize_model = true, ), DecisionModel( template_ed, c_sys5_pjm_rt; optimizer = HiGHS_optimizer, name = "ED", calculate_conflict = true, initialize_model = true, ), ], ) sequence = SimulationSequence(; models = models, feedforwards = Dict( "ED" => [ SemiContinuousFeedforward(; component_type = ThermalStandard, source = OnVariable, affected_values = [ActivePowerVariable], ), ], ), ini_cond_chronology = InterProblemChronology(), ) sim = Simulation(; name = simulation_name, steps = isnothing(partitions) ? num_steps : partitions.num_steps, models = models, sequence = sequence, simulation_folder = output_dir, initial_time = initial_time, ) status = build!(sim; partitions = partitions, index = index) if status != PSI.SimulationBuildStatus.BUILT error("Failed to build simulation: status=$status") end return sim end function execute_simulation(sim, args...; kwargs...) return execute!(sim) end ================================================ FILE: test/runtests.jl ================================================ include("includes.jl") # Code Quality Tests import Aqua Aqua.test_undefined_exports(PowerSimulations) Aqua.test_ambiguities(PowerSimulations) Aqua.test_stale_deps(PowerSimulations) Aqua.find_persistent_tasks_deps(PowerSimulations) Aqua.test_persistent_tasks(PowerSimulations) Aqua.test_unbound_args(PowerSimulations) const LOG_FILE = "power-simulations-test.log" const DISABLED_TEST_FILES = [ # Can generate with ls -1 test | grep "test_.*.jl" # "test_basic_model_structs.jl", # "test_device_branch_constructors.jl", # "test_device_hvdc.jl", # "test_device_lcc.jl", # "test_device_load_constructors.jl", # "test_device_renewable_generation_constructors.jl", # "test_device_source_constructors.jl", # "test_device_thermal_generation_constructors.jl", # "test_events.jl", # "test_formulation_combinations.jl", # "test_import_export_cost.jl", # "test_initialization_problem.jl", # "test_jump_utils.jl", # "test_market_bid_cost.jl", # "test_mbc_sanity_check.jl", # "test_model_decision.jl", # "test_model_emulation.jl", # "test_network_constructors.jl", # "test_network_constructors_with_dlr.jl", # "test_power_flow_in_the_loop.jl", # "test_print.jl", # "test_problem_template.jl", # "test_recorder_events.jl", # "test_services_constructor.jl", # "test_simulation_build.jl", # "test_simulation_execute.jl", # "test_simulation_models.jl", # "test_simulation_partitions.jl", # "test_simulation_results_export.jl", # "test_simulation_results.jl", # "test_simulation_sequence.jl", # "test_simulation_store.jl", # "test_utils.jl", ] LOG_LEVELS = Dict( "Debug" => Logging.Debug, "Info" => Logging.Info, "Warn" => Logging.Warn, "Error" => Logging.Error, ) function get_logging_level(env_name::String, default) level = get(ENV, env_name, default) log_level = get(LOG_LEVELS, level, nothing) if log_level === nothing error("Invalid log level $level: Supported levels: $(values(LOG_LEVELS))") end return log_level end """ Includes the given test files, given as a list without their ".jl" extensions. If none are given it will scan the directory of the calling file and include all the julia files. """ macro includetests(testarg...) if length(testarg) == 0 tests = [] elseif length(testarg) == 1 tests = testarg[1] else error("@includetests takes zero or one argument") end quote tests = $tests rootfile = @__FILE__ if length(tests) == 0 tests = readdir(dirname(rootfile)) tests = filter( f -> startswith(f, "test_") && endswith(f, ".jl") && f != basename(rootfile), tests, ) else tests = map(f -> string(f, ".jl"), tests) end println() if !isempty(DISABLED_TEST_FILES) @warn("Some tests are disabled $DISABLED_TEST_FILES") end for test in tests test ∈ DISABLED_TEST_FILES && continue print(splitext(test)[1], ": ") include(test) println() end end end function get_logging_level_from_env(env_name::String, default) level = get(ENV, env_name, default) return IS.get_logging_level(level) end function run_tests() logging_config_filename = get(ENV, "SIIP_LOGGING_CONFIG", nothing) if logging_config_filename !== nothing config = IS.LoggingConfiguration(logging_config_filename) else config = IS.LoggingConfiguration(; filename = LOG_FILE, file_level = Logging.Info, console_level = Logging.Error, ) end console_logger = ConsoleLogger(config.console_stream, config.console_level) IS.open_file_logger(LOG_FILE, config.file_level) do file_logger levels = (Logging.Info, Logging.Warn, Logging.Error) multi_logger = IS.MultiLogger([console_logger, file_logger], IS.LogEventTracker(levels)) global_logger(multi_logger) if !isempty(config.group_levels) IS.set_group_levels!(multi_logger, config.group_levels) end @time @testset "Begin PowerSimulations tests" begin @includetests ARGS end @test length(IS.get_log_events(multi_logger.tracker, Logging.Error)) == 0 @info IS.report_log_summary(multi_logger) end end logger = global_logger() try run_tests() finally # Guarantee that the global logger is reset. global_logger(logger) nothing end ================================================ FILE: test/test_basic_model_structs.jl ================================================ @testset "DeviceModel Tests" begin @test_throws ArgumentError DeviceModel(ThermalGen, ThermalStandardUnitCommitment) @test_throws ArgumentError DeviceModel(ThermalStandard, PSI.AbstractThermalFormulation) @test_throws ArgumentError NetworkModel(PM.AbstractPowerModel) end @testset "NetworkModel Tests" begin @test_throws ArgumentError NetworkModel(PM.AbstractPowerModel) @test NetworkModel( PTDFPowerModel; use_slacks = true, power_flow_evaluation = [DCPowerFlow(), PSSEExportPowerFlow(:v33, "exports")], ) isa NetworkModel @test NetworkModel( PTDFPowerModel; use_slacks = true, power_flow_evaluation = ACPowerFlow(; exporter = PSSEExportPowerFlow( :v33, "exports"; name = "my_export_name", write_comments = true, overwrite = true, ), ), ) isa NetworkModel end @testset "validate_template dispatch Tests" begin struct CustomDecisionProblem <: PSI.DecisionProblem end struct CustomEmulationProblem <: PSI.EmulationProblem end sys = PSB.build_system(PSITestSystems, "c_sys5") template = ProblemTemplate(CopperPlatePowerModel) # DecisionModel has no inner constructor, so use the default field constructor decision_model = DecisionModel{CustomDecisionProblem}( :test, template, sys, nothing, PSI.SimulationInfo(), PSI.DecisionModelStore(), Dict{String, Any}(), ) @test_throws ErrorException PSI.validate_template(decision_model) # EmulationModel has an inner constructor; build with settings then test settings = PSI.Settings(sys) emulation_model = EmulationModel{CustomEmulationProblem}( deepcopy(template), sys, settings, nothing, ) @test_throws ErrorException PSI.validate_template(emulation_model) end @testset "Feedforward Struct Tests" begin ffs = [ UpperBoundFeedforward(; component_type = RenewableDispatch, source = ActivePowerVariable, affected_values = [ActivePowerVariable], add_slacks = true, ), LowerBoundFeedforward(; component_type = RenewableDispatch, source = ActivePowerVariable, affected_values = [ActivePowerVariable], add_slacks = true, ), SemiContinuousFeedforward(; component_type = ThermalMultiStart, source = OnVariable, affected_values = [ActivePowerVariable, ReactivePowerVariable], ), ] for ff in ffs for av in PSI.get_affected_values(ff) @test isa(av, PSI.VariableKey) end end ff = FixValueFeedforward(; component_type = HydroDispatch, source = OnVariable, affected_values = [OnStatusParameter], ) for av in PSI.get_affected_values(ff) @test isa(av, PSI.ParameterKey) end @test_throws ErrorException UpperBoundFeedforward( component_type = RenewableDispatch, source = ActivePowerVariable, affected_values = [OnStatusParameter], add_slacks = true, ) @test_throws ErrorException LowerBoundFeedforward( component_type = RenewableDispatch, source = ActivePowerVariable, affected_values = [OnStatusParameter], add_slacks = true, ) @test_throws ErrorException SemiContinuousFeedforward( component_type = ThermalMultiStart, source = OnVariable, affected_values = [ActivePowerVariable, OnStatusParameter], ) end ================================================ FILE: test/test_data/results_export.json ================================================ { "models": [ { "name": "ED", "variables": [ "ActivePowerVariable__ThermalStandard" ], "store_all_parameters": true, "optimizer_stats": true }, { "name": "UC", "variables": [ "OnVariable__ThermalStandard" ], "store_all_duals": true, "store_all_parameters": true, "store_all_aux_variables": true, "optimizer_stats": true } ], "start_time": "2020-01-01T04:00:00", "end_time": null, "path": "export_path", "format": "csv" } ================================================ FILE: test/test_device_branch_constructors.jl ================================================ @testset "DC Power Flow Models Monitored Line Flow Constraints and Static Unbounded" begin system = PSB.build_system(PSITestSystems, "c_sys5_ml") limits = PSY.get_flow_limits(PSY.get_component(MonitoredLine, system, "1")) for model in [DCPPowerModel, PTDFPowerModel] template = get_thermal_dispatch_template_network( NetworkModel(model; PTDF_matrix = PTDF(system)), ) model_m = DecisionModel(template, system; optimizer = HiGHS_optimizer) @test build!(model_m; output_dir = mktempdir(; cleanup = true)) == PSI.ModelBuildStatus.BUILT @test check_variable_bounded(model_m, FlowActivePowerVariable, MonitoredLine) @test solve!(model_m) == PSI.RunStatus.SUCCESSFULLY_FINALIZED @test check_flow_variable_values( model_m, FlowActivePowerVariable, MonitoredLine, "1", limits.from_to, ) end end @testset "DC Power Flow Models MonitoredLine Asymmetric Flow Limits" begin system = PSB.build_system(PSITestSystems, "c_sys5_ml") ml = PSY.get_component(MonitoredLine, system, "1") rating = PSY.get_rating(ml) # Set asymmetric flow limits where from_to < to_from (and both < rating) asymmetric_limits = (from_to = rating * 0.5, to_from = rating * 0.8) PSY.set_flow_limits!(ml, asymmetric_limits) expected_limit = min(rating, asymmetric_limits.from_to, asymmetric_limits.to_from) @test expected_limit == asymmetric_limits.from_to # Directly verify get_min_max_limits returns the minimum of the two flow limits and rating minmax = PSI.get_min_max_limits(ml, FlowRateConstraint, StaticBranch) @test minmax.max ≈ expected_limit atol = 1e-6 @test minmax.min ≈ -expected_limit atol = 1e-6 for net_model in [DCPPowerModel, PTDFPowerModel] template = get_thermal_dispatch_template_network( NetworkModel(net_model; PTDF_matrix = PTDF(system)), ) set_device_model!(template, DeviceModel(MonitoredLine, StaticBranch)) model_m = DecisionModel(template, system; optimizer = HiGHS_optimizer) @test build!(model_m; output_dir = mktempdir(; cleanup = true)) == PSI.ModelBuildStatus.BUILT @test solve!(model_m) == PSI.RunStatus.SUCCESSFULLY_FINALIZED @test check_flow_variable_values( model_m, FlowActivePowerVariable, MonitoredLine, "1", expected_limit, ) end end @testset "AC Power Flow Monitored Line Flow Constraints" begin system = PSB.build_system(PSITestSystems, "c_sys5_ml") 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.ModelBuildStatus.BUILT @test check_variable_bounded(model_m, FlowActivePowerFromToVariable, MonitoredLine) @test check_variable_unbounded(model_m, FlowReactivePowerFromToVariable, MonitoredLine) @test solve!(model_m) == PSI.RunStatus.SUCCESSFULLY_FINALIZED @test check_flow_variable_values( model_m, FlowActivePowerFromToVariable, FlowReactivePowerFromToVariable, MonitoredLine, "1", 0.0, limits.from_to, ) end @testset "DC Power Flow Models Monitored Line Flow Constraints and Static with inequalities" begin system = PSB.build_system(PSITestSystems, "c_sys5_ml") 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)), ) set_device_model!(template, DeviceModel(Line, StaticBranch)) 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.ModelBuildStatus.BUILT @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_rating!(PSY.get_component(Line, system, "2"), 1.5) for model in [DCPPowerModel, PTDFPowerModel] template = get_thermal_dispatch_template_network(NetworkModel(model)) set_device_model!(template, DeviceModel(Line, StaticBranchBounds)) 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.ModelBuildStatus.BUILT @test check_variable_bounded(model_m, FlowActivePowerVariable, Line) @test solve!(model_m) == PSI.RunStatus.SUCCESSFULLY_FINALIZED @test check_flow_variable_values(model_m, FlowActivePowerVariable, Line, "2", 1.5) end # Test the addition of slacks template = get_thermal_dispatch_template_network(NetworkModel(PTDFPowerModel)) 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 = HiGHS_optimizer) @test build!(model_m; output_dir = mktempdir(; cleanup = true)) == PSI.ModelBuildStatus.BUILT @test check_variable_bounded(model_m, FlowActivePowerVariable, Line) @test check_variable_bounded(model_m, FlowActivePowerVariable, MonitoredLine) @test !check_variable_bounded(model_m, FlowActivePowerSlackLowerBound, Line) @test !check_variable_bounded(model_m, FlowActivePowerSlackUpperBound, Line) @test !check_variable_bounded(model_m, FlowActivePowerSlackLowerBound, MonitoredLine) @test !check_variable_bounded(model_m, FlowActivePowerSlackUpperBound, MonitoredLine) @test solve!(model_m) == PSI.RunStatus.SUCCESSFULLY_FINALIZED end @testset "DC Power Flow Models for TwoTerminalGenericHVDCLine with with Line Flow Constraints, TapTransformer & Transformer2W Unbounded" begin ratelimit_constraint_keys = [ PSI.ConstraintKey(FlowRateConstraint, Transformer2W, "ub"), PSI.ConstraintKey(FlowRateConstraint, Transformer2W, "lb"), PSI.ConstraintKey(FlowRateConstraint, TapTransformer, "ub"), PSI.ConstraintKey(FlowRateConstraint, TapTransformer, "lb"), ] system = PSB.build_system(PSITestSystems, "c_sys14_dc") hvdc_line = PSY.get_component(TwoTerminalGenericHVDCLine, system, "DCLine3") limits_from = PSY.get_active_power_limits_from(hvdc_line) limits_to = PSY.get_active_power_limits_to(hvdc_line) limits_min = min(limits_from.min, limits_to.min) limits_max = min(limits_from.max, limits_to.max) tap_transformer = PSY.get_component(TapTransformer, system, "Trans3") rate_limit = PSY.get_rating(tap_transformer) transformer = PSY.get_component(Transformer2W, system, "Trans4") rate_limit2w = PSY.get_rating(tap_transformer) for model in [DCPPowerModel, PTDFPowerModel] template = get_template_dispatch_with_network( NetworkModel(model), ) set_device_model!(template, TwoTerminalGenericHVDCLine, 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.ModelBuildStatus.BUILT psi_constraint_test(model_m, ratelimit_constraint_keys) @test solve!(model_m) == PSI.RunStatus.SUCCESSFULLY_FINALIZED @test check_flow_variable_values( model_m, FlowActivePowerVariable, TwoTerminalGenericHVDCLine, "DCLine3", limits_min, limits_max, ) @test check_flow_variable_values( model_m, FlowActivePowerVariable, TapTransformer, "Trans3", -rate_limit, rate_limit, ) @test check_flow_variable_values( model_m, FlowActivePowerVariable, Transformer2W, "Trans4", -rate_limit2w, rate_limit2w, ) end end @testset "DC Power Flow Models for Unbounded TwoTerminalGenericHVDCLine , and StaticBranchBounds for TapTransformer & Transformer2W" begin system = PSB.build_system(PSITestSystems, "c_sys14_dc") hvdc_line = PSY.get_component(TwoTerminalGenericHVDCLine, system, "DCLine3") limits_from = PSY.get_active_power_limits_from(hvdc_line) limits_to = PSY.get_active_power_limits_to(hvdc_line) limits_min = min(limits_from.min, limits_to.min) limits_max = min(limits_from.max, limits_to.max) tap_transformer = PSY.get_component(TapTransformer, system, "Trans3") rate_limit = PSY.get_rating(tap_transformer) transformer = PSY.get_component(Transformer2W, system, "Trans4") rate_limit2w = PSY.get_rating(tap_transformer) for model in [DCPPowerModel, PTDFPowerModel] template = get_template_dispatch_with_network( NetworkModel(model; PTDF_matrix = PTDF(system)), ) set_device_model!( template, DeviceModel(TwoTerminalGenericHVDCLine, HVDCTwoTerminalUnbounded), ) set_device_model!(template, DeviceModel(TapTransformer, StaticBranchBounds)) 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.ModelBuildStatus.BUILT @test check_variable_unbounded( model_m, FlowActivePowerVariable, TwoTerminalGenericHVDCLine, ) @test check_variable_bounded(model_m, FlowActivePowerVariable, TapTransformer) @test check_variable_bounded(model_m, FlowActivePowerVariable, TapTransformer) @test solve!(model_m) == PSI.RunStatus.SUCCESSFULLY_FINALIZED @test check_flow_variable_values( model_m, FlowActivePowerVariable, TwoTerminalGenericHVDCLine, "DCLine3", limits_min, limits_max, ) @test check_flow_variable_values( model_m, FlowActivePowerVariable, TapTransformer, "Trans3", -rate_limit, rate_limit, ) @test check_flow_variable_values( model_m, FlowActivePowerVariable, Transformer2W, "Trans4", -rate_limit2w, rate_limit2w, ) end end @testset "HVDCTwoTerminalLossless values check between network models" begin # Test to compare lossless models with lossless formulation sys_5 = build_system(PSITestSystems, "c_sys5_uc") line = get_component(Line, sys_5, "1") remove_component!(sys_5, line) hvdc = TwoTerminalGenericHVDCLine(; name = get_name(line), available = true, active_power_flow = 0.0, # Force the flow in the opposite direction for testing purposes active_power_limits_from = (min = -0.5, max = -0.5), active_power_limits_to = (min = -3.0, max = 2.0), reactive_power_limits_from = (min = -1.0, max = 1.0), reactive_power_limits_to = (min = -1.0, max = 1.0), arc = get_arc(line), loss = LinearCurve(0.0), ) add_component!(sys_5, hvdc) template_uc = ProblemTemplate( NetworkModel(PTDFPowerModel), ) 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)) set_device_model!( template_uc, DeviceModel(TwoTerminalGenericHVDCLine, HVDCTwoTerminalLossless), ) model = DecisionModel( template_uc, sys_5; name = "UC", optimizer = HiGHS_optimizer, ) build!(model; output_dir = mktempdir()) solve!(model) ptdf_vars = read_variables(OptimizationProblemResults(model); table_format = TableFormat.WIDE) ptdf_values = ptdf_vars["FlowActivePowerVariable__TwoTerminalGenericHVDCLine"] ptdf_objective = PSI.get_optimization_container(model).optimizer_stats.objective_value set_network_model!(template_uc, NetworkModel(DCPPowerModel)) model = DecisionModel( template_uc, sys_5; name = "UC", optimizer = HiGHS_optimizer, ) solve!(model; output_dir = mktempdir()) dcp_vars = read_variables(OptimizationProblemResults(model); table_format = TableFormat.WIDE) dcp_values = dcp_vars["FlowActivePowerVariable__TwoTerminalGenericHVDCLine"] 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 @test all(isapprox.(ptdf_values[!, "1"], dcp_values[!, "1"]; atol = 10)) end @testset "HVDCDispatch Model Tests" begin # Test to compare lossless models with lossless formulation sys_5 = build_system(PSITestSystems, "c_sys5_uc") # Revert to previous rating before data change to prevent different optimal solutions for the lossless model and lossless formulation: PSY.set_rating!(PSY.get_component(PSY.Line, sys_5, "6"), 2.0) line = get_component(Line, sys_5, "1") remove_component!(sys_5, line) hvdc = TwoTerminalGenericHVDCLine(; name = get_name(line), available = true, active_power_flow = 0.0, # Force the flow in the opposite direction for testing purposes active_power_limits_from = (min = -2.0, max = 2.0), active_power_limits_to = (min = -2.0, max = 2.0), reactive_power_limits_from = (min = -1.0, max = 1.0), reactive_power_limits_to = (min = -1.0, max = 1.0), arc = get_arc(line), loss = LinearCurve(0.0), ) add_component!(sys_5, hvdc) for net_model in [DCPPowerModel, PTDFPowerModel] @testset "$net_model" begin PSY.set_loss!(hvdc, PSY.LinearCurve(0.0)) template_uc = ProblemTemplate( NetworkModel(net_model; use_slacks = true), ) 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, StaticBranchBounds)) set_device_model!( template_uc, DeviceModel(TwoTerminalGenericHVDCLine, HVDCTwoTerminalLossless), ) model_ref = DecisionModel( template_uc, sys_5; name = "UC", optimizer = HiGHS_optimizer, store_variable_names = true, ) solve!(model_ref; output_dir = mktempdir()) ref_vars = read_variables( OptimizationProblemResults(model_ref); table_format = TableFormat.WIDE, ) ref_values = ref_vars["FlowActivePowerVariable__Line"] hvdc_ref_values = ref_vars["FlowActivePowerVariable__TwoTerminalGenericHVDCLine"] ref_objective = model_ref.internal.container.optimizer_stats.objective_value ref_total_gen = sum( sum.( eachrow( DataFrames.select( ref_vars["ActivePowerVariable__ThermalStandard"], Not(:DateTime), ), ) ), ) set_device_model!( template_uc, DeviceModel(TwoTerminalGenericHVDCLine, HVDCTwoTerminalDispatch), ) model = DecisionModel( template_uc, sys_5; name = "UC", optimizer = HiGHS_optimizer, ) solve!(model; output_dir = mktempdir()) no_loss_vars = read_variables( OptimizationProblemResults(model); table_format = TableFormat.WIDE, ) no_loss_values = no_loss_vars["FlowActivePowerVariable__Line"] hvdc_ft_no_loss_values = no_loss_vars["FlowActivePowerFromToVariable__TwoTerminalGenericHVDCLine"] hvdc_tf_no_loss_values = no_loss_vars["FlowActivePowerToFromVariable__TwoTerminalGenericHVDCLine"] no_loss_objective = PSI.get_optimization_container(model).optimizer_stats.objective_value no_loss_total_gen = sum( sum.( eachrow( DataFrames.select( no_loss_vars["ActivePowerVariable__ThermalStandard"], Not(:DateTime), ), ), ), ) @test isapprox(no_loss_objective, ref_objective; atol = 0.1) for col in names(ref_values) test_result = all(isapprox.(ref_values[!, col], no_loss_values[!, col]; atol = 0.1)) @test test_result test_result || break end @test all( isapprox.( hvdc_ft_no_loss_values[!, "1"], -hvdc_tf_no_loss_values[!, "1"]; atol = 1e-3, ), ) @test isapprox(no_loss_total_gen, ref_total_gen; atol = 0.1) PSY.set_loss!(hvdc, PSY.LinearCurve(0.005, 0.1)) model_wl = DecisionModel( template_uc, sys_5; name = "UC", optimizer = HiGHS_optimizer, ) solve!(model_wl; output_dir = mktempdir()) dispatch_vars = read_variables( OptimizationProblemResults(model_wl); table_format = TableFormat.WIDE, ) dispatch_values_ft = dispatch_vars["FlowActivePowerFromToVariable__TwoTerminalGenericHVDCLine"] dispatch_values_tf = dispatch_vars["FlowActivePowerToFromVariable__TwoTerminalGenericHVDCLine"] wl_total_gen = sum( sum.( eachrow( DataFrames.select( dispatch_vars["ActivePowerVariable__ThermalStandard"], Not(:DateTime), ), ), ), ) dispatch_objective = model_wl.internal.container.optimizer_stats.objective_value # Note: for this test data the system does better by allowing more losses so # the total cost is lower. @test wl_total_gen > no_loss_total_gen for col in names(dispatch_values_tf) test_result = all(dispatch_values_tf[!, col] .<= dispatch_values_ft[!, col]) @test test_result test_result || break end end end end @testset "DC Power Flow Models for TwoTerminalGenericHVDCLine Dispatch and TapTransformer & Transformer2W Unbounded" begin ratelimit_constraint_keys = [ PSI.ConstraintKey(FlowRateConstraint, Transformer2W, "ub"), PSI.ConstraintKey(FlowRateConstraint, Line, "ub"), PSI.ConstraintKey(FlowRateConstraint, Line, "lb"), PSI.ConstraintKey(FlowRateConstraint, TapTransformer, "ub"), PSI.ConstraintKey(FlowRateConstraint, Transformer2W, "lb"), PSI.ConstraintKey(FlowRateConstraint, TapTransformer, "lb"), PSI.ConstraintKey(FlowRateConstraint, TwoTerminalGenericHVDCLine, "ub"), PSI.ConstraintKey(FlowRateConstraint, TwoTerminalGenericHVDCLine, "lb"), ] system = PSB.build_system(PSITestSystems, "c_sys14_dc") hvdc_line = PSY.get_component(TwoTerminalGenericHVDCLine, system, "DCLine3") limits_from = PSY.get_active_power_limits_from(hvdc_line) limits_to = PSY.get_active_power_limits_to(hvdc_line) limits_min = min(limits_from.min, limits_to.min) limits_max = min(limits_from.max, limits_to.max) tap_transformer = PSY.get_component(TapTransformer, system, "Trans3") rate_limit = PSY.get_rating(tap_transformer) transformer = PSY.get_component(Transformer2W, system, "Trans4") rate_limit2w = PSY.get_rating(tap_transformer) template = get_template_dispatch_with_network( NetworkModel(PTDFPowerModel), ) set_device_model!(template, DeviceModel(TapTransformer, StaticBranch)) set_device_model!(template, DeviceModel(Transformer2W, StaticBranch)) set_device_model!( template, DeviceModel(TwoTerminalGenericHVDCLine, HVDCTwoTerminalLossless), ) model_m = DecisionModel(template, system; optimizer = HiGHS_optimizer) @test build!(model_m; output_dir = mktempdir(; cleanup = true)) == PSI.ModelBuildStatus.BUILT psi_constraint_test(model_m, ratelimit_constraint_keys) @test solve!(model_m) == PSI.RunStatus.SUCCESSFULLY_FINALIZED @test check_flow_variable_values( model_m, FlowActivePowerVariable, TwoTerminalGenericHVDCLine, "DCLine3", limits_max, ) @test check_flow_variable_values( model_m, FlowActivePowerVariable, TapTransformer, "Trans3", rate_limit, ) @test check_flow_variable_values( model_m, FlowActivePowerVariable, Transformer2W, "Trans4", rate_limit2w, ) end @testset "DC Power Flow Models for PhaseShiftingTransformer and Line" begin system = build_system(PSITestSystems, "c_sys5_uc") line = get_component(Line, system, "1") remove_component!(system, line) ps = PhaseShiftingTransformer(; name = get_name(line), available = true, active_power_flow = 0.0, reactive_power_flow = 0.0, r = get_r(line), x = get_r(line), primary_shunt = 0.0, tap = 1.0, α = 0.0, rating = get_rating(line), arc = get_arc(line), base_power = get_base_power(system), ) add_component!(system, ps) template = get_template_dispatch_with_network( NetworkModel(PTDFPowerModel; PTDF_matrix = PTDF(system)), ) 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.ModelBuildStatus.BUILT @test check_variable_unbounded( model_m, FlowActivePowerVariable, PhaseShiftingTransformer, ) @test solve!(model_m) == PSI.RunStatus.SUCCESSFULLY_FINALIZED @test check_flow_variable_values( model_m, FlowActivePowerVariable, PhaseShiftingTransformer, "1", get_rating(ps), ) @test check_flow_variable_values( model_m, PhaseShifterAngle, PhaseShiftingTransformer, "1", -π / 2, π / 2, ) end @testset "AC Power Flow Models for TwoTerminalGenericHVDCLine Flow Constraints and TapTransformer & Transformer2W Unbounded" begin ratelimit_constraint_keys = [ PSI.ConstraintKey(FlowRateConstraintFromTo, Transformer2W), PSI.ConstraintKey(FlowRateConstraintToFrom, Transformer2W), PSI.ConstraintKey(FlowRateConstraintFromTo, TapTransformer), PSI.ConstraintKey(FlowRateConstraintToFrom, TapTransformer), PSI.ConstraintKey(FlowRateConstraint, TwoTerminalGenericHVDCLine, "ub"), PSI.ConstraintKey(FlowRateConstraint, TwoTerminalGenericHVDCLine, "lb"), ] system = PSB.build_system(PSITestSystems, "c_sys14_dc") hvdc_line = PSY.get_component(TwoTerminalGenericHVDCLine, system, "DCLine3") limits_from = PSY.get_active_power_limits_from(hvdc_line) limits_to = PSY.get_active_power_limits_to(hvdc_line) limits_min = min(limits_from.min, limits_to.min) limits_max = min(limits_from.max, limits_to.max) tap_transformer = PSY.get_component(TapTransformer, system, "Trans3") rate_limit = PSY.get_rating(tap_transformer) transformer = PSY.get_component(Transformer2W, system, "Trans4") 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(TwoTerminalGenericHVDCLine, HVDCTwoTerminalLossless), ) model_m = DecisionModel(template, system; optimizer = ipopt_optimizer) @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) @test check_variable_unbounded(model_m, FlowReactivePowerToFromVariable, Transformer2W) psi_constraint_test(model_m, ratelimit_constraint_keys) @test solve!(model_m) == PSI.RunStatus.SUCCESSFULLY_FINALIZED @test check_flow_variable_values( model_m, FlowActivePowerVariable, FlowReactivePowerToFromVariable, TwoTerminalGenericHVDCLine, "DCLine3", limits_max, ) @test check_flow_variable_values( model_m, FlowActivePowerFromToVariable, FlowReactivePowerFromToVariable, TapTransformer, "Trans3", rate_limit, ) @test check_flow_variable_values( model_m, FlowActivePowerToFromVariable, FlowReactivePowerToFromVariable, Transformer2W, "Trans4", rate_limit2w, ) end @testset "Test Line and Monitored Line models with slacks" begin system = PSB.build_system(PSITestSystems, "c_sys5_ml") # This rating (0.247479) was previously inferred in PSY.check_component after setting the rating to 0.0 in the tests set_rating!(PSY.get_component(Line, system, "2"), 0.247479) for (model, optimizer) in NETWORKS_FOR_TESTING if model ∈ [PM.SDPWRMPowerModel, 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"; table_format = TableFormat.WIDE, ) # 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.BUILT @test solve!(model_m) == PSI.RunStatus.SUCCESSFULLY_FINALIZED res = OptimizationProblemResults(model_m) vars = read_variable( res, "FlowActivePowerSlackUpperBound__Line"; table_format = TableFormat.WIDE, ) # some relaxations will find a solution with 0.0 slack @test sum(vars[!, "2"]) >= -1e-6 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"; table_format = TableFormat.WIDE, ) # some relaxations will find a solution with 0.0 slack @test sum(vars[!, "2"]) >= -1e-6 end @testset "Three Winding Transformer Test - Basic Setup and Model" begin # Start with the base system system = PSB.build_system(PSITestSystems, "c_sys5_ml") busD = PSY.get_component(ACBus, system, "nodeD") # Create a new bus for the tertiary winding (connected via transformer to Bus 4) new_bus1 = ACBus(; number = 101, name = "Bus3WT_1", available = true, bustype = ACBusTypes.PQ, angle = 0.0, magnitude = 1.0, voltage_limits = (min = 0.95, max = 1.05), base_voltage = 230.0, area = PSY.get_area(busD), load_zone = PSY.get_load_zone(busD), ) PSY.add_component!(system, new_bus1) new_bus2 = ACBus(; number = 102, name = "Bus3WT_2", available = true, bustype = ACBusTypes.PQ, angle = 0.0, magnitude = 1.0, voltage_limits = (min = 0.95, max = 1.05), base_voltage = 230.0, area = PSY.get_area(busD), load_zone = PSY.get_load_zone(busD), ) PSY.add_component!(system, new_bus2) # Add a new load at the new bus new_load = PowerLoad(; name = "Load_Bus3WT", available = true, bus = new_bus1, active_power = 0.5, reactive_power = 0.1, base_power = 100.0, max_active_power = 0.5, max_reactive_power = 0.1, ) PSY.add_component!(system, new_load) # Add a new generator at the new bus to provide power new_gen = ThermalStandard(; name = "Gen_Bus100", available = true, status = true, bus = new_bus2, active_power = 0.4, reactive_power = 0.0, rating = 0.5, prime_mover_type = PrimeMovers.ST, fuel = ThermalFuels.COAL, active_power_limits = (min = 0.0, max = 0.5), reactive_power_limits = (min = -0.3, max = 0.3), ramp_limits = (up = 0.5, down = 0.5), operation_cost = ThermalGenerationCost(; variable = CostCurve(LinearCurve(0.0)), start_up = 0.0, shut_down = 0.0, fixed = 0.0, ), base_power = 100.0, time_limits = nothing, ) PSY.add_component!(system, new_gen) # Create a star bus for the Transformer3W star_bus = ACBus(; number = 103, name = "Star_Bus_T3W", available = true, bustype = ACBusTypes.PQ, angle = 0.0, magnitude = 1.0, voltage_limits = (min = 0.95, max = 1.05), base_voltage = 230.0, area = PSY.get_area(busD), load_zone = PSY.get_load_zone(busD), ) PSY.add_component!(system, star_bus) transformer3w = Transformer3W(; name = "Transformer3W_busD", available = true, primary_star_arc = Arc(; from = busD, to = star_bus), secondary_star_arc = Arc(; from = new_bus1, to = star_bus), tertiary_star_arc = Arc(; from = new_bus2, to = star_bus), star_bus = star_bus, active_power_flow_primary = 0.0, reactive_power_flow_primary = 0.0, active_power_flow_secondary = 0.0, reactive_power_flow_secondary = 0.0, active_power_flow_tertiary = 0.0, reactive_power_flow_tertiary = 0.0, # Star-to-winding impedances r_primary = 0.01, x_primary = 0.1, r_secondary = 0.01, x_secondary = 0.1, r_tertiary = 0.01, x_tertiary = 0.1, # Winding-to-winding impedances r_12 = 0.01, x_12 = 0.1, r_23 = 0.01, x_23 = 0.1, r_13 = 0.01, x_13 = 0.1, # Base powers for each winding pair base_power_12 = 100.0, base_power_23 = 100.0, base_power_13 = 100.0, # Ratings for each winding rating = nothing, rating_primary = 1.0, rating_secondary = 1.0, rating_tertiary = 0.5, ) PSY.add_component!(system, transformer3w) # Add Transformer3W device model when available # Test with DC Power Flow Model for net_model in [DCPPowerModel, PTDFPowerModel] template = get_template_dispatch_with_network( NetworkModel(net_model; PTDF_matrix = PTDF(system)), ) # Set device model for Transformer3W set_device_model!(template, DeviceModel(Transformer3W, StaticBranch)) set_device_model!(template, MonitoredLine, StaticBranch) model_m = DecisionModel(template, system; optimizer = HiGHS_optimizer) @test build!(model_m; output_dir = mktempdir(; cleanup = true)) == PSI.ModelBuildStatus.BUILT @test solve!(model_m) == PSI.RunStatus.SUCCESSFULLY_FINALIZED # Test flow constraints transformer = PSY.get_component(Transformer3W, system, "Transformer3W_busD") @test check_flow_variable_values( model_m, FlowActivePowerVariable, Transformer3W, "Transformer3W_busD_winding_3", PSY.get_rating_tertiary(transformer), ) end template_ac = get_thermal_dispatch_template_network(ACPPowerModel) set_device_model!(template_ac, DeviceModel(Transformer3W, StaticBranch)) model_ac = DecisionModel(template_ac, system; optimizer = ipopt_optimizer) @test build!(model_ac; output_dir = mktempdir(; cleanup = true)) == PSI.ModelBuildStatus.BUILT @test solve!(model_ac) == PSI.RunStatus.SUCCESSFULLY_FINALIZED end ================================================ FILE: test/test_device_hvdc.jl ================================================ @testset "HVDC System Tests" begin sys_5 = build_system(PSISystems, "sys10_pjm_ac_dc") template_uc = ProblemTemplate(NetworkModel( DCPPowerModel, #use_slacks=true, #PTDF_matrix=PTDF(sys_5), #duals=[CopperPlateBalanceConstraint], )) 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)) set_hvdc_network_model!(template_uc, TransportHVDCNetworkModel) model = DecisionModel(template_uc, sys_5; name = "UC", optimizer = HiGHS_optimizer) @test build!(model; output_dir = mktempdir()) == PSI.ModelBuildStatus.BUILT moi_tests(model, 1656, 288, 1248, 528, 888, true) @test solve!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED template_uc = ProblemTemplate(NetworkModel( PTDFPowerModel; #use_slacks=true, PTDF_matrix = PTDF(sys_5), #duals=[CopperPlateBalanceConstraint], )) 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)) set_hvdc_network_model!(template_uc, TransportHVDCNetworkModel) model = DecisionModel(template_uc, sys_5; name = "UC", optimizer = HiGHS_optimizer) @test build!(model; output_dir = mktempdir()) == PSI.ModelBuildStatus.BUILT moi_tests(model, 1128, 0, 1248, 528, 384, true) @test solve!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED end function _generate_test_hvdc_sys() sys = build_system(PSISystems, "sys10_pjm_ac_dc"; force_build = true) th_names_2 = ["Alta-2", "Sundance-2", "Park City-2", "Solitude-2", "Brighton-2"] for th_name in th_names_2 g = PSY.get_component(PSY.ThermalStandard, sys, th_name) op_cost = g.operation_cost val_curve = op_cost.variable.value_curve new_prop_term = get_proportional_term(val_curve) * 2.0 if g.name == "Park City-2" new_prop_term = new_prop_term + 5.0 end new_quad_cost = QuadraticCurve( get_quadratic_term(val_curve), new_prop_term, get_constant_term(val_curve), ) new_op_cost = ThermalGenerationCost( CostCurve( new_quad_cost, op_cost.variable.power_units, op_cost.variable.vom_cost, ), op_cost.fixed, op_cost.start_up, op_cost.shut_down, ) set_operation_cost!(g, new_op_cost) end for ipc in get_components(InterconnectingConverter, sys) new_dc_loss = QuadraticCurve(0.01, 0.01, 0.0) set_loss_function!(ipc, new_dc_loss) set_max_dc_current!(ipc, 2.0) end return sys end @testset "HVDC System with Transport Network" begin sys = _generate_test_hvdc_sys() template = ProblemTemplate() set_device_model!(template, ThermalStandard, ThermalDispatchNoMin) set_device_model!(template, PowerLoad, StaticPowerLoad) set_device_model!(template, TModelHVDCLine, LosslessLine) set_device_model!(template, InterconnectingConverter, LosslessConverter) set_hvdc_network_model!(template, TransportHVDCNetworkModel) model = DecisionModel( template, sys; store_variable_names = true, optimizer = HiGHS_optimizer, ) @test build!(model; output_dir = mktempdir(; cleanup = true)) == PSI.ModelBuildStatus.BUILT @test solve!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED end @testset "HVDC System with Losses Network" begin sys = _generate_test_hvdc_sys() template = ProblemTemplate() set_device_model!(template, ThermalStandard, ThermalDispatchNoMin) set_device_model!(template, PowerLoad, StaticPowerLoad) set_device_model!(template, TModelHVDCLine, DCLossyLine) ipc_model = DeviceModel( InterconnectingConverter, QuadraticLossConverter; attributes = Dict( "voltage_segments" => 3, "current_segments" => 3, "bilinear_segments" => 3, "use_linear_loss" => true, ), ) set_device_model!(template, ipc_model) set_hvdc_network_model!(template, VoltageDispatchHVDCNetworkModel) model = DecisionModel( template, sys; store_variable_names = true, optimizer = HiGHS_optimizer, ) @test build!(model; output_dir = mktempdir(; cleanup = true)) == PSI.ModelBuildStatus.BUILT @test solve!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED end ================================================ FILE: test/test_device_lcc.jl ================================================ @testset "LCC HVDC System Tests" begin sys5 = build_system(PSISystems, "2Area 5 Bus System") hvdc = first(get_components(TwoTerminalGenericHVDCLine, sys5)) lcc = TwoTerminalLCCLine(; name = "lcc", available = true, arc = hvdc.arc, active_power_flow = 0.1, r = 0.000189, transfer_setpoint = -100.0, scheduled_dc_voltage = 7.5, rectifier_bridges = 2, rectifier_delay_angle_limits = (min = 0.31590, max = 1.570), rectifier_rc = 2.6465e-5, rectifier_xc = 0.001092, rectifier_base_voltage = 230.0, inverter_bridges = 2, inverter_extinction_angle_limits = (min = 0.3037, max = 1.57076), inverter_rc = 2.6465e-5, inverter_xc = 0.001072, inverter_base_voltage = 230.0, power_mode = true, switch_mode_voltage = 0.0, compounding_resistance = 0.0, min_compounding_voltage = 0.0, rectifier_transformer_ratio = 0.09772, rectifier_tap_setting = 1.0, rectifier_tap_limits = (min = 1, max = 1), rectifier_tap_step = 0.00624, rectifier_delay_angle = 0.31590, rectifier_capacitor_reactance = 0.1, inverter_transformer_ratio = 0.07134, inverter_tap_setting = 1.0, inverter_tap_limits = (min = 1, max = 1), inverter_tap_step = 0.00625, inverter_extinction_angle = 0.31416, inverter_capacitor_reactance = 0.0, active_power_limits_from = (min = -3.0, max = 3.0), active_power_limits_to = (min = -3.0, max = 3.0), reactive_power_limits_from = (min = -3.0, max = 3.0), reactive_power_limits_to = (min = -3.0, max = 3.0), ) add_component!(sys5, lcc) remove_component!(sys5, hvdc) template = get_thermal_dispatch_template_network( NetworkModel( ACPPowerModel; use_slacks = false, ), ) set_device_model!(template, TwoTerminalLCCLine, PSI.HVDCTwoTerminalLCC) set_device_model!(template, ThermalStandard, ThermalDispatchNoMin) model = DecisionModel( template, sys5; optimizer = optimizer_with_attributes(Ipopt.Optimizer), horizon = Hour(2), ) @test build!(model; output_dir = mktempdir(; cleanup = true)) == PSI.ModelBuildStatus.BUILT @test solve!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED end ================================================ FILE: test/test_device_load_constructors.jl ================================================ test_path = mktempdir() @testset "StaticPowerLoad" begin models = [StaticPowerLoad, PowerLoadDispatch, PowerLoadInterruption] c_sys5_il = PSB.build_system(PSITestSystems, "c_sys5_il") networks = [DCPPowerModel, ACPPowerModel] for m in models, n in networks device_model = DeviceModel(PowerLoad, m) model = DecisionModel(MockOperationProblem, n, c_sys5_il) mock_construct_device!(model, device_model) moi_tests(model, 0, 0, 0, 0, 0, false) psi_checkobjfun_test(model, GAEVF) model = DecisionModel(MockOperationProblem, n, c_sys5_il) mock_construct_device!(model, device_model; add_event_model = true) moi_tests(model, 0, 0, 0, 0, 0, false) end end @testset "PowerLoadDispatch DC- PF" begin models = [PowerLoadDispatch] c_sys5_il = PSB.build_system(PSITestSystems, "c_sys5_il") networks = [DCPPowerModel] for m in models, n in networks device_model = DeviceModel(InterruptiblePowerLoad, m) model = DecisionModel(MockOperationProblem, n, c_sys5_il) mock_construct_device!(model, device_model) moi_tests(model, 24, 0, 24, 0, 0, false) psi_checkobjfun_test(model, GAEVF) model = DecisionModel(MockOperationProblem, n, c_sys5_il) mock_construct_device!(model, device_model; add_event_model = true) moi_tests(model, 24, 0, 48, 0, 0, false) end end @testset "PowerLoadDispatch AC- PF" begin models = [PowerLoadDispatch] c_sys5_il = PSB.build_system(PSITestSystems, "c_sys5_il") networks = [ACPPowerModel] for m in models, n in networks device_model = DeviceModel(InterruptiblePowerLoad, m) model = DecisionModel(MockOperationProblem, n, c_sys5_il) mock_construct_device!(model, device_model) moi_tests(model, 48, 0, 24, 0, 24, false) psi_checkobjfun_test(model, GAEVF) model = DecisionModel(MockOperationProblem, n, c_sys5_il) mock_construct_device!(model, device_model; add_event_model = true) moi_tests(model, 48, 0, 48, 0, 24, false, 24) end end @testset "PowerLoadDispatch AC- PF with MarketBidCost Invalid" begin models = [PowerLoadDispatch] c_sys5_il = PSB.build_system(PSITestSystems, "c_sys5_il") iloadbus4 = get_component(InterruptiblePowerLoad, c_sys5_il, "IloadBus4") set_operation_cost!( iloadbus4, MarketBidCost(; no_load_cost = 0.0, start_up = (hot = 0.0, warm = 0.0, cold = 0.0), shut_down = 0.0, incremental_offer_curves = make_market_bid_curve( [0.0, 100.0, 200.0, 300.0, 400.0, 500.0, 600.0], [25.0, 25.5, 26.0, 27.0, 28.0, 30.0], 0.0, ), ), ) networks = [ACPPowerModel] for m in models, n in networks device_model = DeviceModel(InterruptiblePowerLoad, m) model = DecisionModel(MockOperationProblem, n, c_sys5_il) @test_throws ArgumentError mock_construct_device!(model, device_model) end end @testset "PowerLoadDispatch AC- PF with MarketBidCost" begin c_sys5_il = PSB.build_system(PSITestSystems, "c_sys5_il") iloadbus4 = get_component(InterruptiblePowerLoad, c_sys5_il, "IloadBus4") set_operation_cost!( iloadbus4, MarketBidCost(; no_load_cost = 0.0, start_up = (hot = 0.0, warm = 0.0, cold = 0.0), shut_down = 0.0, decremental_offer_curves = make_market_bid_curve( [0.0, 10.0, 20.0, 30.0, 40.0, 50.0, 60.0, 70.0, 80.0, 90.0, 100.0], [90.0, 85.0, 75.0, 70.0, 60.0, 50.0, 45.0, 40.0, 30.0, 25.0], 0.0, ), ), ) template = ProblemTemplate(NetworkModel(CopperPlatePowerModel)) set_device_model!(template, ThermalStandard, ThermalBasicUnitCommitment) set_device_model!(template, InterruptiblePowerLoad, PowerLoadDispatch) model = DecisionModel(template, c_sys5_il; name = "UC_fixed_market_bid_cost", optimizer = HiGHS_optimizer, 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__InterruptiblePowerLoad"; table_format = TableFormat.WIDE, ) p_l = read_variable( results, "ActivePowerVariable__InterruptiblePowerLoad"; table_format = TableFormat.WIDE, ) index = findfirst(row -> isapprox(100, row; atol = 1e-6), p_l.IloadBus4) calculated_cost = expr[index, "IloadBus4"][1] @test isapprox(-5700, calculated_cost; atol = 1) end @testset "PowerLoadInterruption DC- PF" begin models = [PowerLoadInterruption] c_sys5_il = PSB.build_system(PSITestSystems, "c_sys5_il") networks = [DCPPowerModel] for m in models, n in networks device_model = DeviceModel(InterruptiblePowerLoad, m) model = DecisionModel(MockOperationProblem, n, c_sys5_il) mock_construct_device!(model, device_model) moi_tests(model, 48, 0, 48, 0, 0, true) psi_checkobjfun_test(model, GAEVF) model = DecisionModel(MockOperationProblem, n, c_sys5_il) mock_construct_device!(model, device_model; add_event_model = true) moi_tests(model, 48, 0, 72, 0, 0, true) end end @testset "PowerLoadInterruption AC- PF" begin models = [PowerLoadInterruption] c_sys5_il = PSB.build_system(PSITestSystems, "c_sys5_il") networks = [ACPPowerModel] for m in models, n in networks device_model = DeviceModel(InterruptiblePowerLoad, m) model = DecisionModel(MockOperationProblem, n, c_sys5_il) mock_construct_device!(model, device_model) moi_tests(model, 72, 0, 48, 0, 24, true) psi_checkobjfun_test(model, GAEVF) model = DecisionModel(MockOperationProblem, n, c_sys5_il) mock_construct_device!(model, device_model; add_event_model = true) moi_tests(model, 72, 0, 72, 0, 24, true), 24 end end @testset "Loads without TimeSeries" begin sys = build_system(PSITestSystems, "c_sys5_uc"; force_build = true) load = get_component(PowerLoad, sys, "Bus2") remove_time_series!(sys, Deterministic, load, "max_active_power") networks = [CopperPlatePowerModel, PTDFPowerModel, DCPPowerModel, ACPPowerModel] solvers = [HiGHS_optimizer, HiGHS_optimizer, HiGHS_optimizer, ipopt_optimizer] for (ix, net) in enumerate(networks) template = ProblemTemplate( NetworkModel( net; ), ) set_device_model!(template, ThermalStandard, ThermalDispatchNoMin) set_device_model!(template, PowerLoad, StaticPowerLoad) set_device_model!(template, Line, StaticBranch) model = DecisionModel( template, sys; name = "UC", store_variable_names = true, optimizer = solvers[ix], ) @test build!(model; output_dir = mktempdir(; cleanup = true)) == PSI.ModelBuildStatus.BUILT @test solve!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED end end @testset "Loads with MotorLoad" begin sys = build_system(PSITestSystems, "c_sys5_uc"; force_build = true) load = get_component(PowerLoad, sys, "Bus2") mload = MotorLoad(; name = "MotorLoadBus2", available = true, bus = load.bus, active_power = load.active_power / 10.0, reactive_power = load.reactive_power / 10.0, base_power = load.base_power, rating = load.max_active_power / 10.0, max_active_power = load.max_active_power / 10.0, reactive_power_limits = nothing, ) add_component!(sys, mload) networks = [CopperPlatePowerModel, PTDFPowerModel, DCPPowerModel, ACPPowerModel] solvers = [HiGHS_optimizer, HiGHS_optimizer, HiGHS_optimizer, ipopt_optimizer] for (ix, net) in enumerate(networks) template = ProblemTemplate( NetworkModel( net; ), ) set_device_model!(template, ThermalStandard, ThermalDispatchNoMin) set_device_model!(template, PowerLoad, StaticPowerLoad) set_device_model!(template, MotorLoad, StaticPowerLoad) set_device_model!(template, Line, StaticBranch) model = DecisionModel( template, sys; name = "UC", store_variable_names = true, optimizer = solvers[ix], ) @test build!(model; output_dir = mktempdir(; cleanup = true)) == PSI.ModelBuildStatus.BUILT @test solve!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED end end @testset "PowerLoadShift with NonAnticipativityConstraint" begin c_sys5_il = PSB.build_system(PSITestSystems, "c_sys5_il"; add_single_time_series = true) il_load = first(PSY.get_components(InterruptiblePowerLoad, c_sys5_il)) shiftable_load = ShiftablePowerLoad(; name = "shiftable_load", available = true, bus = PSY.get_bus(il_load), active_power = PSY.get_active_power(il_load), active_power_limits = (min = 0.0, max = PSY.get_active_power(il_load)), reactive_power = PSY.get_reactive_power(il_load), max_active_power = PSY.get_max_active_power(il_load), max_reactive_power = PSY.get_max_reactive_power(il_load), base_power = PSY.get_base_power(il_load), load_balance_time_horizon = 1, operation_cost = LoadCost(; variable = CostCurve( LinearCurve(0.0), UnitSystem.NATURAL_UNITS, LinearCurve(1.0), ), fixed = 0.0, ), ) PSY.add_component!(c_sys5_il, shiftable_load) PSY.set_available!(il_load, false) PSY.copy_time_series!(shiftable_load, il_load) tstamps = TimeSeries.timestamp( PSY.get_time_series_array(SingleTimeSeries, shiftable_load, "max_active_power"), ) n = length(tstamps) up_vals = ones(n) down_vals = ones(n) PSY.add_time_series!( c_sys5_il, shiftable_load, SingleTimeSeries( "shift_up_max_active_power", TimeArray(tstamps, up_vals); scaling_factor_multiplier = PSY.get_max_active_power, ), ) PSY.add_time_series!( c_sys5_il, shiftable_load, SingleTimeSeries( "shift_down_max_active_power", TimeArray(tstamps, down_vals); scaling_factor_multiplier = PSY.get_max_active_power, ), ) PSY.transform_single_time_series!(c_sys5_il, Hour(24), Hour(24)) template = ProblemTemplate( NetworkModel( CopperPlatePowerModel; duals = [CopperPlateBalanceConstraint], ), ) set_device_model!(template, ThermalStandard, ThermalBasicUnitCommitment) set_device_model!(template, PowerLoad, StaticPowerLoad) set_device_model!( template, DeviceModel( ShiftablePowerLoad, PowerLoadShift; attributes = Dict{String, Any}("additional_balance_interval" => Hour(12)), ), ) model = DecisionModel( template, c_sys5_il; name = "UC_shiftable", store_variable_names = true, optimizer = HiGHS_optimizer, ) @test build!(model; output_dir = mktempdir(; cleanup = true)) == PSI.ModelBuildStatus.BUILT @test solve!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED results = OptimizationProblemResults(model) up = read_variable( results, "ShiftUpActivePowerVariable__ShiftablePowerLoad"; table_format = TableFormat.WIDE, ) dn = read_variable( results, "ShiftDownActivePowerVariable__ShiftablePowerLoad"; table_format = TableFormat.WIDE, ) # Verify the non-anticipativity constraint holds in the solution: # the running sum of (shift_down - shift_up) must be >= 0 at every time step. @test all( cumsum(dn[!, "shiftable_load"] .- up[!, "shiftable_load"]) .>= -1e-6, ) end ================================================ FILE: test/test_device_renewable_generation_constructors.jl ================================================ @testset "Renewable DCPLossless FullDispatch" begin device_model = DeviceModel(RenewableDispatch, RenewableFullDispatch) c_sys5_re = PSB.build_system(PSITestSystems, "c_sys5_re") model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys5_re) mock_construct_device!(model, device_model) moi_tests(model, 72, 0, 72, 0, 0, false) psi_checkobjfun_test(model, GAEVF) model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys5_re) mock_construct_device!(model, device_model; add_event_model = true) moi_tests(model, 72, 0, 96, 0, 0, false) end @testset "Renewable ACPPower Full Dispatch" begin device_model = DeviceModel(RenewableDispatch, RenewableFullDispatch) c_sys5_re = PSB.build_system(PSITestSystems, "c_sys5_re") model = DecisionModel(MockOperationProblem, ACPPowerModel, c_sys5_re;) mock_construct_device!(model, device_model) moi_tests(model, 144, 0, 144, 72, 0, false) psi_checkobjfun_test(model, GAEVF) model = DecisionModel(MockOperationProblem, ACPPowerModel, c_sys5_re;) mock_construct_device!(model, device_model; add_event_model = true) moi_tests(model, 144, 0, 168, 72, 0, false, 24) end @testset "Renewable DCPLossless Constantpower_factor" begin device_model = DeviceModel(RenewableDispatch, RenewableConstantPowerFactor) c_sys5_re = PSB.build_system(PSITestSystems, "c_sys5_re") model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys5_re) mock_construct_device!(model, device_model) moi_tests(model, 72, 0, 72, 0, 0, false) psi_checkobjfun_test(model, GAEVF) model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys5_re) mock_construct_device!(model, device_model; add_event_model = true) moi_tests(model, 72, 0, 96, 0, 0, false) end @testset "Renewable ACPPower Constantpower_factor" begin device_model = DeviceModel(RenewableDispatch, RenewableConstantPowerFactor) c_sys5_re = PSB.build_system(PSITestSystems, "c_sys5_re") model = DecisionModel(MockOperationProblem, ACPPowerModel, c_sys5_re;) mock_construct_device!(model, device_model) moi_tests(model, 144, 0, 72, 0, 72, false) psi_checkobjfun_test(model, GAEVF) model = DecisionModel(MockOperationProblem, ACPPowerModel, c_sys5_re;) mock_construct_device!(model, device_model; add_event_model = true) moi_tests(model, 144, 0, 96, 0, 72, false, 24) end @testset "Renewable DCPLossless FixedOutput" begin device_model = DeviceModel(RenewableDispatch, FixedOutput) c_sys5_re = PSB.build_system(PSITestSystems, "c_sys5_re") model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys5_re;) mock_construct_device!(model, device_model) moi_tests(model, 0, 0, 0, 0, 0, false) psi_checkobjfun_test(model, GAEVF) model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys5_re;) mock_construct_device!(model, device_model; add_event_model = true) moi_tests(model, 0, 0, 0, 0, 0, false) end @testset "Renewable ACPPowerModel FixedOutput" begin device_model = DeviceModel(RenewableDispatch, FixedOutput) c_sys5_re = PSB.build_system(PSITestSystems, "c_sys5_re") model = DecisionModel(MockOperationProblem, ACPPowerModel, c_sys5_re;) mock_construct_device!(model, device_model) moi_tests(model, 0, 0, 0, 0, 0, false) psi_checkobjfun_test(model, GAEVF) model = DecisionModel(MockOperationProblem, ACPPowerModel, c_sys5_re;) mock_construct_device!(model, device_model; add_event_model = true) moi_tests(model, 0, 0, 0, 0, 0, false) end @testset "Test Renewable CurtailmentCostExpression nonnegativity" begin c_sys5_re = PSB.build_system(PSITestSystems, "c_sys5_re") template = ProblemTemplate(NetworkModel(CopperPlatePowerModel)) set_device_model!(template, RenewableDispatch, RenewableFullDispatch) set_device_model!(template, ThermalStandard, ThermalStandardDispatch) set_device_model!(template, PowerLoad, StaticPowerLoad) model = DecisionModel( template, c_sys5_re; name = "RE_curtailment_cost", optimizer = HiGHS_optimizer, 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_curt = read_expression( results, "CurtailmentCostExpression__RenewableDispatch"; table_format = TableFormat.WIDE, ) tol = 1e-8 for unit in names(expr_curt)[2:end] @test all(expr_curt[!, unit] .>= -tol) end end ================================================ FILE: test/test_device_source_constructors.jl ================================================ # See also test_import_export_cost.jl const BASIC_SOURCE_CONSTRAINT_KEYS = [ PSI.ConstraintKey(ImportExportBudgetConstraint, PSY.Source, "import"), PSI.ConstraintKey(ImportExportBudgetConstraint, PSY.Source, "export"), PSI.ConstraintKey(ActivePowerVariableLimitsConstraint, PSY.Source, "ub"), PSI.ConstraintKey(ActivePowerVariableLimitsConstraint, PSY.Source, "lb"), PSI.ConstraintKey(InputActivePowerVariableLimitsConstraint, PSY.Source, "ub"), PSI.ConstraintKey(InputActivePowerVariableLimitsConstraint, PSY.Source, "lb"), PSI.ConstraintKey(PiecewiseLinearBlockIncrementalOfferConstraint, PSY.Source), PSI.ConstraintKey(PiecewiseLinearBlockDecrementalOfferConstraint, PSY.Source), ] const TS_SOURCE_CONSTRAINT_KEYS = [ BASIC_SOURCE_CONSTRAINT_KEYS..., PSI.ConstraintKey(ActivePowerOutVariableTimeSeriesLimitsConstraint, Source, "ub"), PSI.ConstraintKey(ActivePowerInVariableTimeSeriesLimitsConstraint, Source, "ub"), ] @testset "ImportExportSource Source With CopperPlate" begin sys = make_5_bus_with_import_export(; add_single_time_series = false) model = DecisionModel(MockOperationProblem, CopperPlatePowerModel, sys) device_model = DeviceModel( Source, ImportExportSourceModel; attributes = Dict("reservation" => false), ) mock_construct_device!(model, device_model) moi_tests(model, 240, 0, 242, 48, 48, false) psi_constraint_test(model, BASIC_SOURCE_CONSTRAINT_KEYS) psi_checkobjfun_test(model, GAEVF) model = DecisionModel(MockOperationProblem, CopperPlatePowerModel, sys) device_model = DeviceModel( Source, ImportExportSourceModel; attributes = Dict("reservation" => true), ) mock_construct_device!(model, device_model) moi_tests(model, 264, 0, 242, 48, 48, true) psi_constraint_test(model, BASIC_SOURCE_CONSTRAINT_KEYS) psi_checkobjfun_test(model, GAEVF) end @testset "ImportExportSource Source With ACPPowerModel" begin sys = make_5_bus_with_import_export(; add_single_time_series = false) model = DecisionModel(MockOperationProblem, ACPPowerModel, sys) device_model = DeviceModel( Source, ImportExportSourceModel; attributes = Dict("reservation" => false), ) mock_construct_device!(model, device_model) moi_tests(model, 264, 0, 266, 72, 48, false) psi_constraint_test(model, BASIC_SOURCE_CONSTRAINT_KEYS) psi_checkobjfun_test(model, GAEVF) model = DecisionModel(MockOperationProblem, ACPPowerModel, sys) device_model = DeviceModel( Source, ImportExportSourceModel; attributes = Dict("reservation" => true), ) mock_construct_device!(model, device_model) moi_tests(model, 288, 0, 266, 72, 48, true) psi_constraint_test(model, BASIC_SOURCE_CONSTRAINT_KEYS) psi_checkobjfun_test(model, GAEVF) end @testset "Source With CopperPlate and TimeSeries" begin for source_formulation in [ImportExportSourceModel, FixedOutput] sys = make_5_bus_with_import_export(; add_single_time_series = true) source = get_component(Source, sys, "source") load = first(get_components(PowerLoad, sys)) tstamp = TimeSeries.timestamp( get_time_series_array(SingleTimeSeries, load, "max_active_power"), ) 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, ] ts_data = repeat(day_data, 2) ts_out = SingleTimeSeries( "max_active_power_out", TimeArray(tstamp, ts_data); scaling_factor_multiplier = get_max_active_power, ) ts_in = SingleTimeSeries( "max_active_power_in", TimeArray(tstamp, ts_data); scaling_factor_multiplier = get_max_active_power, ) add_time_series!(sys, source, ts_out) add_time_series!(sys, source, ts_in) transform_single_time_series!(sys, Hour(24), Hour(24)) template = ProblemTemplate(NetworkModel(CopperPlatePowerModel)) set_device_model!(template, ThermalStandard, ThermalStandardUnitCommitment) set_device_model!(template, PowerLoad, StaticPowerLoad) source_model = DeviceModel( Source, source_formulation; attributes = Dict("reservation" => false), ) set_device_model!(template, source_model) model = DecisionModel( template, sys; name = "UC", optimizer = HiGHS_optimizer, store_variable_names = true, optimizer_solve_log_print = false, ) @test build!(model; output_dir = mktempdir()) == PSI.ModelBuildStatus.BUILT @test solve!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED res = OptimizationProblemResults(model) if source_formulation == ImportExportSourceModel p_out = read_variable( res, "ActivePowerOutVariable__Source"; table_format = TableFormat.WIDE, )[ !, 2, ] p_in = read_variable( res, "ActivePowerInVariable__Source"; table_format = TableFormat.WIDE, )[ !, 2, ] elseif source_formulation == FixedOutput p_out = read_parameter( res, "ActivePowerOutTimeSeriesParameter__Source"; table_format = TableFormat.WIDE, )[ !, 2, ] p_in = read_parameter( res, "ActivePowerInTimeSeriesParameter__Source"; table_format = TableFormat.WIDE, )[ !, 2, ] end # Test that is zero when the time series is zero @test p_out[5] == 0.0 @test p_in[5] == 0.0 @test p_out[6] == 0.0 @test p_in[6] == 0.0 end end @testset "ImportExportSource Source With ACPPowerModel and TimeSeries" begin sys = make_5_bus_with_import_export(; add_single_time_series = true) source = get_component(Source, sys, "source") load = first(get_components(PowerLoad, sys)) tstamp = TimeSeries.timestamp( get_time_series_array(SingleTimeSeries, load, "max_active_power"), ) 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, ] ts_data = repeat(day_data, 2) ts_out = SingleTimeSeries( "max_active_power_out", TimeArray(tstamp, ts_data); scaling_factor_multiplier = get_max_active_power, ) ts_in = SingleTimeSeries( "max_active_power_in", TimeArray(tstamp, ts_data); scaling_factor_multiplier = get_max_active_power, ) add_time_series!(sys, source, ts_out) add_time_series!(sys, source, ts_in) transform_single_time_series!(sys, Hour(24), Hour(24)) model = DecisionModel(MockOperationProblem, ACPPowerModel, sys) device_model = DeviceModel( Source, ImportExportSourceModel; attributes = Dict("reservation" => false), ) mock_construct_device!(model, device_model) moi_tests(model, 264, 0, 314, 72, 48, false) psi_constraint_test(model, TS_SOURCE_CONSTRAINT_KEYS) psi_checkobjfun_test(model, GAEVF) model = DecisionModel(MockOperationProblem, ACPPowerModel, sys) device_model = DeviceModel( Source, ImportExportSourceModel; attributes = Dict("reservation" => true), ) mock_construct_device!(model, device_model) moi_tests(model, 288, 0, 314, 72, 48, true) psi_constraint_test(model, TS_SOURCE_CONSTRAINT_KEYS) psi_checkobjfun_test(model, GAEVF) end @testset "FixedOutput Source With PTDFPowerModel and TimeSeries" begin sys = make_5_bus_with_import_export(; add_single_time_series = true) source = get_component(Source, sys, "source") load = first(get_components(PowerLoad, sys)) tstamp = TimeSeries.timestamp( get_time_series_array(SingleTimeSeries, load, "max_active_power"), ) 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, ] ts_data = repeat(day_data, 2) ts_out = SingleTimeSeries( "max_active_power_out", TimeArray(tstamp, ts_data); scaling_factor_multiplier = get_max_active_power, ) ts_in = SingleTimeSeries( "max_active_power_in", TimeArray(tstamp, ts_data); scaling_factor_multiplier = get_max_active_power, ) add_time_series!(sys, source, ts_out) add_time_series!(sys, source, ts_in) transform_single_time_series!(sys, Hour(24), Hour(24)) model = DecisionModel(MockOperationProblem, PTDFPowerModel, sys) device_model = DeviceModel( Source, FixedOutput; attributes = Dict("reservation" => false), ) mock_construct_device!(model, device_model) # Fixed output does not add variables nor constraints. moi_tests(model, 0, 0, 0, 0, 0, false) psi_checkobjfun_test(model, GAEVF) model = DecisionModel(MockOperationProblem, PTDFPowerModel, sys) device_model = DeviceModel( Source, FixedOutput; attributes = Dict("reservation" => true), ) mock_construct_device!(model, device_model) # Fixed output does not add variables nor constraints. moi_tests(model, 0, 0, 0, 0, 0, false) psi_checkobjfun_test(model, GAEVF) end ================================================ FILE: test/test_device_synchronous_condenser_constructors.jl ================================================ @testset "SynchronousCondenserBasicDispatch SynchronousCondenser With Power Models" begin sys = build_system(PSITestSystems, "c_sys5_uc"; add_single_time_series = true) syncon = SynchronousCondenser(; name = "syncon_test", available = true, bus = get_component(ACBus, sys, "nodeB"), reactive_power = 0.0, rating = 2.0, reactive_power_limits = (min = -2.0, max = 2.0), base_power = 100.0, ) add_component!(sys, syncon) template = ProblemTemplate(ACPPowerModel) set_device_model!(template, ThermalStandard, ThermalDispatchNoMin) set_device_model!(template, PowerLoad, StaticPowerLoad) set_device_model!(template, Line, StaticBranch) set_device_model!(template, SynchronousCondenser, SynchronousCondenserBasicDispatch) transform_single_time_series!(sys, Hour(24), Hour(24)) model = DecisionModel( template, sys; name = "UC", optimizer = Ipopt.Optimizer, store_variable_names = true, ) @test build!(model; output_dir = mktempdir(; cleanup = true)) == PSI.ModelBuildStatus.BUILT @test solve!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED res = OptimizationProblemResults(model) q_syncon = read_variable( res, "ReactivePowerVariable__SynchronousCondenser"; table_format = TableFormat.WIDE, ) @test any(q_syncon[!, 2] != 0.0) template = ProblemTemplate(PTDFPowerModel) set_device_model!(template, ThermalStandard, ThermalDispatchNoMin) set_device_model!(template, PowerLoad, StaticPowerLoad) set_device_model!(template, Line, StaticBranch) set_device_model!(template, SynchronousCondenser, SynchronousCondenserBasicDispatch) model = DecisionModel( template, sys; name = "UC", optimizer = HiGHS_optimizer, store_variable_names = true, ) @test build!(model; output_dir = mktempdir(; cleanup = true)) == PSI.ModelBuildStatus.BUILT @test solve!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED end ================================================ FILE: test/test_device_thermal_generation_constructors.jl ================================================ test_path = mktempdir() const TIME1 = DateTime("2024-01-01T00:00:00") @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), ("pwl_average_cost_test", 3424.43, ThermalBasicUnitCommitment), ("pwl_average_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, 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"; table_format = TableFormat.WIDE, ) unit = "Test Unit" var_unit_cost = sum(expr[!, unit]) @test isapprox(var_unit_cost, cost_reference; atol = 1) if thermal_formulation == ThermalBasicUnitCommitment # Tests shut down cost @test expr[!, unit][end] == 0.75 # Decomposition: production == fuel + startup + shutdown + fixed + VOM expr_fuel = read_expression( results, "FuelCostExpression__ThermalStandard"; table_format = TableFormat.WIDE, ) expr_su = read_expression( results, "StartUpCostExpression__ThermalStandard"; table_format = TableFormat.WIDE, ) expr_sd = read_expression( results, "ShutDownCostExpression__ThermalStandard"; table_format = TableFormat.WIDE, ) expr_fixed = read_expression( results, "FixedCostExpression__ThermalStandard"; table_format = TableFormat.WIDE, ) expr_VOM = read_expression( results, "VOMCostExpression__ThermalStandard"; table_format = TableFormat.WIDE, ) decomp_vec = expr_fuel[!, unit] .+ expr_su[!, unit] .+ expr_sd[!, unit] .+ expr_fixed[!, unit] .+ expr_VOM[!, unit] @test all(isapprox.(decomp_vec, expr[!, unit]; atol = 1e-6)) @test isapprox(sum(expr[!, unit]), sum(decomp_vec); atol = 1e-6) # Nonnegativity (tolerate tiny numerical negatives) tol = 1e-8 @test all(expr_fuel[!, unit] .>= -tol) @test all(expr_su[!, unit] .>= -tol) @test all(expr_sd[!, unit] .>= -tol) @test all(expr_fixed[!, unit] .>= -tol) @test all(expr_VOM[!, unit] .>= -tol) else @test expr[!, unit][end] == 0.0 end end end @testset "Test startup cost tracking" begin template = ProblemTemplate(NetworkModel(CopperPlatePowerModel)) set_device_model!(template, ThermalStandard, ThermalBasicUnitCommitment) set_device_model!(template, PowerLoad, StaticPowerLoad) unit = "Test Unit" # Run 1: units initially ON — no startup expected sys_no_startup = build_system(PSITestSystems, "c_linear_cost_test") model_no = DecisionModel( template, sys_no_startup; name = "UC_no_startup", optimizer = HiGHS_optimizer, optimizer_solve_log_print = true, ) @test build!(model_no; output_dir = test_path) == PSI.ModelBuildStatus.BUILT @test solve!(model_no) == PSI.RunStatus.SUCCESSFULLY_FINALIZED res_no = OptimizationProblemResults(model_no) prod_no = read_expression( res_no, "ProductionCostExpression__ThermalStandard"; table_format = TableFormat.WIDE, ) cost_no_t1 = prod_no[1, unit] # Run 2: units initially OFF — startup forced in first timestep sys_yes = build_system(PSITestSystems, "c_linear_cost_test") for u in collect(get_components(ThermalStandard, sys_yes)) set_status!(u, false) set_time_at_status!(u, 10.0) # > min down time end model_yes = DecisionModel( template, sys_yes; name = "UC_with_startup", optimizer = HiGHS_optimizer, optimizer_solve_log_print = true, ) @test build!(model_yes; output_dir = test_path) == PSI.ModelBuildStatus.BUILT @test solve!(model_yes) == PSI.RunStatus.SUCCESSFULLY_FINALIZED res_yes = OptimizationProblemResults(model_yes) prod_yes = read_expression( res_yes, "ProductionCostExpression__ThermalStandard"; table_format = TableFormat.WIDE, ) cost_yes_t1 = prod_yes[1, unit] start_vars = read_variable( res_yes, "StartVariable__ThermalStandard"; table_format = TableFormat.WIDE, ) @test start_vars[1, unit] > 0.5 expr_su = read_expression( res_yes, "StartUpCostExpression__ThermalStandard"; table_format = TableFormat.WIDE, ) startup_cost_t1 = expr_su[1, unit] @test startup_cost_t1 > 0.0 @test cost_yes_t1 > cost_no_t1 @test isapprox(cost_yes_t1 - cost_no_t1, startup_cost_t1; atol = 1e-6) end end #TODO: ThermalGen #= @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, ) @test build!(model; output_dir = test_path) == PSI.ModelBuildStatus.BUILT @test solve!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED =# end end end =# ################################### Unit Commitment tests ################################## @testset "Thermal UC With DC - PF" begin bin_variable_keys = [ PSI.VariableKey(OnVariable, PSY.ThermalStandard), PSI.VariableKey(StartVariable, PSY.ThermalStandard), PSI.VariableKey(StopVariable, PSY.ThermalStandard), ] uc_constraint_keys = [ PSI.ConstraintKey(RampConstraint, PSY.ThermalStandard, "up"), PSI.ConstraintKey(RampConstraint, PSY.ThermalStandard, "dn"), PSI.ConstraintKey(DurationConstraint, PSY.ThermalStandard, "up"), PSI.ConstraintKey(DurationConstraint, PSY.ThermalStandard, "dn"), ] aux_variables_keys = [ PSI.AuxVarKey(PSI.TimeDurationOff, ThermalStandard), PSI.AuxVarKey(PSI.TimeDurationOn, ThermalStandard), ] device_model = DeviceModel(ThermalStandard, ThermalStandardUnitCommitment) c_sys5_uc = PSB.build_system(PSITestSystems, "c_sys5_uc") model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys5_uc) mock_construct_device!(model, device_model) moi_tests(model, 480, 0, 480, 120, 120, true) psi_constraint_test(model, uc_constraint_keys) psi_checkbinvar_test(model, bin_variable_keys) psi_checkobjfun_test(model, GAEVF) psi_aux_variable_test(model, aux_variables_keys) model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys5_uc) mock_construct_device!(model, device_model; add_event_model = true) moi_tests(model, 480, 0, 504, 120, 120, true) c_sys14 = PSB.build_system(PSITestSystems, "c_sys14") device_model = DeviceModel(ThermalStandard, ThermalStandardUnitCommitment) model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys14) mock_construct_device!(model, device_model) moi_tests(model, 480, 0, 240, 120, 120, true) psi_checkbinvar_test(model, bin_variable_keys) psi_checkobjfun_test(model, GQEVF) model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys14) mock_construct_device!(model, device_model; add_event_model = true) moi_tests(model, 480, 0, 264, 120, 120, true) end @testset "Thermal UC With AC - PF" begin bin_variable_keys = [ PSI.VariableKey(OnVariable, PSY.ThermalStandard), PSI.VariableKey(StartVariable, PSY.ThermalStandard), PSI.VariableKey(StopVariable, PSY.ThermalStandard), ] uc_constraint_keys = [ PSI.ConstraintKey(RampConstraint, PSY.ThermalStandard, "up"), PSI.ConstraintKey(RampConstraint, PSY.ThermalStandard, "dn"), PSI.ConstraintKey(DurationConstraint, PSY.ThermalStandard, "up"), PSI.ConstraintKey(DurationConstraint, PSY.ThermalStandard, "dn"), ] aux_variables_keys = [ PSI.AuxVarKey(PSI.TimeDurationOff, ThermalStandard), PSI.AuxVarKey(PSI.TimeDurationOn, ThermalStandard), ] device_model = DeviceModel(ThermalStandard, ThermalStandardUnitCommitment) c_sys5_uc = PSB.build_system(PSITestSystems, "c_sys5_uc") model = DecisionModel(MockOperationProblem, ACPPowerModel, c_sys5_uc) mock_construct_device!(model, device_model) moi_tests(model, 600, 0, 600, 240, 120, true) psi_constraint_test(model, uc_constraint_keys) psi_checkbinvar_test(model, bin_variable_keys) psi_checkobjfun_test(model, GAEVF) psi_aux_variable_test(model, aux_variables_keys) model = DecisionModel(MockOperationProblem, ACPPowerModel, c_sys5_uc) mock_construct_device!(model, device_model; add_event_model = true) moi_tests(model, 600, 0, 624, 240, 120, true, 24) device_model = DeviceModel(ThermalStandard, ThermalStandardUnitCommitment) c_sys14 = PSB.build_system(PSITestSystems, "c_sys14") model = DecisionModel(MockOperationProblem, ACPPowerModel, c_sys14;) mock_construct_device!(model, device_model) moi_tests(model, 600, 0, 360, 240, 120, true) psi_checkbinvar_test(model, bin_variable_keys) psi_checkobjfun_test(model, GQEVF) model = DecisionModel(MockOperationProblem, ACPPowerModel, c_sys14;) mock_construct_device!(model, device_model; add_event_model = true) moi_tests(model, 600, 0, 384, 240, 120, true, 24) end @testset "Thermal MultiStart UC With DC - PF" begin bin_variable_keys = [ PSI.VariableKey(OnVariable, PSY.ThermalMultiStart), PSI.VariableKey(StartVariable, PSY.ThermalMultiStart), PSI.VariableKey(StopVariable, PSY.ThermalMultiStart), ] uc_constraint_keys = [ PSI.ConstraintKey(RampConstraint, PSY.ThermalMultiStart, "up"), PSI.ConstraintKey(RampConstraint, PSY.ThermalMultiStart, "dn"), PSI.ConstraintKey(DurationConstraint, PSY.ThermalMultiStart, "up"), PSI.ConstraintKey(DurationConstraint, PSY.ThermalMultiStart, "dn"), ] device_model = DeviceModel(ThermalMultiStart, ThermalStandardUnitCommitment) c_sys5_uc = PSB.build_system(PSITestSystems, "c_sys5_pglib") model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys5_uc;) mock_construct_device!(model, device_model) moi_tests(model, 384, 0, 240, 48, 144, true) psi_constraint_test(model, uc_constraint_keys) psi_checkbinvar_test(model, bin_variable_keys) psi_checkobjfun_test(model, GAEVF) model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys5_uc;) mock_construct_device!(model, device_model; add_event_model = true) moi_tests(model, 384, 0, 264, 48, 144, true) end @testset "Thermal MultiStart UC With AC - PF" begin bin_variable_keys = [ PSI.VariableKey(OnVariable, PSY.ThermalMultiStart), PSI.VariableKey(StartVariable, PSY.ThermalMultiStart), PSI.VariableKey(StopVariable, PSY.ThermalMultiStart), ] uc_constraint_keys = [ PSI.ConstraintKey(RampConstraint, PSY.ThermalMultiStart, "up"), PSI.ConstraintKey(RampConstraint, PSY.ThermalMultiStart, "dn"), PSI.ConstraintKey(DurationConstraint, PSY.ThermalMultiStart, "up"), PSI.ConstraintKey(DurationConstraint, PSY.ThermalMultiStart, "dn"), ] device_model = DeviceModel(ThermalMultiStart, ThermalStandardUnitCommitment) c_sys5_uc = PSB.build_system(PSITestSystems, "c_sys5_pglib") model = DecisionModel(MockOperationProblem, ACPPowerModel, c_sys5_uc;) mock_construct_device!(model, device_model) moi_tests(model, 432, 0, 288, 96, 144, true) psi_constraint_test(model, uc_constraint_keys) psi_checkbinvar_test(model, bin_variable_keys) psi_checkobjfun_test(model, GAEVF) model = DecisionModel(MockOperationProblem, ACPPowerModel, c_sys5_uc;) mock_construct_device!(model, device_model; add_event_model = true) moi_tests(model, 432, 0, 312, 96, 144, true, 24) end ################################### Basic Unit Commitment tests ############################ @testset "Thermal Basic UC With DC - PF" begin bin_variable_keys = [ PSI.VariableKey(OnVariable, PSY.ThermalStandard), PSI.VariableKey(StartVariable, PSY.ThermalStandard), PSI.VariableKey(StopVariable, PSY.ThermalStandard), ] device_model = DeviceModel(ThermalStandard, ThermalBasicUnitCommitment) c_sys5_uc = PSB.build_system(PSITestSystems, "c_sys5_uc") model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys5_uc) mock_construct_device!(model, device_model) moi_tests(model, 480, 0, 240, 120, 120, true) psi_checkbinvar_test(model, bin_variable_keys) psi_checkobjfun_test(model, GAEVF) model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys5_uc) mock_construct_device!(model, device_model; add_event_model = true) moi_tests(model, 480, 0, 264, 120, 120, true) device_model = DeviceModel(ThermalStandard, ThermalBasicUnitCommitment) c_sys14 = PSB.build_system(PSITestSystems, "c_sys14") model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys14;) mock_construct_device!(model, device_model) moi_tests(model, 480, 0, 240, 120, 120, true) psi_checkbinvar_test(model, bin_variable_keys) psi_checkobjfun_test(model, GQEVF) model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys14;) mock_construct_device!(model, device_model; add_event_model = true) moi_tests(model, 480, 0, 264, 120, 120, true) end @testset "Thermal Basic UC With AC - PF" begin bin_variable_keys = [ PSI.VariableKey(OnVariable, PSY.ThermalStandard), PSI.VariableKey(StartVariable, PSY.ThermalStandard), PSI.VariableKey(StopVariable, PSY.ThermalStandard), ] device_model = DeviceModel(ThermalStandard, ThermalBasicUnitCommitment) c_sys5_uc = PSB.build_system(PSITestSystems, "c_sys5_uc") model = DecisionModel(MockOperationProblem, ACPPowerModel, c_sys5_uc) mock_construct_device!(model, device_model) moi_tests(model, 600, 0, 360, 240, 120, true) psi_checkbinvar_test(model, bin_variable_keys) psi_checkobjfun_test(model, GAEVF) model = DecisionModel(MockOperationProblem, ACPPowerModel, c_sys5_uc) mock_construct_device!(model, device_model; add_event_model = true) moi_tests(model, 600, 0, 384, 240, 120, true, 24) device_model = DeviceModel(ThermalStandard, ThermalBasicUnitCommitment) c_sys14 = PSB.build_system(PSITestSystems, "c_sys14") model = DecisionModel(MockOperationProblem, ACPPowerModel, c_sys14;) mock_construct_device!(model, device_model) moi_tests(model, 600, 0, 360, 240, 120, true) psi_checkbinvar_test(model, bin_variable_keys) psi_checkobjfun_test(model, GQEVF) model = DecisionModel(MockOperationProblem, ACPPowerModel, c_sys14;) mock_construct_device!(model, device_model; add_event_model = true) moi_tests(model, 600, 0, 384, 240, 120, true, 24) end @testset "Thermal MultiStart Basic UC With DC - PF" begin bin_variable_keys = [ PSI.VariableKey(OnVariable, PSY.ThermalMultiStart), PSI.VariableKey(StartVariable, PSY.ThermalMultiStart), PSI.VariableKey(StopVariable, PSY.ThermalMultiStart), ] device_model = DeviceModel(ThermalMultiStart, ThermalBasicUnitCommitment) c_sys5_uc = PSB.build_system(PSITestSystems, "c_sys5_pglib") model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys5_uc;) mock_construct_device!(model, device_model) moi_tests(model, 384, 0, 96, 48, 144, true) psi_checkbinvar_test(model, bin_variable_keys) psi_checkobjfun_test(model, GAEVF) model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys5_uc;) mock_construct_device!(model, device_model; add_event_model = true) moi_tests(model, 384, 0, 120, 48, 144, true) end @testset "Thermal MultiStart Basic UC With AC - PF" begin bin_variable_keys = [ PSI.VariableKey(OnVariable, PSY.ThermalMultiStart), PSI.VariableKey(StartVariable, PSY.ThermalMultiStart), PSI.VariableKey(StopVariable, PSY.ThermalMultiStart), ] device_model = DeviceModel(ThermalMultiStart, ThermalBasicUnitCommitment) c_sys5_uc = PSB.build_system(PSITestSystems, "c_sys5_pglib") model = DecisionModel(MockOperationProblem, ACPPowerModel, c_sys5_uc;) mock_construct_device!(model, device_model) moi_tests(model, 432, 0, 144, 96, 144, true) psi_checkbinvar_test(model, bin_variable_keys) psi_checkobjfun_test(model, GAEVF) model = DecisionModel(MockOperationProblem, ACPPowerModel, c_sys5_uc;) mock_construct_device!(model, device_model; add_event_model = true) moi_tests(model, 432, 0, 168, 96, 144, true, 24) end ################################### Basic Dispatch tests ################################### @testset "ThermalStandard with ThermalBasicDispatch With DC - PF" begin device_model = DeviceModel(ThermalStandard, ThermalBasicDispatch) c_sys5 = PSB.build_system(PSITestSystems, "c_sys5") model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys5) mock_construct_device!(model, device_model) moi_tests(model, 120, 0, 120, 120, 0, false) psi_checkobjfun_test(model, GAEVF) c_sys14 = PSB.build_system(PSITestSystems, "c_sys14") model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys14) mock_construct_device!(model, device_model) moi_tests(model, 120, 0, 120, 120, 0, false) psi_checkobjfun_test(model, GQEVF) end @testset "ThermalStandard with ThermalBasicDispatch With AC - PF" begin device_model = DeviceModel(ThermalStandard, ThermalBasicDispatch) c_sys5 = PSB.build_system(PSITestSystems, "c_sys5") model = DecisionModel(MockOperationProblem, ACPPowerModel, c_sys5) mock_construct_device!(model, device_model) moi_tests(model, 240, 0, 240, 240, 0, false) psi_checkobjfun_test(model, GAEVF) model = DecisionModel(MockOperationProblem, ACPPowerModel, c_sys5) mock_construct_device!(model, device_model; add_event_model = true) moi_tests(model, 240, 0, 264, 240, 0, false, 24) device_model = DeviceModel(ThermalStandard, ThermalBasicDispatch) c_sys14 = PSB.build_system(PSITestSystems, "c_sys14") model = DecisionModel(MockOperationProblem, ACPPowerModel, c_sys14;) mock_construct_device!(model, device_model) moi_tests(model, 240, 0, 240, 240, 0, false) psi_checkobjfun_test(model, GQEVF) model = DecisionModel(MockOperationProblem, ACPPowerModel, c_sys14;) mock_construct_device!(model, device_model; add_event_model = true) moi_tests(model, 240, 0, 264, 240, 0, false, 24) end # This Formulation is currently broken @testset "ThermalMultiStart with ThermalBasicDispatch With DC - PF" begin device_model = DeviceModel(ThermalMultiStart, ThermalBasicDispatch) c_sys5 = PSB.build_system(PSITestSystems, "c_sys5_pglib") model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys5) mock_construct_device!(model, device_model) moi_tests(model, 240, 0, 48, 48, 96, false) psi_checkobjfun_test(model, GAEVF) model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys5) mock_construct_device!(model, device_model; add_event_model = true) moi_tests(model, 240, 0, 72, 48, 96, false) end @testset "ThermalMultiStart with ThermalBasicDispatch With AC - PF" begin device_model = DeviceModel(ThermalMultiStart, ThermalBasicDispatch) c_sys5 = PSB.build_system(PSITestSystems, "c_sys5_pglib") model = DecisionModel(MockOperationProblem, ACPPowerModel, c_sys5) mock_construct_device!(model, device_model) moi_tests(model, 288, 0, 96, 96, 96, false) psi_checkobjfun_test(model, GAEVF) model = DecisionModel(MockOperationProblem, ACPPowerModel, c_sys5) mock_construct_device!(model, device_model; add_event_model = true) moi_tests(model, 288, 0, 120, 96, 96, false, 24) end ################################### No Minimum Dispatch tests ############################## @testset "Thermal Dispatch NoMin With DC - PF" begin device_model = DeviceModel(ThermalStandard, ThermalDispatchNoMin) c_sys5 = PSB.build_system(PSITestSystems, "c_sys5") model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys5) mock_construct_device!(model, device_model) moi_tests(model, 120, 0, 120, 120, 0, false) key = PSI.ConstraintKey(ActivePowerVariableLimitsConstraint, ThermalStandard, "lb") moi_lbvalue_test(model, key, 0.0) psi_checkobjfun_test(model, GAEVF) model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys5) mock_construct_device!(model, device_model; add_event_model = true) moi_tests(model, 120, 0, 144, 120, 0, false) device_model = DeviceModel(ThermalStandard, ThermalDispatchNoMin) c_sys14 = PSB.build_system(PSITestSystems, "c_sys14") model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys14) mock_construct_device!(model, device_model) moi_tests(model, 120, 0, 120, 120, 0, false) key = PSI.ConstraintKey(ActivePowerVariableLimitsConstraint, ThermalStandard, "lb") moi_lbvalue_test(model, key, 0.0) psi_checkobjfun_test(model, GQEVF) model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys14) mock_construct_device!(model, device_model; add_event_model = true) moi_tests(model, 120, 0, 144, 120, 0, false) end @testset "Thermal Dispatch NoMin With AC - PF" begin device_model = DeviceModel(ThermalStandard, ThermalDispatchNoMin) c_sys5 = PSB.build_system(PSITestSystems, "c_sys5") model = DecisionModel(MockOperationProblem, ACPPowerModel, c_sys5) mock_construct_device!(model, device_model) moi_tests(model, 240, 0, 240, 240, 0, false) key = PSI.ConstraintKey(ActivePowerVariableLimitsConstraint, ThermalStandard, "lb") moi_lbvalue_test(model, key, 0.0) psi_checkobjfun_test(model, GAEVF) model = DecisionModel(MockOperationProblem, ACPPowerModel, c_sys5) mock_construct_device!(model, device_model; add_event_model = true) moi_tests(model, 240, 0, 264, 240, 0, false, 24) device_model = DeviceModel(ThermalStandard, ThermalDispatchNoMin) c_sys14 = PSB.build_system(PSITestSystems, "c_sys14") model = DecisionModel(MockOperationProblem, ACPPowerModel, c_sys14;) mock_construct_device!(model, device_model) moi_tests(model, 240, 0, 240, 240, 0, false) key = PSI.ConstraintKey(ActivePowerVariableLimitsConstraint, ThermalStandard, "lb") moi_lbvalue_test(model, key, 0.0) psi_checkobjfun_test(model, GQEVF) model = DecisionModel(MockOperationProblem, ACPPowerModel, c_sys14;) mock_construct_device!(model, device_model; add_event_model = true) moi_tests(model, 240, 0, 264, 240, 0, false, 24) end @testset "Thermal Dispatch NoMin With DC - PF" begin device_model = DeviceModel(ThermalMultiStart, ThermalDispatchNoMin) c_sys5 = PSB.build_system(PSITestSystems, "c_sys5_pglib") model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys5) @test_throws IS.ConflictingInputsError mock_construct_device!(model, device_model) end @testset "ThermalMultiStart Dispatch NoMin With AC - PF" begin device_model = DeviceModel(ThermalMultiStart, ThermalDispatchNoMin) c_sys5 = PSB.build_system(PSITestSystems, "c_sys5_pglib") model = DecisionModel(MockOperationProblem, ACPPowerModel, c_sys5;) @test_throws IS.ConflictingInputsError mock_construct_device!(model, device_model) end @testset "Operation Model ThermalDispatchNoMin - and PWL Non Convex" begin c_sys5_pwl_ed_nonconvex = PSB.build_system(PSITestSystems, "c_sys5_pwl_ed_nonconvex") template = get_thermal_dispatch_template_network() set_device_model!(template, DeviceModel(ThermalStandard, ThermalDispatchNoMin)) model = DecisionModel( MockOperationProblem, CopperPlatePowerModel, c_sys5_pwl_ed_nonconvex; export_pwl_vars = true, initialize_model = false, ) @test_throws IS.InvalidValue mock_construct_device!( model, DeviceModel(ThermalStandard, ThermalDispatchNoMin), ) end ################################## Ramp Limited Testing ################################## @testset "ThermalStandard with ThermalStandardDispatch With DC - PF" begin constraint_keys = [ PSI.ConstraintKey(RampConstraint, PSY.ThermalStandard, "up"), PSI.ConstraintKey(RampConstraint, PSY.ThermalStandard, "dn"), ] device_model = DeviceModel(ThermalStandard, ThermalStandardDispatch) c_sys5_uc = PSB.build_system(PSITestSystems, "c_sys5_uc") model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys5_uc;) mock_construct_device!(model, device_model) moi_tests(model, 120, 0, 216, 120, 0, false) psi_constraint_test(model, constraint_keys) psi_checkobjfun_test(model, GAEVF) model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys5_uc;) mock_construct_device!(model, device_model; add_event_model = true) moi_tests(model, 120, 0, 240, 120, 0, false) device_model = DeviceModel(ThermalStandard, ThermalStandardDispatch) c_sys14 = PSB.build_system(PSITestSystems, "c_sys14") model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys14;) mock_construct_device!(model, device_model) moi_tests(model, 120, 0, 120, 120, 0, false) psi_checkobjfun_test(model, GQEVF) model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys14;) mock_construct_device!(model, device_model; add_event_model = true) moi_tests(model, 120, 0, 144, 120, 0, false) end @testset "ThermalStandard with ThermalStandardDispatch With AC - PF" begin constraint_keys = [ PSI.ConstraintKey(RampConstraint, PSY.ThermalStandard, "up"), PSI.ConstraintKey(RampConstraint, PSY.ThermalStandard, "dn"), ] device_model = DeviceModel(ThermalStandard, ThermalStandardDispatch) c_sys5_uc = PSB.build_system(PSITestSystems, "c_sys5_uc") model = DecisionModel(MockOperationProblem, ACPPowerModel, c_sys5_uc;) mock_construct_device!(model, device_model) moi_tests(model, 240, 0, 336, 240, 0, false) psi_constraint_test(model, constraint_keys) psi_checkobjfun_test(model, GAEVF) model = DecisionModel(MockOperationProblem, ACPPowerModel, c_sys5_uc;) mock_construct_device!(model, device_model; add_event_model = true) moi_tests(model, 240, 0, 360, 240, 0, false, 24) device_model = DeviceModel(ThermalStandard, ThermalStandardDispatch) c_sys14 = PSB.build_system(PSITestSystems, "c_sys14") model = DecisionModel(MockOperationProblem, ACPPowerModel, c_sys14;) mock_construct_device!(model, device_model) moi_tests(model, 240, 0, 240, 240, 0, false) psi_checkobjfun_test(model, GQEVF) model = DecisionModel(MockOperationProblem, ACPPowerModel, c_sys14;) mock_construct_device!(model, device_model; add_event_model = true) moi_tests(model, 240, 0, 264, 240, 0, false, 24) end @testset "ThermalMultiStart with ThermalStandardDispatch With DC - PF" begin constraint_keys = [ PSI.ConstraintKey(RampConstraint, PSY.ThermalMultiStart, "up"), PSI.ConstraintKey(RampConstraint, PSY.ThermalMultiStart, "dn"), ] device_model = DeviceModel(ThermalMultiStart, ThermalStandardDispatch) c_sys5_uc = PSB.build_system(PSITestSystems, "c_sys5_pglib") model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys5_uc;) mock_construct_device!(model, device_model) moi_tests(model, 240, 0, 144, 48, 96, false) psi_constraint_test(model, constraint_keys) psi_checkobjfun_test(model, GAEVF) model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys5_uc;) mock_construct_device!(model, device_model; add_event_model = true) moi_tests(model, 240, 0, 168, 48, 96, false) end @testset "ThermalMultiStart with ThermalStandardDispatch With AC - PF" begin constraint_keys = [ PSI.ConstraintKey(RampConstraint, PSY.ThermalMultiStart, "up"), PSI.ConstraintKey(RampConstraint, PSY.ThermalMultiStart, "dn"), ] device_model = DeviceModel(ThermalMultiStart, ThermalStandardDispatch) c_sys5_uc = PSB.build_system(PSITestSystems, "c_sys5_pglib") model = DecisionModel(MockOperationProblem, ACPPowerModel, c_sys5_uc;) mock_construct_device!(model, device_model) moi_tests(model, 288, 0, 192, 96, 96, false) psi_constraint_test(model, constraint_keys) psi_checkobjfun_test(model, GAEVF) model = DecisionModel(MockOperationProblem, ACPPowerModel, c_sys5_uc;) mock_construct_device!(model, device_model; add_event_model = true) moi_tests(model, 288, 0, 216, 96, 96, false, 24) end ################################### ThermalMultiStart Testing ############################## @testset "Thermal MultiStart with MultiStart UC and DC - PF" begin constraint_keys = [ PSI.ConstraintKey(ActiveRangeICConstraint, PSY.ThermalMultiStart), PSI.ConstraintKey(StartTypeConstraint, PSY.ThermalMultiStart), PSI.ConstraintKey( StartupTimeLimitTemperatureConstraint, PSY.ThermalMultiStart, "warm", ), PSI.ConstraintKey( StartupTimeLimitTemperatureConstraint, PSY.ThermalMultiStart, "hot", ), PSI.ConstraintKey( StartupInitialConditionConstraint, PSY.ThermalMultiStart, "lb", ), PSI.ConstraintKey( StartupInitialConditionConstraint, PSY.ThermalMultiStart, "ub", ), ] device_model = DeviceModel(PSY.ThermalMultiStart, PSI.ThermalMultiStartUnitCommitment) c_sys5_pglib = PSB.build_system(PSITestSystems, "c_sys5_pglib") model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys5_pglib;) mock_construct_device!(model, device_model) moi_tests(model, 528, 0, 282, 108, 192, true) psi_constraint_test(model, constraint_keys) psi_checkobjfun_test(model, GAEVF) model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys5_pglib;) mock_construct_device!(model, device_model; add_event_model = true) moi_tests(model, 528, 0, 306, 108, 192, true) end @testset "Thermal MultiStart with MultiStart UC and AC - PF" begin constraint_keys = [ PSI.ConstraintKey(ActiveRangeICConstraint, PSY.ThermalMultiStart), PSI.ConstraintKey(StartTypeConstraint, PSY.ThermalMultiStart), PSI.ConstraintKey( StartupTimeLimitTemperatureConstraint, PSY.ThermalMultiStart, "warm", ), PSI.ConstraintKey( StartupTimeLimitTemperatureConstraint, PSY.ThermalMultiStart, "hot", ), PSI.ConstraintKey( StartupInitialConditionConstraint, PSY.ThermalMultiStart, "lb", ), PSI.ConstraintKey( StartupInitialConditionConstraint, PSY.ThermalMultiStart, "ub", ), ] device_model = DeviceModel(PSY.ThermalMultiStart, PSI.ThermalMultiStartUnitCommitment) c_sys5_pglib = PSB.build_system(PSITestSystems, "c_sys5_pglib") model = DecisionModel(MockOperationProblem, ACPPowerModel, c_sys5_pglib;) mock_construct_device!(model, device_model) moi_tests(model, 576, 0, 330, 156, 192, true) psi_constraint_test(model, constraint_keys) psi_checkobjfun_test(model, GAEVF) model = DecisionModel(MockOperationProblem, ACPPowerModel, c_sys5_pglib;) mock_construct_device!(model, device_model; add_event_model = true) moi_tests(model, 576, 0, 354, 156, 192, true, 24) end ################################ Thermal Compact UC Testing ################################ @testset "Thermal Standard with Compact UC and DC - PF" begin device_model = DeviceModel(PSY.ThermalStandard, PSI.ThermalCompactUnitCommitment) c_sys5 = PSB.build_system(PSITestSystems, "c_sys5") model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys5) mock_construct_device!(model, device_model) moi_tests(model, 480, 0, 480, 120, 120, true) psi_checkobjfun_test(model, GAEVF) model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys5) mock_construct_device!(model, device_model; add_event_model = true) moi_tests(model, 480, 0, 504, 120, 120, true) end @testset "Thermal MultiStart with Compact UC and DC - PF" begin device_model = DeviceModel(PSY.ThermalMultiStart, PSI.ThermalCompactUnitCommitment) c_sys5_pglib = PSB.build_system(PSITestSystems, "c_sys5_pglib") model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys5_pglib;) mock_construct_device!(model, device_model) moi_tests(model, 384, 0, 240, 48, 144, true) psi_checkobjfun_test(model, GAEVF) model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys5_pglib;) mock_construct_device!(model, device_model; add_event_model = true) moi_tests(model, 384, 0, 264, 48, 144, true) end @testset "Thermal Standard with Compact UC and AC - PF" begin device_model = DeviceModel(PSY.ThermalStandard, PSI.ThermalCompactUnitCommitment) c_sys5 = PSB.build_system(PSITestSystems, "c_sys5") model = DecisionModel(MockOperationProblem, ACPPowerModel, c_sys5) mock_construct_device!(model, device_model) moi_tests(model, 600, 0, 600, 240, 120, true) psi_checkobjfun_test(model, GAEVF) model = DecisionModel(MockOperationProblem, ACPPowerModel, c_sys5) mock_construct_device!(model, device_model; add_event_model = true) moi_tests(model, 600, 0, 624, 240, 120, true, 24) end @testset "Thermal MultiStart with Compact UC and AC - PF" begin device_model = DeviceModel(PSY.ThermalMultiStart, PSI.ThermalCompactUnitCommitment) c_sys5_pglib = PSB.build_system(PSITestSystems, "c_sys5_pglib") model = DecisionModel(MockOperationProblem, ACPPowerModel, c_sys5_pglib;) mock_construct_device!(model, device_model) moi_tests(model, 432, 0, 288, 96, 144, true) psi_checkobjfun_test(model, GAEVF) model = DecisionModel(MockOperationProblem, ACPPowerModel, c_sys5_pglib;) mock_construct_device!(model, device_model; add_event_model = true) moi_tests(model, 432, 0, 312, 96, 144, true, 24) end ################################ Thermal Basic Compact UC Testing ################################ @testset "Thermal Standard with Compact UC and DC - PF" begin device_model = DeviceModel(PSY.ThermalStandard, PSI.ThermalBasicCompactUnitCommitment) c_sys5 = PSB.build_system(PSITestSystems, "c_sys5") model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys5) mock_construct_device!(model, device_model) moi_tests(model, 480, 0, 240, 120, 120, true) psi_checkobjfun_test(model, GAEVF) model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys5) mock_construct_device!(model, device_model; add_event_model = true) moi_tests(model, 480, 0, 264, 120, 120, true) end @testset "Thermal MultiStart with Compact UC and DC - PF" begin device_model = DeviceModel(PSY.ThermalMultiStart, PSI.ThermalBasicCompactUnitCommitment) c_sys5_pglib = PSB.build_system(PSITestSystems, "c_sys5_pglib") model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys5_pglib;) mock_construct_device!(model, device_model) moi_tests(model, 384, 0, 96, 48, 144, true) psi_checkobjfun_test(model, GAEVF) model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys5_pglib;) mock_construct_device!(model, device_model; add_event_model = true) moi_tests(model, 384, 0, 120, 48, 144, true) end @testset "Thermal Standard with Compact UC and AC - PF" begin device_model = DeviceModel(PSY.ThermalStandard, PSI.ThermalBasicCompactUnitCommitment) c_sys5 = PSB.build_system(PSITestSystems, "c_sys5") model = DecisionModel(MockOperationProblem, ACPPowerModel, c_sys5) mock_construct_device!(model, device_model) moi_tests(model, 600, 0, 360, 240, 120, true) psi_checkobjfun_test(model, GAEVF) model = DecisionModel(MockOperationProblem, ACPPowerModel, c_sys5) mock_construct_device!(model, device_model; add_event_model = true) moi_tests(model, 600, 0, 384, 240, 120, true, 24) end @testset "Thermal MultiStart with Compact UC and AC - PF" begin device_model = DeviceModel(PSY.ThermalMultiStart, PSI.ThermalBasicCompactUnitCommitment) c_sys5_pglib = PSB.build_system(PSITestSystems, "c_sys5_pglib") model = DecisionModel(MockOperationProblem, ACPPowerModel, c_sys5_pglib;) mock_construct_device!(model, device_model) moi_tests(model, 432, 0, 144, 96, 144, true) psi_checkobjfun_test(model, GAEVF) model = DecisionModel(MockOperationProblem, ACPPowerModel, c_sys5_pglib;) mock_construct_device!(model, device_model; add_event_model = true) moi_tests(model, 432, 0, 168, 96, 144, true, 24) end ############################ Thermal Compact Dispatch Testing ############################## @testset "Thermal Standard with Compact Dispatch and DC - PF" begin device_model = DeviceModel(PSY.ThermalStandard, PSI.ThermalCompactDispatch) c_sys5 = PSB.build_system(PSITestSystems, "c_sys5") model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys5) mock_construct_device!(model, device_model; built_for_recurrent_solves = true) moi_tests(model, 245, 0, 144, 144, 0, false) psi_checkobjfun_test(model, GAEVF) model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys5) mock_construct_device!( model, device_model; built_for_recurrent_solves = true, add_event_model = true, ) moi_tests(model, 293, 0, 168, 144, 0, false) end @testset "Thermal MultiStart with Compact Dispatch and DC - PF" begin device_model = DeviceModel(PSY.ThermalMultiStart, PSI.ThermalCompactDispatch) c_sys5_pglib = PSB.build_system(PSITestSystems, "c_sys5_pglib") model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys5_pglib) mock_construct_device!(model, device_model; built_for_recurrent_solves = true) moi_tests(model, 290, 0, 96, 96, 96, false) psi_checkobjfun_test(model, GAEVF) model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys5_pglib) mock_construct_device!( model, device_model; built_for_recurrent_solves = true, add_event_model = true, ) moi_tests(model, 338, 0, 120, 96, 96, false) end @testset "Thermal Standard with Compact Dispatch and AC - PF" begin device_model = DeviceModel(PSY.ThermalStandard, PSI.ThermalCompactDispatch) c_sys5 = PSB.build_system(PSITestSystems, "c_sys5") model = DecisionModel(MockOperationProblem, ACPPowerModel, c_sys5) mock_construct_device!(model, device_model; built_for_recurrent_solves = true) moi_tests(model, 365, 0, 264, 264, 0, false) psi_checkobjfun_test(model, GAEVF) model = DecisionModel(MockOperationProblem, ACPPowerModel, c_sys5) mock_construct_device!( model, device_model; built_for_recurrent_solves = true, add_event_model = true, ) moi_tests(model, 413, 0, 288, 264, 0, false, 24) end @testset "Thermal MultiStart with Compact Dispatch and AC - PF" begin device_model = DeviceModel(PSY.ThermalMultiStart, PSI.ThermalCompactDispatch) c_sys5_pglib = PSB.build_system(PSITestSystems, "c_sys5_pglib") model = DecisionModel(MockOperationProblem, ACPPowerModel, c_sys5_pglib) mock_construct_device!(model, device_model; built_for_recurrent_solves = true) moi_tests(model, 338, 0, 144, 144, 96, false) psi_checkobjfun_test(model, GAEVF) model = DecisionModel(MockOperationProblem, ACPPowerModel, c_sys5_pglib) mock_construct_device!( model, device_model; built_for_recurrent_solves = true, add_event_model = true, ) moi_tests(model, 386, 0, 168, 144, 96, false), 24 end ############################# Model validation tests ####################################### @testset "Solving ED with CopperPlate for testing Ramping Constraints" begin ramp_test_sys = PSB.build_system(PSITestSystems, "c_ramp_test") template = ProblemTemplate(CopperPlatePowerModel) set_device_model!(template, ThermalStandard, ThermalStandardDispatch) set_device_model!(template, PowerLoad, StaticPowerLoad) ED = DecisionModel( EconomicDispatchProblem, template, ramp_test_sys; optimizer = HiGHS_optimizer, initialize_model = false, ) @test build!(ED; output_dir = mktempdir(; cleanup = true)) == PSI.ModelBuildStatus.BUILT moi_tests(ED, 10, 0, 20, 10, 5, false) psi_checksolve_test(ED, [MOI.OPTIMAL], 11191.00) end # Testing Duration Constraints @testset "Solving UC with CopperPlate for testing Duration Constraints" begin template = get_thermal_standard_uc_template() UC = DecisionModel( UnitCommitmentProblem, template, PSB.build_system(PSITestSystems, "c_duration_test"); optimizer = HiGHS_optimizer, initialize_model = false, store_variable_names = true, ) 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 @testset "Solving UC Models with Linear Networks" begin c_sys5 = PSB.build_system(PSITestSystems, "c_sys5") c_sys5_dc = PSB.build_system(PSITestSystems, "c_sys5_dc") systems = [c_sys5, c_sys5_dc] networks = [ DCPPowerModel, NFAPowerModel, PTDFPowerModel, CopperPlatePowerModel, ] commitment_models = [ThermalStandardUnitCommitment, ThermalCompactUnitCommitment] for net in networks, sys in systems, model in commitment_models template = get_thermal_dispatch_template_network( NetworkModel(net), ) set_device_model!(template, ThermalStandard, model) UC = DecisionModel(template, sys; optimizer = HiGHS_optimizer) @test build!(UC; output_dir = mktempdir(; cleanup = true)) == PSI.ModelBuildStatus.BUILT psi_checksolve_test(UC, [MOI.OPTIMAL, MOI.LOCALLY_SOLVED], 340000, 100000) end end @testset "Test Feedforwards to ThermalStandard with ThermalStandardDispatch" begin device_model = DeviceModel(ThermalStandard, ThermalStandardDispatch) ff_sc = SemiContinuousFeedforward(; component_type = ThermalStandard, source = OnVariable, affected_values = [ActivePowerVariable], ) ff_ub = UpperBoundFeedforward(; component_type = ThermalStandard, source = ActivePowerVariable, affected_values = [ActivePowerVariable], ) PSI.attach_feedforward!(device_model, ff_sc) PSI.attach_feedforward!(device_model, ff_ub) c_sys5 = PSB.build_system(PSITestSystems, "c_sys5") model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys5) mock_construct_device!(model, device_model; built_for_recurrent_solves = true) moi_tests(model, 365, 0, 288, 120, 0, false) model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys5) mock_construct_device!( model, device_model; built_for_recurrent_solves = true, add_event_model = true, ) moi_tests(model, 413, 0, 312, 120, 0, false) end @testset "Test Feedforwards to ThermalStandard with ThermalBasicDispatch" begin device_model = DeviceModel(ThermalStandard, ThermalBasicDispatch) ff_sc = SemiContinuousFeedforward(; component_type = ThermalStandard, source = OnVariable, affected_values = [ActivePowerVariable], ) ff_ub = UpperBoundFeedforward(; component_type = ThermalStandard, source = ActivePowerVariable, affected_values = [ActivePowerVariable], ) PSI.attach_feedforward!(device_model, ff_sc) PSI.attach_feedforward!(device_model, ff_ub) c_sys5 = PSB.build_system(PSITestSystems, "c_sys5") model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys5) mock_construct_device!(model, device_model; built_for_recurrent_solves = true) moi_tests(model, 360, 0, 240, 120, 0, false) model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys5) mock_construct_device!( model, device_model; built_for_recurrent_solves = true, add_event_model = true, ) moi_tests(model, 408, 0, 264, 120, 0, false) end @testset "Test Feedforwards to ThermalStandard with ThermalCompactDispatch" begin device_model = DeviceModel(PSY.ThermalStandard, PSI.ThermalCompactDispatch) ff_sc = SemiContinuousFeedforward(; component_type = ThermalStandard, source = OnVariable, affected_values = [PowerAboveMinimumVariable], ) ff_ub = UpperBoundFeedforward(; component_type = ThermalStandard, source = PSI.PowerAboveMinimumVariable, affected_values = [PSI.PowerAboveMinimumVariable], ) PSI.attach_feedforward!(device_model, ff_sc) PSI.attach_feedforward!(device_model, ff_ub) c_sys5 = PSB.build_system(PSITestSystems, "c_sys5") model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys5) mock_construct_device!(model, device_model; built_for_recurrent_solves = true) moi_tests(model, 365, 0, 264, 144, 0, false) model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys5) mock_construct_device!( model, device_model; built_for_recurrent_solves = true, add_event_model = true, ) moi_tests(model, 413, 0, 288, 144, 0, false) end @testset "Test Feedforwards to ThermalMultiStart with ThermalStandardDispatch" begin device_model = DeviceModel(ThermalMultiStart, ThermalStandardDispatch) ff_sc = SemiContinuousFeedforward(; component_type = ThermalMultiStart, source = OnVariable, affected_values = [ActivePowerVariable], ) ff_ub = UpperBoundFeedforward(; component_type = ThermalMultiStart, source = ActivePowerVariable, affected_values = [ActivePowerVariable], ) PSI.attach_feedforward!(device_model, ff_sc) PSI.attach_feedforward!(device_model, ff_ub) c_sys5 = PSB.build_system(PSITestSystems, "c_sys5_pglib") model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys5) mock_construct_device!(model, device_model; built_for_recurrent_solves = true) moi_tests(model, 338, 0, 192, 48, 96, false) model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys5) mock_construct_device!( model, device_model; built_for_recurrent_solves = true, add_event_model = true, ) moi_tests(model, 386, 0, 216, 48, 96, false) end @testset "Test Feedforwards to ThermalMultiStart with ThermalBasicDispatch" begin device_model = DeviceModel(ThermalMultiStart, ThermalBasicDispatch) ff_sc = SemiContinuousFeedforward(; component_type = ThermalMultiStart, source = OnVariable, affected_values = [ActivePowerVariable], ) ff_ub = UpperBoundFeedforward(; component_type = ThermalMultiStart, source = ActivePowerVariable, affected_values = [ActivePowerVariable], ) PSI.attach_feedforward!(device_model, ff_sc) PSI.attach_feedforward!(device_model, ff_ub) c_sys5 = PSB.build_system(PSITestSystems, "c_sys5_pglib") model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys5) mock_construct_device!(model, device_model; built_for_recurrent_solves = true) moi_tests(model, 336, 0, 96, 48, 96, false) model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys5) mock_construct_device!( model, device_model; built_for_recurrent_solves = true, add_event_model = true, ) moi_tests(model, 384, 0, 120, 48, 96, false) end @testset "Test Feedforwards to ThermalMultiStart with ThermalCompactDispatch" begin device_model = DeviceModel(PSY.ThermalMultiStart, PSI.ThermalCompactDispatch) ff_sc = SemiContinuousFeedforward(; component_type = ThermalMultiStart, source = OnVariable, affected_values = [PSI.PowerAboveMinimumVariable], ) ff_ub = UpperBoundFeedforward(; component_type = ThermalMultiStart, source = PSI.PowerAboveMinimumVariable, affected_values = [PSI.PowerAboveMinimumVariable], ) PSI.attach_feedforward!(device_model, ff_sc) PSI.attach_feedforward!(device_model, ff_ub) c_sys5 = PSB.build_system(PSITestSystems, "c_sys5_pglib") model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys5) mock_construct_device!(model, device_model; built_for_recurrent_solves = true) moi_tests(model, 338, 0, 144, 96, 96, false) model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys5) mock_construct_device!( model, device_model; built_for_recurrent_solves = true, add_event_model = true, ) moi_tests(model, 386, 0, 168, 96, 96, false) end @testset "Test Must Run ThermalGen" begin sys_5 = build_system(PSITestSystems, "c_sys5_uc") template_uc = ProblemTemplate(NetworkModel(CopperPlatePowerModel)) 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, StaticBranchUnbounded)) # Set Must Run the most expensive one: Sundance sundance = get_component(ThermalStandard, sys_5, "Sundance") set_must_run!(sundance, true) for rebuild in [true, false] model = DecisionModel( template_uc, sys_5; name = "UC", optimizer = HiGHS_optimizer, store_variable_names = true, rebuild_model = rebuild, ) solve!(model; output_dir = mktempdir()) ptdf_vars = read_variables( OptimizationProblemResults(model); table_format = TableFormat.WIDE, ) power = ptdf_vars["ActivePowerVariable__ThermalStandard"] on = ptdf_vars["OnVariable__ThermalStandard"] start = ptdf_vars["StartVariable__ThermalStandard"] stop = ptdf_vars["StopVariable__ThermalStandard"] power_sundance = power[!, "Sundance"] @test all(power_sundance .>= 1.0) for v in [on, start, stop] @test "Sundance" ∉ names(v) end end end @testset "Thermal with max_active_power time series" begin device_model = DeviceModel( ThermalStandard, ThermalStandardUnitCommitment; time_series_names = Dict(ActivePowerTimeSeriesParameter => "max_active_power")) c_sys5 = PSB.build_system(PSITestSystems, "c_sys5") derate_data = SortedDict{Dates.DateTime, TimeSeries.TimeArray}() data_ts = collect( DateTime("1/1/2024 0:00:00", "d/m/y H:M:S"):Hour(1):DateTime( "1/1/2024 23:00:00", "d/m/y H:M:S", ), ) for t in 1:2 ini_time = data_ts[1] + Day(t - 1) derate_data[ini_time] = TimeArray(data_ts + Day(t - 1), fill!(Vector{Float64}(undef, 24), 0.8)) end solitude = get_component(ThermalStandard, c_sys5, "Solitude") PSY.add_time_series!( c_sys5, solitude, PSY.Deterministic("max_active_power", derate_data), ) model = DecisionModel( MockOperationProblem, DCPPowerModel, c_sys5) mock_construct_device!(model, device_model) moi_tests(model, 480, 0, 504, 120, 120, true) key = PSI.ConstraintKey( ActivePowerVariableTimeSeriesLimitsConstraint, ThermalStandard, "ub", ) constraint = PSI.get_constraint(PSI.get_optimization_container(model), key) ub_value = get_max_active_power(solitude) * 0.8 for ix in eachindex(constraint) @test JuMP.normalized_rhs(constraint[ix]) == ub_value end psi_checkobjfun_test(model, GAEVF) model = DecisionModel( MockOperationProblem, DCPPowerModel, c_sys5) mock_construct_device!(model, device_model; add_event_model = true) moi_tests(model, 480, 0, 528, 120, 120, true) end @testset "Thermal with fuel cost time series" begin sys = PSB.build_system(PSITestSystems, "c_sys5_re_fuel_cost") template = ProblemTemplate( NetworkModel( CopperPlatePowerModel; duals = [CopperPlateBalanceConstraint], ), ) set_device_model!(template, ThermalStandard, ThermalDispatchNoMin) set_device_model!(template, PowerLoad, StaticPowerLoad) set_device_model!(template, RenewableDispatch, RenewableFullDispatch) model = DecisionModel( template, sys; name = "UC", optimizer = HiGHS_optimizer, store_variable_names = true, optimizer_solve_log_print = false, ) models = SimulationModels(; decision_models = [ model, ], ) sequence = SimulationSequence(; models = models, feedforwards = Dict( ), ini_cond_chronology = InterProblemChronology(), ) sim = Simulation(; name = "compact_sim", steps = 2, models = models, sequence = sequence, initial_time = TIME1, simulation_folder = mktempdir(), ) build!(sim; console_level = Logging.Error) moi_tests(model, 432, 0, 192, 120, 72, false) execute!(sim) sim_res = SimulationResults(sim) res_uc = get_decision_problem_results(sim_res, "UC") # Test time series <-> parameter correspondence fc_uc = read_parameter( res_uc, PSI.FuelCostParameter, PSY.ThermalStandard; table_format = TableFormat.WIDE, ) for (step_dt, step_df) in pairs(fc_uc) for gen_name in names(DataFrames.select(step_df, Not(:DateTime))) fc_comp = get_fuel_cost( get_component(ThermalStandard, sys, gen_name); start_time = step_dt, ) @test all(step_df[!, :DateTime] .== TimeSeries.timestamp(fc_comp)) @test all(isapprox.(step_df[!, gen_name], TimeSeries.values(fc_comp))) end end # Test effect on decision th_uc = read_realized_variable( res_uc, "ActivePowerVariable__ThermalStandard"; table_format = TableFormat.WIDE, ) p_brighton = th_uc[!, "Brighton"] p_solitude = th_uc[!, "Solitude"] @test sum(p_brighton[1:24]) < 50.0 # Barely used when expensive @test sum(p_brighton[25:48]) > 5000.0 # Used a lot when cheap @test sum(p_solitude[1:24]) > 5000.0 # Used a lot when cheap @test sum(p_solitude[25:48]) < 50.0 # Barely used when expensive end @testset "Thermal with fuel cost time series with Quadratic and PWL" begin sys = PSB.build_system(PSITestSystems, "c_sys5_re_fuel_cost") template = ProblemTemplate( NetworkModel( CopperPlatePowerModel; duals = [CopperPlateBalanceConstraint], ), ) solitude = get_component(ThermalStandard, sys, "Solitude") op_cost = get_operation_cost(solitude) ts = deepcopy(get_time_series(Deterministic, solitude, "fuel_cost")) remove_time_series!(sys, Deterministic, solitude, "fuel_cost") quad_curve = QuadraticCurve(0.05, 1.0, 0.0) new_th_cost = ThermalGenerationCost(; variable = FuelCurve(; value_curve = quad_curve, fuel_cost = 1.0, ), fixed = op_cost.fixed, start_up = op_cost.start_up, shut_down = op_cost.shut_down, ) set_operation_cost!(solitude, new_th_cost) add_time_series!( sys, solitude, ts, ) # There is no free MIQP solver, we need to use ThermalDisptchNoMin for testing set_device_model!(template, ThermalStandard, ThermalDispatchNoMin) set_device_model!(template, PowerLoad, StaticPowerLoad) set_device_model!(template, RenewableDispatch, RenewableFullDispatch) model = DecisionModel( template, sys; name = "UC", optimizer = ipopt_optimizer, store_variable_names = true, optimizer_solve_log_print = false, ) @test build!(model; output_dir = mktempdir(; cleanup = true)) == PSI.ModelBuildStatus.BUILT solve!(model) moi_tests(model, 288, 0, 192, 120, 72, false) container = PSI.get_optimization_container(model) @test isa( PSI.get_invariant_terms(PSI.get_objective_expression(container)), JuMP.QuadExpr, ) end @testset "Thermal UC With Slack on Ramps" begin bin_variable_keys = [ PSI.VariableKey(OnVariable, PSY.ThermalStandard), PSI.VariableKey(StartVariable, PSY.ThermalStandard), PSI.VariableKey(StopVariable, PSY.ThermalStandard), ] uc_constraint_keys = [ PSI.ConstraintKey(RampConstraint, PSY.ThermalStandard, "up"), PSI.ConstraintKey(RampConstraint, PSY.ThermalStandard, "dn"), PSI.ConstraintKey(DurationConstraint, PSY.ThermalStandard, "up"), PSI.ConstraintKey(DurationConstraint, PSY.ThermalStandard, "dn"), ] aux_variables_keys = [ PSI.AuxVarKey(PSI.TimeDurationOff, ThermalStandard), PSI.AuxVarKey(PSI.TimeDurationOn, ThermalStandard), ] # Unit Commitment # device_model = DeviceModel(ThermalStandard, ThermalStandardUnitCommitment; use_slacks = true) c_sys5_uc = PSB.build_system(PSITestSystems, "c_sys5_uc") model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys5_uc) mock_construct_device!(model, device_model) moi_tests(model, 720, 0, 480, 120, 120, true) psi_constraint_test(model, uc_constraint_keys) psi_checkbinvar_test(model, bin_variable_keys) psi_checkobjfun_test(model, GAEVF) psi_aux_variable_test(model, aux_variables_keys) model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys5_uc) mock_construct_device!(model, device_model; add_event_model = true) moi_tests(model, 720, 0, 504, 120, 120, true) device_model = DeviceModel(ThermalStandard, ThermalStandardUnitCommitment; use_slacks = true) c_sys14 = PSB.build_system(PSITestSystems, "c_sys14") model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys14) mock_construct_device!(model, device_model) moi_tests(model, 720, 0, 240, 120, 120, true) psi_checkbinvar_test(model, bin_variable_keys) psi_checkobjfun_test(model, GQEVF) model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys14) mock_construct_device!(model, device_model; add_event_model = true) moi_tests(model, 720, 0, 264, 120, 120, true) # Dispatch # device_model = DeviceModel(ThermalStandard, ThermalStandardDispatch; use_slacks = true) uc_constraint_keys = [ PSI.ConstraintKey(RampConstraint, PSY.ThermalStandard, "up"), PSI.ConstraintKey(RampConstraint, PSY.ThermalStandard, "dn"), ] c_sys5_uc = PSB.build_system(PSITestSystems, "c_sys5_uc") model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys5_uc) mock_construct_device!(model, device_model) moi_tests(model, 360, 0, 216, 120, 0, false) psi_constraint_test(model, uc_constraint_keys) psi_checkobjfun_test(model, GAEVF) model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys5_uc) mock_construct_device!(model, device_model; add_event_model = true) moi_tests(model, 360, 0, 240, 120, 0, false) device_model = DeviceModel(ThermalStandard, ThermalStandardDispatch; use_slacks = true) c_sys14 = PSB.build_system(PSITestSystems, "c_sys14") model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys14) mock_construct_device!(model, device_model) moi_tests(model, 360, 0, 120, 120, 0, false) psi_checkobjfun_test(model, GQEVF) model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys14) mock_construct_device!(model, device_model; add_event_model = true) moi_tests(model, 360, 0, 144, 120, 0, false) end @testset "ThermalDispatchNoMin with PWL Costs" begin sys = build_system(PSISystems, "modified_RTS_GMLC_DA_sys") template = ProblemTemplate(NetworkModel(PTDFPowerModel)) set_device_model!(template, ThermalStandard, ThermalDispatchNoMin) set_device_model!(template, Line, StaticBranchBounds) set_device_model!(template, TapTransformer, StaticBranchBounds) set_device_model!(template, Transformer2W, StaticBranchBounds) set_device_model!(template, PowerLoad, StaticPowerLoad) solver = HiGHS_optimizer problem = DecisionModel(template, sys; optimizer = solver, horizon = Hour(1), optimizer_solve_log_print = true, calculate_conflict = true, store_variable_names = true, detailed_optimizer_stats = false, ) build!(problem; output_dir = mktempdir()) solve!(problem) res = OptimizationProblemResults(problem) # Test that plant 101_STEAM_3 (using max power) have proper cost expression cost = read_expression( res, "ProductionCostExpression__ThermalStandard"; table_format = TableFormat.WIDE, ) p_th = read_variable( res, "ActivePowerVariable__ThermalStandard"; table_format = TableFormat.WIDE, ) steam3 = get_component(ThermalStandard, sys, "101_STEAM_3") val_curve = PSY.get_value_curve(PSY.get_variable(PSY.get_operation_cost(steam3))) io_curve = InputOutputCurve(val_curve) fuel_cost = PSY.get_fuel_cost(steam3) x_last = last(io_curve.function_data.points).x y_last = last(io_curve.function_data.points).y * fuel_cost p_steam3 = p_th[!, "101_STEAM_3"] cost_steam3 = cost[!, "101_STEAM_3"] @test isapprox(p_steam3[1], x_last) # max @test isapprox(cost_steam3[1], y_last) # last cost end ================================================ FILE: test/test_events.jl ================================================ ### HOURLY DATA ### #Note: if using basic for ed, emulator fails at timestep after outage due to OutageConstraint_ub @testset "Hourly; uc basic; ed nomin; no ff" begin res = run_events_simulation(; sys_emulator = build_system(PSITestSystems, "c_sys5_events"), networks = repeat([PSI.CopperPlatePowerModel], 3), optimizers = repeat([HiGHS_optimizer_small_gap], 3), outage_time = DateTime("2024-01-01T18:00:00"), outage_length = 3.0, uc_formulation = "basic", ed_formulation = "nomin", feedforward = false, in_memory = true, ) test_event_results(; res = res, outage_time = DateTime("2024-01-01T18:00:00"), outage_length = 3.0, expected_power_recovery = DateTime("2024-01-01T22:00:00"), expected_on_variable_recovery = DateTime("2024-01-01T22:00:00"), ) #Test no ramping constraint in D2 model results d2 = get_decision_problem_results(res, "D2") p_d2 = read_realized_variables(d2; table_format = TableFormat.WIDE)["ActivePowerVariable__ThermalStandard"] p_recover_ix = indexin([DateTime("2024-01-01T22:00:00")], p_d2[!, :DateTime])[1] @test p_d2[p_recover_ix, "Alta"] == 40.0 end #This passes with nomin or basic dispatch @testset "Hourly; uc basic; ed basic; ff" begin res = run_events_simulation(; sys_emulator = build_system(PSITestSystems, "c_sys5_events"), networks = repeat([PSI.CopperPlatePowerModel], 3), optimizers = repeat([HiGHS_optimizer_small_gap], 3), outage_time = DateTime("2024-01-01T18:00:00"), outage_length = 3.0, uc_formulation = "basic", ed_formulation = "basic", #should also pass with nomin feedforward = true, in_memory = true, ) test_event_results(; res = res, outage_time = DateTime("2024-01-01T18:00:00"), outage_length = 3.0, expected_power_recovery = DateTime("2024-01-01T22:00:00"), expected_on_variable_recovery = DateTime("2024-01-01T22:00:00"), ) #Test no ramping constraint in D2 model results d2 = get_decision_problem_results(res, "D2") p_d2 = read_realized_variables(d2; table_format = TableFormat.WIDE)["ActivePowerVariable__ThermalStandard"] p_recover_ix = indexin([DateTime("2024-01-01T22:00:00")], p_d2[!, :DateTime])[1] @test p_d2[p_recover_ix, "Alta"] == 40.0 end # Note: Running a standard UC formulation without a feedforward to the ED is not a feasible modeling setup #Active power can change in Em without regard for OnVariable which messes up initializing the standard UC models. # This tests for both min up and down times being handled properly with events. # Generator not turned back on until 4 hours after the event (event only lasts 3 hours) # Generator is only on for one hour when the event happens; the constraint is bypassed by resetting the TimeDurationOn variable to a large value. @testset "Hourly; uc standard; ed basic; ff" begin res = run_events_simulation(; sys_emulator = build_system(PSITestSystems, "c_sys5_events"), networks = repeat([PSI.CopperPlatePowerModel], 3), optimizers = repeat([HiGHS_optimizer_small_gap], 3), outage_time = DateTime("2024-01-01T17:00:00"), outage_length = 3.0, uc_formulation = "standard", ed_formulation = "basic", #should also pass with nomin feedforward = true, in_memory = true, ) test_event_results(; res = res, outage_time = DateTime("2024-01-01T17:00:00"), outage_length = 3.0, expected_power_recovery = DateTime("2024-01-01T22:00:00"), expected_on_variable_recovery = DateTime("2024-01-01T22:00:00"), ) #Test ramping constraint in D2 model results d2 = get_decision_problem_results(res, "D2") p_d2 = read_realized_variables(d2; table_format = TableFormat.WIDE)["ActivePowerVariable__ThermalStandard"] p_recover_ix = indexin([DateTime("2024-01-01T22:00:00")], p_d2[!, :DateTime])[1] @test p_d2[p_recover_ix, "Alta"] < 40.0 end ### 5 MINUTE DATA (RESOLUTION MISMATCH) ### #Note: if using basic for ed, emulator fails at timestep after outage due to OutageConstraint_ub @testset "5 min; uc basic; ed nomin; no ff" begin res = run_events_simulation(; sys_emulator = build_system(PSITestSystems, "c_sys5_events_rt"), networks = repeat([PSI.CopperPlatePowerModel], 3), optimizers = repeat([HiGHS_optimizer_small_gap], 3), outage_time = DateTime("2024-01-01T18:00:00"), outage_length = 3.0, uc_formulation = "basic", ed_formulation = "nomin", feedforward = false, in_memory = true, ) test_event_results(; res = res, outage_time = DateTime("2024-01-01T18:00:00"), outage_length = 3.0, expected_power_recovery = DateTime("2024-01-01T21:05:00"), expected_on_variable_recovery = DateTime("2024-01-01T22:00:00"), ) end @testset "5 min; uc basic; ed basic; ff" begin res = run_events_simulation(; sys_emulator = build_system(PSITestSystems, "c_sys5_events_rt"), networks = repeat([PSI.CopperPlatePowerModel], 3), optimizers = repeat([HiGHS_optimizer_small_gap], 3), outage_time = DateTime("2024-01-01T18:00:00"), outage_length = 3.0, uc_formulation = "basic", ed_formulation = "basic", feedforward = true, in_memory = false, ) test_event_results(; res = res, outage_time = DateTime("2024-01-01T18:00:00"), outage_length = 3.0, expected_power_recovery = DateTime("2024-01-01T22:00:00"), expected_on_variable_recovery = DateTime("2024-01-01T22:00:00"), ) end # Note: Running a standard UC formulation without a feedforward to the ED is not a feasible modeling setup #Active power can change in Em without regard for OnVariable which messes up initializing the standard UC models. @testset "5 min; uc standard; ed basic; ff" begin res = run_events_simulation(; sys_emulator = build_system(PSITestSystems, "c_sys5_events_rt"), networks = repeat([PSI.CopperPlatePowerModel], 3), optimizers = repeat([HiGHS_optimizer_small_gap], 3), outage_time = DateTime("2024-01-01T17:00:00"), outage_length = 3.0, uc_formulation = "standard", ed_formulation = "basic", #should also pass with nomin feedforward = true, in_memory = true, ) test_event_results(; res = res, outage_time = DateTime("2024-01-01T17:00:00"), outage_length = 3.0, expected_power_recovery = DateTime("2024-01-01T22:00:00"), expected_on_variable_recovery = DateTime("2024-01-01T22:00:00"), ) #Test ramping constraint in D2 model results d2 = get_decision_problem_results(res, "D2") p_d2 = read_realized_variables(d2; table_format = TableFormat.WIDE)["ActivePowerVariable__ThermalStandard"] p_recover_ix = indexin([DateTime("2024-01-01T22:00:00")], p_d2[!, :DateTime])[1] @test p_d2[p_recover_ix, "Alta"] < 40.0 end @testset "FixedForcedOutage with timeseries" begin dates_ts = collect( DateTime("2024-01-01T00:00:00"):Hour(1):DateTime("2024-01-02T23:00:00"), ) outage_data = fill!(Vector{Int64}(undef, 48), 0) outage_data[3] = 1 outage_data[10:11] .= 1 outage_data[23:22] .= 1 outage_timeseries = TimeArray(dates_ts, outage_data) res = run_fixed_forced_outage_sim_with_timeseries(; sys = build_system(PSITestSystems, "c_sys5_events"), networks = repeat([PSI.CopperPlatePowerModel], 3), optimizers = repeat([HiGHS_optimizer_small_gap], 3), outage_status_timeseries = outage_timeseries, device_type = ThermalStandard, device_names = ["Alta"], renewable_formulation = RenewableFullDispatch, ) em = get_emulation_problem_results(res) status = read_realized_variable( em, "AvailableStatusParameter__ThermalStandard"; table_format = TableFormat.WIDE, ) apv = read_realized_variable( em, "ActivePowerVariable__ThermalStandard"; table_format = TableFormat.WIDE, ) for (ix, x) in enumerate(outage_data[1:24]) @test x != Int64(status[!, "Alta"][ix]) if Int64(status[!, "Alta"][ix]) == 0.0 @test apv[!, "Alta"][ix] == 0.0 end end end @testset "Renewable outage" begin dates_ts = collect( DateTime("2024-01-01T00:00:00"):Hour(1):DateTime("2024-01-02T23:00:00"), ) outage_data = fill!(Vector{Int64}(undef, 48), 0) outage_data[3] = 1 outage_data[10:11] .= 1 outage_data[23:22] .= 1 outage_timeseries = TimeArray(dates_ts, outage_data) res = run_fixed_forced_outage_sim_with_timeseries(; sys = build_system(PSITestSystems, "c_sys5_events"), networks = repeat([PSI.CopperPlatePowerModel], 3), optimizers = repeat([HiGHS_optimizer_small_gap], 3), outage_status_timeseries = outage_timeseries, device_type = RenewableDispatch, device_names = ["WindBus1"], renewable_formulation = RenewableFullDispatch, ) em = get_emulation_problem_results(res) status = read_realized_variable( em, "AvailableStatusParameter__RenewableDispatch"; table_format = TableFormat.WIDE, ) apv = read_realized_variable( em, "ActivePowerVariable__RenewableDispatch"; table_format = TableFormat.WIDE, ) for (ix, x) in enumerate(outage_data[1:24]) @test x != Int64(status[!, "WindBus1"][ix]) if Int64(status[!, "WindBus1"][ix]) == 0.0 @test apv[!, "WindBus1"][ix] == 0.0 end end end @testset "Load outage" begin dates_ts = collect( DateTime("2024-01-01T00:00:00"):Hour(1):DateTime("2024-01-02T23:00:00"), ) outage_data = fill!(Vector{Int64}(undef, 48), 0) outage_data[3] = 1 outage_data[10:11] .= 1 outage_data[23:22] .= 1 outage_timeseries = TimeArray(dates_ts, outage_data) res = run_fixed_forced_outage_sim_with_timeseries(; sys = build_system(PSITestSystems, "c_sys5_events"), networks = repeat([PSI.CopperPlatePowerModel], 3), optimizers = repeat([HiGHS_optimizer_small_gap], 3), outage_status_timeseries = outage_timeseries, device_type = InterruptiblePowerLoad, device_names = ["IloadBus4"], renewable_formulation = RenewableFullDispatch, ) em = get_emulation_problem_results(res) status = read_realized_variable( em, "AvailableStatusParameter__InterruptiblePowerLoad"; table_format = TableFormat.WIDE, ) apv = read_realized_variable( em, "ActivePowerVariable__InterruptiblePowerLoad"; table_format = TableFormat.WIDE, ) for (ix, x) in enumerate(outage_data[1:24]) @test x != Int64(status[!, "IloadBus4"][ix]) if Int64(status[!, "IloadBus4"][ix]) == 0.0 @test apv[!, "IloadBus4"][ix] == 0.0 end end end @testset "StaticPowerLoad outage" begin dates_ts = collect( DateTime("2024-01-01T00:00:00"):Hour(1):DateTime("2024-01-02T23:00:00"), ) outage_data = fill!(Vector{Int64}(undef, 48), 0) outage_timeseries = TimeArray(dates_ts, outage_data) res = run_fixed_forced_outage_sim_with_timeseries(; sys = build_system(PSITestSystems, "c_sys5_events"), networks = repeat([PSI.CopperPlatePowerModel], 3), optimizers = repeat([HiGHS_optimizer_small_gap], 3), outage_status_timeseries = outage_timeseries, device_type = PowerLoad, device_names = ["Bus2"], renewable_formulation = RenewableFullDispatch, ) em = get_emulation_problem_results(res) active_power_thermal_no_outage = read_realized_variable( em, "ActivePowerVariable__ThermalStandard"; table_format = TableFormat.WIDE, ) outage_data[3] = 1 outage_data[10:11] .= 1 outage_data[23:22] .= 1 outage_timeseries = TimeArray(dates_ts, outage_data) res = run_fixed_forced_outage_sim_with_timeseries(; sys = build_system(PSITestSystems, "c_sys5_events"), networks = repeat([PSI.CopperPlatePowerModel], 3), optimizers = repeat([HiGHS_optimizer_small_gap], 3), outage_status_timeseries = outage_timeseries, device_type = PowerLoad, device_names = ["Bus2"], renewable_formulation = RenewableFullDispatch, ) em = get_emulation_problem_results(res) status = read_realized_variable( em, "AvailableStatusParameter__PowerLoad"; table_format = TableFormat.WIDE, ) active_power_load = read_realized_variable( em, "ActivePowerTimeSeriesParameter__PowerLoad"; table_format = TableFormat.WIDE, ) active_power_thermal_outage = read_realized_variable( em, "ActivePowerVariable__ThermalStandard"; table_format = TableFormat.WIDE, ) for (ix, x) in enumerate(outage_data[1:24]) @test x != Int64(status[!, "Bus2"][ix]) if outage_data[ix] == 1.0 change_in_thermal_generation = sum( Vector(active_power_thermal_outage[ix, 2:end]) .- Vector(active_power_thermal_no_outage[ix, 2:end]), ) active_power_outaged_load = active_power_load[ix, "Bus2"] @test isapprox(change_in_thermal_generation, active_power_outaged_load) end end end @testset "FixedOutput outage" begin dates_ts = collect( DateTime("2024-01-01T00:00:00"):Hour(1):DateTime("2024-01-02T23:00:00"), ) outage_data = fill!(Vector{Int64}(undef, 48), 0) outage_timeseries = TimeArray(dates_ts, outage_data) res = run_fixed_forced_outage_sim_with_timeseries(; sys = build_system(PSITestSystems, "c_sys5_events"), networks = repeat([PSI.CopperPlatePowerModel], 3), optimizers = repeat([HiGHS_optimizer_small_gap], 3), outage_status_timeseries = outage_timeseries, device_type = PowerLoad, device_names = ["Bus2"], renewable_formulation = RenewableFullDispatch, ) em = get_emulation_problem_results(res) active_power_thermal_no_outage = read_realized_variable( em, "ActivePowerVariable__ThermalStandard"; table_format = TableFormat.WIDE, ) outage_data[3] = 1 outage_data[10:11] .= 1 outage_data[23:22] .= 1 outage_timeseries = TimeArray(dates_ts, outage_data) res = run_fixed_forced_outage_sim_with_timeseries(; sys = build_system(PSITestSystems, "c_sys5_events"), networks = repeat([PSI.CopperPlatePowerModel], 3), optimizers = repeat([HiGHS_optimizer_small_gap], 3), outage_status_timeseries = outage_timeseries, device_type = RenewableDispatch, device_names = ["WindBus1"], renewable_formulation = FixedOutput, ) em = get_emulation_problem_results(res) renewable_status = read_realized_variable( em, "AvailableStatusParameter__RenewableDispatch"; table_format = TableFormat.WIDE, ) active_power_thermal_outage = read_realized_variable( em, "ActivePowerVariable__ThermalStandard"; table_format = TableFormat.WIDE, ) active_power_renewable = read_realized_variable( em, "ActivePowerTimeSeriesParameter__RenewableDispatch"; table_format = TableFormat.WIDE, ) for (ix, x) in enumerate(outage_data[1:24]) @test x != Int64(renewable_status[!, "WindBus1"][ix]) if outage_data[ix] == 1.0 change_in_thermal_generation = sum( Vector(active_power_thermal_outage[ix, 2:end]) .- Vector(active_power_thermal_no_outage[ix, 2:end]), ) active_power_outaged_renewable = active_power_renewable[ix, "WindBus1"] @test isapprox(change_in_thermal_generation, active_power_outaged_renewable) end end end @testset "Reactive power formulation w/ outage" begin res = run_events_simulation(; sys_emulator = build_system(PSITestSystems, "c_sys5_events"), networks = [PSI.PTDFPowerModel, PSI.PTDFPowerModel, PSI.SOCWRPowerModel], optimizers = [ HiGHS_optimizer_small_gap, HiGHS_optimizer_small_gap, ipopt_optimizer, ], outage_time = DateTime("2024-01-01T18:00:00"), outage_length = 3.0, uc_formulation = "basic", ed_formulation = "basic", feedforward = true, in_memory = true, ) test_event_results(; res = res, outage_time = DateTime("2024-01-01T18:00:00"), outage_length = 3.0, expected_power_recovery = DateTime("2024-01-01T22:00:00"), expected_on_variable_recovery = DateTime("2024-01-01T22:00:00"), test_reactive_power = true, ) end ================================================ FILE: test/test_formulation_combinations.jl ================================================ @testset "Test generate_formulation_combinations" begin res = PSI.generate_formulation_combinations() found_valid_device = false found_invalid_device = false found_valid_service = false found_invalid_service = false for item in res["device_formulations"] if item["device_type"] == PSY.ThermalStandard && item["formulation"] == PSI.ThermalBasicCompactUnitCommitment found_valid_device = true end end for item in res["service_formulations"] 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 end @test found_valid_device @test !found_invalid_device @test found_valid_service @test !found_invalid_service end @testset "Test generate_formulation_combinations with system" begin sys = PSB.build_system(PSITestSystems, "c_sys5_hy_uc") res1 = PSI.generate_formulation_combinations() res2 = PSI.generate_formulation_combinations(sys) @test length(res1["device_formulations"]) > length(res2["device_formulations"]) @test length(res1["service_formulations"]) > length(res2["service_formulations"]) device_types = Set((typeof(x) for x in PSY.get_components(PSY.Device, sys))) diff = setdiff((x["device_type"] for x in res2["device_formulations"]), device_types) @test isempty(diff) service_types = Set((typeof(x) for x in PSY.get_components(PSY.Service, sys))) diff = setdiff((x["service_type"] for x in res2["service_formulations"]), service_types) @test isempty(diff) end @testset "Test write_formulation_combinations" begin res = PSI.generate_formulation_combinations() filename = joinpath(tempdir(), "data.json") @test !isfile(filename) try PSI.write_formulation_combinations(filename) @test isfile(filename) data = open(filename) do io JSON3.read(io, Dict) end @test "device_formulations" in keys(data) @test length(data["device_formulations"]) == length(res["device_formulations"]) @test "service_formulations" in keys(data) @test length(data["service_formulations"]) == length(res["service_formulations"]) finally isfile(filename) && rm(filename) end end ================================================ FILE: test/test_import_export_cost.jl ================================================ # See also test_device_source_constructors.jl @testset "ImportExportCost incremental+decremental Source, no time series versus constant time series, reservation off" begin sys_no_ts = make_5_bus_with_import_export(; name = "sys_no_ts") sys_constant_ts = make_5_bus_with_ie_ts(false, false, false, false; name = "sys_constant_ts") test_generic_mbc_equivalence(sys_no_ts, sys_constant_ts; device_to_formulation = FormulationDict( Source => DeviceModel( Source, ImportExportSourceModel; attributes = Dict("reservation" => false), ), ), ) end @testset "ImportExportCost incremental+decremental Source, no time series versus constant time series, reservation on" begin sys_no_ts = make_5_bus_with_import_export(; name = "sys_no_ts") sys_constant_ts = make_5_bus_with_ie_ts(false, false, false, false; name = "sys_constant_ts") test_generic_mbc_equivalence(sys_no_ts, sys_constant_ts; device_to_formulation = FormulationDict( Source => DeviceModel( Source, ImportExportSourceModel; attributes = Dict("reservation" => true), ), ), ) end @testset "ImportExportCost constant time series, reservation sanity checks" begin sys_constant_ts = make_5_bus_with_ie_ts(false, false, false, false; name = "sys_constant_ts") for use_simulation in (false, true), in_memory_store in (use_simulation ? (false, true) : (false,)), reservation in (false, true) run_iec_sim(sys_constant_ts, IEC_COMPONENT_NAME, IECComponentType; simulation = use_simulation, in_memory_store = in_memory_store, reservation = true, ) end end @testset "ImportExportCost with time varying import slopes, reservation off" begin import_scalar = 0.5 # ultimately multiplies ActivePowerOutVariable objective function coefficient export_scalar = 2.0 # ultimately multiplies ActivePowerInVariable objective function coefficient sys_constant = make_5_bus_with_ie_ts(false, false, false, false; import_scalar = import_scalar, export_scalar = export_scalar, name = "sys_constant") sys_varying_import_slopes = make_5_bus_with_ie_ts(false, true, false, false; import_scalar = import_scalar, export_scalar = export_scalar, name = "sys_varying_import_slopes") iec_obj_fun_test_wrapper(sys_constant, sys_varying_import_slopes) end @testset "ImportExportCost with time varying import breakpoints, reservation off" begin import_scalar = 0.2 # NOTE this maxes out ActivePowerOutVariable export_scalar = 2.0 sys_constant = make_5_bus_with_ie_ts(false, false, false, false; import_scalar = import_scalar, export_scalar = export_scalar, name = "sys_constant") sys_varying_import_breakpoints = make_5_bus_with_ie_ts(true, false, false, false; import_scalar = import_scalar, export_scalar = export_scalar, name = "sys_varying_import_breakpoints") iec_obj_fun_test_wrapper(sys_constant, sys_varying_import_breakpoints) end @testset "ImportExportCost with time varying export slopes, reservation off" begin import_scalar = 0.5 export_scalar = 2.0 sys_constant = make_5_bus_with_ie_ts(false, false, false, false; import_scalar = import_scalar, export_scalar = export_scalar, name = "sys_constant") sys_varying_export_slopes = make_5_bus_with_ie_ts(false, false, false, true; import_scalar = import_scalar, export_scalar = export_scalar, name = "sys_varying_export_slopes") iec_obj_fun_test_wrapper(sys_constant, sys_varying_export_slopes) end @testset "ImportExportCost with time varying export breakpoints, reservation off" begin import_scalar = 1.0 export_scalar = 50.0 # NOTE this maxes out ActivePowerInVariable sys_constant = make_5_bus_with_ie_ts(false, false, false, false; import_scalar = import_scalar, export_scalar = export_scalar, name = "sys_constant") sys_varying_export_breakpoints = make_5_bus_with_ie_ts(false, false, true, false; import_scalar = import_scalar, export_scalar = export_scalar, name = "sys_varying_export_breakpoints") iec_obj_fun_test_wrapper(sys_constant, sys_varying_export_breakpoints) end @testset "ImportExportCost with time varying everything, reservation off" begin import_scalar = 0.2 export_scalar = 40.0 sys_constant = make_5_bus_with_ie_ts(false, false, false, false; import_scalar = import_scalar, export_scalar = export_scalar, name = "sys_constant") sys_varying_everything = make_5_bus_with_ie_ts(true, true, true, true; import_scalar = import_scalar, export_scalar = export_scalar, name = "sys_varying_everything") iec_obj_fun_test_wrapper(sys_constant, sys_varying_everything) end ================================================ FILE: test/test_initialization_problem.jl ================================================ # Cbc isn't performant enough and SCIP is too problematic test_months = (get(ENV, "CI", nothing) == "true") ? 4 : 6 sys_rts = PSB.build_system(PSISystems, "modified_RTS_GMLC_DA_sys") @testset "Decision Model test for Initialization with RTS GMLC system, Case 1" begin ######## Test with ThermalStandardUnitCommitment ######## template = ProblemTemplate(CopperPlatePowerModel) set_device_model!(template, ThermalStandard, ThermalStandardUnitCommitment) set_device_model!(template, PowerLoad, StaticPowerLoad) set_device_model!(template, RenewableDispatch, RenewableFullDispatch) set_device_model!(template, HydroDispatch, HydroDispatchRunOfRiver) for init_time in DateTime("2020-01-01T00:00:00"):Month(test_months):DateTime("2020-12-31T00:00:00") @info("Decision Model initial_conditions test with RTS-GMLC for $init_time") model = DecisionModel( template, sys_rts; optimizer = HiGHS_optimizer, initial_time = init_time, horizon = Hour(48), ) @test build!(model; output_dir = mktempdir(; cleanup = true)) == PSI.ModelBuildStatus.BUILT ####### Check initialization problem check_initialization_variable_count(model, ActivePowerVariable(), ThermalStandard) check_initialization_variable_count(model, OnVariable(), ThermalStandard) check_initialization_variable_count(model, StopVariable(), ThermalStandard) check_initialization_variable_count(model, StartVariable(), ThermalStandard) check_initialization_variable_count(model, ActivePowerVariable(), RenewableDispatch) check_initialization_variable_count(model, ActivePowerVariable(), HydroDispatch) ####### Check initial condition from initialization step check_duration_on_initial_conditions_values(model, ThermalStandard) check_duration_off_initial_conditions_values(model, ThermalStandard) check_active_power_initial_condition_values(model, ThermalStandard) check_status_initial_conditions_values(model, ThermalStandard) ####### Check variables check_variable_count(model, ActivePowerVariable(), ThermalStandard) check_variable_count(model, StopVariable(), ThermalStandard) check_variable_count(model, OnVariable(), ThermalStandard) check_variable_count(model, StartVariable(), ThermalStandard) check_variable_count(model, ActivePowerVariable(), RenewableDispatch) check_variable_count(model, ActivePowerVariable(), HydroDispatch) ####### Check constraints check_constraint_count( model, ActivePowerVariableLimitsConstraint(), ThermalStandard; meta = "lb", ) check_constraint_count( model, ActivePowerVariableLimitsConstraint(), ThermalStandard; meta = "ub", ) check_constraint_count(model, DurationConstraint(), ThermalStandard) check_constraint_count(model, RampConstraint(), ThermalStandard) check_constraint_count(model, CommitmentConstraint(), ThermalStandard) check_constraint_count(model, CommitmentConstraint(), ThermalStandard; meta = "aux") check_constraint_count( model, PSI.ActivePowerVariableTimeSeriesLimitsConstraint(), HydroDispatch; meta = "ub", ) check_constraint_count( model, PSI.ActivePowerVariableTimeSeriesLimitsConstraint(), RenewableDispatch; meta = "ub", ) # @test solve!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED end end @testset "Decision Model test for Initialization with RTS GMLC system, Case 2" begin ######## Test with HydroCommitmentRunOfRiver ######## template = ProblemTemplate(CopperPlatePowerModel) set_device_model!(template, ThermalStandard, ThermalBasicUnitCommitment) set_device_model!(template, PowerLoad, StaticPowerLoad) set_device_model!(template, RenewableDispatch, RenewableFullDispatch) set_device_model!(template, HydroDispatch, HydroCommitmentRunOfRiver) for init_time in DateTime("2020-01-01T00:00:00"):Month(test_months):DateTime("2020-12-31T00:00:00") @info("Decision Model initial_conditions test with RTS-GMLC for $init_time") model = DecisionModel( template, sys_rts; optimizer = HiGHS_optimizer, initial_time = init_time, horizon = Hour(48), ) @test build!(model; output_dir = mktempdir(; cleanup = true)) == PSI.ModelBuildStatus.BUILT ####### Check initialization problem check_initialization_variable_count(model, ActivePowerVariable(), ThermalStandard) check_initialization_variable_count(model, OnVariable(), ThermalStandard) check_initialization_variable_count(model, ActivePowerVariable(), RenewableDispatch) check_initialization_variable_count(model, ActivePowerVariable(), HydroDispatch) ####### Check initial condition from initialization step check_status_initial_conditions_values(model, ThermalStandard) ####### Check variables check_variable_count(model, ActivePowerVariable(), ThermalStandard) check_variable_count(model, StopVariable(), ThermalStandard) check_variable_count(model, OnVariable(), ThermalStandard) check_variable_count(model, StartVariable(), ThermalStandard) check_variable_count(model, ActivePowerVariable(), RenewableDispatch) check_variable_count(model, ActivePowerVariable(), HydroDispatch) check_variable_count(model, OnVariable(), HydroDispatch) ####### Check constraints check_constraint_count( model, ActivePowerVariableLimitsConstraint(), ThermalStandard; meta = "lb", ) check_constraint_count( model, ActivePowerVariableLimitsConstraint(), ThermalStandard; meta = "ub", ) check_constraint_count(model, CommitmentConstraint(), ThermalStandard) check_constraint_count(model, CommitmentConstraint(), ThermalStandard; meta = "aux") check_constraint_count( model, PSI.ActivePowerVariableTimeSeriesLimitsConstraint(), RenewableDispatch; meta = "ub", ) check_constraint_count( model, PSI.ActivePowerVariableTimeSeriesLimitsConstraint(), HydroDispatch; meta = "ub", ) check_constraint_count( model, PSI.ActivePowerVariableLimitsConstraint(), HydroDispatch; meta = "lb", ) check_constraint_count( model, PSI.ActivePowerVariableLimitsConstraint(), HydroDispatch; meta = "ub", ) # @test solve!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED end end @testset "Decision Model test for Initialization with RTS GMLC system, Case 4" begin ######## Test with ThermalCompactUnitCommitment ######## template = ProblemTemplate(CopperPlatePowerModel) set_device_model!(template, ThermalStandard, ThermalCompactUnitCommitment) set_device_model!(template, PowerLoad, StaticPowerLoad) set_device_model!(template, RenewableDispatch, RenewableFullDispatch) set_device_model!(template, HydroDispatch, HydroDispatchRunOfRiver) for init_time in DateTime("2020-01-01T00:00:00"):Month(test_months):DateTime("2020-12-31T00:00:00") @info("Decision Model initial_conditions test with RTS-GMLC for $init_time") model = DecisionModel( template, sys_rts; optimizer = HiGHS_optimizer, initial_time = init_time, 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( model, ActivePowerVariableLimitsConstraint(), ThermalStandard; meta = "lb", ) check_initialization_constraint_count( model, ActivePowerVariableLimitsConstraint(), ThermalStandard; meta = "ub", ) check_initialization_constraint_count( model, CommitmentConstraint(), ThermalStandard, ) check_initialization_constraint_count( model, CommitmentConstraint(), ThermalStandard; meta = "aux", ) check_initialization_constraint_count( model, PSI.ActivePowerVariableTimeSeriesLimitsConstraint(), RenewableDispatch; meta = "ub", ) check_initialization_constraint_count( model, ActivePowerVariableLimitsConstraint(), HydroDispatch; meta = "lb", ) check_initialization_constraint_count( model, ActivePowerVariableLimitsConstraint(), HydroDispatch; meta = "ub", ) check_initialization_constraint_count( model, PSI.ActivePowerVariableTimeSeriesLimitsConstraint(), HydroDispatch; meta = "ub", ) PSI.reset!(model) @test build!(model; output_dir = mktempdir(; cleanup = true)) == PSI.ModelBuildStatus.BUILT ####### Check initialization problem check_initialization_variable_count( model, PSI.PowerAboveMinimumVariable(), ThermalStandard, ) check_initialization_variable_count(model, OnVariable(), ThermalStandard) check_initialization_variable_count(model, StopVariable(), ThermalStandard) check_initialization_variable_count(model, StartVariable(), ThermalStandard) check_initialization_variable_count(model, ActivePowerVariable(), RenewableDispatch) check_initialization_variable_count(model, ActivePowerVariable(), HydroDispatch) ####### Check initial condition from initialization step check_duration_on_initial_conditions_values(model, ThermalStandard) check_duration_off_initial_conditions_values(model, ThermalStandard) check_active_power_abovemin_initial_condition_values(model, ThermalStandard) check_status_initial_conditions_values(model, ThermalStandard) ####### Check variables check_variable_count(model, PSI.PowerAboveMinimumVariable(), ThermalStandard) check_variable_count(model, OnVariable(), ThermalStandard) check_variable_count(model, StopVariable(), ThermalStandard) check_variable_count(model, StartVariable(), ThermalStandard) check_variable_count(model, ActivePowerVariable(), RenewableDispatch) check_variable_count(model, ActivePowerVariable(), HydroDispatch) ####### Check constraints check_constraint_count( model, ActivePowerVariableLimitsConstraint(), ThermalStandard; meta = "lb", ) check_constraint_count( model, ActivePowerVariableLimitsConstraint(), ThermalStandard; meta = "ub", ) check_constraint_count(model, RampConstraint(), ThermalStandard) check_constraint_count(model, DurationConstraint(), ThermalStandard) check_constraint_count(model, CommitmentConstraint(), ThermalStandard) check_constraint_count(model, CommitmentConstraint(), ThermalStandard; meta = "aux") check_constraint_count( model, PSI.ActivePowerVariableTimeSeriesLimitsConstraint(), RenewableDispatch; meta = "ub", ) check_constraint_count( model, PSI.ActivePowerVariableTimeSeriesLimitsConstraint(), HydroDispatch; meta = "ub", ) check_constraint_count( model, PSI.ActivePowerVariableLimitsConstraint(), HydroDispatch; meta = "lb", ) check_constraint_count( model, PSI.ActivePowerVariableLimitsConstraint(), HydroDispatch; meta = "ub", ) # @test solve!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED end end ================================================ FILE: test/test_jump_utils.jl ================================================ @testset "Test get_column_names_from_key" begin key = PSI.VariableKey(PSI.ActivePowerVariable, PSY.ThermalStandard) @test PSI.get_column_names_from_key(key) == (["ActivePowerVariable__ThermalStandard"],) end @testset "Test get_column_names_from_axis_array with DenseAxisArray 1D" begin @test PSI.get_column_names_from_axis_array(DenseAxisArray(rand(3), 1:3)) == (["1", "2", "3"],) @test PSI.get_column_names_from_axis_array(DenseAxisArray(rand(3), collect(1:3))) == (["1", "2", "3"],) key = PSI.VariableKey(PSI.ActivePowerVariable, PSY.ThermalStandard) @test PSI.get_column_names_from_axis_array(key, DenseAxisArray(rand(3), 1:3)) == (["ActivePowerVariable__ThermalStandard"],) end @testset "Test get_column_names_from_axis_array with DenseAxisArray 2D" begin key = PSI.VariableKey(PSI.ActivePowerVariable, PSY.ThermalStandard) components = ["component1", "component2"] array = DenseAxisArray(rand(2, 3), components, 1:3) @test PSI.get_column_names_from_axis_array(key, array) == (components,) @test PSI.get_column_names_from_axis_array(array) == (components,) @test PSI.get_column_names_from_axis_array(DenseAxisArray(rand(2, 3), [1, 2], 1:3)) == (["1", "2"],) @test PSI.get_column_names_from_axis_array(DenseAxisArray(rand(2, 3), 1:2, 1:3)) == (["1", "2"],) end @testset "Test get_column_names_from_axis_array with DenseAxisArray 3D" begin key = PSI.VariableKey(PSI.ActivePowerVariable, PSY.ThermalStandard) components = ["component1", "component2"] extra = ["1", "2", "3", "4"] array = DenseAxisArray(rand(2, 4, 3), components, extra, 1:3) @test PSI.get_column_names_from_axis_array(key, array) == (components, extra) @test PSI.get_column_names_from_axis_array(array) == (components, extra) @test PSI.get_column_names_from_axis_array( DenseAxisArray(rand(2, 4, 3), components, 1:4, 1:3), ) == (components, extra) end @testset "Test to_dataframe with DenseAxisArray 1D" begin data = rand(3) array = DenseAxisArray(data, 1:3) key = PSI.VariableKey(PSI.ActivePowerVariable, PSY.ThermalStandard) df = PSI.to_dataframe(array, key) @test size(df) == (3, 1) @test names(df) == ["ActivePowerVariable__ThermalStandard"] @test df[!, "ActivePowerVariable__ThermalStandard"] == data end @testset "Test to_dataframe with DenseAxisArray 2D" begin data = rand(2, 3) components = ["component1", "component2"] array = DenseAxisArray(data, components, 1:3) key = PSI.VariableKey(PSI.ActivePowerVariable, PSY.ThermalStandard) df = PSI.to_dataframe(array, key) @test size(df) == (3, 2) @test names(df) == components @test df[!, "component1"] == permutedims(data)[:, 1] @test df[!, "component2"] == permutedims(data)[:, 2] end @testset "Test to_results_dataframe with 2D DenseAxisArray - LONG format with timestamps" begin data = rand(2, 3) components = ["component1", "component2"] array = DenseAxisArray(data, components, 1:3) timestamps = [ DateTime(2024, 1, 1, 0), DateTime(2024, 1, 1, 1), DateTime(2024, 1, 1, 2), ] df = PSI.to_results_dataframe(array, timestamps, Val(IS.TableFormat.LONG)) @test size(df) == (6, 3) # 2 components × 3 timestamps = 6 rows, 3 columns @test names(df) == ["DateTime", "name", "value"] @test df.DateTime == repeat(timestamps, 2) @test df.name == repeat(components; inner = 3) @test df.value == reshape(permutedims(data), 6) # Test error with mismatched timestamps. wrong_timestamps = [DateTime(2024, 1, 1, 1)] @test_throws ErrorException PSI.to_results_dataframe( array, wrong_timestamps, Val(IS.TableFormat.LONG), ) end @testset "Test to_results_dataframe with 2D DenseAxisArray - LONG format without timestamps" begin data = rand(2, 3) components = ["component1", "component2"] array = DenseAxisArray(data, components, 1:3) df = PSI.to_results_dataframe(array, nothing, Val(IS.TableFormat.LONG)) @test size(df) == (6, 3) # 2 components × 3 timestamps = 6 rows, 3 columns @test names(df) == ["time_index", "name", "value"] @test df.time_index == repeat([1, 2, 3], 2) @test df.name == repeat(components; inner = 3) @test df.value == reshape(permutedims(data), 6) end @testset "Test to_results_dataframe with 2D DenseAxisArray - WIDE format with timestamps" begin data = rand(2, 3) components = ["component1", "component2"] array = DenseAxisArray(data, components, 1:3) timestamps = [ DateTime(2024, 1, 1, 0), DateTime(2024, 1, 1, 1), DateTime(2024, 1, 1, 2), ] df = PSI.to_results_dataframe(array, timestamps, Val(IS.TableFormat.WIDE)) @test size(df) == (3, 3) # 3 timestamps, 3 columns (DateTime + 2 components) @test names(df) == ["DateTime", "component1", "component2"] @test df.DateTime == timestamps exp_data = permutedims(data) @test df.component1 == exp_data[:, 1] @test df.component2 == exp_data[:, 2] end @testset "Test to_results_dataframe with 2D DenseAxisArray - WIDE format without timestamps" begin data = rand(2, 3) components = ["component1", "component2"] array = DenseAxisArray(data, components, 1:3) df = PSI.to_results_dataframe(array, nothing, Val(IS.TableFormat.WIDE)) @test size(df) == (3, 3) # 3 timestamps, 3 columns (DateTime + 2 components) @test names(df) == ["time_index", "component1", "component2"] @test df.time_index == [1, 2, 3] exp_data = permutedims(data) @test df.component1 == exp_data[:, 1] @test df.component2 == exp_data[:, 2] end function _fill_3d_data() components = ["component1", "component2"] extra = ["1", "2", "3", "4"] array = DenseAxisArray(zeros(2, 4, 3), components, extra, 1:3) array["component1", "1", :] = [1.0, 2.0, 3.0] array["component1", "2", :] = [2.0, 3.0, 4.0] array["component1", "3", :] = [3.0, 4.0, 5.0] array["component1", "4", :] = [6.0, 7.0, 8.0] array["component2", "1", :] = [11.0, 12.0, 13.0] array["component2", "2", :] = [12.0, 13.0, 14.0] array["component2", "3", :] = [13.0, 14.0, 15.0] array["component2", "4", :] = [16.0, 17.0, 18.0] return array end function _check_3d_data(df) @test size(df) == (24, 4) # 2 components x 4 extra × 3 timestamps = 24 rows, 4 columns @test @rsubset(df, :name == "component1" && :name2 == "1")[!, :value] == [1.0, 2.0, 3.0] @test @rsubset(df, :name == "component1" && :name2 == "2")[!, :value] == [2.0, 3.0, 4.0] @test @rsubset(df, :name == "component1" && :name2 == "3")[!, :value] == [3.0, 4.0, 5.0] @test @rsubset(df, :name == "component1" && :name2 == "4")[!, :value] == [6.0, 7.0, 8.0] @test @rsubset(df, :name == "component2" && :name2 == "1")[!, :value] == [11.0, 12.0, 13.0] @test @rsubset(df, :name == "component2" && :name2 == "2")[!, :value] == [12.0, 13.0, 14.0] @test @rsubset(df, :name == "component2" && :name2 == "3")[!, :value] == [13.0, 14.0, 15.0] @test @rsubset(df, :name == "component2" && :name2 == "4")[!, :value] == [16.0, 17.0, 18.0] end @testset "Test to_results_dataframe with 3D DenseAxisArray - LONG format with timestamps" begin array = _fill_3d_data() timestamps = [ DateTime(2024, 1, 1, 0), DateTime(2024, 1, 1, 1), DateTime(2024, 1, 1, 2), ] df = PSI.to_results_dataframe(array, timestamps, Val(IS.TableFormat.LONG)) _check_3d_data(df) # Test error with mismatched timestamps. wrong_timestamps = [DateTime(2024, 1, 1, 1)] @test_throws ErrorException PSI.to_results_dataframe( array, wrong_timestamps, Val(IS.TableFormat.LONG), ) end @testset "Test to_results_dataframe with 3D DenseAxisArray - LONG format without timestamps" begin array = _fill_3d_data() df = PSI.to_results_dataframe(array, nothing, Val(IS.TableFormat.LONG)) @test names(df) == ["time_index", "name", "name2", "value"] _check_3d_data(df) @test df.time_index == repeat([1, 2, 3], 8) end @testset "Test to_matrix" begin @test PSI.to_matrix([1, 2, 3]) == [1; 2; 3;;] @test PSI.to_matrix([1 2 3]) == [1 2 3] data = rand(2, 3) @test PSI.to_matrix(DenseAxisArray(data, ["a", "b"], 1:3)) == permutedims(data) end @testset "Dual processing helpers" begin @testset "_first_element with DenseAxisArray" begin arr = DenseAxisArray([1.1, 2.9], 1:2) @test PSI._first_element(arr) == 1.1 end @testset "_first_element with SparseAxisArray" begin arr = SparseAxisArray(Dict((1,) => 1.1, (2,) => 2.9)) @test PSI._first_element(arr) ∈ [1.1, 2.9] end @testset "_round_cache_values! with DenseAxisArray" begin arr = DenseAxisArray([1.1, 2.9], 1:2) PSI._round_cache_values!(arr) @test arr[1] == 1.0 @test arr[2] == 3.0 end @testset "_round_cache_values! with SparseAxisArray" begin arr = SparseAxisArray(Dict((1,) => 1.1, (2,) => 2.9)) PSI._round_cache_values!(arr) @test arr[(1,)] == 1.0 @test arr[(2,)] == 3.0 end end ================================================ FILE: test/test_market_bid_cost.jl ================================================ function test_market_bid_cost_models(sys::PSY.System, test_unit::PSY.Component, my_no_load::Float64, my_initial_input::Float64; skip_setting = false, device_to_formulation = FormulationDict(), filename::Union{String, Nothing} = nothing, ) fcn_data = get_function_data( get_value_curve( get_incremental_offer_curves(get_operation_cost(test_unit)), ), ) if !skip_setting new_vc = PiecewiseIncrementalCurve(fcn_data, my_initial_input, my_no_load) set_incremental_offer_curves!( get_operation_cost(test_unit), CostCurve(new_vc), ) end set_no_load_cost!(get_operation_cost(test_unit), my_no_load) template = ProblemTemplate(NetworkModel(CopperPlatePowerModel)) set_formulations!( template, sys, device_to_formulation, ) model = DecisionModel( template, sys; name = "UC_test_mbc", optimizer = HiGHS_optimizer_small_gap, optimizer_solve_log_print = true, store_variable_names = true, ) @test build!(model; output_dir = test_path) == PSI.ModelBuildStatus.BUILT @test solve!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED if !isnothing(filename) save_loc = joinpath(DOWNLOADS, "thermal_vs_renewable") @assert isdir(save_loc) save_objective_function( model, joinpath( save_loc, "objective_function_$(filename)_$(get_name(test_unit)).txt", ), ) save_constraints( model, joinpath(save_loc, "constraints_$(filename)_$(get_name(test_unit)).txt"), ) end return OptimizationProblemResults(model) end function verify_market_bid_cost_models( sys::PSY.System, test_unit::PSY.Component, cost_reference::Float64, no_load_cost::Float64, my_initial_input::Float64, ) results = test_market_bid_cost_models( sys, test_unit, no_load_cost, my_initial_input, ) expr = read_expression(results, "ProductionCostExpression__ThermalStandard") component_df = @rsubset(expr, :name == get_name(test_unit)) shutdown_cost = PSY.get_shut_down(PSY.get_operation_cost(test_unit)) var_unit_cost = sum(@rsubset(component_df, :value != shutdown_cost)[:, :value]) unit_cost_due_to_initial = nrow(@rsubset(component_df, :value != shutdown_cost)) * my_initial_input @test isapprox( var_unit_cost - PSY.get_start_up(PSY.get_operation_cost(test_unit))[:hot], cost_reference + unit_cost_due_to_initial; atol = 1, ) end @testset "Test Thermal Generation MarketBidCost models" begin test_cases = [ ("Base case", "fixed_market_bid_cost", 18487.236, 30.0, 30.0), ("Greater initial input, no load", "fixed_market_bid_cost", 18487.236, 31.0, 31.0), ("Greater initial input only", "fixed_market_bid_cost", 18487.236, 30.0, 31.0), ] for (name, sys_name, cost_reference, my_no_load, my_initial_input) in test_cases @testset "$name" begin sys = PSB.build_system(PSITestSystems, "c_$(sys_name)") unit1 = get_component(ThermalStandard, sys, "Test Unit1") verify_market_bid_cost_models( sys, unit1, cost_reference, my_no_load, my_initial_input, ) end end end @testset "Test Renewable Dispatch MarketBidCost models" begin test_cases = [ ("Base case", "fixed_market_bid_cost", 18487.236, 30.0, 30.0), ("Greater initial input, no load", "fixed_market_bid_cost", 18487.236, 31.0, 31.0), ("Greater initial input only", "fixed_market_bid_cost", 18487.236, 30.0, 31.0), ] for (name, sys_name, _, my_no_load, my_initial_input) in test_cases @testset "$name" begin sys = build_system(PSITestSystems, "c_$(sys_name)") unit1 = get_component(ThermalStandard, sys, "Test Unit1") replace_with_renewable!(sys, unit1) rg1 = get_component(PSY.RenewableDispatch, sys, "RG1") test_market_bid_cost_models( sys, rg1, my_no_load, my_initial_input, ) end end end @testset "Compare Renewable and Standard Thermal MarketBidCost" begin (name, sys_name) = ("Base case", "fixed_market_bid_cost") sys = build_system(PSITestSystems, "c_$(sys_name)") unit1 = get_component(ThermalStandard, sys, "Test Unit1") replace_with_renewable!(sys, unit1; use_thermal_max_power = true) rg1 = get_component(PSY.RenewableDispatch, sys, "RG1") zero_out_non_incremental_curve!(sys, rg1) set_name!(sys, "sys_renewable") results_renewable = test_market_bid_cost_models( sys, rg1, 0.0, 0.0; skip_setting = true, ) sys_thermal = build_system(PSITestSystems, "c_$(sys_name)") unit1 = get_component(ThermalStandard, sys_thermal, "Test Unit1") set_active_power_limits!( unit1, (min = 0.0, max = get_active_power_limits(unit1).max), ) set_operation_cost!(unit1, deepcopy(get_operation_cost(rg1))) set_name!(sys_thermal, "sys_thermal") results_thermal = test_market_bid_cost_models( sys_thermal, unit1, 0.0, 0.0; skip_setting = true, ) # check that the operation costs are the same. IS.compare_values(get_operation_cost(rg1), get_operation_cost(unit1)) for thermal_unit in get_components(ThermalStandard, sys) sys_thermal_unit = get_component(ThermalStandard, sys_thermal, get_name(thermal_unit)) IS.compare_values( thermal_unit, sys_thermal_unit, ) end for load in get_components(PSY.PowerLoad, sys) IS.compare_values(load, get_component(PSY.PowerLoad, sys_thermal, get_name(load))) end @test isapprox(PSI.read_optimizer_stats(results_thermal)[!, "objective_value"], PSI.read_optimizer_stats(results_renewable)[!, "objective_value"]) end """ Run a simple simulation with the system and return information useful for testing time-varying startup and shutdown functionality. Pass `simulation = false` to use a single decision model, `true` for a full simulation. """ function run_startup_shutdown_test( sys::System; multistart::Bool = false, simulation = true, in_memory_store::Bool = false, ) model, res = if simulation run_generic_mbc_sim(sys; multistart = multistart, in_memory_store = in_memory_store) else run_generic_mbc_prob(sys; multistart = multistart) end # Test correctness of written shutdown cost parameters # TODO test startup too once we are able to write those gentype = multistart ? ThermalMultiStart : ThermalStandard genname = multistart ? "115_STEAM_1" : "Test Unit1" sh_param = read_parameter_dict(res, PSI.ShutdownCostParameter, gentype) for (step_dt, step_df) in pairs(sh_param) for gen_name in unique(step_df.name) comp = get_component(gentype, sys, gen_name) fc_comp = get_shut_down(comp, PSY.get_operation_cost(comp); start_time = step_dt) @test all(step_df[!, :DateTime] .== TimeSeries.timestamp(fc_comp)) @test all( isapprox.( @rsubset(step_df, :name == gen_name).value, TimeSeries.values(fc_comp), ), ) end end # These decisions need to be equal between certain pairs of problems/simulations and also need to be approx_geq_1 decisions = if multistart ( _read_one_value(res, PSI.HotStartVariable, gentype, genname), _read_one_value(res, PSI.WarmStartVariable, gentype, genname), _read_one_value(res, PSI.ColdStartVariable, gentype, genname), _read_one_value(res, PSI.StopVariable, gentype, genname), _read_one_value(res, PSI.OnVariable, gentype, genname), ) else ( _read_one_value(res, PSI.StartVariable, gentype, genname), _read_one_value(res, PSI.StopVariable, gentype, genname), _read_one_value(res, PSI.OnVariable, gentype, genname), ) end # These decisions need to be equal between certain pairs of problems/simulations but need not be approx_geq_1 for the test to be valid nullable_decisions = if multistart ( _read_one_value(res, PSI.PowerAboveMinimumVariable, gentype, genname), # sometimes useful for debugging clarity to check *another* generator's decisions _read_one_value(res, PSI.OnVariable, gentype, "101_CT_1"), ) else () end return model, res, decisions, nullable_decisions end "Read the relevant startup variables: no multistart case" _read_start_vars(::Val{false}, res::IS.Results) = read_variable_dict(res, PSI.StartVariable, ThermalStandard) "Read the relevant startup variables: yes multistart case" function _read_start_vars(::Val{true}, res::IS.Results) hot_vars = read_variable_dict(res, PSI.HotStartVariable, ThermalMultiStart) warm_vars = read_variable_dict(res, PSI.WarmStartVariable, ThermalMultiStart) cold_vars = read_variable_dict(res, PSI.ColdStartVariable, ThermalMultiStart) @assert all(keys(hot_vars) .== keys(warm_vars)) @assert all(keys(hot_vars) .== keys(cold_vars)) @assert all( all(hot_vars[k][!, :DateTime] .== warm_vars[k][!, :DateTime]) for k in keys(hot_vars) ) @assert all( all(hot_vars[k][!, :DateTime] .== cold_vars[k][!, :DateTime]) for k in keys(hot_vars) ) combined_vars = Dict{DateTime, DataFrame}() for timestamp in keys(hot_vars) hot = hot_vars[timestamp] warm = warm_vars[timestamp] cold = cold_vars[timestamp] combined_vars[timestamp] = @chain DataFrames.rename(hot, :value => :hot) begin innerjoin(DataFrames.rename(warm, :value => :warm); on = [:DateTime, :name]) innerjoin( DataFrames.rename(cold, :value => :cold); on = [:DateTime, :name], ) @transform(@byrow(:value = (:hot, :warm, :cold))) @select(:DateTime, :name, :value) end end return combined_vars end """ Read startup and shutdown cost time series from a `System` and multiply by relevant start and stop variables in the `IS.Results` to determine the cost that should have been incurred by time-varying `MarketBidCost` startup and shutdown costs. Must run separately for multistart vs. not. """ function cost_due_to_time_varying_startup_shutdown( sys::System, res::IS.Results; multistart = false, ) gentype = multistart ? ThermalMultiStart : ThermalStandard start_vars = _read_start_vars(Val(multistart), res) stop_vars = read_variable_dict(res, PSI.StopVariable, gentype) result = SortedDict{DateTime, DataFrame}() IS.@assert_op Set(collect(keys(start_vars))) == Set(collect(keys(stop_vars))) for step_dt in keys(start_vars) start_df = start_vars[step_dt] stop_df = stop_vars[step_dt] @assert unique(start_df.name) == unique(stop_df.name) @assert start_df[!, :DateTime] == stop_df[!, :DateTime] timestamps = unique(start_df.DateTime) component_names = unique(start_df.name) dfs = Vector{DataFrame}() for gen_name in component_names comp = get_component(gentype, sys, gen_name) cost = PSY.get_operation_cost(comp) (cost isa PSY.MarketBidCost) || continue PSI.is_time_variant(get_start_up(cost)) || continue @assert PSI.is_time_variant(get_shut_down(cost)) startup_ts = get_start_up(comp, cost; start_time = step_dt) shutdown_ts = get_shut_down(comp, cost; start_time = step_dt) @assert all(unique(start_df.DateTime) .== TimeSeries.timestamp(startup_ts)) @assert all(unique(start_df.DateTime) .== TimeSeries.timestamp(shutdown_ts)) startup_values = if multistart TimeSeries.values(startup_ts) else getproperty.(TimeSeries.values(startup_ts), :hot) end push!( dfs, DataFrame( :DateTime => timestamps, :name => repeat([gen_name], length(timestamps)), :value => LinearAlgebra.dot.( @rsubset(start_df, :name == gen_name).value, startup_values, ) .+ @rsubset(stop_df, :name == gen_name).value .* TimeSeries.values(shutdown_ts), ), ) end if !isempty(dfs) result[step_dt] = vcat(dfs...) end end return result end """ The methodology here is: run a model or simulation where the startup and shutdown time series have constant values through time, then run a nearly identical model/simulation where the values vary very slightly through time, not enough to affect the decisions but enough to affect the objective value, then compare the size of the objective value change to an expectation computed manually. Pass `simulation = false` to use a single decision model, `true` for a full simulation. Pass `in_memory_store = true` to use an in-memory store for the simulation. Default is HDF5. """ function run_startup_shutdown_obj_fun_test( sys1, sys2; multistart::Bool = false, simulation = true, in_memory_store::Bool = false, ) _, res1, decisions1, nullable_decisions1 = run_startup_shutdown_test( sys1; multistart = multistart, simulation = simulation, in_memory_store = in_memory_store, ) _, res2, decisions2, nullable_decisions2 = run_startup_shutdown_test( sys2; multistart = multistart, simulation = simulation, in_memory_store = in_memory_store, ) all_decisions1 = (decisions1..., nullable_decisions1...) all_decisions2 = (decisions2..., nullable_decisions2...) if !all(isapprox.(all_decisions1, all_decisions2; atol = 1)) @error all_decisions1 @error all_decisions2 # Given the solver tolerance, this method can result in up to 1 change in the commitment result @assert false "Decisions between constant and time-varying startup/shutdown do not match approximately" end # The last decision is the objetive function we can test that with a smaller tolerance @test (isapprox(all_decisions1[end], all_decisions2[end]; atol = 1e-3)) ground_truth_1 = cost_due_to_time_varying_startup_shutdown(sys1, res1; multistart = multistart) ground_truth_2 = cost_due_to_time_varying_startup_shutdown(sys2, res2; multistart = multistart) obj_fun_test_helper(ground_truth_1, ground_truth_2, res1, res2) return decisions1, decisions2 end @testset "MarketBidCost with time series startup and shutdown, ThermalStandard" begin # Test that constant time series has the same objective value as no time series sys0 = load_and_fix_system(PSITestSystems, "c_fixed_market_bid_cost") tweak_for_startup_shutdown!(sys0) cost = get_operation_cost(get_component(ThermalStandard, sys0, "Test Unit1")) set_start_up!(cost, (hot = 1.0, warm = 1.5, cold = 2.0)) set_shut_down!(cost, 0.5) sys1 = load_and_fix_system(PSITestSystems, "c_fixed_market_bid_cost") tweak_for_startup_shutdown!(sys1) add_startup_shutdown_ts_a!(sys1, false) test_generic_mbc_equivalence(sys0, sys1; multistart = false) # Test that perturbing the time series perturbs the objective value as expected sys2 = load_and_fix_system(PSITestSystems, "c_fixed_market_bid_cost") tweak_for_startup_shutdown!(sys2) add_startup_shutdown_ts_a!(sys2, true) for use_simulation in (false, true) in_memory_store_opts = use_simulation ? [false, true] : [false] for in_memory_store in in_memory_store_opts (decisions1, decisions2) = run_startup_shutdown_obj_fun_test( sys1, sys2; simulation = use_simulation, in_memory_store = in_memory_store, ) # Make sure our tests included sufficent startups and shutdowns @assert all(approx_geq_1.(decisions1)) end end end @testset "MarketBidCost with time series startup and shutdown, ThermalMultiStart" begin # The arguments to create_multistart_sys were tuned empirically to ensure (a) the # behavior under test is exercised and (b) the small perturbations to the costs aren't # enough to change the decisions that form the correct solution # Scenario 1: hot and warm starts # TODO the process to empirically tune these values so the tests work everywhere is # absolutely horrible, we need a more robust system ASAP # https://github.com/Sienna-Platform/PowerSimulations.jl/issues/1460 load_pow_mult_a = 1.01 therm_pow_mult_a = 1.07 therm_price_mult_a = 7.40 c_sys5_pglib0a = create_multistart_sys( false, load_pow_mult_a, therm_pow_mult_a, therm_price_mult_a; add_ts = false, ) c_sys5_pglib1a = create_multistart_sys(false, load_pow_mult_a, therm_pow_mult_a, therm_price_mult_a) c_sys5_pglib2a = create_multistart_sys(true, load_pow_mult_a, therm_pow_mult_a, therm_price_mult_a) # Scenario 2: hot and cold starts load_pow_mult_b = 1.05 therm_pow_mult_b = 1.0 therm_price_mult_b = 7.4 c_sys5_pglib0b = create_multistart_sys( false, load_pow_mult_b, therm_pow_mult_b, therm_price_mult_b; add_ts = false, ) c_sys5_pglib1b = create_multistart_sys(false, load_pow_mult_b, therm_pow_mult_b, therm_price_mult_b) c_sys5_pglib2b = create_multistart_sys(true, load_pow_mult_b, therm_pow_mult_b, therm_price_mult_b) test_generic_mbc_equivalence(c_sys5_pglib0a, c_sys5_pglib1a; multistart = true) test_generic_mbc_equivalence(c_sys5_pglib0b, c_sys5_pglib1b; multistart = true) for use_simulation in (false, true) (decisions1, decisions2) = run_startup_shutdown_obj_fun_test( c_sys5_pglib1a, c_sys5_pglib2a; multistart = true, simulation = use_simulation, ) # NOTE not all of the decision types here have >= 1, we'll do another scenario such that we get full decision coverage across both of them: (decisions1_2, decisions2_2) = run_startup_shutdown_obj_fun_test( c_sys5_pglib1b, c_sys5_pglib2b; multistart = true, simulation = use_simulation, ) @test all(isapprox.(decisions1, decisions2)) @test all(isapprox.(decisions1_2, decisions2_2)) # Make sure our tests included all types of startups and shutdowns @test all(approx_geq_1.(decisions1 .+ decisions1_2)) end end @testset "MarketBidCost incremental ThermalStandard, no time series versus constant time series" begin sys_no_ts = load_sys_incr() set_name!(sys_no_ts, "thermal_no_ts") sys_constant_ts = build_sys_incr(false, false, false) set_name!(sys_constant_ts, "thermal_constant_ts") test_generic_mbc_equivalence( sys_no_ts, sys_constant_ts, ) end @testset "MarketBidCost incremental RenewableDispatch, no time series versus constant time series" begin sys_no_ts = load_sys_incr() sys_constant_ts = build_sys_incr(false, false, false) for sys in (sys_no_ts, sys_constant_ts) unit1 = get_component(SEL_INCR, sys) replace_with_renewable!(sys, unit1; magnitude = 1.0, random_variation = 0.1) end test_generic_mbc_equivalence(sys_no_ts, sys_constant_ts) end # debugging option: change to true to save text files of objective functions for # certain tests that aren't passing. const SAVE_FILES = false for decremental in (false, true) adj = decremental ? "decremental" : "incremental" build_func = decremental ? build_sys_decr2 : build_sys_incr comp_type = decremental ? InterruptiblePowerLoad : ThermalStandard comp_name = decremental ? "Bus1_interruptible" : "Test Unit1" device_models = if decremental [PowerLoadInterruption, PowerLoadDispatch] else [ThermalBasicUnitCommitment] end @testset for device_model in device_models device_to_formulation = FormulationDict(comp_type => device_model) init_input_bool = !decremental || device_model != PowerLoadDispatch if init_input_bool @testset "MarketBidCost $(adj) with time varying min gen cost" begin baseline = build_func(false, false, false) varying = build_func(true, false, false) if decremental tweak_for_decremental_initial!(varying) tweak_for_decremental_initial!(baseline) end for use_simulation in (false, true) in_memory_store_opts = use_simulation ? [false, true] : [false] for in_memory_store in in_memory_store_opts decisions1, decisions2 = run_mbc_obj_fun_test( baseline, varying, comp_name, comp_type; is_decremental = decremental, has_initial_input = init_input_bool, simulation = use_simulation, in_memory_store = in_memory_store, device_to_formulation = device_to_formulation, ) if !all(isapprox.(decisions1, decisions2)) @error decisions1 @error decisions2 end @assert all(approx_geq_1.(decisions1)) end end end end @testset "MarketBidCost $(adj) with time varying slopes" begin baseline = build_func(false, false, false) varying = build_func(false, false, true) set_name!(baseline, "baseline") set_name!(varying, "varying") for use_simulation in (false, true) in_memory_store_opts = use_simulation ? [false, true] : [false] for in_memory_store in in_memory_store_opts decisions1, decisions2 = run_mbc_obj_fun_test( baseline, varying, comp_name, comp_type; is_decremental = decremental, has_initial_input = init_input_bool, simulation = use_simulation, in_memory_store = in_memory_store, filename = SAVE_FILES ? "slopes_" : nothing, device_to_formulation = device_to_formulation, ) if !all(isapprox.(decisions1, decisions2)) @error decisions1 @error decisions2 end @assert all(approx_geq_1.(decisions1)) end end end @testset "MarketBidCost $(adj) with time varying breakpoints" begin baseline = build_func(false, false, false) varying = build_func(false, true, false) set_name!(baseline, "baseline") set_name!(varying, "varying") for use_simulation in (false, true) in_memory_store_opts = use_simulation ? [false, true] : [false] for in_memory_store in in_memory_store_opts decisions1, decisions2 = run_mbc_obj_fun_test( baseline, varying, comp_name, comp_type; is_decremental = decremental, has_initial_input = init_input_bool, simulation = use_simulation, in_memory_store = in_memory_store, filename = SAVE_FILES ? "breakpoints_" : nothing, device_to_formulation = device_to_formulation, ) if !all(isapprox.(decisions1, decisions2)) @error decisions1 @error decisions2 end @assert all(approx_geq_1.(decisions1)) end end end @testset "MarketBidCost $(adj) with time varying everything" begin baseline = build_func(false, false, false) varying = build_func(init_input_bool, true, true) set_name!(baseline, "baseline") set_name!(varying, "varying") for use_simulation in (false, true) decisions1, decisions2 = run_mbc_obj_fun_test( baseline, varying, comp_name, comp_type; simulation = use_simulation, has_initial_input = init_input_bool, is_decremental = decremental, filename = SAVE_FILES ? "everything_" : nothing, device_to_formulation = device_to_formulation, ) if !all(isapprox.(decisions1, decisions2)) @error decisions1 @error decisions2 end @assert all(approx_geq_1.(decisions1)) end end @testset "MarketBidCost $(adj) with variable number of tranches" begin baseline = build_func(init_input_bool, true, true) set_name!(baseline, "baseline") variable_tranches = build_func(init_input_bool, true, true; create_extra_tranches = true) set_name!(variable_tranches, "variable") test_generic_mbc_equivalence( baseline, variable_tranches; filename = SAVE_FILES ? "tranches_" : nothing, is_decremental = decremental, device_to_formulation = device_to_formulation, ) end end end @testset "MarketBidCost incremental with heterogeneous time series names" begin sel = make_selector(x -> get_operation_cost(x) isa MarketBidCost, ThermalStandard) baseline = build_sys_incr(true, true, true; active_components = sel) @assert length(get_components(sel, baseline)) == 2 # Should succeed for varying initial input time series names: variable_ii_names = build_sys_incr( true, true, true; active_components = sel, initial_input_names_vary = true, ) test_generic_mbc_equivalence(baseline, variable_ii_names) # Should give an informative error for varying variable cost time series names: variable_vc_names = build_sys_incr( true, true, true; active_components = sel, variable_cost_names_vary = true, ) model = build_generic_mbc_model(variable_vc_names; multistart = false) test_path = mktempdir() PSI.set_output_dir!(model, test_path) # Commented out temporarily as the error changed # @test_throws "All time series names must be equal" PSI.build_impl!(model) # see below re: build_impl! end @testset "Test some MarketBidCost data validations" begin # Test multistart and convexity validation nonconvex = build_sys_incr( false, false, false; modify_baseline_pwl = pwl -> begin y_coords = get_y_coords(pwl) y_coords[3] = y_coords[1] pwl end, ) set_start_up!( get_operation_cost(get_component(ThermalStandard, nonconvex, "Test Unit2")), (hot = 1.0, warm = 1.5, cold = 2.0), ) model = build_generic_mbc_model(nonconvex; multistart = false) # We'll use build_impl! rather than build! to keep PSI's logging configuration from interfering with @test_logs and polluting the test output mkpath(test_path) PSI.set_output_dir!(model, test_path) @test_logs (:warn, r"Multi-start costs detected for non-multi-start unit Test Unit2.*") ( match_mode = :any ) (@test_throws "is non-convex" PSI.build_impl!(model)) # Test constant P1 validation variable_p1 = build_sys_incr(false, true, false; do_override_min_x = false) model = build_generic_mbc_model(variable_p1; multistart = false) mkpath(test_path) PSI.set_output_dir!(model, test_path) @test_throws "Inconsistent minimum breakpoint values" PSI.build_impl!(model) end @testset "Test 3d results" begin # TODO: Test actual values varying = build_sys_incr(true, true, true) for in_memory_store in (false, true) # model1, res1 = run_generic_mbc_sim(baseline) model2, res2 = run_generic_mbc_sim(varying; in_memory_store = in_memory_store) parameters = read_parameters(res2) @test haskey( parameters, "IncrementalPiecewiseLinearBreakpointParameter__ThermalStandard", ) for df in values( parameters["IncrementalPiecewiseLinearBreakpointParameter__ThermalStandard"], ) @test names(df) == ["DateTime", "name", "name2", "value"] end for (key, df) in read_realized_parameters(res2) if key in ( "IncrementalPiecewiseLinearBreakpointParameter__ThermalStandard", "IncrementalPiecewiseLinearSlopeParameter__ThermalStandard", ) @test names(df) == ["DateTime", "name", "name2", "value"] else @test names(df) == ["DateTime", "name", "value"] end end # TODO: Test actual values end end @testset "concavity check error" begin sys = build_system(PSITestSystems, "c_sys5_il") load = first(get_components(PSY.InterruptiblePowerLoad, sys)) selector = make_selector(PSY.InterruptiblePowerLoad, get_name(load)) non_decr_slopes = [0.13, 0.11, 0.12] # Non-decreasing slopes (should trigger error) x_coords = [0.1, 0.3, 0.6, 1.0] pw_curve = PiecewiseIncrementalCurve(0.0, 0.0, x_coords, non_decr_slopes) add_mbc_inner!(sys, selector; decr_curve = pw_curve) # Fixed: pass selector, not slopes comp = first(get_components(selector, sys)) @assert typeof(get_operation_cost(comp)) == PSY.MarketBidCost model = build_generic_mbc_model(sys) mkpath(test_path) PSI.set_output_dir!(model, test_path) msg = "ArgumentError: Decremental MarketBidCost for component $(get_name(load)) is non-concave" @test_throws msg PSI.build_impl!(model) end @testset "MarketBidCost decremental basic: single problem" begin sys = build_system(PSITestSystems, "c_sys5_il") load = first(get_components(PSY.InterruptiblePowerLoad, sys)) selector = make_selector(PSY.InterruptiblePowerLoad, get_name(load)) add_mbc!(sys, selector; incremental = false, decremental = true) @assert typeof(get_operation_cost(load)) == PSY.MarketBidCost _, res = run_generic_mbc_prob(sys) end @testset "MarketBidCost decremental basic: simulation" begin sys = build_system(PSITestSystems, "c_sys5_il") load = first(get_components(PSY.InterruptiblePowerLoad, sys)) selector = make_selector(PSY.InterruptiblePowerLoad, get_name(load)) add_mbc!(sys, selector; incremental = false, decremental = true) extend_mbc!(sys, selector) op_cost = get_operation_cost(load) @assert typeof(op_cost) == PSY.MarketBidCost @assert typeof(get_decremental_offer_curves(op_cost)) <: PSY.TimeSeriesKey _, res = run_generic_mbc_sim(sys) end @testset "MarketBidCost decremental PowerLoadInterruption, no time series vs constant time series" begin sys_no_ts = load_sys_decr2() sys_constant_ts = build_sys_decr2(false, false, false) test_generic_mbc_equivalence(sys_no_ts, sys_constant_ts) end # TODO error if there's nonzero decremental initial input for PowerLoadDispatch. @testset "MarketBidCost decremental PowerLoadDispatch, no time series vs constant time series" begin device_to_formulation = FormulationDict(PSY.InterruptiblePowerLoad => PowerLoadDispatch) sys_no_ts = load_sys_decr2() sys_constant_ts = build_sys_decr2(false, false, false) test_generic_mbc_equivalence( sys_no_ts, sys_constant_ts; device_to_formulation = device_to_formulation, ) end @testset "Test VOM cost time normalization across different resolutions" begin # Test that VOM costs scale correctly with time resolution # This validates the bugfix in common.jl lines 188-196 # Build system at hourly resolution sys_hourly = build_system(PSITestSystems, "c_sys5") # Add VOM cost to a thermal unit thermal_unit = first(get_components(ThermalStandard, sys_hourly)) op_cost = get_operation_cost(thermal_unit) # Modify the VOM cost on the existing variable cost structure # VOM cost is stored in the CostCurve's vom_cost field if op_cost isa PSY.ThermalGenerationCost var_cost = PSY.get_variable(op_cost) value_curve = PSY.get_value_curve(var_cost) power_units = PSY.get_power_units(var_cost) # Create new CostCurve with non-zero VOM (LinearCurve with proportional term = 5.0) vom_value = LinearCurve(5.0) # $/MWh new_var_cost = CostCurve(value_curve, power_units, vom_value) new_op_cost = PSY.ThermalGenerationCost(; variable = new_var_cost, fixed = get_fixed(op_cost), start_up = get_start_up(op_cost), shut_down = get_shut_down(op_cost), ) set_operation_cost!(thermal_unit, new_op_cost) end # Build and solve at hourly resolution template_hourly = ProblemTemplate(NetworkModel(CopperPlatePowerModel)) set_device_model!(template_hourly, ThermalStandard, ThermalDispatchNoMin) set_device_model!(template_hourly, PowerLoad, StaticPowerLoad) model_hourly = DecisionModel( template_hourly, sys_hourly; name = "VOM_hourly", optimizer = HiGHS_optimizer, optimizer_solve_log_print = false, ) @test build!(model_hourly; output_dir = test_path) == PSI.ModelBuildStatus.BUILT @test solve!(model_hourly) == PSI.RunStatus.SUCCESSFULLY_FINALIZED results_hourly = OptimizationProblemResults(model_hourly) expr_hourly = read_expression( results_hourly, "ProductionCostExpression__ThermalStandard"; table_format = TableFormat.WIDE, ) # Build system at 30-minute resolution (same system, different model resolution) sys_30min = build_system(PSITestSystems, "c_sys5") # Add same VOM cost to thermal unit thermal_unit_30 = first(get_components(ThermalStandard, sys_30min)) op_cost_30 = get_operation_cost(thermal_unit_30) if op_cost_30 isa PSY.ThermalGenerationCost var_cost_30 = PSY.get_variable(op_cost_30) value_curve_30 = PSY.get_value_curve(var_cost_30) power_units_30 = PSY.get_power_units(var_cost_30) # Create new CostCurve with same VOM cost vom_value_30 = LinearCurve(5.0) # $/MWh new_var_cost_30 = CostCurve(value_curve_30, power_units_30, vom_value_30) new_op_cost_30 = PSY.ThermalGenerationCost(; variable = new_var_cost_30, fixed = get_fixed(op_cost_30), start_up = get_start_up(op_cost_30), shut_down = get_shut_down(op_cost_30), ) set_operation_cost!(thermal_unit_30, new_op_cost_30) end # Build and solve at 30-minute resolution template_30min = ProblemTemplate(NetworkModel(CopperPlatePowerModel)) set_device_model!(template_30min, ThermalStandard, ThermalDispatchNoMin) set_device_model!(template_30min, PowerLoad, StaticPowerLoad) model_30min = DecisionModel( template_30min, sys_30min; name = "VOM_30min", optimizer = HiGHS_optimizer, optimizer_solve_log_print = false, resolution = Dates.Minute(30), # Set 30-minute resolution here ) @test build!(model_30min; output_dir = test_path) == PSI.ModelBuildStatus.BUILT @test solve!(model_30min) == PSI.RunStatus.SUCCESSFULLY_FINALIZED results_30min = OptimizationProblemResults(model_30min) expr_30min = read_expression( results_30min, "ProductionCostExpression__ThermalStandard"; table_format = TableFormat.WIDE, ) # Get active power values to compute expected VOM costs p_hourly = read_variable( results_hourly, "ActivePowerVariable__ThermalStandard"; table_format = TableFormat.WIDE, ) p_30min = read_variable( results_30min, "ActivePowerVariable__ThermalStandard"; table_format = TableFormat.WIDE, ) # Verify VOM costs scale with resolution # For 30-min resolution, each time step is 0.5 hours, so VOM cost = vom_value * power * 0.5 # For hourly resolution, each time step is 1.0 hours, so VOM cost = vom_value * power * 1.0 unit_name = get_name(thermal_unit) # Sum total costs over all time steps total_cost_hourly = sum(expr_hourly[!, unit_name]) total_cost_30min = sum(expr_30min[!, unit_name]) # The total costs should be approximately equal because: # - Hourly: 24 steps × power × VOM × 1.0 hour # - 30-min: 48 steps × power × VOM × 0.5 hour # Both should sum to roughly the same total cost over 24 hours @test isapprox(total_cost_hourly, total_cost_30min; rtol = 0.05) end @testset "Test MarketBidCost VOM cost time normalization across different resolutions" begin # Test that VOM costs in MarketBidCost (OfferCurveCost path) scale correctly # with time resolution. This validates the bugfix in market_bid.jl # _add_vom_cost_to_objective_helper! (GitHub issue #1531) function _build_mbc_vom_system() sys = load_sys_incr() unit = get_component(ThermalStandard, sys, "Test Unit1") mbc = get_operation_cost(unit) offer_curves = get_incremental_offer_curves(mbc) # Add VOM cost to the existing offer curves new_offer_curves = CostCurve( get_value_curve(offer_curves), get_power_units(offer_curves), LinearCurve(5.0), # $/MWh VOM cost ) set_incremental_offer_curves!(mbc, new_offer_curves) return sys, get_name(unit) end # Build and solve at hourly resolution sys_hourly, unit_name = _build_mbc_vom_system() template_hourly = ProblemTemplate(NetworkModel(CopperPlatePowerModel)) set_device_model!(template_hourly, ThermalStandard, ThermalBasicUnitCommitment) set_device_model!(template_hourly, PowerLoad, StaticPowerLoad) model_hourly = DecisionModel( template_hourly, sys_hourly; name = "MBC_VOM_hourly", optimizer = HiGHS_optimizer, optimizer_solve_log_print = false, ) @test build!(model_hourly; output_dir = test_path) == PSI.ModelBuildStatus.BUILT @test solve!(model_hourly) == PSI.RunStatus.SUCCESSFULLY_FINALIZED results_hourly = OptimizationProblemResults(model_hourly) expr_hourly = read_expression( results_hourly, "ProductionCostExpression__ThermalStandard"; table_format = TableFormat.WIDE, ) # Build and solve at 30-minute resolution sys_30min, _ = _build_mbc_vom_system() template_30min = ProblemTemplate(NetworkModel(CopperPlatePowerModel)) set_device_model!(template_30min, ThermalStandard, ThermalBasicUnitCommitment) set_device_model!(template_30min, PowerLoad, StaticPowerLoad) model_30min = DecisionModel( template_30min, sys_30min; name = "MBC_VOM_30min", optimizer = HiGHS_optimizer, optimizer_solve_log_print = false, resolution = Dates.Minute(30), ) @test build!(model_30min; output_dir = test_path) == PSI.ModelBuildStatus.BUILT @test solve!(model_30min) == PSI.RunStatus.SUCCESSFULLY_FINALIZED results_30min = OptimizationProblemResults(model_30min) expr_30min = read_expression( results_30min, "ProductionCostExpression__ThermalStandard"; table_format = TableFormat.WIDE, ) total_cost_hourly = sum(expr_hourly[!, unit_name]) total_cost_30min = sum(expr_30min[!, unit_name]) # Total costs should be approximately equal: # - Hourly: 24 steps × power × cost × 1.0 hour # - 30-min: 48 steps × power × cost × 0.5 hour @test isapprox(total_cost_hourly, total_cost_30min; rtol = 0.05) end @testset "Test Market Bid Cost With Single Time Serie" begin sys = build_system(PSITestSystems, "c_sys5_uc"; add_single_time_series = true) existing_ts = get_time_series_array( SingleTimeSeries, first(get_components(PowerLoad, sys)), "max_active_power", ) tstamps = timestamp(existing_ts) psd1 = PiecewiseStepData([0.0, 40.0], [5.0]) psd2 = PiecewiseStepData([0.0, 30.0, 400.0], [10.0, 20.0]) psd3 = PiecewiseStepData([0.0, 40.0], [500.0]) # Cheap the first 10 hours, moderate next 4 hours, expensive last 34 hours total_step_data = vcat([psd1 for x in 1:10], [psd2 for x in 1:4], [psd3 for x in 1:34]) mbid_tarray = TimeArray(tstamps, total_step_data) ts_mbid = SingleTimeSeries(; name = "variable_cost", data = mbid_tarray) th = get_component(ThermalStandard, sys, "Alta") # Create an empty market bid and set it th_cost = MarketBidCost(; no_load_cost = 0.0, start_up = (hot = 0.0, warm = 0.0, cold = 0.0), shut_down = 0.0, ) set_operation_cost!(th, th_cost) # Wrapper for adding the timeseries in incremental market bid cost set_variable_cost!(sys, th, ts_mbid, UnitSystem.NATURAL_UNITS) # It is also needed to create the initial input time series for market bid. That is the cost at 0 power at each time step. We will use zero for now. zero_input = zeros(length(tstamps)) zero_tarray = TimeArray(tstamps, zero_input) ts_zero = SingleTimeSeries(; name = "initial_input", data = zero_tarray) set_incremental_initial_input!(sys, th, ts_zero) transform_single_time_series!(sys, Hour(24), Hour(24)) template = ProblemTemplate(NetworkModel(CopperPlatePowerModel)) set_device_model!(template, ThermalStandard, ThermalBasicUnitCommitment) set_device_model!(template, HydroDispatch, HydroDispatchRunOfRiver) set_device_model!(template, PowerLoad, StaticPowerLoad) model = DecisionModel( template, sys; name = "UC_MBCost", optimizer = HiGHS_optimizer) @test build!(model; output_dir = mktempdir()) == PSI.ModelBuildStatus.BUILT @test solve!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED p_var = read_variable( OptimizationProblemResults(model), "ActivePowerVariable__ThermalStandard"; table_format = TableFormat.WIDE, ) @test isapprox(sum(p_var[!, "Alta"][15:24]), 0.0, atol = 1e-4) end ================================================ FILE: test/test_mbc_sanity_check.jl ================================================ # Test setup test_path = mktempdir() """ Create cost curves with varying slope breakpoints, slope magnitudes, and cost at minimum generation for InterruptibleLoad market bid cost testing. Each test scenario should produce measurably different objective values when the load curtailment levels change. """ function build_test_systems_with_different_curves() systems = Dict{String, PSY.System}() # Base system base_sys = PSB.build_system(PSITestSystems, "c_sys5_il") load_selector = make_selector(PSY.InterruptiblePowerLoad, "IloadBus4") # Test Case 1: Baseline - moderate slopes, moderate cost at min sys1 = deepcopy(base_sys) x_coords = [0.0, 25.0, 50.0, 75.0, 100.0] # MW breakpoints slopes = [40.0, 25.0, 15.0, 10.0] # $/MWh slopes (decreasing for decremental) initial_input = 8.0 # $/h cost at minimum generation curve1 = PiecewiseIncrementalCurve(0.0, initial_input, x_coords, slopes) add_mbc_inner!(sys1, load_selector; decr_curve = curve1) systems["baseline"] = sys1 # Test Case 2: Steeper slopes - should make curtailment more expensive sys2 = deepcopy(base_sys) steep_slopes = [80.0, 50.0, 30.0, 20.0] # Steeper decreasing slopes curve2 = PiecewiseIncrementalCurve(0.0, initial_input, x_coords, steep_slopes) add_mbc_inner!(sys2, load_selector; decr_curve = curve2) systems["steep_slopes"] = sys2 # Test Case 3: Different breakpoints - earlier steep decrease sys3 = deepcopy(base_sys) early_steep_x = [0.0, 10.0, 30.0, 60.0, 100.0] # Earlier transition to low cost early_steep_slopes = [80.0, 60.0, 40.0, 5.0] # High initial, then very low curve3 = PiecewiseIncrementalCurve(0.0, initial_input, early_steep_x, early_steep_slopes) add_mbc_inner!(sys3, load_selector; decr_curve = curve3) systems["early_steep"] = sys3 # Test Case 4: Higher cost at minimum generation sys4 = deepcopy(base_sys) high_min_cost = 25.0 # Much higher fixed cost curve4 = PiecewiseIncrementalCurve(0.0, high_min_cost, x_coords, slopes) # Use the correct decreasing slopes add_mbc_inner!(sys4, load_selector; decr_curve = curve4) systems["high_min_cost"] = sys4 # Test Case 5: Very flat curve - cheap curtailment sys5 = deepcopy(base_sys) flat_slopes = [5.0, 4.0, 3.0, 2.0] # Very shallow decreasing slopes low_min_cost = 2.0 # Low fixed cost curve5 = PiecewiseIncrementalCurve(0.0, low_min_cost, x_coords, flat_slopes) add_mbc_inner!(sys5, load_selector; decr_curve = curve5) systems["cheap_curtailment"] = sys5 return systems end function run_test_simulations(systems) results = Dict{String, Any}() for (name, sys) in systems # Build model # TODO move run_generic_mbc_sim from test_market_bid_cost.jl into test_utils # to prevent against include order "undefined function" errors. _, res = run_generic_mbc_sim(sys) # Extract key metrics obj_value = read_optimizer_stats(res)[1, "objective_value"] il = first(get_components(InterruptiblePowerLoad, sys)) il_ts = get_time_series(il, first(get_time_series_keys(il))) @assert il !== nothing "InterruptibleLoad component not found in system" @assert length(PSY.get_components(InterruptiblePowerLoad, sys)) == 1 "Expected exactly one InterruptibleLoad component" load_power = read_variable(res, ActivePowerVariable, InterruptiblePowerLoad) load_curtailments = Dict{DateTime, TimeArray}() total_curtailment = 0.0 for window in iterate_windows(il_ts) initial_time = first(TimeSeries.timestamp(window)) load_curtailments[initial_time] = TimeArray( window .- load_power[initial_time][!, :value]; colnames = [:value], ) total_curtailment += sum(TimeSeries.values(load_curtailments[initial_time])) end # Get load cost expression if available load_cost = read_expression(res, "ProductionCostExpression__InterruptiblePowerLoad") load_cost_out = Dict( k => TimeArray(v[!, [:DateTime, :value]]; timestamp = :DateTime) for (k, v) in load_cost ) total_load_cost = sum(sum(v[!, :value]) for v in values(load_cost)) results[name] = Dict( "objective" => obj_value, "curtailment" => load_curtailments, "total_curtailment" => total_curtailment, "load_cost" => load_cost_out, "total_load_cost" => total_load_cost, ) end return results end function analyze_results(results) baseline_obj = results["baseline"]["objective"] baseline_curtail = results["baseline"]["total_curtailment"] for (name, data) in results name == "baseline" && continue obj_diff = data["objective"] - baseline_obj curtail_diff = data["total_curtailment"] - baseline_curtail obj_pct = (obj_diff / baseline_obj) * 100 # we're minimizing the objective function if name == "steep_slopes" @test obj_diff < 0 # steeper demand curve => more $ of benefit per MWh => lower objective @test curtail_diff < 0 # Steeper slopes should reduce curtailment elseif name == "high_min_cost" @test obj_diff < 0 # more benefit per MWh => lower objective elseif name == "cheap_curtailment" @test obj_diff > 0 # less $ benefit per MWh => higher objective @test curtail_diff > 0 # Cheaper curtailment should increase curtailment amount elseif name == "early_steep" # The early steep curve should affect behavior differently depending on curtailment levels end end end @testset "MBC Sanity Check" begin systems = build_test_systems_with_different_curves() results = run_test_simulations(systems) analyze_results(results) end ================================================ FILE: test/test_model_decision.jl ================================================ @testset "Decision Model kwargs" begin template = get_thermal_dispatch_template_network() c_sys5 = PSB.build_system(PSITestSystems, "c_sys5") c_sys5_re = PSB.build_system(PSITestSystems, "c_sys5_re") @test_throws MethodError DecisionModel(template, c_sys5; bad_kwarg = 10) model = DecisionModel(template, c_sys5; optimizer = HiGHS_optimizer) @test build!(model; output_dir = mktempdir(; cleanup = true)) == PSI.ModelBuildStatus.BUILT model = DecisionModel( MockOperationProblem, get_thermal_dispatch_template_network( NetworkModel(CopperPlatePowerModel; use_slacks = true), ), c_sys5_re; optimizer = HiGHS_optimizer, ) @test build!(model; output_dir = mktempdir(; cleanup = true)) == PSI.ModelBuildStatus.BUILT model = DecisionModel( get_thermal_dispatch_template_network(), c_sys5; optimizer = HiGHS_optimizer, ) @test build!(model; output_dir = mktempdir(; cleanup = true)) == PSI.ModelBuildStatus.BUILT #"Test passing custom JuMP model" my_model = JuMP.Model() my_model.ext[:PSI_Testing] = 1 c_sys5 = PSB.build_system(PSITestSystems, "c_sys5") model = DecisionModel( get_thermal_dispatch_template_network(), c_sys5, my_model; optimizer = HiGHS_optimizer, ) @test build!(model; output_dir = mktempdir(; cleanup = true)) == PSI.ModelBuildStatus.BUILT @test haskey(PSI.get_optimization_container(model).JuMPmodel.ext, :PSI_Testing) end @testset "Set optimizer at solve call" begin c_sys5 = PSB.build_system(PSITestSystems, "c_sys5") template = get_thermal_standard_uc_template() UC = DecisionModel(template, c_sys5; optimizer = HiGHS_optimizer) output_dir = mktempdir(; cleanup = true) @test build!(UC; output_dir = output_dir) == PSI.ModelBuildStatus.BUILT @test solve!(UC; optimizer = HiGHS_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) @test size(read_variable(res, "StartVariable__ThermalStandard")) == (120, 3) @test size( read_variable( res, "StartVariable__ThermalStandard"; table_format = TableFormat.WIDE, ), ) == (24, 6) @test size(read_parameter(res, "ActivePowerTimeSeriesParameter__PowerLoad")) == (72, 3) @test size(read_expression(res, "ProductionCostExpression__ThermalStandard")) == (120, 3) @test size(read_aux_variable(res, "TimeDurationOn__ThermalStandard")) == (120, 3) @test length(read_variables(res; table_format = TableFormat.WIDE)) == 4 @test length(read_parameters(res; table_format = TableFormat.WIDE)) == 1 @test length(read_duals(res; table_format = TableFormat.WIDE)) == 0 @test length(read_expressions(res; table_format = TableFormat.WIDE)) == 7 @test read_variables( res, ["StartVariable__ThermalStandard"]; table_format = TableFormat.WIDE, )["StartVariable__ThermalStandard"] == read_variable( res, "StartVariable__ThermalStandard"; table_format = TableFormat.WIDE, ) @test read_variables( res, [(StartVariable, ThermalStandard)]; table_format = TableFormat.WIDE, )["StartVariable__ThermalStandard"] == read_variable( res, StartVariable, ThermalStandard; table_format = TableFormat.WIDE, ) @test read_parameters( res, ["ActivePowerTimeSeriesParameter__PowerLoad"]; table_format = TableFormat.WIDE, )["ActivePowerTimeSeriesParameter__PowerLoad"] == read_parameter( res, "ActivePowerTimeSeriesParameter__PowerLoad"; table_format = TableFormat.WIDE, ) @test read_parameters( res, [(ActivePowerTimeSeriesParameter, PowerLoad)]; table_format = TableFormat.WIDE, )["ActivePowerTimeSeriesParameter__PowerLoad"] == read_parameter( res, ActivePowerTimeSeriesParameter, PowerLoad; table_format = TableFormat.WIDE, ) @test read_aux_variables( res, ["TimeDurationOff__ThermalStandard"]; table_format = TableFormat.WIDE, )["TimeDurationOff__ThermalStandard"] == read_aux_variable( res, "TimeDurationOff__ThermalStandard"; table_format = TableFormat.WIDE, ) @test read_aux_variables( res, [(TimeDurationOff, ThermalStandard)]; table_format = TableFormat.WIDE, )["TimeDurationOff__ThermalStandard"] == read_aux_variable( res, TimeDurationOff, ThermalStandard; table_format = TableFormat.WIDE, ) @test read_expressions( res, ["ProductionCostExpression__ThermalStandard"]; table_format = TableFormat.WIDE, )["ProductionCostExpression__ThermalStandard"] == read_expression( res, "ProductionCostExpression__ThermalStandard"; table_format = TableFormat.WIDE, ) @test read_expressions( res, [(PSI.ProductionCostExpression, ThermalStandard)]; table_format = TableFormat.WIDE, )["ProductionCostExpression__ThermalStandard"] == read_expression( res, PSI.ProductionCostExpression, ThermalStandard; table_format = TableFormat.WIDE, ) @test length(read_aux_variables(res; table_format = TableFormat.WIDE)) == 2 @test first( keys( read_aux_variables( res, [(PSI.TimeDurationOff, ThermalStandard)]; table_format = TableFormat.WIDE, ), ), ) == "TimeDurationOff__ThermalStandard" export_results(res) results_dir = joinpath(output_dir, "results") @test isfile(joinpath(results_dir, "optimizer_stats.csv")) variables_dir = joinpath(results_dir, "variables") @test isfile(joinpath(variables_dir, "ActivePowerVariable__ThermalStandard.csv")) end @testset "Test optimization debugging functions" begin c_sys5 = PSB.build_system(PSITestSystems, "c_sys5") template = get_thermal_standard_uc_template() model = DecisionModel(template, c_sys5; optimizer = HiGHS_optimizer) @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) for (key, index, moi_index) in constraint_indices val1 = get_constraint_index(model, moi_index) val2 = container.constraints[key].data[index] @test val1 == val2 end @test get_constraint_index(model, length(constraint_indices) + 1) === nothing var_keys = PSI.get_all_variable_keys(model) 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] == ISOPT.encode_key(key) @test index_tuple[2] == index @test index_tuple[3] == moi_index val1 = get_variable_index(model, moi_index) val2 = container.variables[key].data[index] @test val1 == val2 end @test get_variable_index(model, length(var_index) + 1) === nothing end @testset "Decision Model Solve with Slacks" begin c_sys5_re = PSB.build_system(PSITestSystems, "c_sys5_re") networks = [PTDFPowerModel, DCPPowerModel, ACPPowerModel] for network in networks template = get_thermal_dispatch_template_network( NetworkModel(network; use_slacks = true, PTDF_matrix = PTDF(c_sys5_re)), ) model = DecisionModel(template, c_sys5_re; optimizer = ipopt_optimizer) @test build!(model; output_dir = mktempdir(; cleanup = true)) == PSI.ModelBuildStatus.BUILT @test solve!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED end end @testset "Test Locational Marginal Prices between DC lossless with PowerModels vs PTDFPowerModel" begin networks = [DCPPowerModel, PTDFPowerModel] sys = PSB.build_system(PSITestSystems, "c_sys5") ptdf = VirtualPTDF(sys) # These are the duals of interest for the test dual_constraint = [[NodalBalanceActiveConstraint], [CopperPlateBalanceConstraint]] LMPs = [] for (ix, network) in enumerate(networks) template = get_template_dispatch_with_network( NetworkModel(network; PTDF_matrix = ptdf, duals = dual_constraint[ix]), ) if network == PTDFPowerModel set_device_model!( template, DeviceModel(PSY.Line, PSI.StaticBranch; duals = [FlowRateConstraint]), ) end model = DecisionModel(template, sys; optimizer = HiGHS_optimizer) @test build!(model; output_dir = mktempdir(; cleanup = true)) == PSI.ModelBuildStatus.BUILT @test solve!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED res = OptimizationProblemResults(model) # These tests require results to be working if network == PTDFPowerModel push!(LMPs, abs.(psi_ptdf_lmps(res, ptdf))) else duals = read_dual( res, NodalBalanceActiveConstraint, ACBus; table_format = TableFormat.WIDE, ) duals = abs.(duals[:, propertynames(duals) .!== :DateTime]) push!(LMPs, duals[!, sort(propertynames(duals))]) end end @test isapprox(LMPs[1], LMPs[2], atol = 100.0) end @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.ModelBuildStatus.BUILT @test solve!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED res = OptimizationProblemResults(model) container = PSI.get_optimization_container(model) constraint_key = PSI.ConstraintKey(CopperPlateBalanceConstraint, PSY.System) constraints = PSI.get_constraints(container)[constraint_key] dual_results = PSI.read_duals(container)[constraint_key] dual_results_read = read_dual(res, constraint_key; table_format = TableFormat.WIDE) realized_dual_results = read_duals(res, [constraint_key]; table_format = TableFormat.WIDE)[PSI.encode_key_as_string( constraint_key, )] realized_dual_results_string = read_duals( res, [PSI.encode_key_as_string(constraint_key)]; table_format = TableFormat.WIDE, )[PSI.encode_key_as_string( constraint_key, )] @test dual_results == dual_results_read[:, propertynames(dual_results_read) .!= :DateTime] == realized_dual_results[:, propertynames(realized_dual_results) .!= :DateTime] == realized_dual_results_string[ :, propertynames(realized_dual_results_string) .!= :DateTime, ] for i in axes(constraints)[1], j in axes(constraints)[2] dual = JuMP.dual(constraints[i, j]) @test isapprox(dual, dual_results[j, 1]) end system = PSI.get_system(model) parameter_key = PSI.ParameterKey(ActivePowerTimeSeriesParameter, PSY.PowerLoad) param_vals = PSI.read_parameters(container)[parameter_key] for load in get_components(PowerLoad, system) name = get_name(load) vals = get_time_series_values(Deterministic, load, "max_active_power") vals = vals .* get_max_active_power(load) * -1.0 @test all(vals .== param_vals[name, :]) end 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; table_format = TableFormat.WIDE), Dict{String, DataFrames.DataFrame}, ) @test isa(ISOPT.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; table_format = TableFormat.WIDE), Dict{String, DataFrames.DataFrame}, ) @test isa(res.parameter_values, Dict{PSI.ParameterKey, DataFrames.DataFrame}) @test isa( read_parameters(res; table_format = TableFormat.WIDE), Dict{String, DataFrames.DataFrame}, ) @test isa(PSI.get_resolution(res), Dates.TimePeriod) @test isa(PSI.get_forecast_horizon(res), Int64) @test isa(get_realized_timestamps(res), StepRange{DateTime}) @test isa(ISOPT.get_source_data(res), PSY.System) @test length(get_timestamps(res)) == 24 PSY.set_available!(first(get_components(ThermalStandard, get_system(res))), false) @test collect(get_components(ThermalStandard, res)) == collect(get_available_components(ThermalStandard, get_system(res))) sel = PSY.make_selector(ThermalStandard; groupby = :each) @test collect(get_groups(sel, res)) == collect(get_available_groups(sel, get_system(res))) end @testset "Solve DecisionModelModel with auto-build" begin c_sys5 = PSB.build_system(PSITestSystems, "c_sys5") template = get_thermal_standard_uc_template() UC = DecisionModel(template, c_sys5; optimizer = HiGHS_optimizer) output_dir = mktempdir(; cleanup = true) @test_throws ErrorException solve!(UC) @test solve!(UC; optimizer = HiGHS_optimizer, output_dir = output_dir) == PSI.RunStatus.SUCCESSFULLY_FINALIZED end @testset "Test NonSpinning reseve model" begin c_sys5 = PSB.build_system(PSITestSystems, "c_sys5_uc_non_spin"; add_reserves = true) template = get_thermal_standard_uc_template() set_device_model!( template, DeviceModel(ThermalMultiStart, ThermalStandardUnitCommitment), ) set_service_model!( template, ServiceModel(VariableReserveNonSpinning, NonSpinningReserve, "NonSpinningReserve"), ) UC = DecisionModel(template, c_sys5; optimizer = HiGHS_optimizer) output_dir = mktempdir(; cleanup = true) @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, PSY.VariableReserveNonSpinning, "NonSpinningReserve", ) @test service_key in keys(vars) end @testset "Test serialization/deserialization of DecisionModel results" begin path = mktempdir(; cleanup = true) 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 = 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 = 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, ISOPT._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) isa PSY.System exp_file = joinpath(path, "results", "variables", "ActivePowerVariable__ThermalStandard.csv") var4 = PSI.read_dataframe(exp_file) # Manually Multiply by the base power var1_a has natural units and export writes directly from the solver @test var1_a.value == var4.value .* 100.0 exported = readdir(ISOPT.export_realized_results(results1)) @test length(exported) >= 12 @test any(contains.(exported, "ProductionCostExpression")) end @testset "Test Numerical Stability of Constraints" begin template = get_thermal_dispatch_template_network() c_sys5 = PSB.build_system(PSITestSystems, "c_sys5") valid_bounds = (coefficient = (min = 1.0, max = 1.0), rhs = (min = 0.4, max = 9.930296584)) model = DecisionModel(template, c_sys5; optimizer = HiGHS_optimizer) @test build!(model; output_dir = mktempdir(; cleanup = true)) == PSI.ModelBuildStatus.BUILT bounds = PSI.get_constraint_numerical_bounds(model) _check_constraint_bounds(bounds, valid_bounds) model_bounds = PSI.get_detailed_constraint_numerical_bounds(model) valid_model_bounds = Dict( :CopperPlateBalanceConstraint__System => ( coefficient = (min = 1.0, max = 1.0), rhs = (min = 6.434489705000001, max = 9.930296584), ), :ActivePowerVariableLimitsConstraint__ThermalStandard__lb => (coefficient = (min = 1.0, max = 1.0), rhs = (min = Inf, max = -Inf)), :ActivePowerVariableLimitsConstraint__ThermalStandard__ub => (coefficient = (min = 1.0, max = 1.0), rhs = (min = 0.4, max = 6.0)), ) for (constraint_key, constraint_bounds) in model_bounds _check_constraint_bounds( constraint_bounds, valid_model_bounds[ISOPT.encode_key(constraint_key)], ) end end @testset "Test Numerical Stability of Variables" begin template = get_template_basic_uc_simulation() c_sys5 = PSB.build_system(PSITestSystems, "c_sys5_uc") valid_bounds = (min = 0.0, max = 6.0) model = DecisionModel(template, c_sys5; optimizer = HiGHS_optimizer) @test build!(model; output_dir = mktempdir(; cleanup = true)) == PSI.ModelBuildStatus.BUILT bounds = PSI.get_variable_numerical_bounds(model) _check_variable_bounds(bounds, valid_bounds) model_bounds = PSI.get_detailed_variable_numerical_bounds(model) valid_model_bounds = Dict( :StopVariable__ThermalStandard => (min = 0.0, max = 1.0), :StartVariable__ThermalStandard => (min = 0.0, max = 1.0), :ActivePowerVariable__ThermalStandard => (min = 0.4, max = 6.0), :OnVariable__ThermalStandard => (min = 0.0, max = 1.0), ) for (variable_key, variable_bounds) in model_bounds _check_variable_bounds( variable_bounds, valid_model_bounds[ISOPT.encode_key(variable_key)], ) end end @testset "Decision Model initial_conditions test for ThermalGen" begin ######## 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, ThermalStandardUnitCommitment) model = DecisionModel(template, c_sys5_uc; optimizer = HiGHS_optimizer) @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) == 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)) == 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) == PSI.RunStatus.SUCCESSFULLY_FINALIZED ######## 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, ThermalStandardUnitCommitment) set_device_model!(template, ThermalStandard, ThermalStandardUnitCommitment) model = DecisionModel(template, c_sys5_uc; optimizer = HiGHS_optimizer) @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) == PSI.RunStatus.SUCCESSFULLY_FINALIZED end @testset "Decision Model initial_conditions test for Hydro" begin ######## Test with HydroDispatchRunOfRiver ######## template = get_thermal_dispatch_template_network() c_sys5_hyd = PSB.build_system(PSITestSystems, "c_sys5_hyd"; force_build = true) set_device_model!(template, HydroDispatch, HydroDispatchRunOfRiver) set_device_model!(template, HydroTurbine, HydroTurbineEnergyDispatch) set_device_model!(template, HydroReservoir, HydroEnergyModelReservoir) model = DecisionModel(template, c_sys5_hyd; optimizer = HiGHS_optimizer) @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( initial_conditions_data, ActivePowerVariable(), HydroTurbine, ) @test solve!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED ######## Test with HydroCommitmentRunOfRiver ######## template = get_thermal_dispatch_template_network() c_sys5_hyd = PSB.build_system(PSITestSystems, "c_sys5_hyd"; force_build = true) set_device_model!(template, HydroDispatch, HydroCommitmentRunOfRiver) set_device_model!(template, HydroTurbine, HydroTurbineEnergyCommitment) set_device_model!(template, HydroReservoir, HydroEnergyModelReservoir) model = DecisionModel(template, c_sys5_hyd; optimizer = HiGHS_optimizer) @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( initial_conditions_data, OnVariable(), HydroTurbine, ) @test solve!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED end @testset "Test serialization of InitialConditionsData" begin sys = PSB.build_system(PSITestSystems, "c_sys5") template = get_thermal_standard_uc_template() optimizer = HiGHS_optimizer # Construct and build with default behavior that builds initial conditions. model = DecisionModel(template, sys; optimizer = optimizer) output_dir = mktempdir(; cleanup = true) @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) == PSI.RunStatus.SUCCESSFULLY_FINALIZED # Build again. Initial conditions should be rebuilt. PSI.reset!(model) @test build!(model; output_dir = output_dir) == PSI.ModelBuildStatus.BUILT test_ic_serialization_outputs(model; ic_file_exists = true, message = "make") @test solve!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED # Build again, use existing initial conditions. model = DecisionModel( template, sys; optimizer = optimizer, deserialize_initial_conditions = true, ) @test build!(model; output_dir = output_dir) == PSI.ModelBuildStatus.BUILT test_ic_serialization_outputs(model; ic_file_exists = true, message = "deserialize") @test solve!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED # Construct and build again with custom initial conditions file. initialization_file = joinpath(output_dir, ic_file * ".old") mv(ic_file, initialization_file) touch(ic_file) model = DecisionModel( template, sys; optimizer = optimizer, initialization_file = initialization_file, deserialize_initial_conditions = true, ) @test build!(model; output_dir = output_dir) == PSI.ModelBuildStatus.BUILT test_ic_serialization_outputs(model; ic_file_exists = true, message = "deserialize") @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.ModelBuildStatus.BUILT test_ic_serialization_outputs(model; ic_file_exists = false, message = "skip") @test solve!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED # Conflicting inputs model = DecisionModel( template, sys; optimizer = optimizer, initialize_model = false, deserialize_initial_conditions = true, ) @test build!(model; output_dir = output_dir, console_level = Logging.AboveMaxLevel) == PSI.ModelBuildStatus.FAILED model = DecisionModel( template, sys; optimizer = optimizer, initialize_model = false, initialization_file = "init_file.bin", ) build!(model; output_dir = output_dir, console_level = Logging.AboveMaxLevel) == PSI.ModelBuildStatus.FAILED end @testset "Solve with detailed optimizer stats" begin c_sys5 = PSB.build_system(PSITestSystems, "c_sys5") template = get_thermal_standard_uc_template() UC = DecisionModel( template, c_sys5; optimizer = HiGHS_optimizer, detailed_optimizer_stats = true, ) output_dir = mktempdir(; cleanup = true) @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 @testset "Test filter function atttribute" begin c_sys5 = PSB.build_system(PSITestSystems, "c_sys5") template = get_thermal_standard_uc_template() new_model = DeviceModel( ThermalStandard, ThermalBasicUnitCommitment; attributes = Dict("filter_function" => x -> PSY.get_name(x) != "Alta"), ) set_device_model!(template, new_model) UC = DecisionModel( template, c_sys5; optimizer = HiGHS_optimizer, detailed_optimizer_stats = true, ) output_dir = mktempdir(; cleanup = true) @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), ActivePowerVariable(), ThermalStandard, ) @test "Alta" ∉ axes(p_variable, 1) end @testset "Test for isolated buses" begin c_sys5 = PSB.build_system(PSITestSystems, "c_sys5") add_component!(c_sys5, ACBus( 10, "node_none", true, "ISOLATED", 0, 1.0, (min = 0.9, max = 1.05), 230, nothing, nothing, ), ) template = get_thermal_standard_uc_template() new_model = DeviceModel( ThermalStandard, ThermalBasicUnitCommitment; ) set_device_model!(template, new_model) UC = DecisionModel( template, c_sys5; optimizer = HiGHS_optimizer, detailed_optimizer_stats = true, ) output_dir = mktempdir(; cleanup = true) @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( EnergyReservoirStorage, StorageDispatchWithReserves; attributes = Dict{String, Any}( "reservation" => true, "cycling_limits" => false, "energy_target" => true, "complete_coverage" => false, "regularization" => false, ), ) set_device_model!(template, device_model) output_dir = mktempdir(; cleanup = true) model = DecisionModel( template, c_sys5_bat; optimizer = HiGHS_optimizer, ) @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 ================================================ FILE: test/test_model_emulation.jl ================================================ @testset "Emulation Model Build" begin template = get_thermal_dispatch_template_network() c_sys5 = PSB.build_system( PSITestSystems, "c_sys5_uc"; add_single_time_series = true, force_build = true, ) model = EmulationModel(template, c_sys5; optimizer = HiGHS_optimizer) @test build!(model; executions = 10, output_dir = mktempdir(; cleanup = true)) == PSI.ModelBuildStatus.BUILT @test run!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED template = get_thermal_standard_uc_template() c_sys5_uc_re = PSB.build_system( PSITestSystems, "c_sys5_uc_re"; add_single_time_series = true, force_build = true, ) 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)) == PSI.ModelBuildStatus.BUILT @test run!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED @test !isempty(collect(readdir(PSI.get_recorder_dir(model)))) end @testset "Emulation Model initial_conditions test for ThermalGen" begin ######## Test with ThermalStandardUnitCommitment ######## template = get_thermal_standard_uc_template() c_sys5_uc_re = PSB.build_system( PSITestSystems, "c_sys5_uc_re"; add_single_time_series = true, force_build = true, ) 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)) == PSI.ModelBuildStatus.BUILT check_duration_on_initial_conditions_values(model, ThermalStandard) check_duration_off_initial_conditions_values(model, ThermalStandard) @test run!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED ######## Test with ThermalMultiStartUnitCommitment ######## template = get_thermal_standard_uc_template() c_sys5_uc = PSB.build_system( PSITestSystems, "c_sys5_pglib"; add_single_time_series = true, force_build = true, ) 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)) == 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) == PSI.RunStatus.SUCCESSFULLY_FINALIZED ######## Test with ThermalStandardUnitCommitment ######## template = get_thermal_standard_uc_template() c_sys5_uc = PSB.build_system( PSITestSystems, "c_sys5_pglib"; add_single_time_series = true, force_build = true, ) 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)) == 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) == PSI.RunStatus.SUCCESSFULLY_FINALIZED ######## Test with ThermalStandardDispatch ######## template = get_thermal_standard_uc_template() c_sys5_uc = PSB.build_system( PSITestSystems, "c_sys5_pglib"; add_single_time_series = true, force_build = true, ) 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)) == PSI.ModelBuildStatus.BUILT end @testset "Emulation Model initial_conditions test for Hydro" begin ######## Test with HydroDispatchRunOfRiver ######## template = get_thermal_dispatch_template_network() c_sys5_hyd = PSB.build_system( PSITestSystems, "c_sys5_hyd"; add_single_time_series = true, force_build = true, ) set_device_model!(template, HydroDispatch, HydroDispatchRunOfRiver) set_device_model!(template, HydroTurbine, HydroTurbineEnergyDispatch) set_device_model!(template, HydroReservoir, HydroEnergyModelReservoir) model = EmulationModel(template, c_sys5_hyd; optimizer = HiGHS_optimizer) @test build!(model; executions = 10, 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( initial_conditions_data, ActivePowerVariable(), HydroTurbine, ) @test run!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED ######## Test with HydroCommitmentRunOfRiver ######## template = get_thermal_dispatch_template_network() c_sys5_hyd = PSB.build_system( PSITestSystems, "c_sys5_hyd"; add_single_time_series = true, force_build = true, ) set_device_model!(template, HydroDispatch, HydroCommitmentRunOfRiver) set_device_model!(template, HydroTurbine, HydroTurbineEnergyCommitment) set_device_model!(template, HydroReservoir, HydroEnergyModelReservoir) model = EmulationModel(template, c_sys5_hyd; optimizer = HiGHS_optimizer) @test build!(model; executions = 10, 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( initial_conditions_data, OnVariable(), HydroTurbine, ) @test run!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED end @testset "Emulation Model Results" begin template = get_thermal_dispatch_template_network() c_sys5 = PSB.build_system( PSITestSystems, "c_sys5_uc"; add_single_time_series = true, force_build = true, ) model = EmulationModel(template, c_sys5; optimizer = HiGHS_optimizer) executions = 10 @test build!( model; executions = executions, output_dir = mktempdir(; cleanup = true), ) == 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"] @test list_variable_keys(results) == [PSI.VariableKey(ActivePowerVariable, ThermalStandard)] @test list_dual_names(results) == [] @test list_dual_keys(results) == [] @test list_parameter_names(results) == ["ActivePowerTimeSeriesParameter__PowerLoad"] @test list_parameter_keys(results) == [PSI.ParameterKey(ActivePowerTimeSeriesParameter, PowerLoad)] @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 DataFrame @test read_parameter(results, "ActivePowerTimeSeriesParameter__PowerLoad") isa DataFrame @test read_parameter(results, ActivePowerTimeSeriesParameter, PowerLoad) isa DataFrame @test read_parameter( results, PSI.ParameterKey(ActivePowerTimeSeriesParameter, PowerLoad), ) isa DataFrame @test read_optimizer_stats(model) isa DataFrame for n in names(read_optimizer_stats(model)) stats_values = read_optimizer_stats(model)[!, n] if any(ismissing.(stats_values)) @test ismissing.(stats_values) == ismissing.(read_optimizer_stats(results)[!, n]) elseif any(isnan.(stats_values)) @test isnan.(stats_values) == isnan.(read_optimizer_stats(results)[!, n]) else @test stats_values == read_optimizer_stats(results)[!, n] end end for i in 1:executions @test get_objective_value(results, i) isa Float64 end end @testset "Run EmulationModel with auto-build" begin for serialize in (true, false) template = get_thermal_dispatch_template_network() c_sys5 = PSB.build_system( PSITestSystems, "c_sys5_uc"; add_single_time_series = true, force_build = true, ) model = EmulationModel(template, c_sys5; optimizer = HiGHS_optimizer) @test_throws ErrorException run!(model, executions = 10) @test run!( model; executions = 10, output_dir = mktempdir(; cleanup = true), export_optimization_model = serialize, ) == PSI.RunStatus.SUCCESSFULLY_FINALIZED end end @testset "Test serialization/deserialization of EmulationModel results" begin path = mktempdir(; cleanup = true) template = get_thermal_dispatch_template_network() c_sys5 = PSB.build_system( PSITestSystems, "c_sys5_uc"; add_single_time_series = true, force_build = true, ) model = EmulationModel(template, c_sys5; optimizer = HiGHS_optimizer) executions = 10 @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 = OptimizationProblemResults(PSI.get_output_dir(model)) var2 = read_variable(results2, ActivePowerVariable, ThermalStandard) @test var1_a == var2 @test get_system(results2) === nothing # Commented out for now, as we no longer automatically serialize the system with results, but this should be added back in the future. #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, ISOPT._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 exp_file = joinpath(path, "results", "variables", "ActivePowerVariable__ThermalStandard.csv") var4 = PSI.read_dataframe(exp_file) # Manually Multiply by the base power var1_a has natural units and export writes directly from the solver @test var1_a.value == var4.value .* 100.0 end @testset "Test deserialization and re-run of EmulationModel" begin path = mktempdir(; cleanup = true) template = get_thermal_dispatch_template_network() c_sys5 = PSB.build_system( PSITestSystems, "c_sys5_uc"; add_single_time_series = true, force_build = true, ) model = EmulationModel(template, c_sys5; optimizer = HiGHS_optimizer) executions = 10 @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))) @test PSI._JUMP_MODEL_FILENAME in file_list end @testset "Test serialization of InitialConditionsData" begin template = get_thermal_standard_uc_template() sys = PSB.build_system( PSITestSystems, "c_sys5_pglib"; add_single_time_series = true, force_build = true, ) optimizer = HiGHS_optimizer set_device_model!(template, ThermalMultiStart, ThermalMultiStartUnitCommitment) model = EmulationModel(template, sys; optimizer = HiGHS_optimizer) output_dir = mktempdir(; cleanup = true) @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) == PSI.RunStatus.SUCCESSFULLY_FINALIZED # Build again, use existing initial conditions. PSI.reset!(model) @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) == PSI.RunStatus.SUCCESSFULLY_FINALIZED # Build again, use existing initial conditions. model = EmulationModel( template, sys; optimizer = optimizer, deserialize_initial_conditions = true, ) @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) == PSI.RunStatus.SUCCESSFULLY_FINALIZED # Construct and build again with custom initial conditions file. initialization_file = joinpath(output_dir, ic_file * ".old") mv(ic_file, initialization_file) touch(ic_file) model = EmulationModel( template, sys; optimizer = optimizer, initialization_file = initialization_file, deserialize_initial_conditions = true, ) @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.ModelBuildStatus.BUILT test_ic_serialization_outputs(model; ic_file_exists = false, message = "skip") @test run!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED end ================================================ FILE: test/test_multi_interval.jl ================================================ @testset "Multi-interval validation errors" begin c_sys5 = PSB.build_system(PSITestSystems, "c_sys5"; add_single_time_series = true) transform_single_time_series!(c_sys5, Hour(24), Hour(12); delete_existing = false) transform_single_time_series!(c_sys5, Hour(24), Hour(24); delete_existing = false) template = get_thermal_dispatch_template_network() # Without interval kwarg on a multi-interval system, should error @test_throws IS.ConflictingInputsError DecisionModel( template, c_sys5; optimizer = HiGHS_optimizer, horizon = Hour(24), ) # With a non-existent interval on a system that has no SingleTimeSeries to auto-transform from, should error. c_sys5_forecasts = PSB.build_system(PSITestSystems, "c_sys5"; add_forecasts = true) @test_throws IS.ConflictingInputsError DecisionModel( template, c_sys5_forecasts; optimizer = HiGHS_optimizer, horizon = Hour(24), interval = Hour(6), ) end @testset "DecisionModel with explicit interval" begin c_sys5 = PSB.build_system(PSITestSystems, "c_sys5"; add_single_time_series = true) transform_single_time_series!(c_sys5, Hour(24), Hour(12); delete_existing = false) transform_single_time_series!(c_sys5, Hour(24), Hour(24); delete_existing = false) template = get_thermal_dispatch_template_network() # Build with interval = 24h model_24h = DecisionModel( template, c_sys5; optimizer = HiGHS_optimizer, horizon = Hour(24), interval = Hour(24), ) @test build!(model_24h; output_dir = mktempdir(; cleanup = true)) == PSI.ModelBuildStatus.BUILT @test solve!(model_24h) == PSI.RunStatus.SUCCESSFULLY_FINALIZED # Build with interval = 12h on the same system model_12h = DecisionModel( template, c_sys5; optimizer = HiGHS_optimizer, horizon = Hour(24), interval = Hour(12), ) @test build!(model_12h; output_dir = mktempdir(; cleanup = true)) == PSI.ModelBuildStatus.BUILT @test solve!(model_12h) == PSI.RunStatus.SUCCESSFULLY_FINALIZED # Verify models have the correct interval in store params @test PSI.get_interval(model_24h) == Dates.Millisecond(Hour(24)) @test PSI.get_interval(model_12h) == Dates.Millisecond(Hour(12)) end @testset "Auto-transform SingleTimeSeries with interval" begin c_sys5 = PSB.build_system(PSITestSystems, "c_sys5"; add_single_time_series = true) template = get_thermal_dispatch_template_network() model = DecisionModel( template, c_sys5; optimizer = HiGHS_optimizer, horizon = Hour(24), interval = Hour(24), ) @test build!(model; output_dir = mktempdir(; cleanup = true)) == PSI.ModelBuildStatus.BUILT @test solve!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED # Second model with a different interval reuses the same system and auto-transforms. model2 = DecisionModel( template, c_sys5; optimizer = HiGHS_optimizer, horizon = Hour(24), interval = Hour(12), ) @test build!(model2; output_dir = mktempdir(; cleanup = true)) == PSI.ModelBuildStatus.BUILT @test solve!(model2) == PSI.RunStatus.SUCCESSFULLY_FINALIZED end @testset "RTS system shared across two intervals - build only" begin sys_rts = PSB.build_system(PSISystems, "modified_RTS_GMLC_DA_sys") # Clear any pre-existing transform so we can attach two fresh intervals. PSY.transform_single_time_series!(sys_rts, Hour(24), Hour(24); delete_existing = true) PSY.transform_single_time_series!(sys_rts, Hour(24), Hour(12); delete_existing = false) template = get_template_standard_uc_simulation() set_network_model!(template, NetworkModel(CopperPlatePowerModel)) model_24h = DecisionModel( template, sys_rts; name = "UC_24h", optimizer = HiGHS_optimizer, horizon = Hour(24), interval = Hour(24), ) @test build!(model_24h; output_dir = mktempdir(; cleanup = true)) == PSI.ModelBuildStatus.BUILT @test PSI.get_interval(model_24h) == Dates.Millisecond(Hour(24)) model_12h = DecisionModel( template, sys_rts; name = "UC_12h", optimizer = HiGHS_optimizer, horizon = Hour(24), interval = Hour(12), ) @test build!(model_12h; output_dir = mktempdir(; cleanup = true)) == PSI.ModelBuildStatus.BUILT @test PSI.get_interval(model_12h) == Dates.Millisecond(Hour(12)) # Same underlying system, different intervals selected per model. @test get_system(model_24h) === get_system(model_12h) end @testset "Single interval system works without interval kwarg" begin # Backward compatibility: existing single-interval systems work without changes c_sys5 = PSB.build_system(PSITestSystems, "c_sys5") template = get_thermal_dispatch_template_network() model = DecisionModel(template, c_sys5; optimizer = HiGHS_optimizer) @test build!(model; output_dir = mktempdir(; cleanup = true)) == PSI.ModelBuildStatus.BUILT @test solve!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED end ================================================ FILE: test/test_network_constructors.jl ================================================ # Note to devs. Use HiGHS for models with linear constraints and linear cost functions # Use OSQP for models with quadratic cost function and linear constraints and ipopt otherwise @testset "All PowerModels models construction" begin c_sys5 = PSB.build_system(PSITestSystems, "c_sys5") for (network, solver) in NETWORKS_FOR_TESTING template = get_thermal_dispatch_template_network( NetworkModel(network; PTDF_matrix = PTDF(c_sys5)), ) ps_model = DecisionModel(template, c_sys5; optimizer = solver) @test build!(ps_model; output_dir = mktempdir(; cleanup = true)) == PSI.ModelBuildStatus.BUILT opt_container = PSI.get_optimization_container(ps_model) @test opt_container.pm !== nothing @test PSI.has_container_key(opt_container, ActivePowerBalance, ACBus) end end @testset "Network Copper Plate" begin template = get_thermal_dispatch_template_network(CopperPlatePowerModel) c_sys5 = PSB.build_system(PSITestSystems, "c_sys5") c_sys14 = PSB.build_system(PSITestSystems, "c_sys14") c_sys14_dc = PSB.build_system(PSITestSystems, "c_sys14_dc") systems = [c_sys5, c_sys14, c_sys14_dc] test_results = IdDict{System, Vector{Int}}( c_sys5 => [120, 0, 120, 120, 24], c_sys14 => [120, 0, 120, 120, 24], c_sys14_dc => [120, 0, 120, 120, 24], ) constraint_keys = [PSI.ConstraintKey(CopperPlateBalanceConstraint, PSY.System)] objfuncs = [GAEVF, GQEVF, GQEVF] test_obj_values = IdDict{System, Float64}( c_sys5 => 240000.0, c_sys14 => 142000.0, c_sys14_dc => 142000.0, ) for (ix, sys) in enumerate(systems) ps_model = DecisionModel(template, sys; optimizer = HiGHS_optimizer) @test build!(ps_model; output_dir = mktempdir(; cleanup = true)) == PSI.ModelBuildStatus.BUILT psi_constraint_test(ps_model, constraint_keys) moi_tests( ps_model, test_results[sys]..., false, ) psi_checkobjfun_test(ps_model, objfuncs[ix]) psi_checksolve_test(ps_model, [MOI.OPTIMAL], test_obj_values[sys], 10000) end template = get_thermal_dispatch_template_network( NetworkModel(CopperPlatePowerModel; use_slacks = true), ) ps_model_re = DecisionModel( template, PSB.build_system(PSITestSystems, "c_sys5_re"); optimizer = HiGHS_optimizer, ) @test build!(ps_model_re; output_dir = mktempdir(; cleanup = true)) == PSI.ModelBuildStatus.BUILT psi_checksolve_test(ps_model_re, [MOI.OPTIMAL], 240000.0, 10000) end @testset "Network DC-PF with PTDF Model" begin template = get_thermal_dispatch_template_network(PTDFPowerModel) c_sys5 = PSB.build_system(PSITestSystems, "c_sys5") c_sys14 = PSB.build_system(PSITestSystems, "c_sys14") c_sys14_dc = PSB.build_system(PSITestSystems, "c_sys14_dc") systems = [c_sys5, c_sys14, c_sys14_dc] constraint_keys = [ PSI.ConstraintKey(FlowRateConstraint, PSY.Line, "lb"), PSI.ConstraintKey(FlowRateConstraint, PSY.Line, "ub"), PSI.ConstraintKey(CopperPlateBalanceConstraint, PSY.System), ] PTDF_ref = IdDict{System, PTDF}( c_sys5 => PTDF(c_sys5), c_sys14 => PTDF(c_sys14), c_sys14_dc => PTDF(c_sys14_dc), ) test_results = IdDict{System, Vector{Int}}( c_sys5 => [120, 0, 264, 264, 24], c_sys14 => [120, 0, 600, 600, 24], c_sys14_dc => [168, 0, 648, 552, 24], ) test_obj_values = IdDict{System, Float64}( c_sys5 => 240000.0, c_sys14 => 142000.0, c_sys14_dc => 142000.0, ) test_objfuncs_types = IdDict{System, Type}( c_sys5 => GAEVF, c_sys14 => GQEVF, c_sys14_dc => GQEVF, ) for sys in systems template = get_thermal_dispatch_template_network( NetworkModel(PTDFPowerModel; PTDF_matrix = PTDF_ref[sys]), ) ps_model = DecisionModel(template, sys; optimizer = HiGHS_optimizer) @test build!(ps_model; output_dir = mktempdir(; cleanup = true)) == PSI.ModelBuildStatus.BUILT psi_constraint_test(ps_model, constraint_keys) moi_tests( ps_model, test_results[sys]..., false, ) psi_checkobjfun_test(ps_model, test_objfuncs_types[sys]) psi_checksolve_test( ps_model, [MOI.OPTIMAL, MOI.ALMOST_OPTIMAL], test_obj_values[sys], 10000, ) end end @testset "Network DC-PF with VirtualPTDF Model" begin c_sys5 = PSB.build_system(PSITestSystems, "c_sys5") c_sys14 = PSB.build_system(PSITestSystems, "c_sys14") c_sys14_dc = PSB.build_system(PSITestSystems, "c_sys14_dc") systems = [c_sys5, c_sys14, c_sys14_dc] objfuncs = [GAEVF, GQEVF, GQEVF] constraint_keys = [ PSI.ConstraintKey(FlowRateConstraint, PSY.Line, "lb"), PSI.ConstraintKey(FlowRateConstraint, PSY.Line, "ub"), PSI.ConstraintKey(CopperPlateBalanceConstraint, PSY.System), ] PTDF_ref = IdDict{System, VirtualPTDF}( c_sys5 => VirtualPTDF(c_sys5), c_sys14 => VirtualPTDF(c_sys14), c_sys14_dc => VirtualPTDF(c_sys14_dc), ) test_results = IdDict{System, Vector{Int}}( c_sys5 => [120, 0, 264, 264, 24], c_sys14 => [120, 0, 600, 600, 24], c_sys14_dc => [168, 0, 648, 552, 24], ) test_obj_values = IdDict{System, Float64}( c_sys5 => 240000.0, c_sys14 => 142000.0, c_sys14_dc => 142000.0, ) test_objfuncs_types = IdDict{System, Type}( c_sys5 => GAEVF, c_sys14 => GQEVF, c_sys14_dc => GQEVF, ) for (ix, sys) in enumerate(systems) template = get_thermal_dispatch_template_network(PTDFPowerModel) template = get_thermal_dispatch_template_network( NetworkModel(PTDFPowerModel; PTDF_matrix = PTDF_ref[sys]), ) ps_model = DecisionModel(template, sys; optimizer = HiGHS_optimizer) @test build!(ps_model; output_dir = mktempdir(; cleanup = true)) == PSI.ModelBuildStatus.BUILT psi_constraint_test(ps_model, constraint_keys) moi_tests( ps_model, test_results[sys]..., false, ) psi_checkobjfun_test(ps_model, objfuncs[ix]) psi_checksolve_test( ps_model, [MOI.OPTIMAL, MOI.ALMOST_OPTIMAL], test_obj_values[sys], 10000, ) end end @testset "Network DC lossless -PF network with PowerModels DCPlosslessForm" begin c_sys5 = PSB.build_system(PSITestSystems, "c_sys5") c_sys14 = PSB.build_system(PSITestSystems, "c_sys14") c_sys14_dc = PSB.build_system(PSITestSystems, "c_sys14_dc") systems = [c_sys5, c_sys14, c_sys14_dc] objfuncs = [GAEVF, GQEVF, GQEVF] constraint_keys = [ PSI.ConstraintKey(PSI.FlowRateConstraint, PSY.Line, "ub"), PSI.ConstraintKey(PSI.FlowRateConstraint, PSY.Line, "lb"), PSI.ConstraintKey(PSI.NodalBalanceActiveConstraint, PSY.ACBus), ] test_results = IdDict{System, Vector{Int}}( c_sys5 => [384, 144, 264, 264, 288], c_sys14 => [936, 480, 600, 600, 840], c_sys14_dc => [984, 432, 648, 552, 840], ) test_obj_values = IdDict{System, Float64}( c_sys5 => 242000.0, c_sys14 => 143000.0, c_sys14_dc => 143000.0, ) for (ix, sys) in enumerate(systems) 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.ModelBuildStatus.BUILT psi_constraint_test(ps_model, constraint_keys) moi_tests( ps_model, test_results[sys]..., false, ) psi_checkobjfun_test(ps_model, objfuncs[ix]) psi_checksolve_test( ps_model, [MOI.OPTIMAL, MOI.LOCALLY_SOLVED], test_obj_values[sys], 1000, ) end end @testset "Network Solve AC-PF PowerModels StandardACPModel" begin c_sys5 = PSB.build_system(PSITestSystems, "c_sys5") c_sys14 = PSB.build_system(PSITestSystems, "c_sys14") c_sys14_dc = PSB.build_system(PSITestSystems, "c_sys14_dc") systems = [c_sys5, c_sys14, c_sys14_dc] objfuncs = [GAEVF, GQEVF, GQEVF] # Check for voltage and angle constraints constraint_keys = [ PSI.ConstraintKey(FlowRateConstraintFromTo, PSY.Line), PSI.ConstraintKey(FlowRateConstraintToFrom, PSY.Line), PSI.ConstraintKey(PSI.NodalBalanceActiveConstraint, PSY.ACBus), PSI.ConstraintKey(PSI.NodalBalanceReactiveConstraint, PSY.ACBus), ] test_results = IdDict{System, Vector{Int}}( c_sys5 => [1056, 144, 240, 240, 264], c_sys14 => [2832, 480, 240, 240, 696], c_sys14_dc => [2832, 432, 336, 240, 744], ) test_obj_values = IdDict{System, Float64}( c_sys5 => 240000.0, c_sys14 => 142000.0, c_sys14_dc => 142000.0, ) for (ix, sys) in enumerate(systems) 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.ModelBuildStatus.BUILT psi_constraint_test(ps_model, constraint_keys) moi_tests( ps_model, test_results[sys]..., false, ) psi_checkobjfun_test(ps_model, objfuncs[ix]) psi_checksolve_test( ps_model, [MOI.TIME_LIMIT, MOI.OPTIMAL, MOI.LOCALLY_SOLVED], test_obj_values[sys], 10000, ) end end @testset "Network Solve AC-PF PowerModels NFAPowerModel" begin c_sys5 = PSB.build_system(PSITestSystems, "c_sys5") c_sys14 = PSB.build_system(PSITestSystems, "c_sys14") c_sys14_dc = PSB.build_system(PSITestSystems, "c_sys14_dc") systems = [c_sys5, c_sys14, c_sys14_dc] objfuncs = [GAEVF, GQEVF, GQEVF] constraint_keys = [PSI.ConstraintKey(PSI.NodalBalanceActiveConstraint, PSY.ACBus)] test_results = Dict{System, Vector{Int}}( c_sys5 => [264, 0, 264, 264, 120], c_sys14 => [600, 0, 600, 600, 336], c_sys14_dc => [648, 0, 648, 552, 384], ) test_obj_values = IdDict{System, Float64}( c_sys5 => 240000.0, c_sys14 => 142000.0, c_sys14_dc => 142000.0, ) for (ix, sys) in enumerate(systems) 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.ModelBuildStatus.BUILT psi_constraint_test(ps_model, constraint_keys) moi_tests( ps_model, test_results[sys]..., false, ) psi_checkobjfun_test(ps_model, objfuncs[ix]) psi_checksolve_test( ps_model, [MOI.OPTIMAL, MOI.LOCALLY_SOLVED], test_obj_values[sys], 10000, ) end end @testset "Other Network AC PowerModels models" begin networks = [#ACPPowerModel, Already tested ACRPowerModel, ACTPowerModel, ] c_sys5 = PSB.build_system(PSITestSystems, "c_sys5") c_sys14 = PSB.build_system(PSITestSystems, "c_sys14") c_sys14_dc = PSB.build_system(PSITestSystems, "c_sys14_dc") systems = [c_sys5, c_sys14, c_sys14_dc] # TODO: add model specific constraints to this list. Voltages, etc. constraint_keys = [ PSI.ConstraintKey(PSI.NodalBalanceActiveConstraint, PSY.ACBus), PSI.ConstraintKey(PSI.NodalBalanceReactiveConstraint, PSY.ACBus), ] ACR_test_results = Dict{System, Vector{Int}}( c_sys5 => [1056, 0, 240, 240, 264], c_sys14 => [2832, 0, 240, 240, 696], c_sys14_dc => [2832, 0, 336, 240, 744], ) ACT_test_results = Dict{System, Vector{Int}}( c_sys5 => [1344, 144, 240, 240, 840], c_sys14 => [3792, 480, 240, 240, 2616], c_sys14_dc => [3696, 432, 336, 240, 2472], ) test_results = Dict(zip(networks, [ACR_test_results, ACT_test_results])) for network in networks, sys in systems 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.ModelBuildStatus.BUILT psi_constraint_test(ps_model, constraint_keys) moi_tests( ps_model, test_results[network][sys][1], test_results[network][sys][2], test_results[network][sys][3], test_results[network][sys][4], test_results[network][sys][5], false, ) @test PSI.get_optimization_container(ps_model).pm !== nothing end end # TODO: Add constraint tests for these models, other is redundant with first test @testset "Network DC-PF PowerModels quadratic loss approximations models" begin networks = [DCPLLPowerModel, LPACCPowerModel] c_sys5 = PSB.build_system(PSITestSystems, "c_sys5") c_sys14 = PSB.build_system(PSITestSystems, "c_sys14") c_sys14_dc = PSB.build_system(PSITestSystems, "c_sys14_dc") systems = [c_sys5, c_sys14, c_sys14_dc] # TODO: add model specific constraints to this list. Bi-directional flows etc constraint_keys = [PSI.ConstraintKey(PSI.NodalBalanceActiveConstraint, PSY.ACBus)] test_obj_values = IdDict{System, Float64}( c_sys5 => 240000.0, c_sys14 => 142000.0, c_sys14_dc => 142000.0, ) DCPLL_test_results = Dict{System, Vector{Int}}( c_sys5 => [528, 144, 264, 264, 288], c_sys14 => [1416, 480, 600, 600, 840], c_sys14_dc => [1416, 432, 648, 552, 840], ) LPACC_test_results = Dict{System, Vector{Int}}( c_sys5 => [1200, 144, 240, 240, 840], c_sys14 => [3312, 480, 240, 240, 2616], c_sys14_dc => [3264, 432, 336, 240, 2472], ) test_results = Dict(zip(networks, [DCPLL_test_results, LPACC_test_results])) for network in networks, (ix, sys) in enumerate(systems) 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.ModelBuildStatus.BUILT psi_constraint_test(ps_model, constraint_keys) moi_tests( ps_model, test_results[network][sys][1], test_results[network][sys][2], test_results[network][sys][3], test_results[network][sys][4], test_results[network][sys][5], false, ) @test PSI.get_optimization_container(ps_model).pm !== nothing psi_checksolve_test( ps_model, [MOI.OPTIMAL, MOI.LOCALLY_SOLVED], test_obj_values[sys], 10000, ) end end @testset "Network Unsupported Power Model Formulations" begin for network in PSI.UNSUPPORTED_POWERMODELS template = get_thermal_dispatch_template_network(network) ps_model = DecisionModel( template, PSB.build_system(PSITestSystems, "c_sys5"); optimizer = ipopt_optimizer, ) @test build!( ps_model; console_level = Logging.AboveMaxLevel, # Ignore expected errors. output_dir = mktempdir(; cleanup = true), ) == PSI.ModelBuildStatus.FAILED end end @testset "2 Subnetworks HVDC DC-PF with CopperPlatePowerModel" begin c_sys5 = PSB.build_system(PSISystems, "2Area 5 Bus System") 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.ModelBuildStatus.BUILT solve!(ps_model) moi_tests(ps_model, 264, 0, 288, 240, 48, false) opt_container = PSI.get_optimization_container(ps_model) copper_plate_constraints = PSI.get_constraint(opt_container, CopperPlateBalanceConstraint(), PSY.System) @test size(copper_plate_constraints) == (2, 24) psi_checksolve_test(ps_model, [MOI.OPTIMAL], 480288, 100) results = OptimizationProblemResults(ps_model) hvdc_flow = read_variable( results, "FlowActivePowerVariable__TwoTerminalGenericHVDCLine"; table_format = TableFormat.WIDE, ) @test all(hvdc_flow[!, "nodeC-nodeC2"] .<= 200) @test all(hvdc_flow[!, "nodeC-nodeC2"] .>= -200) load = read_parameter( results, "ActivePowerTimeSeriesParameter__PowerLoad"; table_format = TableFormat.WIDE, ) thermal_gen = read_variable( results, "ActivePowerVariable__ThermalStandard"; table_format = TableFormat.WIDE, ) zone_1_load = sum(eachcol(load[!, ["Load-nodeC", "Load-nodeD", "Load-nodeB"]])) zone_1_gen = sum( eachcol(thermal_gen[!, ["Solitude", "Park City", "Sundance", "Brighton", "Alta"]]), ) @test all( isapprox.( sum(zone_1_gen .+ zone_1_load .- hvdc_flow[!, "nodeC-nodeC2"]; dims = 2), 0.0; atol = 1e-3, ), ) zone_2_load = sum(eachcol(load[!, ["Load-nodeC2", "Load-nodeD2", "Load-nodeB2"]])) 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 .+ hvdc_flow[!, "nodeC-nodeC2"]; dims = 2), 0.0; atol = 1e-3, ), ) # Test forcing flows to 0.0 hvdc_link = get_component(TwoTerminalGenericHVDCLine, c_sys5, "nodeC-nodeC2") set_active_power_limits_from!(hvdc_link, (min = 0.0, max = 0.0)) set_active_power_limits_to!(hvdc_link, (min = 0.0, max = 0.0)) # Test not passing the PTDF to the Template 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.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 = OptimizationProblemResults(ps_model) hvdc_flow = read_variable( results, "FlowActivePowerVariable__TwoTerminalGenericHVDCLine"; table_format = TableFormat.WIDE, ) @test all(hvdc_flow[!, "nodeC-nodeC2"] .== 0.0) @test all(hvdc_flow[!, "nodeC-nodeC2"] .== 0.0) load = read_parameter( results, "ActivePowerTimeSeriesParameter__PowerLoad"; table_format = TableFormat.WIDE, ) thermal_gen = read_variable( results, "ActivePowerVariable__ThermalStandard"; table_format = TableFormat.WIDE, ) zone_1_load = sum(eachcol(load[!, ["Load-nodeC", "Load-nodeD", "Load-nodeB"]])) zone_1_gen = sum( eachcol(thermal_gen[!, ["Solitude", "Park City", "Sundance", "Brighton", "Alta"]]), ) @test all(isapprox.(sum(zone_1_gen .+ zone_1_load; dims = 2), 0.0; atol = 1e-3)) zone_2_load = sum(eachcol(load[!, ["Load-nodeC2", "Load-nodeD2", "Load-nodeB2"]])) 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; dims = 2), 0.0; atol = 1e-3)) end @testset "2 Subnetworks DC-PF with PTDF Model" begin c_sys5 = PSB.build_system(PSISystems, "2Area 5 Bus System") # Test passing a VirtualPTDF Model template = get_thermal_dispatch_template_network( NetworkModel(PTDFPowerModel; PTDF_matrix = VirtualPTDF(c_sys5)), ) ps_model = DecisionModel(template, c_sys5; optimizer = HiGHS_optimizer) @test build!(ps_model; output_dir = mktempdir(; cleanup = true)) == PSI.ModelBuildStatus.BUILT solve!(ps_model) moi_tests(ps_model, 264, 0, 576, 528, 48, false) opt_container = PSI.get_optimization_container(ps_model) copper_plate_constraints = PSI.get_constraint(opt_container, CopperPlateBalanceConstraint(), PSY.System) @test size(copper_plate_constraints) == (2, 24) psi_checksolve_test(ps_model, [MOI.OPTIMAL], 482587, 100) results = OptimizationProblemResults(ps_model) hvdc_flow = read_variable( results, "FlowActivePowerVariable__TwoTerminalGenericHVDCLine"; table_format = TableFormat.WIDE, ) @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"; table_format = TableFormat.WIDE, ) thermal_gen = read_variable( results, "ActivePowerVariable__ThermalStandard"; table_format = TableFormat.WIDE, ) zone_1_load = sum(eachcol(load[!, ["Load-nodeC", "Load-nodeD", "Load-nodeB"]])) zone_1_gen = sum( eachcol(thermal_gen[!, ["Solitude", "Park City", "Sundance", "Brighton", "Alta"]]), ) @test all( isapprox.( sum(zone_1_gen .+ zone_1_load .- hvdc_flow[!, "nodeC-nodeC2"]; dims = 2), 0.0; atol = 1e-3, ), ) zone_2_load = sum(eachcol(load[!, ["Load-nodeC2", "Load-nodeD2", "Load-nodeB2"]])) 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 .+ hvdc_flow[!, "nodeC-nodeC2"]; dims = 2), 0.0; atol = 1e-3, ), ) # Test forcing flows to 0.0 hvdc_link = get_component(PSY.TwoTerminalGenericHVDCLine, c_sys5, "nodeC-nodeC2") set_active_power_limits_from!(hvdc_link, (min = 0.0, max = 0.0)) set_active_power_limits_to!(hvdc_link, (min = 0.0, max = 0.0)) # Test not passing the PTDF to the Template 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.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 = OptimizationProblemResults(ps_model) hvdc_flow = read_variable( results, "FlowActivePowerVariable__TwoTerminalGenericHVDCLine"; table_format = TableFormat.WIDE, ) @test all(hvdc_flow[!, "nodeC-nodeC2"] .== 0.0) @test all(hvdc_flow[!, "nodeC-nodeC2"] .== 0.0) load = read_parameter( results, "ActivePowerTimeSeriesParameter__PowerLoad"; table_format = TableFormat.WIDE, ) thermal_gen = read_variable( results, "ActivePowerVariable__ThermalStandard"; table_format = TableFormat.WIDE, ) zone_1_load = sum(eachcol(load[!, ["Load-nodeC", "Load-nodeD", "Load-nodeB"]])) zone_1_gen = sum( eachcol(thermal_gen[!, ["Solitude", "Park City", "Sundance", "Brighton", "Alta"]]), ) @test all(isapprox.(sum(zone_1_gen .+ zone_1_load; dims = 2), 0.0; atol = 1e-3)) zone_2_load = sum(eachcol(load[!, ["Load-nodeC2", "Load-nodeD2", "Load-nodeB2"]])) 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; dims = 2), 0.0; atol = 1e-3)) # Test passing a Virtual PTDF Model with higher tolerance c_sys5 = PSB.build_system(PSISystems, "2Area 5 Bus System") # Test passing a VirtualPTDF Model template = get_thermal_dispatch_template_network( NetworkModel(PTDFPowerModel; PTDF_matrix = VirtualPTDF(c_sys5; tol = 1e-2)), ) ps_model = DecisionModel(template, c_sys5; optimizer = HiGHS_optimizer) @test build!(ps_model; output_dir = mktempdir(; cleanup = true)) == PSI.ModelBuildStatus.BUILT @test solve!(ps_model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED end # These models are easier to test due to their lossless nature @testset "StandardPTDF/DCPPowerModel Radial Branches Test" begin new_sys = PSB.build_system(PSITestSystems, "c_sys5_radial") for net_model in [DCPPowerModel, PTDFPowerModel] template_uc = template_unit_commitment(; network = NetworkModel(net_model; reduce_radial_branches = true, use_slacks = false, ), ) thermal_model = ThermalStandardUnitCommitment set_device_model!(template_uc, ThermalStandard, thermal_model) ##### Solve Reduced Model #### uc_model_red = DecisionModel( template_uc, new_sys; optimizer = HiGHS_optimizer, name = "UC_RED", store_variable_names = true, ) @test build!(uc_model_red; output_dir = mktempdir(; cleanup = true)) == PSI.ModelBuildStatus.BUILT solve!(uc_model_red) res_red = OptimizationProblemResults(uc_model_red) if net_model == DCPPowerModel flow_lines = read_variable( res_red, "FlowActivePowerVariable__Line"; table_format = TableFormat.WIDE, ) else flow_lines = read_expression( res_red, "PTDFBranchFlow__Line"; table_format = TableFormat.WIDE, ) end line_names = DataFrames.names(flow_lines)[2:end] ##### Solve Original Model #### template_uc_orig = template_unit_commitment(; network = NetworkModel(net_model; reduce_radial_branches = false, use_slacks = false, ), ) set_device_model!(template_uc_orig, ThermalStandard, thermal_model) uc_model_orig = DecisionModel( template_uc_orig, new_sys; optimizer = HiGHS_optimizer, name = "UC_ORIG", store_variable_names = true, ) @test build!(uc_model_orig; output_dir = mktempdir(; cleanup = true)) == PSI.ModelBuildStatus.BUILT solve!(uc_model_orig) res_orig = OptimizationProblemResults(uc_model_orig) if net_model == DCPPowerModel flow_lines_orig = read_variable( res_orig, "FlowActivePowerVariable__Line"; table_format = TableFormat.WIDE, ) else flow_lines_orig = read_expression( res_orig, "PTDFBranchFlow__Line"; table_format = TableFormat.WIDE, ) end for line in line_names @test isapprox(flow_lines[!, line], flow_lines_orig[!, line]) end end end @testset "StandardPTDF with Ward reduction Test" begin new_sys = PSB.build_system(PSITestSystems, "c_sys5_radial") #This ward reduction is equivalent to the radial reduciton, therefore flows should be unchanged nr = NetworkReduction[WardReduction([1, 2, 3, 4, 5])] ptdf = PTDF(new_sys; network_reductions = nr) template_uc = template_unit_commitment(; network = NetworkModel(PTDFPowerModel; PTDF_matrix = ptdf, use_slacks = false, ), ) thermal_model = ThermalStandardUnitCommitment set_device_model!(template_uc, ThermalStandard, thermal_model) ##### Solve Reduced Model #### solver = HiGHS_optimizer uc_model_red = DecisionModel( template_uc, new_sys; optimizer = solver, name = "UC_RED", store_variable_names = true, ) @test build!(uc_model_red; output_dir = mktempdir(; cleanup = true)) == PSI.ModelBuildStatus.BUILT solve!(uc_model_red) res_red = OptimizationProblemResults(uc_model_red) flow_lines = read_expression( res_red, "PTDFBranchFlow__Line"; table_format = TableFormat.WIDE, ) line_names = DataFrames.names(flow_lines)[2:end] ##### Solve Original Model #### template_uc_orig = template_unit_commitment(; network = NetworkModel(PTDFPowerModel; reduce_radial_branches = false, use_slacks = false, ), ) set_device_model!(template_uc_orig, ThermalStandard, thermal_model) uc_model_orig = DecisionModel( template_uc_orig, new_sys; optimizer = solver, name = "UC_ORIG", store_variable_names = true, ) @test build!(uc_model_orig; output_dir = mktempdir(; cleanup = true)) == PSI.ModelBuildStatus.BUILT solve!(uc_model_orig) res_orig = OptimizationProblemResults(uc_model_orig) flow_lines_orig = read_expression( res_orig, "PTDFBranchFlow__Line"; table_format = TableFormat.WIDE, ) for line in line_names @test isapprox(flow_lines[!, line], flow_lines_orig[!, line]) end end @testset "All PowerModels models construction with reduced radial branches" begin new_sys = PSB.build_system(PSITestSystems, "c_sys5_radial") for (network, solver) in NETWORKS_FOR_TESTING template = get_thermal_dispatch_template_network( NetworkModel(network; PTDF_matrix = PTDF(new_sys), reduce_radial_branches = true, use_slacks = true), ) ps_model = DecisionModel(template, new_sys; optimizer = solver) @test build!(ps_model; output_dir = mktempdir(; cleanup = true)) == PSI.ModelBuildStatus.BUILT @test PSI.get_optimization_container(ps_model).pm !== nothing end end @testset "2 Areas AreaBalance PowerModel - with slacks" begin c_sys = build_system(PSITestSystems, "c_sys5_uc") # Extend the system with two areas areas = [Area("Area_1", 0, 0, 0), Area("Area_2", 0, 0, 0)] add_components!(c_sys, areas) for (i, comp) in enumerate(get_components(ACBus, c_sys)) (i < 3) ? set_area!(comp, areas[1]) : set_area!(comp, areas[2]) end # Deactivate generators on Area 1: as there is no area interchange defined, # slacks will be required for feasibility for gen in get_components(x -> (get_area(get_bus(x)) == areas[1]), Generator, c_sys) set_available!(gen, false) end template = get_thermal_dispatch_template_network( NetworkModel(AreaBalancePowerModel; use_slacks = true), ) ps_model = DecisionModel(template, c_sys; optimizer = HiGHS_optimizer) @test build!(ps_model; output_dir = mktempdir(; cleanup = true)) == PSI.ModelBuildStatus.BUILT @test solve!(ps_model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED 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) results = OptimizationProblemResults(ps_model) slacks_up = read_variable( results, "SystemBalanceSlackUp__Area"; table_format = TableFormat.WIDE, ) @test all(slacks_up[!, "Area_1"] .> 0.0) @test all(slacks_up[!, "Area_2"] .≈ 0.0) 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"; table_format = TableFormat.WIDE, ) # 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"; table_format = TableFormat.WIDE, ) thermal_gen = read_variable( results, "ActivePowerVariable__ThermalStandard"; table_format = TableFormat.WIDE, ) 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"; table_format = TableFormat.WIDE, ) # 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, 264, 0, 576, 576, 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], 497551, 1) results = OptimizationProblemResults(ps_model) interarea_flow = read_variable( results, "FlowActivePowerVariable__AreaInterchange"; table_format = TableFormat.WIDE, ) # 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"; table_format = TableFormat.WIDE, ) thermal_gen = read_variable( results, "ActivePowerVariable__ThermalStandard"; table_format = TableFormat.WIDE, ) 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 Double Circuit" begin c_sys = PSB.build_system(PSISystems, "two_area_pjm_DA") l = get_component(MonitoredLine, c_sys, "inter_area_line") l2 = MonitoredLine(; name = "inter_area_line_2", available = get_available(l), active_power_flow = get_active_power_flow(l), reactive_power_flow = get_reactive_power_flow(l), arc = get_arc(l), r = get_r(l), x = get_x(l), b = get_b(l), flow_limits = get_flow_limits(l), rating = get_rating(l), angle_limits = get_angle_limits(l), rating_b = get_rating_b(l), rating_c = get_rating_c(l), g = get_g(l), services = get_services(l), ) add_component!(c_sys, l2) 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, 264, 0, 576, 576, 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], 497551, 1) results = OptimizationProblemResults(ps_model) interarea_flow = read_variable( results, "FlowActivePowerVariable__AreaInterchange"; table_format = TableFormat.WIDE, ) # 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"; table_format = TableFormat.WIDE, ) thermal_gen = read_variable( results, "ActivePowerVariable__ThermalStandard"; table_format = TableFormat.WIDE, ) 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, 264, 0, 600, 600, 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], 489842, 1) results = OptimizationProblemResults(ps_model) interarea_flow = read_variable( results, "FlowActivePowerVariable__AreaInterchange"; table_format = TableFormat.WIDE, ) # 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 function add_dummy_time_series_data!(sys) # Attach dummy data so the problem builds: dummy_data = Dict( DateTime("2020-01-01T08:00:00") => [5.0, 6, 7, 7, 7], DateTime("2020-01-01T08:30:00") => [9.0, 9, 9, 9, 8], DateTime("2020-01-01T09:00:00") => [6.0, 6, 5, 5, 4], ) resolution = Dates.Minute(5) dummy_forecast = Deterministic("max_active_power", dummy_data, resolution) load = collect(get_components(StandardLoad, sys))[1] add_time_series!(sys, load, dummy_forecast) return sys end @testset "Network reductions - PTDF StaticBranch" begin # Base Case : Only reductions for double circuits: sys = build_system(PSITestSystems, "case11_network_reductions") add_dummy_time_series_data!(sys) nr = NetworkReduction[] ptdf = PTDF(sys; network_reductions = nr) template = ProblemTemplate( NetworkModel(PTDFPowerModel; PTDF_matrix = ptdf, reduce_radial_branches = PNM.has_radial_reduction(ptdf.network_reduction_data), reduce_degree_two_branches = PNM.has_degree_two_reduction( ptdf.network_reduction_data, ), use_slacks = false), ) set_device_model!(template, Line, StaticBranch) set_device_model!(template, Transformer2W, StaticBranch) ps_model = DecisionModel(template, sys; optimizer = HiGHS_optimizer) @test build!(ps_model; output_dir = mktempdir(; cleanup = true)) == PSI.ModelBuildStatus.BUILT moi_tests(ps_model, 0, 0, 60, 60, 5, false) container = PSI.get_optimization_container(ps_model) expression = PSI.get_expression( container, PTDFBranchFlow(), Line, ) @test size(expression) == (10, 5) # Radial Reduction : sys = build_system(PSITestSystems, "case11_network_reductions") add_dummy_time_series_data!(sys) nr = NetworkReduction[RadialReduction()] ptdf = PTDF(sys; network_reductions = nr) template = ProblemTemplate( NetworkModel(PTDFPowerModel; PTDF_matrix = ptdf, reduce_radial_branches = PNM.has_radial_reduction(ptdf.network_reduction_data), reduce_degree_two_branches = PNM.has_degree_two_reduction( ptdf.network_reduction_data, ), use_slacks = false), ) set_device_model!(template, Line, StaticBranch) set_device_model!(template, Transformer2W, StaticBranch) ps_model = DecisionModel(template, sys; optimizer = HiGHS_optimizer) @test build!(ps_model; output_dir = mktempdir(; cleanup = true)) == PSI.ModelBuildStatus.BUILT moi_tests(ps_model, 0, 0, 55, 55, 5, false) container = PSI.get_optimization_container(ps_model) expression = PSI.get_expression( container, PTDFBranchFlow(), Line, ) @test size(expression) == (9, 5) # Degree Two Reduction : sys = build_system(PSITestSystems, "case11_network_reductions") add_dummy_time_series_data!(sys) nr = NetworkReduction[DegreeTwoReduction()] ptdf = PTDF(sys; network_reductions = nr) template = ProblemTemplate( NetworkModel(PTDFPowerModel; PTDF_matrix = ptdf, reduce_radial_branches = PNM.has_radial_reduction(ptdf.network_reduction_data), reduce_degree_two_branches = PNM.has_degree_two_reduction( ptdf.network_reduction_data, ), use_slacks = false), ) set_device_model!(template, Line, StaticBranch) set_device_model!(template, Transformer2W, StaticBranch) ps_model = DecisionModel(template, sys; optimizer = HiGHS_optimizer) @test build!(ps_model; output_dir = mktempdir(; cleanup = true)) == PSI.ModelBuildStatus.BUILT moi_tests(ps_model, 0, 0, 35, 35, 5, false) expression = PSI.get_expression( container, PTDFBranchFlow(), Line, ) @test size(expression) == (9, 5) container = PSI.get_optimization_container(ps_model) line_flow = PSI.get_expression(container, PTDFBranchFlow(), Line) tfw_flow = PSI.get_expression(container, PTDFBranchFlow(), Transformer2W) # Parallel branch within chain to d2: @test line_flow["2-10-i_double_circuit", :] == line_flow["10-3-i_1", :] # D2 chain with different component types: @test line_flow["1-9-i_1", :] == tfw_flow["9-5-i_1", :] # Parallel branch within chain to d2 with mixed types (parallel comes from tracker): @test line_flow["3-11-i_1", :] == tfw_flow["11-4-i_double_circuit", :] # Radial + Degree Two Reduction : sys = build_system(PSITestSystems, "case11_network_reductions") add_dummy_time_series_data!(sys) nr = NetworkReduction[RadialReduction(), DegreeTwoReduction()] ptdf = PTDF(sys; network_reductions = nr) template = ProblemTemplate( NetworkModel(PTDFPowerModel; PTDF_matrix = ptdf, reduce_radial_branches = PNM.has_radial_reduction(ptdf.network_reduction_data), reduce_degree_two_branches = PNM.has_degree_two_reduction( ptdf.network_reduction_data, ), use_slacks = false), ) set_device_model!(template, Line, StaticBranch) set_device_model!(template, Transformer2W, StaticBranch) ps_model = DecisionModel(template, sys; optimizer = HiGHS_optimizer) @test build!(ps_model; output_dir = mktempdir(; cleanup = true)) == PSI.ModelBuildStatus.BUILT moi_tests(ps_model, 0, 0, 30, 30, 5, false) expression = PSI.get_expression( container, PTDFBranchFlow(), Line, ) @test size(expression) == (10, 5) end @testset "Network reductions - PTDF StaticBranchBounds" begin # Base Case : Only reductions for double circuits: sys = build_system(PSITestSystems, "case11_network_reductions") add_dummy_time_series_data!(sys) nr = NetworkReduction[] ptdf = PTDF(sys; network_reductions = nr) template = ProblemTemplate( NetworkModel(PTDFPowerModel; PTDF_matrix = ptdf, reduce_radial_branches = PNM.has_radial_reduction(ptdf.network_reduction_data), reduce_degree_two_branches = PNM.has_degree_two_reduction( ptdf.network_reduction_data, ), use_slacks = false), ) set_device_model!(template, Line, StaticBranchBounds) set_device_model!(template, Transformer2W, StaticBranchBounds) ps_model = DecisionModel(template, sys; optimizer = HiGHS_optimizer) @test build!(ps_model; output_dir = mktempdir(; cleanup = true)) == PSI.ModelBuildStatus.BUILT moi_tests(ps_model, 60, 0, 0, 0, 65, false) container = PSI.get_optimization_container(ps_model) expression = PSI.get_expression( container, PTDFBranchFlow(), Line, ) @test size(expression) == (10, 5) flow_var = PSI.get_variable( container, FlowActivePowerVariable(), Line, ) @test size(expression) == (10, 5) # Radial Reduction : sys = build_system(PSITestSystems, "case11_network_reductions") add_dummy_time_series_data!(sys) nr = NetworkReduction[RadialReduction()] ptdf = PTDF(sys; network_reductions = nr) template = ProblemTemplate( NetworkModel(PTDFPowerModel; PTDF_matrix = ptdf, reduce_radial_branches = PNM.has_radial_reduction(ptdf.network_reduction_data), reduce_degree_two_branches = PNM.has_degree_two_reduction( ptdf.network_reduction_data, ), use_slacks = false), ) set_device_model!(template, Line, StaticBranchBounds) set_device_model!(template, Transformer2W, StaticBranchBounds) ps_model = DecisionModel(template, sys; optimizer = HiGHS_optimizer) @test build!(ps_model; output_dir = mktempdir(; cleanup = true)) == PSI.ModelBuildStatus.BUILT moi_tests(ps_model, 55, 0, 0, 0, 60, false) container = PSI.get_optimization_container(ps_model) expression = PSI.get_expression( container, PTDFBranchFlow(), Line, ) @test size(expression) == (9, 5) # Degree Two Reduction : sys = build_system(PSITestSystems, "case11_network_reductions") add_dummy_time_series_data!(sys) nr = NetworkReduction[DegreeTwoReduction()] ptdf = PTDF(sys; network_reductions = nr) template = ProblemTemplate( NetworkModel(PTDFPowerModel; PTDF_matrix = ptdf, reduce_radial_branches = PNM.has_radial_reduction(ptdf.network_reduction_data), reduce_degree_two_branches = PNM.has_degree_two_reduction( ptdf.network_reduction_data, ), use_slacks = false), ) set_device_model!(template, Line, StaticBranchBounds) set_device_model!(template, Transformer2W, StaticBranchBounds) ps_model = DecisionModel(template, sys; optimizer = HiGHS_optimizer) @test build!(ps_model; output_dir = mktempdir(; cleanup = true)) == PSI.ModelBuildStatus.BUILT moi_tests(ps_model, 35, 0, 0, 0, 40, false) expression = PSI.get_expression( container, PTDFBranchFlow(), Line, ) @test size(expression) == (9, 5) flow_var = PSI.get_variable( container, FlowActivePowerVariable(), Line, ) @test size(expression) == (9, 5) container = PSI.get_optimization_container(ps_model) line_flow_exp = PSI.get_expression(container, PTDFBranchFlow(), Line) tfw_flow_exp = PSI.get_expression(container, PTDFBranchFlow(), Transformer2W) # Parallel branch within chain to d2: @test line_flow_exp["2-10-i_double_circuit", :] == line_flow_exp["10-3-i_1", :] # D2 chain with different component types: @test line_flow_exp["1-9-i_1", :] == tfw_flow_exp["9-5-i_1", :] # Parallel branch within chain to d2 with mixed types (parallel comes from tracker): @test line_flow_exp["3-11-i_1", :] == tfw_flow_exp["11-4-i_double_circuit", :] container = PSI.get_optimization_container(ps_model) line_flow_var = PSI.get_variable(container, FlowActivePowerVariable(), Line) tfw_flow_var = PSI.get_variable(container, FlowActivePowerVariable(), Transformer2W) # Parallel branch within chain to d2: @test line_flow_var["2-10-i_double_circuit", :] == line_flow_var["10-3-i_1", :] # D2 chain with different component types: @test line_flow_var["1-9-i_1", :] == tfw_flow_var["9-5-i_1", :] # Parallel branch within chain to d2 with mixed types (parallel comes from tracker): @test line_flow_var["3-11-i_1", :] == tfw_flow_var["11-4-i_double_circuit", :] # Radial + Degree Two Reduction : sys = build_system(PSITestSystems, "case11_network_reductions") add_dummy_time_series_data!(sys) nr = NetworkReduction[RadialReduction(), DegreeTwoReduction()] ptdf = PTDF(sys; network_reductions = nr) template = ProblemTemplate( NetworkModel(PTDFPowerModel; PTDF_matrix = ptdf, reduce_radial_branches = PNM.has_radial_reduction(ptdf.network_reduction_data), reduce_degree_two_branches = PNM.has_degree_two_reduction( ptdf.network_reduction_data, ), use_slacks = false), ) set_device_model!(template, Line, StaticBranchBounds) set_device_model!(template, Transformer2W, StaticBranchBounds) ps_model = DecisionModel(template, sys; optimizer = HiGHS_optimizer) @test build!(ps_model; output_dir = mktempdir(; cleanup = true)) == PSI.ModelBuildStatus.BUILT moi_tests(ps_model, 30, 0, 0, 0, 35, false) expression = PSI.get_expression( container, PTDFBranchFlow(), Line, ) @test size(expression) == (10, 5) flow_var = PSI.get_variable( container, FlowActivePowerVariable(), Line, ) @test size(expression) == (10, 5) end @testset "Network reductions for system with subnetworks" begin sys = build_system(PSISystems, "HVDC_TWO_RTO_RTS_1Hr_sys") nr = NetworkReduction[RadialReduction(), DegreeTwoReduction()] ptdf = PTDF(sys; network_reductions = nr) template = ProblemTemplate( NetworkModel(PTDFPowerModel; PTDF_matrix = ptdf, reduce_radial_branches = PNM.has_radial_reduction(ptdf.network_reduction_data), reduce_degree_two_branches = PNM.has_degree_two_reduction( ptdf.network_reduction_data, ), use_slacks = false), ) set_device_model!(template, Line, StaticBranch) set_device_model!(template, Transformer2W, StaticBranch) ps_model = DecisionModel(template, sys; optimizer = HiGHS_optimizer) @test build!(ps_model; output_dir = mktempdir(; cleanup = true)) == PSI.ModelBuildStatus.BUILT end # Tests a case with series reductions containing different branch types, some with and some without filters applied @testset "Network reductions + branch filter edge cases" begin sys = build_system(PSITestSystems, "case11_network_reductions") add_dummy_time_series_data!(sys) nr = NetworkReduction[RadialReduction(), DegreeTwoReduction()] ptdf = PTDF(sys; network_reductions = nr) template = ProblemTemplate( NetworkModel(PTDFPowerModel; PTDF_matrix = ptdf, reduce_radial_branches = PNM.has_radial_reduction(ptdf.network_reduction_data), reduce_degree_two_branches = PNM.has_degree_two_reduction( ptdf.network_reduction_data, ), use_slacks = false), ) set_device_model!(template, DeviceModel(Line, StaticBranch)) modeled_transformer_names = ["9-5-i_1"] set_device_model!( template, DeviceModel( Transformer2W, StaticBranch; attributes = Dict( "filter_function" => x -> PSY.get_name(x) in modeled_transformer_names, ), ), ) ps_model = DecisionModel(template, sys; optimizer = HiGHS_optimizer) @test build!(ps_model; output_dir = mktempdir(; cleanup = true)) == PSI.ModelBuildStatus.BUILT end # Regression test for https://github.com/Sienna-Platform/PowerSimulations.jl/issues/1594 # Combines a NetworkModel with radial + degree-two reductions, a Line DeviceModel # with a filter_function, and a request for FlowRateConstraint duals. Before the # fix in src/devices_models/devices/common/add_constraint_dual.jl, the dual # container was sized along PSY.get_name.(devices) — every device passing the # filter — while the FlowRateConstraint container was sized along the # post-reduction axis from get_branch_argument_constraint_axis. The resulting # axis mismatch raised DimensionMismatch in process_duals during dual # extraction. Building a model is enough to detect the regression: after the # fix, axes(dual)[1] must equal axes(constraint)[1] for every meta. @testset "FlowRateConstraint duals with branch filter and network reductions" begin sys = build_system(PSITestSystems, "case11_network_reductions") add_dummy_time_series_data!(sys) nr = NetworkReduction[RadialReduction(), DegreeTwoReduction()] ptdf = PTDF(sys; network_reductions = nr) template = ProblemTemplate( NetworkModel(PTDFPowerModel; PTDF_matrix = ptdf, duals = [CopperPlateBalanceConstraint], reduce_radial_branches = PNM.has_radial_reduction(ptdf.network_reduction_data), reduce_degree_two_branches = PNM.has_degree_two_reduction( ptdf.network_reduction_data, ), use_slacks = false), ) # Mirror the filter shape from issue #1594: a voltage threshold that selects # all lines in this all-230 kV system. The filter is registered (so the # filter_function code path runs) but does not exclude any branch from a # series chain, so reductions still drop lines from the constraint axis. set_device_model!( template, DeviceModel( Line, StaticBranch; duals = [FlowRateConstraint], attributes = Dict( "filter_function" => x -> PSY.get_base_voltage(PSY.get_from(PSY.get_arc(x))) >= 230.0, ), ), ) set_device_model!(template, Transformer2W, StaticBranch) ps_model = DecisionModel(template, sys; optimizer = HiGHS_optimizer) @test build!(ps_model; output_dir = mktempdir(; cleanup = true)) == PSI.ModelBuildStatus.BUILT container = PSI.get_optimization_container(ps_model) # The unfiltered Line set has 12 entries; full reduction leaves 6 entries # in the constraint axis. The dual container must use the same 6 entries. for meta in ("lb", "ub") cons_key = PSI.ConstraintKey(FlowRateConstraint, Line, meta) cons = PSI.get_constraint(container, cons_key) dual = PSI.get_duals(container)[cons_key] @test axes(dual)[1] == axes(cons)[1] @test length(axes(cons)[1]) < length(collect(get_components(Line, sys))) @test "4-5-i_1" in axes(cons)[1] end end @testset "Branch bounds of parallel and series reductions" begin sys = build_system(PSITestSystems, "case11_network_reductions") add_dummy_time_series_data!(sys) nr = NetworkReduction[DegreeTwoReduction()] ptdf = PTDF(sys; network_reductions = nr) template = ProblemTemplate( NetworkModel(PTDFPowerModel; PTDF_matrix = ptdf, reduce_radial_branches = PNM.has_radial_reduction(ptdf.network_reduction_data), reduce_degree_two_branches = PNM.has_degree_two_reduction( ptdf.network_reduction_data, ), use_slacks = false), ) set_device_model!(template, Line, StaticBranchBounds) set_device_model!(template, Transformer2W, StaticBranchBounds) ps_model = DecisionModel(template, sys; optimizer = HiGHS_optimizer) @test build!(ps_model; output_dir = mktempdir(; cleanup = true)) == PSI.ModelBuildStatus.BUILT l1_parallel = PSY.get_rating(PSY.get_component(ACTransmission, sys, "1-4-i_1")) l2_parallel = PSY.get_rating(PSY.get_component(ACTransmission, sys, "1-4-i_2")) container = PSI.get_optimization_container(ps_model) line_flow_var = PSI.get_variable( container, FlowActivePowerVariable(), Line, ) @test JuMP.upper_bound(line_flow_var["1-4-i_double_circuit", 1]) == minimum([l1_parallel, l2_parallel]) l1_series = PSY.get_rating(PSY.get_component(ACTransmission, sys, "9-5-i_1")) l2_series = PSY.get_rating(PSY.get_component(ACTransmission, sys, "1-9-i_1")) tx_flow_var = PSI.get_variable( container, FlowActivePowerVariable(), Transformer2W, ) @test JuMP.upper_bound(tx_flow_var["9-5-i_1", 1]) == minimum([l1_series, l2_series]) @test JuMP.upper_bound(line_flow_var["1-9-i_1", 1]) == minimum([l1_series, l2_series]) end @testset "Network reductions - PowerModels" begin sys = build_system(PSITestSystems, "case11_network_reductions") add_dummy_time_series_data!(sys) for (network_model, optimizer) in NETWORKS_FOR_TESTING @testset "Network Model: $(network_model)" begin # Only default reductions: template = ProblemTemplate( NetworkModel(network_model; reduce_radial_branches = false, reduce_degree_two_branches = false, use_slacks = false), ) set_device_model!(template, Line, StaticBranch) set_device_model!(template, Transformer2W, StaticBranch) ps_model = DecisionModel(template, sys; optimizer = optimizer) @test build!(ps_model; output_dir = mktempdir(; cleanup = true)) == PSI.ModelBuildStatus.BUILT JuMPmodel = PSI.get_jump_model(ps_model) n_vars = JuMP.num_variables(JuMPmodel) # Radial reductions: template = ProblemTemplate( NetworkModel(network_model; reduce_radial_branches = true, reduce_degree_two_branches = false, use_slacks = false), ) set_device_model!(template, Line, StaticBranch) set_device_model!(template, Transformer2W, StaticBranch) ps_model = DecisionModel(template, sys; optimizer = optimizer) @test build!(ps_model; output_dir = mktempdir(; cleanup = true)) == PSI.ModelBuildStatus.BUILT JuMPmodel = PSI.get_jump_model(ps_model) n_vars_radial = JuMP.num_variables(JuMPmodel) # Radial + degree two reductions: template = ProblemTemplate( NetworkModel(network_model; reduce_radial_branches = true, reduce_degree_two_branches = true, use_slacks = false), ) set_device_model!(template, Line, StaticBranch) set_device_model!(template, Transformer2W, StaticBranch) ps_model = DecisionModel(template, sys; optimizer = optimizer) @test build!(ps_model; output_dir = mktempdir(; cleanup = true)) == PSI.ModelBuildStatus.BUILT JuMPmodel = PSI.get_jump_model(ps_model) n_vars_radial_d2 = JuMP.num_variables(JuMPmodel) @test n_vars_radial_d2 < n_vars_radial < n_vars end end end @testset "Network reductions - PowerModels with slacks" begin sys = build_system(PSITestSystems, "case11_network_reductions") add_dummy_time_series_data!(sys) for (network_model, optimizer) in NETWORKS_FOR_TESTING @testset "Network Model: $(network_model)" begin template = ProblemTemplate( NetworkModel(network_model; reduce_radial_branches = true, reduce_degree_two_branches = true, use_slacks = true), ) set_device_model!( template, DeviceModel(Line, StaticBranch; use_slacks = true), ) set_device_model!( template, DeviceModel(Transformer2W, StaticBranch; use_slacks = true), ) ps_model = DecisionModel(template, sys; optimizer = optimizer) @test build!(ps_model; output_dir = mktempdir(; cleanup = true)) == PSI.ModelBuildStatus.BUILT end end end ================================================ FILE: test/test_network_constructors_with_dlr.jl ================================================ function check_dlr_branch_flows!( res::Union{OptimizationProblemResults, PSI.SimulationProblemResults}, sys::PSY.System, branches_dlr::Vector{<:AbstractString}, dlr_factors::Vector{Float64}, add_parallel_line_name::Union{Nothing, AbstractString} = nothing, ) for branch_name in branches_dlr branch = get_component(PSY.ACTransmission, sys, branch_name) col_key = if ( add_parallel_line_name !== nothing && contains(branch_name, add_parallel_line_name) ) replace(branch_name, "_copy" => "") * "double_circuit" else branch_name end static_rating = get_rating(branch) * get_base_power(sys) branch_type = string(typeof(branch)) if typeof(res) <: PSI.SimulationProblemResults flow = read_realized_expression( res, "PTDFBranchFlow__$branch_type"; table_format = TableFormat.WIDE, )[ :, col_key, ] else flow = read_expression( res, "PTDFBranchFlow__$branch_type"; table_format = TableFormat.WIDE, )[ :, col_key, ] end n_dlr = length(dlr_factors) for (i, f) in enumerate(flow) dlr_idx = mod1(i, n_dlr) @test f <= static_rating * dlr_factors[dlr_idx] + 1e-5 @test f >= -static_rating * dlr_factors[dlr_idx] - 1e-5 end end end @testset "Network DC-PF with VirtualPTDF Model and implementing Dynamic Branch Ratings" begin line_device_model = DeviceModel( Line, StaticBranch; time_series_names = Dict( DynamicBranchRatingTimeSeriesParameter => "dynamic_line_ratings", )) TapTransf_device_model = DeviceModel( TapTransformer, StaticBranch; time_series_names = Dict( DynamicBranchRatingTimeSeriesParameter => "dynamic_line_ratings", )) c_sys5 = PSB.build_system(PSITestSystems, "c_sys5") c_sys14 = PSB.build_system(PSITestSystems, "c_sys14") c_sys14_dc = PSB.build_system(PSITestSystems, "c_sys14_dc") systems = [c_sys5, c_sys14, c_sys14_dc] objfuncs = [GAEVF, GQEVF, GQEVF] constraint_keys = [ PSI.ConstraintKey(FlowRateConstraint, PSY.Line, "lb"), PSI.ConstraintKey(FlowRateConstraint, PSY.Line, "ub"), PSI.ConstraintKey(CopperPlateBalanceConstraint, PSY.System), ] PTDF_ref = IdDict{System, PTDF}( c_sys5 => PTDF(c_sys5), c_sys14 => PTDF(c_sys14), c_sys14_dc => PTDF(c_sys14_dc), ) branches_dlr = IdDict{System, Vector{String}}( c_sys5 => ["1", "2", "6"], c_sys14 => ["Line1", "Line2", "Line9", "Line10", "Line12", "Trans2"], c_sys14_dc => ["Line1", "Line9", "Line10", "Line12", "Trans2"], ) dlr_factors = vcat([fill(x, 6) for x in [0.99, 0.98, 1.0, 0.95]]...) test_results = IdDict{System, Vector{Int}}( c_sys5 => [120, 0, 264, 264, 24], c_sys14 => [120, 0, 600, 600, 24], c_sys14_dc => [168, 0, 648, 552, 24], ) test_obj_values = IdDict{System, Float64}( c_sys5 => 241293.703, c_sys14 => 143365.0, c_sys14_dc => 142000.0, ) n_steps = 2 for (ix, sys) in enumerate(systems) add_dlr_to_system_branches!( sys, branches_dlr[sys], n_steps, dlr_factors; initial_date = "2024-01-01", ) template = get_thermal_dispatch_template_network( NetworkModel( PTDFPowerModel; PTDF_matrix = PTDF_ref[sys], ), ) set_device_model!(template, line_device_model) set_device_model!(template, TapTransf_device_model) ps_model = DecisionModel(template, sys; optimizer = HiGHS_optimizer) @test build!(ps_model; output_dir = mktempdir(; cleanup = true)) == PSI.ModelBuildStatus.BUILT psi_constraint_test(ps_model, constraint_keys) moi_tests( ps_model, test_results[sys]..., false, ) psi_checkobjfun_test(ps_model, objfuncs[ix]) psi_checksolve_test( ps_model, [MOI.OPTIMAL, MOI.ALMOST_OPTIMAL], test_obj_values[sys], 10000, ) res = OptimizationProblemResults(ps_model) check_dlr_branch_flows!(res, sys, branches_dlr[sys], dlr_factors, nothing) end end @testset "Network DC-PF with PTDF Model and implementing Dynamic Branch Ratings with BranchesParallel of different types" begin objfuncs = [GAEVF, GQEVF, GQEVF] constraint_keys = [ PSI.ConstraintKey(FlowRateConstraint, PSY.Line, "lb"), PSI.ConstraintKey(FlowRateConstraint, PSY.Line, "ub"), PSI.ConstraintKey(CopperPlateBalanceConstraint, PSY.System), ] branches_dlr = ["1", "2", "6"] dlr_factors = vcat([fill(x, 6) for x in [0.99, 0.98, 1.0, 0.95]]...) # DLR constraints are now correctly applied to parallel arcs shared between different branch types. # The first two cases (parallel on lines "1" and "2") have DLR, resulting in a higher optimal cost. test_obj_values = [375109.0, 320486.0, 241293.703] parallel_lines_names_to_add = ["1", "2", "3"]#Add parallel lines in lines with and without DLRs n_steps = 2 for slack_flag in [false, true] if slack_flag test_results = [408, 0, 264, 264, 24] else test_results = [120, 0, 264, 264, 24] end line_device_model = DeviceModel( Line, StaticBranch; time_series_names = Dict( DynamicBranchRatingTimeSeriesParameter => "dynamic_line_ratings", ), use_slacks = slack_flag, ) for (ix, add_parallel_line_name) in enumerate(parallel_lines_names_to_add) sys = PSB.build_system(PSITestSystems, "c_sys5") line_to_add_parallel = get_component(Line, sys, add_parallel_line_name) add_equivalent_ac_transmission_with_parallel_circuits!( sys, line_to_add_parallel, PSY.Line, PSY.MonitoredLine, ) add_dlr_to_system_branches!( sys, branches_dlr, n_steps, dlr_factors; initial_date = "2024-01-01", ) template = get_thermal_dispatch_template_network( NetworkModel( PTDFPowerModel; PTDF_matrix = PTDF(sys), ), ) set_device_model!(template, line_device_model) set_device_model!(template, PSY.MonitoredLine, StaticBranch) ps_model = DecisionModel(template, sys; optimizer = HiGHS_optimizer) @test build!(ps_model; output_dir = mktempdir(; cleanup = true)) == PSI.ModelBuildStatus.BUILT psi_constraint_test(ps_model, constraint_keys) moi_tests( ps_model, test_results..., false, ) psi_checkobjfun_test(ps_model, objfuncs[1]) psi_checksolve_test( ps_model, [MOI.OPTIMAL, MOI.ALMOST_OPTIMAL], test_obj_values[ix], 10000, ) res = OptimizationProblemResults(ps_model) check_dlr_branch_flows!( res, sys, branches_dlr, dlr_factors, add_parallel_line_name, ) end end end @testset "Network DC-PF with PTDF Model and implementing Dynamic Branch Ratings with BranchesParallel of different types (MonitoredLine with DLR)" begin objfuncs = [GAEVF, GQEVF, GQEVF] constraint_keys = [ PSI.ConstraintKey(FlowRateConstraint, PSY.Line, "lb"), PSI.ConstraintKey(FlowRateConstraint, PSY.Line, "ub"), PSI.ConstraintKey(CopperPlateBalanceConstraint, PSY.System), ] dlr_factors = vcat([fill(x, 6) for x in [0.99, 0.98, 1.0, 0.95]]...) test_obj_values = [375109.0, 320486.0, 241293.703] parallel_lines_names_to_add = ["1", "2", "3"]#Add parallel lines in lines with and without DLRs n_steps = 2 for slack_flag in [false, true] if slack_flag test_results = [408, 0, 264, 264, 24] else test_results = [120, 0, 264, 264, 24] end line_device_model = DeviceModel( Line, StaticBranch; time_series_names = Dict( DynamicBranchRatingTimeSeriesParameter => "dynamic_line_ratings", ), use_slacks = slack_flag, ) for (ix, add_parallel_line_name) in enumerate(parallel_lines_names_to_add) sys = PSB.build_system(PSITestSystems, "c_sys5") line_to_add_parallel = get_component(Line, sys, add_parallel_line_name) add_equivalent_ac_transmission_with_parallel_circuits!( sys, line_to_add_parallel, PSY.Line, PSY.MonitoredLine, ) add_dlr_to_system_branches!( sys, [add_parallel_line_name * "_copy"], n_steps, dlr_factors; initial_date = "2024-01-01", ) template = get_thermal_dispatch_template_network( NetworkModel( PTDFPowerModel; PTDF_matrix = PTDF(sys), ), ) set_device_model!(template, line_device_model) set_device_model!(template, PSY.MonitoredLine, StaticBranch) ps_model = DecisionModel(template, sys; optimizer = HiGHS_optimizer) @test build!(ps_model; output_dir = mktempdir(; cleanup = true)) == PSI.ModelBuildStatus.BUILT psi_constraint_test(ps_model, constraint_keys) moi_tests( ps_model, test_results..., false, ) psi_checkobjfun_test(ps_model, objfuncs[1]) psi_checksolve_test( ps_model, [MOI.OPTIMAL, MOI.ALMOST_OPTIMAL], test_obj_values[ix], 10000, ) res = OptimizationProblemResults(ps_model) check_dlr_branch_flows!( res, sys, [add_parallel_line_name * "_copy"], dlr_factors, add_parallel_line_name, ) end end end @testset "Network DC-PF with PTDF Model and implementing Dynamic Branch Ratings with BranchesParallel" begin objfuncs = [GAEVF, GQEVF, GQEVF] constraint_keys = [ PSI.ConstraintKey(FlowRateConstraint, PSY.Line, "lb"), PSI.ConstraintKey(FlowRateConstraint, PSY.Line, "ub"), PSI.ConstraintKey(CopperPlateBalanceConstraint, PSY.System), ] branches_dlr = ["1", "2", "6"] dlr_factors = vcat([fill(x, 6) for x in [0.99, 0.98, 1.0, 0.95]]...) test_obj_values = [356577.0, 279735.0, 241293.703] parallel_lines_names_to_add = ["1", "2", "3"]#Add parallel lines in lines with and without DLRs n_steps = 2 for slack_flag in [false, true] if slack_flag test_results = [408, 0, 264, 264, 24] else test_results = [120, 0, 264, 264, 24] end line_device_model = DeviceModel( Line, StaticBranch; time_series_names = Dict( DynamicBranchRatingTimeSeriesParameter => "dynamic_line_ratings", ), use_slacks = slack_flag, ) for (ix, add_parallel_line_name) in enumerate(parallel_lines_names_to_add) sys = PSB.build_system(PSITestSystems, "c_sys5") line_to_add_parallel = get_component(Line, sys, add_parallel_line_name) add_equivalent_ac_transmission_with_parallel_circuits!( sys, line_to_add_parallel, PSY.Line, ) add_dlr_to_system_branches!( sys, branches_dlr, n_steps, dlr_factors; initial_date = "2024-01-01", ) template = get_thermal_dispatch_template_network( NetworkModel( PTDFPowerModel; PTDF_matrix = PTDF(sys), ), ) set_device_model!(template, line_device_model) ps_model = DecisionModel(template, sys; optimizer = HiGHS_optimizer) @test build!(ps_model; output_dir = mktempdir(; cleanup = true)) == PSI.ModelBuildStatus.BUILT psi_constraint_test(ps_model, constraint_keys) moi_tests( ps_model, test_results..., false, ) psi_checkobjfun_test(ps_model, objfuncs[1]) psi_checksolve_test( ps_model, [MOI.OPTIMAL, MOI.ALMOST_OPTIMAL], test_obj_values[ix], 10000, ) res = OptimizationProblemResults(ps_model) check_dlr_branch_flows!( res, sys, branches_dlr, dlr_factors, add_parallel_line_name, ) end end end @testset "Network DC-PF with PTDF Model and implementing Dynamic Branch Ratings with Reductions" begin objfuncs = [GAEVF, GQEVF, GQEVF] constraint_keys = [ PSI.ConstraintKey(FlowRateConstraint, PSY.Line, "lb"), PSI.ConstraintKey(FlowRateConstraint, PSY.Line, "ub"), PSI.ConstraintKey(CopperPlateBalanceConstraint, PSY.System), ] branches_dlr = ["1", "2", "6"] dlr_factors = vcat([fill(x, 6) for x in [0.99, 0.98, 1.0, 0.95]]...) test_obj_values = [356577.0, 279735.0, 241293.703] parallel_lines_names_to_add = ["1", "2", "3"]#Add parallel lines in lines with and without DLRs n_steps = 2 test_results_slacks = Dict( 1 => [456, 0, 288, 288, 24], 2 => [456, 0, 288, 288, 24], 3 => [408, 0, 264, 264, 24], ) test_results_no_slacks = Dict( 1 => [120, 0, 288, 288, 24], 2 => [120, 0, 288, 288, 24], 3 => [120, 0, 264, 264, 24], ) for slack_flag in [false, true] line_device_model = DeviceModel( Line, StaticBranch; time_series_names = Dict( DynamicBranchRatingTimeSeriesParameter => "dynamic_line_ratings", ), use_slacks = slack_flag, ) for (ix, add_parallel_line_name) in enumerate(parallel_lines_names_to_add) if slack_flag test_results = test_results_slacks[ix] else test_results = test_results_no_slacks[ix] end sys = PSB.build_system(PSITestSystems, "c_sys5") line_to_add_parallel = get_component(Line, sys, add_parallel_line_name) add_equivalent_ac_transmission_with_series_parallel_circuits!( sys, line_to_add_parallel, PSY.Line, ) add_dlr_to_system_branches!( sys, branches_dlr, n_steps, dlr_factors; initial_date = "2024-01-01", ) nr = NetworkReduction[DegreeTwoReduction()] ptdf = PTDF(sys; network_reductions = nr) template = get_thermal_dispatch_template_network( NetworkModel( PTDFPowerModel; #PTDF_matrix = ptdf, reduce_degree_two_branches = PNM.has_degree_two_reduction( ptdf.network_reduction_data, ), ), ) set_device_model!(template, line_device_model) ps_model = DecisionModel(template, sys; optimizer = HiGHS_optimizer) @test build!(ps_model; output_dir = mktempdir(; cleanup = true)) == PSI.ModelBuildStatus.BUILT psi_constraint_test(ps_model, constraint_keys) moi_tests( ps_model, test_results..., false, ) psi_checkobjfun_test(ps_model, objfuncs[1]) psi_checksolve_test( ps_model, [MOI.OPTIMAL, MOI.ALMOST_OPTIMAL], test_obj_values[ix], 10000, ) res = OptimizationProblemResults(ps_model) check_dlr_branch_flows!( res, sys, branches_dlr, dlr_factors, add_parallel_line_name, ) end end end @testset "Network DC-PF Simulation with PTDF Model and implementing Dynamic Branch Ratings with Reductions" begin objfuncs = [GAEVF, GQEVF, GQEVF] constraint_keys = [ PSI.ConstraintKey(FlowRateConstraint, PSY.Line, "lb"), PSI.ConstraintKey(FlowRateConstraint, PSY.Line, "ub"), PSI.ConstraintKey(CopperPlateBalanceConstraint, PSY.System), ] branches_dlr = ["1", "2", "6"] dlr_factors = vcat([fill(x, 6) for x in [0.99, 0.98, 1.0, 0.95]]...) parallel_lines_names_to_add = ["1", "2", "3"]#Add parallel lines in lines with and without DLRs n_steps = 2 test_results_slacks = Dict( 1 => [600, 0, 288, 288, 24], 2 => [600, 0, 288, 288, 24], 3 => [552, 0, 264, 264, 24], ) test_results_no_slacks = Dict( 1 => [264, 0, 288, 288, 24], 2 => [264, 0, 288, 288, 24], 3 => [264, 0, 264, 264, 24], ) for slack_flag in [false, true] line_device_model = DeviceModel( Line, StaticBranch; time_series_names = Dict( DynamicBranchRatingTimeSeriesParameter => "dynamic_line_ratings", ), use_slacks = slack_flag, ) for (ix, add_parallel_line_name) in enumerate(parallel_lines_names_to_add) if slack_flag test_results = test_results_slacks[ix] else test_results = test_results_no_slacks[ix] end sys = PSB.build_system(PSITestSystems, "c_sys5") line_to_add_parallel = get_component(Line, sys, add_parallel_line_name) add_equivalent_ac_transmission_with_series_parallel_circuits!( sys, line_to_add_parallel, PSY.Line, ) add_dlr_to_system_branches!( sys, branches_dlr, n_steps, dlr_factors; initial_date = "2024-01-01", ) nr = NetworkReduction[DegreeTwoReduction()] ptdf = PTDF(sys; network_reductions = nr) template = get_thermal_dispatch_template_network( NetworkModel( PTDFPowerModel; #PTDF_matrix = ptdf, reduce_degree_two_branches = PNM.has_degree_two_reduction( ptdf.network_reduction_data, ), ), ) set_device_model!(template, line_device_model) ps_model = DecisionModel(template, sys; optimizer = HiGHS_optimizer, name = "UC") models = SimulationModels(; decision_models = [ps_model], ) DA_sequence = SimulationSequence(; models = models, ini_cond_chronology = InterProblemChronology(), ) current_date = string(today()) steps_sim = 2 sim = Simulation(; name = "", steps = steps_sim, models = models, initial_time = DateTime("2024-01-01T00:00:00"), sequence = DA_sequence, simulation_folder = tempdir()) @test build!(sim) == PSI.SimulationBuildStatus.BUILT @test execute!(sim) == IS.Simulation.RunStatusModule.RunStatus.SUCCESSFULLY_FINALIZED psi_constraint_test(ps_model, constraint_keys) moi_tests( ps_model, test_results..., false, ) psi_checkobjfun_test(ps_model, objfuncs[1]) results = SimulationResults(sim) res = get_decision_problem_results(results, "UC") check_dlr_branch_flows!( res, sys, branches_dlr, dlr_factors, add_parallel_line_name, ) end end end ================================================ FILE: test/test_parallel_branch_parameter_multipliers.jl ================================================ @testset "Parallel-branch multiplier rows are populated per-branch (no NaN)" begin # Regression coverage for the multiplier-axis bug where parallel branches # sharing a time-series UUID had their multipliers written to the wrong row # of the device-name-keyed multiplier array, leaving the other branch's row # at the construction-time NaN fill. line_device_model = DeviceModel( Line, StaticBranch; time_series_names = Dict( DynamicBranchRatingTimeSeriesParameter => "dynamic_line_ratings", ), ) branches_dlr = ["1", "2", "6"] dlr_factors = vcat([fill(x, 6) for x in [0.99, 0.98, 1.0, 0.95]]...) for parallel_line_name in ["1", "2", "3"] sys = PSB.build_system(PSITestSystems, "c_sys5") line_to_add_parallel = get_component(Line, sys, parallel_line_name) add_equivalent_ac_transmission_with_parallel_circuits!( sys, line_to_add_parallel, PSY.Line, ) add_dlr_to_system_branches!( sys, branches_dlr, 2, dlr_factors; initial_date = "2024-01-01", ) template = get_thermal_dispatch_template_network( NetworkModel(PTDFPowerModel; PTDF_matrix = PTDF(sys)), ) set_device_model!(template, line_device_model) ps_model = DecisionModel(template, sys; optimizer = HiGHS_optimizer) @test build!(ps_model; output_dir = mktempdir(; cleanup = true)) == PSI.ModelBuildStatus.BUILT container = PSI.get_optimization_container(ps_model) param_key = PSI.ParameterKey(DynamicBranchRatingTimeSeriesParameter, Line) param_container = PSI.get_parameter(container, param_key) mult_array = PSI.get_multiplier_array(param_container) device_name_axis = axes(mult_array)[1] # Every device row in the multiplier array must be fully populated for # branches that actually carry the time series — no rows left at the # NaN sentinel from the fill! at construction time. for name in device_name_axis row = mult_array[name, :] @test !any(isnan, row) end end end ================================================ FILE: test/test_power_flow_in_the_loop.jl ================================================ @testset "AC Power Flow in the loop for PhaseShiftingTransformer" begin system = build_system(PSITestSystems, "c_sys5_uc") line = get_component(Line, system, "1") arc = get_arc(line) remove_component!(system, line) ps = PhaseShiftingTransformer(; name = get_name(line), available = true, active_power_flow = 0.0, reactive_power_flow = 0.0, r = get_r(line), x = get_x(line), primary_shunt = 0.0, tap = 1.0, α = 0.0, rating = get_rating(line), arc = arc, base_power = get_base_power(system), ) add_component!(system, ps) template = get_template_dispatch_with_network( NetworkModel( PTDFPowerModel; PTDF_matrix = PTDF(system), power_flow_evaluation = ACPowerFlow(), ), ) 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.ModelBuildStatus.BUILT @test solve!(model_m) == PSI.RunStatus.SUCCESSFULLY_FINALIZED results = OptimizationProblemResults(model_m) vd = read_variables(results) data = PSI.get_power_flow_data( only(PSI.get_power_flow_evaluation_data(PSI.get_optimization_container(model_m))), ) base_power = get_base_power(system) phase_results = vd["FlowActivePowerVariable__PhaseShiftingTransformer"] # cannot easily test for the "from" bus because of the generators "Park City" and "Alta" bus_lookup = PFS.get_bus_lookup(data) @test isapprox( data.bus_active_power_injections[bus_lookup[get_number(get_to(arc))], :] * base_power, filter(row -> row[:name] == get_name(line), phase_results)[!, :value], atol = 1e-9, rtol = 0, ) end @testset "AC Power Flow in the loop with parallel lines" begin original_line_flow, parallel_line_flow = zero(ComplexF64), zero(ComplexF64) for replace_line in (true, false) system = build_system(PSITestSystems, "c_sys5_uc") line = get_component(Line, system, "1") # split line into 2 parallel lines. if replace_line original_impedance = get_r(line) + im * get_x(line) original_shunt = get_b(line) remove_component!(system, line) split_impedance = original_impedance * 2 split_shunt = (from = 0.5 * original_shunt.from, to = 0.5 * original_shunt.to) for i in 1:2 l = Line(; name = get_name(line) * "_$i", available = true, active_power_flow = 0.0, reactive_power_flow = 0.0, arc = get_arc(line), r = real(split_impedance), x = imag(split_impedance), b = split_shunt, angle_limits = get_angle_limits(line), rating = get_rating(line), ) add_component!(system, l) end end template = get_template_dispatch_with_network( NetworkModel( PTDFPowerModel; PTDF_matrix = PTDF(system), power_flow_evaluation = ACPowerFlow(), ), ) model_m = DecisionModel(template, system; optimizer = HiGHS_optimizer) @test build!(model_m; output_dir = mktempdir(; cleanup = true)) == PSI.ModelBuildStatus.BUILT @test solve!(model_m) == PSI.RunStatus.SUCCESSFULLY_FINALIZED results = OptimizationProblemResults(model_m) vd = read_aux_variables(results) active_power_ft = vd["PowerFlowBranchActivePowerFromTo__Line"] reactive_power_ft = vd["PowerFlowBranchReactivePowerFromTo__Line"] if replace_line name = "$(get_name(line))_1" parallel_line_flow = filter(row -> row[:name] == name, active_power_ft)[1, :value][1] + im * filter(row -> row[:name] == name, reactive_power_ft)[1, :value][1] else name = get_name(line) original_line_flow = filter(row -> row[:name] == name, active_power_ft)[1, :value][1] + im * filter(row -> row[:name] == name, reactive_power_ft)[1, :value][1] end end @test isapprox( 2 * parallel_line_flow, original_line_flow, atol = 1e-3, ) end @testset "AC Power Flow in the loop with a breaker-switch" begin system = build_system(PSITestSystems, "c_sys5_uc") # we choose a line to replace such that the arc lookup of a different line changes. line = get_component(Line, system, "2") remove_component!(system, line) bs = PSY.DiscreteControlledACBranch( ; name = get_name(line), available = true, active_power_flow = 0.0, reactive_power_flow = 0.0, arc = get_arc(line), r = 0.0, x = 0.0, rating = get_rating(line), discrete_branch_type = PSY.DiscreteControlledBranchType.BREAKER, branch_status = PSY.DiscreteControlledBranchStatus.CLOSED, ) add_component!(system, bs) # these lines end up being parallel, so we set their impedances to be the same line3 = get_component(Line, system, "3") line6 = get_component(Line, system, "6") PSY.set_r!(line3, PSY.get_r(line6)) PSY.set_x!(line3, PSY.get_x(line6)) template = get_template_dispatch_with_network( NetworkModel( PTDFPowerModel; PTDF_matrix = PTDF(system), power_flow_evaluation = ACPowerFlow(), ), ) model_m = DecisionModel(template, system; optimizer = HiGHS_optimizer) @test build!(model_m; output_dir = mktempdir(; cleanup = true)) == PSI.ModelBuildStatus.BUILT @test solve!(model_m) == PSI.RunStatus.SUCCESSFULLY_FINALIZED # the interface currently doesn't allow for power flow in-the-loop on networks with # reductions. we'd have to pass kwargs all the way down to add_power_flow_data!. end # already has a TwoTerminalGenericHVDCLine replace_hvdc!(::PSY.System, ::Type{TwoTerminalGenericHVDCLine}) = nothing function replace_hvdc!(sys::PSY.System, ::Type{TwoTerminalVSCLine}) # required fields for constructor: # name, available, arc, active_power_flow, rating, active_power_limits_from, # active_power_limits_to old_hvdc = only(get_components(TwoTerminalGenericHVDCLine, sys)) remove_component!(sys, old_hvdc) hvdc = TwoTerminalVSCLine(; name = get_name(old_hvdc), available = true, arc = get_arc(old_hvdc), active_power_flow = get_active_power_flow(old_hvdc), rating = 100.0, # arbitrary active_power_limits_from = get_active_power_limits_from(old_hvdc), active_power_limits_to = get_active_power_limits_to(old_hvdc), ) add_component!(sys, hvdc) end function replace_hvdc!(sys::PSY.System, ::Type{TwoTerminalLCCLine}) # required fields for constructor (yikes): # name, available, arc, active_power_flow, r, transfer_setpoint, scheduled_dc_voltage, # rectifier_bridges, rectifier_delay_angle_limits, rectifier_rc, rectifier_xc, #rectifier_base_voltage, inverter_bridges, inverter_extinction_angle_limits, # inverter_rc, inverter_xc, inverter_base_voltage, old_hvdc = only(get_components(TwoTerminalGenericHVDCLine, sys)) remove_component!(sys, old_hvdc) # hand-tuned parameters. r = 0.01 xr = 0.01 xi = 0.01 hvdc = TwoTerminalLCCLine(; name = get_name(old_hvdc), available = true, arc = get_arc(old_hvdc), active_power_flow = get_active_power_flow(old_hvdc), r = r, transfer_setpoint = 50, scheduled_dc_voltage = 200.0, rectifier_bridges = 1, rectifier_delay_angle_limits = (min = 0.0, max = π / 2), rectifier_rc = 0.0, rectifier_xc = xr, rectifier_base_voltage = 100.0, inverter_bridges = 1, inverter_extinction_angle_limits = (min = 0, max = π / 2), inverter_rc = 0.0, inverter_xc = xi, inverter_base_voltage = 100.0, # rest are optional. #=power_mode = true, switch_mode_voltage = 0.0, compounding_resistance = 0.0, min_compounding_voltage = 0.0, rectifier_transformer_ratio = 1.0, rectifier_tap_setting = 1.0, rectifier_tap_limits = (min = 0.5, max = 1.5), rectifier_tap_step = 0.05, rectifier_delay_angle = 0.01, rectifier_capacitor_reactance = 0.0, inverter_transformer_ratio = 1.0, inverter_tap_setting = 1.0, inverter_tap_limits = (min = 0.5, max = 1.5), inverter_tap_step = 0.05, inverter_extinction_angle = 0.0, inverter_capacitor_reactance = 0.0, active_power_limits_from = (min = 0.0, max = 0.0), active_power_limits_to = (min = 0.0, max = 0.0), reactive_power_limits_from = (min = 0.0, max = 0.0), reactive_power_limits_to = (min = 0.0, max = 0.0),=# ) add_component!(sys, hvdc) end @testset "HVDCs with DC PF in the loop" begin for hvdc_type in (TwoTerminalGenericHVDCLine, TwoTerminalLCCLine, TwoTerminalVSCLine) sys = build_system(PSISystems, "2Area 5 Bus System") replace_hvdc!(sys, hvdc_type) template_uc = ProblemTemplate( NetworkModel(PTDFPowerModel; power_flow_evaluation = DCPowerFlow()), ) set_device_model!(template_uc, ThermalStandard, ThermalBasicUnitCommitment) set_device_model!(template_uc, RenewableDispatch, RenewableFullDispatch) set_device_model!(template_uc, PowerLoad, StaticPowerLoad) set_device_model!(template_uc, DeviceModel(Line, StaticBranch)) if hvdc_type == TwoTerminalVSCLine set_device_model!( template_uc, # regardless of formulation, PowerFlows.jl always takes losses into account... DeviceModel(hvdc_type, HVDCTwoTerminalLossless), ) else set_device_model!( template_uc, DeviceModel(hvdc_type, HVDCTwoTerminalDispatch), ) end model = DecisionModel(template_uc, sys; name = "UC", optimizer = HiGHS_optimizer) @test build!(model; output_dir = mktempdir()) == PSI.ModelBuildStatus.BUILT @test solve!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED end end @testset "LCC HVDC with AC PF in the loop" begin sys5 = build_system(PSISystems, "2Area 5 Bus System") hvdc = first(get_components(TwoTerminalGenericHVDCLine, sys5)) lcc = TwoTerminalLCCLine(; name = "lcc", available = true, arc = hvdc.arc, active_power_flow = 0.1, r = 0.000189, transfer_setpoint = -100.0, scheduled_dc_voltage = 7.5, rectifier_bridges = 2, rectifier_delay_angle_limits = (min = 0.31590, max = 1.570), rectifier_rc = 2.6465e-5, rectifier_xc = 0.001092, rectifier_base_voltage = 230.0, inverter_bridges = 2, inverter_extinction_angle_limits = (min = 0.3037, max = 1.57076), inverter_rc = 2.6465e-5, inverter_xc = 0.001072, inverter_base_voltage = 230.0, power_mode = true, switch_mode_voltage = 0.0, compounding_resistance = 0.0, min_compounding_voltage = 0.0, rectifier_transformer_ratio = 0.09772, rectifier_tap_setting = 1.0, rectifier_tap_limits = (min = 1, max = 1), rectifier_tap_step = 0.00624, rectifier_delay_angle = 0.31590, rectifier_capacitor_reactance = 0.1, inverter_transformer_ratio = 0.07134, inverter_tap_setting = 1.0, inverter_tap_limits = (min = 1, max = 1), inverter_tap_step = 0.00625, inverter_extinction_angle = 0.31416, inverter_capacitor_reactance = 0.0, active_power_limits_from = (min = -3.0, max = 3.0), active_power_limits_to = (min = -3.0, max = 3.0), reactive_power_limits_from = (min = -3.0, max = 3.0), reactive_power_limits_to = (min = -3.0, max = 3.0), ) add_component!(sys5, lcc) remove_component!(sys5, hvdc) template = get_thermal_dispatch_template_network( NetworkModel( ACPPowerModel; use_slacks = false, power_flow_evaluation = ACPowerFlow(), ), ) set_device_model!(template, TwoTerminalLCCLine, PSI.HVDCTwoTerminalLCC) set_device_model!(template, ThermalStandard, ThermalDispatchNoMin) model = DecisionModel( template, sys5; optimizer = optimizer_with_attributes(Ipopt.Optimizer), horizon = Hour(2), ) @test build!(model; output_dir = mktempdir(; cleanup = true)) == PSI.ModelBuildStatus.BUILT @test solve!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED end @testset "generic HVDC with AC PF in the loop" begin # TODO replace RTS with something smaller, so this test case doesn't take so long. sys = build_system(PSISystems, "RTS_GMLC_DA_sys") hvdc = only(get_components(TwoTerminalGenericHVDCLine, sys)) from = get_from(get_arc(hvdc)) to = get_to(get_arc(hvdc)) # remove components that impact total bus power at the HVDC line buses. components = collect( get_components( x -> get_number(get_bus(x)) ∈ (get_number(from), get_number(to)), StaticInjection, sys, ), ) foreach(x -> remove_component!(sys, x), components) change_to_PQ = ["Chifa", "Arne"] for bus_name in change_to_PQ bus = get_component(PSY.ACBus, sys, bus_name) @assert !isnothing(bus) "bus does not exist" set_bustype!(bus, PSY.ACBusTypes.PQ) end set_bustype!(get_component(ACBus, sys, "Arthur"), ACBusTypes.REF) template_uc = ProblemTemplate(NetworkModel(PTDFPowerModel; power_flow_evaluation = ACPowerFlow())) set_device_model!(template_uc, ThermalStandard, ThermalBasicUnitCommitment) 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(TwoTerminalGenericHVDCLine, HVDCTwoTerminalDispatch), ) model = DecisionModel(template_uc, sys; name = "UC", optimizer = HiGHS_optimizer) @test build!(model; output_dir = mktempdir()) == PSI.ModelBuildStatus.BUILT @test solve!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED results = OptimizationProblemResults(model) vd = read_variables(results) ad = read_aux_variables(results) data = PSI.get_power_flow_data( only(PSI.get_power_flow_evaluation_data(PSI.get_optimization_container(model))), ) base_power = get_base_power(sys) # test that the power flow results for the HVDC buses match the HVDC power transfer from the simulation bus_lookup = PFS.get_bus_lookup(data) from_to = vd["FlowActivePowerFromToVariable__TwoTerminalGenericHVDCLine"][:, :value] to_from = vd["FlowActivePowerToFromVariable__TwoTerminalGenericHVDCLine"][:, :value] @test isapprox( data.bus_active_power_injections[bus_lookup[get_number(from)], :] * base_power, -1 .* from_to, atol = 1e-9, rtol = 0, ) # verify the line loss curve is exactly 10% so the loss-ratio check below is meaningful hvdc_loss_curve = get_loss(hvdc) @assert hvdc_loss_curve isa PSY.LinearCurve @assert get_proportional_term(hvdc_loss_curve) == 0.1 @assert get_constant_term(hvdc_loss_curve) == 0.0 nonzeros = (abs.(from_to) .> 1e-9) .| (abs.(to_from) .> 1e-9) loss_ratios = (from_to .+ to_from) ./ maximum.(zip(abs.(from_to), abs.(to_from))) ten_percent_loss = abs.(loss_ratios .- 0.1) .< 1e-9 @test all(ten_percent_loss[nonzeros]) @test isapprox( data.bus_active_power_injections[bus_lookup[get_number(to)], :] * base_power, -1 .* to_from, atol = 1e-9, rtol = 0, ) end @testset "Test AC power flow in the loop: small system UCED, PSS/E export" for calculate_loss_factors in (true, false) for calculate_voltage_stability_factors in (true, false) file_path = mktempdir(; cleanup = true) export_path = mktempdir(; cleanup = true) pf_path = mktempdir(; cleanup = true) c_sys5_hy_uc = PSB.build_system(PSITestSystems, "c_sys5_hy_uc") c_sys5_hy_ed = PSB.build_system(PSITestSystems, "c_sys5_hy_ed") sim = run_simulation( c_sys5_hy_uc, c_sys5_hy_ed, file_path, export_path; ed_network_model = NetworkModel( CopperPlatePowerModel; duals = [CopperPlateBalanceConstraint], use_slacks = true, power_flow_evaluation = ACPowerFlow(; exporter = PSSEExportPowerFlow(:v33, pf_path; write_comments = true), calculate_loss_factors = calculate_loss_factors, calculate_voltage_stability_factors = calculate_voltage_stability_factors, ), ), ) results = SimulationResults(sim) results_ed = get_decision_problem_results(results, "ED") thermal_results = first( values( PSI.read_results_with_keys(results_ed, [PSI.VariableKey(ActivePowerVariable, ThermalStandard)]), ), ) min_time = minimum(thermal_results.DateTime) max_time = maximum(thermal_results.DateTime) first_result = filter(row -> row[:DateTime] == min_time, thermal_results) last_result = filter(row -> row[:DateTime] == max_time, thermal_results) available_aux_variables = list_aux_variable_keys(results_ed) loss_factors_aux_var_key = PSI.AuxVarKey(PowerFlowLossFactors, ACBus) voltage_stability_aux_var_key = PSI.AuxVarKey(PowerFlowVoltageStabilityFactors, ACBus) # here we check if the loss factors are stored in the results, the values are tested in PowerFlows.jl if calculate_loss_factors @test loss_factors_aux_var_key ∈ available_aux_variables loss_factors = first( values( PSI.read_results_with_keys(results_ed, [loss_factors_aux_var_key]), ), ) @test !isnothing(loss_factors) # count distinct time periods @test length(unique(loss_factors.DateTime)) == 48 * 12 else @test loss_factors_aux_var_key ∉ available_aux_variables end if calculate_voltage_stability_factors @test voltage_stability_aux_var_key ∈ available_aux_variables voltage_stability = first( values( PSI.read_results_with_keys(results_ed, [voltage_stability_aux_var_key]; table_format = TableFormat.LONG), ), ) @test !isnothing(voltage_stability) @test length(unique(voltage_stability.DateTime)) == 48 * 12 else @test voltage_stability_aux_var_key ∉ available_aux_variables end @test length(filter(x -> isdir(joinpath(pf_path, x)), readdir(pf_path))) == 48 * 12 # this now returns a system?! first_export = load_pf_export(pf_path, "export_1_1") last_export = load_pf_export(pf_path, "export_48_12") # Test that the active powers written to the first and last exports line up with the real simulation results for gen_name in get_name.(get_components(ThermalStandard, c_sys5_hy_ed)) this_first_result = filter(row -> row[:name] == gen_name, first_result)[1, :value] this_first_exported = get_active_power(get_component(ThermalStandard, first_export, gen_name)) @test isapprox(this_first_result, this_first_exported) this_last_result = filter(row -> row[:name] == gen_name, last_result)[1, :value] this_last_exported = get_active_power(get_component(ThermalStandard, last_export, gen_name)) @test isapprox(this_last_result, this_last_exported) end end end @testset "AC Power Flow line active power loss auxiliary variable" begin system = build_system(PSITestSystems, "c_sys5_uc") template = get_template_dispatch_with_network( NetworkModel( PTDFPowerModel; PTDF_matrix = PTDF(system), power_flow_evaluation = ACPowerFlow(), ), ) model_m = DecisionModel(template, system; optimizer = HiGHS_optimizer) @test build!(model_m; output_dir = mktempdir(; cleanup = true)) == PSI.ModelBuildStatus.BUILT @test solve!(model_m) == PSI.RunStatus.SUCCESSFULLY_FINALIZED results = OptimizationProblemResults(model_m) ad = read_aux_variables(results) active_power_ft = ad["PowerFlowBranchActivePowerFromTo__Line"] active_power_tf = ad["PowerFlowBranchActivePowerToFrom__Line"] active_power_loss = ad["PowerFlowBranchActivePowerLoss__Line"] for line_name in unique(active_power_loss.name) ft_vals = filter(row -> row[:name] == line_name, active_power_ft)[!, :value] tf_vals = filter(row -> row[:name] == line_name, active_power_tf)[!, :value] loss_vals = filter(row -> row[:name] == line_name, active_power_loss)[!, :value] @test isapprox(loss_vals, ft_vals .+ tf_vals; atol = 1e-9) end end @testset "AC Power Flow in the loop with headroom-proportional slack" begin system = build_system(PSITestSystems, "c_sys5_uc") template = get_template_dispatch_with_network( NetworkModel( PTDFPowerModel; PTDF_matrix = PTDF(system), power_flow_evaluation = ACPowerFlow(; distribute_slack_proportional_to_headroom = true, correct_bustypes = true, ), ), ) model = DecisionModel(template, system; optimizer = HiGHS_optimizer) @test build!(model; output_dir = mktempdir(; cleanup = true)) == PSI.ModelBuildStatus.BUILT @test solve!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED container = PSI.get_optimization_container(model) pf_e_data = only(PSI.get_power_flow_evaluation_data(container)) data = PSI.get_power_flow_data(pf_e_data) computed_gspf = PFS.get_computed_gspf(data) n_time_steps = length(PSI.get_time_steps(container)) bus_lookup = PFS.get_bus_lookup(data) base_power = get_base_power(system) # Headroom factors should be populated for every time step @test length(computed_gspf) == n_time_steps @test all(!isempty(d) for d in computed_gspf) @test all(all(v > 0.0 for v in values(d)) for d in computed_gspf) # bus_active_power_range should equal the sum of generator headroom per bus per time step for t in 1:n_time_steps bus_headroom_check = zeros(size(data.bus_active_power_range, 1)) for ((comp_type, comp_name), headroom) in computed_gspf[t] comp = get_component(comp_type, system, comp_name) bus_number = get_number(get_bus(comp)) bus_ix = bus_lookup[bus_number] bus_headroom_check[bus_ix] += headroom end @test isapprox( data.bus_active_power_range[:, t], bus_headroom_check; atol = 1e-10, ) end # bus_slack_participation_factors should match bus_active_power_range at participating buses bus_slack_pf = PFS.get_bus_slack_participation_factors(data) for t in 1:n_time_steps for bus_ix in axes(data.bus_active_power_range, 1) R_k = data.bus_active_power_range[bus_ix, t] if R_k > 0.0 @test bus_slack_pf[bus_ix, t] == R_k end end end # Independently recompute expected headroom from optimization results and system limits, # then verify it matches computed_gspf exactly. # NOTE: c_sys5_uc thermals have fixed active power limits (no ActivePowerTimeSeriesParameter), # so this exercises the static P_max path. The time-varying P_max path (using # min(static_limit, ts_param_value)) would need a system with renewable time series at a # REF/PV bus to be exercised. for t in 1:n_time_steps for ((comp_type, comp_name), headroom) in computed_gspf[t] comp = get_component(comp_type, system, comp_name) p_max_sys = PFS.get_active_power_limits_for_power_flow(comp).max # Look up the optimization set point for this generator at this time step var_key = PSI.VariableKey(PSI.ActivePowerVariable, comp_type) result_data = PSI.lookup_value(container, var_key) p_setpoint = JuMP.value(result_data[comp_name, t]) expected_headroom = p_max_sys - p_setpoint @test expected_headroom > 0.0 @test isapprox(headroom, expected_headroom; atol = 1e-10) end end end @testset "Headroom proportional slack excludes FixedOutput generators" begin # c_sys5_uc_re has renewables at PV/REF buses, so they would normally participate # in headroom slack. Setting them to FixedOutput should exclude them. system = build_system(PSITestSystems, "c_sys5_uc_re") template = get_template_dispatch_with_network( NetworkModel( PTDFPowerModel; PTDF_matrix = PTDF(system), use_slacks = true, power_flow_evaluation = ACPowerFlow(; distribute_slack_proportional_to_headroom = true, correct_bustypes = true, ), ), ) set_device_model!(template, RenewableDispatch, FixedOutput) model = DecisionModel(template, system; optimizer = HiGHS_optimizer) @test build!(model; output_dir = mktempdir(; cleanup = true)) == PSI.ModelBuildStatus.BUILT @test solve!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED container = PSI.get_optimization_container(model) pf_e_data = only(PSI.get_power_flow_evaluation_data(container)) data = PSI.get_power_flow_data(pf_e_data) computed_gspf = PFS.get_computed_gspf(data) # The renewable is at a PV bus but uses FixedOutput, so it must not appear in the # headroom factors — only ThermalStandard generators should participate. for t in 1:length(PSI.get_time_steps(container)) for ((comp_type, _), _) in computed_gspf[t] @test comp_type <: ThermalStandard end end end @testset "Headroom proportional slack with time-varying active power limits" begin # c_sys5_uc_re has renewables at PV/REF buses with time series data. system = build_system(PSITestSystems, "c_sys5_uc_re") re_gen = first(get_components(RenewableDispatch, system)) re_name = get_name(re_gen) # Force device_base != system_base so the unit-handling path is exercised. If headroom # ever silently re-introduces a `* device_base / system_base` factor, the recomputed # expected_headroom below will mismatch by 0.5×, failing the assertion. PSY.set_units_base_system!(system, "SYSTEM_BASE") PSY.set_base_power!(re_gen, get_base_power(re_gen) / 2) template = get_template_dispatch_with_network( NetworkModel( PTDFPowerModel; PTDF_matrix = PTDF(system), use_slacks = true, power_flow_evaluation = ACPowerFlow(; distribute_slack_proportional_to_headroom = true, correct_bustypes = true, ), ), ) set_device_model!(template, RenewableDispatch, RenewableFullDispatch) model = DecisionModel(template, system; optimizer = HiGHS_optimizer) @test build!(model; output_dir = mktempdir(; cleanup = true)) == PSI.ModelBuildStatus.BUILT @test solve!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED container = PSI.get_optimization_container(model) pf_e_data = only(PSI.get_power_flow_evaluation_data(container)) data = PSI.get_power_flow_data(pf_e_data) computed_gspf = PFS.get_computed_gspf(data) n_time_steps = length(PSI.get_time_steps(container)) # Verify the renewable's headroom uses min(static_limit, ts_param) at each time step re_type = typeof(re_gen) p_max_static = PFS.get_active_power_limits_for_power_flow(re_gen).max var_key = PSI.VariableKey(PSI.ActivePowerVariable, re_type) var_values = PSI.lookup_value(container, var_key) ts_key = PSI.ParameterKey(PSI.ActivePowerTimeSeriesParameter, re_type) ts_values = PSI.lookup_value(container, ts_key) for t in 1:n_time_steps p_setpoint = JuMP.value(var_values[re_name, t]) p_max_ts = ts_values[re_name, t] p_max_t = min(p_max_static, p_max_ts) expected_headroom = p_max_t - p_setpoint entry = get(computed_gspf[t], (re_type, re_name), nothing) if expected_headroom > 0.0 @test entry !== nothing @test isapprox(entry, expected_headroom; atol = 1e-10) else @test entry === nothing end end # The time series should cause P_max to vary, producing different headroom across steps re_ts_vals = [ts_values[re_name, t] for t in 1:n_time_steps] @test !all(isapprox.(re_ts_vals, re_ts_vals[1]; atol = 1e-10)) end @testset "Power Flow in the loop with separate in/out active power variables" begin # Build a system with a Source component that uses ImportExportSourceModel, # which creates separate ActivePowerInVariable and ActivePowerOutVariable. sys = make_5_bus_with_import_export(; add_single_time_series = false) template = get_template_dispatch_with_network( NetworkModel( PTDFPowerModel; PTDF_matrix = PTDF(sys), power_flow_evaluation = PTDFDCPowerFlow(), ), ) set_device_model!( template, DeviceModel( Source, ImportExportSourceModel; attributes = Dict("reservation" => false), ), ) model = DecisionModel(template, sys; optimizer = HiGHS_optimizer) @test build!(model; output_dir = mktempdir(; cleanup = true)) == PSI.ModelBuildStatus.BUILT @test solve!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED container = PSI.get_optimization_container(model) pf_e_data = only(PSI.get_power_flow_evaluation_data(container)) input_key_map = PSI.get_input_key_map(pf_e_data) # Verify that active_power_in and active_power_out categories are present in the map @test haskey(input_key_map, :active_power_in) @test haskey(input_key_map, :active_power_out) # Check that ActivePowerInVariable and ActivePowerOutVariable for Source are mapped in_keys = collect(keys(input_key_map[:active_power_in])) out_keys = collect(keys(input_key_map[:active_power_out])) @test any( k -> PSI.get_entry_type(k) == PSI.ActivePowerInVariable && PSI.get_component_type(k) == Source, in_keys, ) @test any( k -> PSI.get_entry_type(k) == PSI.ActivePowerOutVariable && PSI.get_component_type(k) == Source, out_keys, ) # Verify the PowerFlowData bus injections reflect the net power (out - in) data = PSI.get_power_flow_data(pf_e_data) base_power = get_base_power(sys) bus_lookup = PFS.get_bus_lookup(data) source = get_component(Source, sys, "source") source_bus_ix = bus_lookup[get_number(get_bus(source))] results = OptimizationProblemResults(model) vd = read_variables(results) p_out_results = vd["ActivePowerOutVariable__Source"] p_in_results = vd["ActivePowerInVariable__Source"] source_p_out = filter(row -> row[:name] == "source", p_out_results)[!, :value] source_p_in = filter(row -> row[:name] == "source", p_in_results)[!, :value] @test length(source_p_out) > 0 @test length(source_p_in) > 0 # The net bus injection (= injections − withdrawals) at the source bus must equal # the sum of every `:active_power` contribution at that bus plus the source's # (out − in). Loads are routed to `bus_active_power_withdrawals` under the same # category, so subtracting withdrawals folds them back into the comparison. other_injection_at_bus = zeros(length(source_p_out)) for (key, comp_map) in input_key_map[:active_power] result_data = PSI.lookup_value(container, key) for (dev_name, bus_ix) in comp_map bus_ix == source_bus_ix || continue for t in eachindex(other_injection_at_bus) other_injection_at_bus[t] += PSI.jump_value(result_data[dev_name, t]) end end end source_net_pu = (source_p_out .- source_p_in) ./ base_power @test isapprox( data.bus_active_power_injections[source_bus_ix, :] .- data.bus_active_power_withdrawals[source_bus_ix, :], other_injection_at_bus .+ source_net_pu; atol = 1e-9, ) end @testset "Headroom proportional slack with in/out active power variables (Source)" begin # nodeC is a PV bus in c_sys5_uc, so a Source there can participate in headroom slack. sys = make_5_bus_with_import_export(; add_single_time_series = false) template = get_template_dispatch_with_network( NetworkModel( PTDFPowerModel; PTDF_matrix = PTDF(sys), power_flow_evaluation = ACPowerFlow(; distribute_slack_proportional_to_headroom = true, correct_bustypes = true, ), ), ) set_device_model!( template, DeviceModel( Source, ImportExportSourceModel; attributes = Dict("reservation" => false), ), ) model = DecisionModel(template, sys; optimizer = HiGHS_optimizer) @test build!(model; output_dir = mktempdir(; cleanup = true)) == PSI.ModelBuildStatus.BUILT @test solve!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED container = PSI.get_optimization_container(model) pf_e_data = only(PSI.get_power_flow_evaluation_data(container)) data = PSI.get_power_flow_data(pf_e_data) computed_gspf = PFS.get_computed_gspf(data) n_time_steps = length(PSI.get_time_steps(container)) source = get_component(Source, sys, "source") p_max_out = PSY.get_active_power_limits(source).max in_key = PSI.VariableKey(PSI.ActivePowerInVariable, Source) out_key = PSI.VariableKey(PSI.ActivePowerOutVariable, Source) p_in_data = PSI.lookup_value(container, in_key) p_out_data = PSI.lookup_value(container, out_key) # When net = p_out - p_in is negative the source is charging and per spec gets zero # headroom (omitted from `computed_gspf`); when net >= 0 the headroom is p_max_out - net. for t in 1:n_time_steps net = JuMP.value(p_out_data["source", t]) - JuMP.value(p_in_data["source", t]) if net < 0.0 @test !haskey(computed_gspf[t], (Source, "source")) else @test isapprox( computed_gspf[t][(Source, "source")], p_max_out - net; atol = 1e-10, ) end end # Guard against a regression that silently drops the in/out accumulation path. @test any(haskey(d, (Source, "source")) for d in computed_gspf) end @testset "Power Flow in the loop with Source FixedOutput (parameter path)" begin # Source under FixedOutput emits ActivePowerIn/OutTimeSeriesParameter and # no In/Out Variables. Verifies that the parameter keys are picked up by # PF_INPUT_KEY_PRECEDENCES (Luke Kiernan, PR #1612), and that the resulting # bus injection follows the same out − in allocation rule as the variable path. sys = make_5_bus_with_import_export(; add_single_time_series = true) source = get_component(Source, sys, "source") load = first(get_components(PowerLoad, sys)) tstamp = TimeSeries.timestamp( get_time_series_array(SingleTimeSeries, load, "max_active_power"), ) 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, ] ts_data = repeat(day_data, 2) ts_out = SingleTimeSeries( "max_active_power_out", TimeArray(tstamp, ts_data); scaling_factor_multiplier = get_max_active_power, ) ts_in = SingleTimeSeries( "max_active_power_in", TimeArray(tstamp, ts_data); scaling_factor_multiplier = get_max_active_power, ) add_time_series!(sys, source, ts_out) add_time_series!(sys, source, ts_in) transform_single_time_series!(sys, Hour(24), Hour(24)) template = get_template_dispatch_with_network( NetworkModel( PTDFPowerModel; PTDF_matrix = PTDF(sys), power_flow_evaluation = PTDFDCPowerFlow(), ), ) set_device_model!(template, DeviceModel(Source, FixedOutput)) model = DecisionModel(template, sys; optimizer = HiGHS_optimizer) @test build!(model; output_dir = mktempdir(; cleanup = true)) == PSI.ModelBuildStatus.BUILT @test solve!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED container = PSI.get_optimization_container(model) pf_e_data = only(PSI.get_power_flow_evaluation_data(container)) input_key_map = PSI.get_input_key_map(pf_e_data) @test haskey(input_key_map, :active_power_in) @test haskey(input_key_map, :active_power_out) in_keys = collect(keys(input_key_map[:active_power_in])) out_keys = collect(keys(input_key_map[:active_power_out])) @test any( k -> PSI.get_entry_type(k) == PSI.ActivePowerInTimeSeriesParameter && PSI.get_component_type(k) == Source, in_keys, ) @test any( k -> PSI.get_entry_type(k) == PSI.ActivePowerOutTimeSeriesParameter && PSI.get_component_type(k) == Source, out_keys, ) # PF data injections at the source bus should equal the sum of every other # device's contribution at that bus plus (out_param − in_param) from the # FixedOutput source — same allocation rule as the in/out variable path. data = PSI.get_power_flow_data(pf_e_data) bus_lookup = PFS.get_bus_lookup(data) source_bus_ix = bus_lookup[get_number(get_bus(source))] n_time_steps = length(PSI.get_time_steps(container)) in_param = PSI.lookup_value( container, PSI.ParameterKey(PSI.ActivePowerInTimeSeriesParameter, Source), ) out_param = PSI.lookup_value( container, PSI.ParameterKey(PSI.ActivePowerOutTimeSeriesParameter, Source), ) other_injection_at_bus = zeros(n_time_steps) for (key, comp_map) in input_key_map[:active_power] result_data = PSI.lookup_value(container, key) for (dev_name, bus_ix) in comp_map bus_ix == source_bus_ix || continue for t in 1:n_time_steps other_injection_at_bus[t] += PSI.jump_value(result_data[dev_name, t]) end end end source_net = [ PSI.jump_value(out_param["source", t]) - PSI.jump_value(in_param["source", t]) for t in 1:n_time_steps ] # Net bus injection = injections − withdrawals; loads route to withdrawals under # the same `:active_power` category and the difference folds them back in. @test isapprox( data.bus_active_power_injections[source_bus_ix, 1:n_time_steps] .- data.bus_active_power_withdrawals[source_bus_ix, 1:n_time_steps], other_injection_at_bus .+ source_net; atol = 1e-9, ) # Guard against a regression that drives both parameters to zero — the test # above would then pass trivially without exercising the parameter path. @test !all(isapprox.(source_net, 0.0; atol = 1e-10)) end ================================================ FILE: test/test_print.jl ================================================ function _test_plain_print_methods(list::Array) for object in list normal = repr(object) io = IOBuffer() show(io, "text/plain", object) grabbed = String(take!(io)) @test grabbed !== nothing end end function _test_html_print_methods(list::Array) for object in list normal = repr(object) io = IOBuffer() show(io, "text/html", object) grabbed = String(take!(io)) @test grabbed !== nothing end end @testset "Test Model Print Methods" begin template = get_thermal_dispatch_template_network() c_sys5 = PSB.build_system(PSITestSystems, "c_sys5") dm_model = DecisionModel(template, c_sys5; optimizer = HiGHS_optimizer) @test build!(dm_model; output_dir = mktempdir(; cleanup = true)) == PSI.ModelBuildStatus.BUILT @test solve!(dm_model; optimizer = HiGHS_optimizer) == PSI.RunStatus.SUCCESSFULLY_FINALIZED results = OptimizationProblemResults(dm_model) variables = read_variables(results) list = [ template, dm_model, PSI.get_model(template, ThermalStandard), PSI.get_network_model(template), results, ] _test_plain_print_methods(list) _test_html_print_methods(list) end @testset "Test Simulation Print Methods" begin template_uc = get_template_basic_uc_simulation() template_ed = get_template_nomin_ed_simulation() set_device_model!(template_ed, InterruptiblePowerLoad, StaticPowerLoad) set_network_model!(template_uc, NetworkModel( CopperPlatePowerModel, # MILP "duals" not supported with free solvers # duals = [CopperPlateBalanceConstraint], )) set_network_model!( template_ed, NetworkModel( CopperPlatePowerModel; duals = [CopperPlateBalanceConstraint], use_slacks = true, ), ) c_sys5_hy_uc = PSB.build_system(PSITestSystems, "c_sys5_hy_uc") c_sys5_hy_ed = PSB.build_system(PSITestSystems, "c_sys5_hy_ed") models = SimulationModels(; decision_models = [ DecisionModel( template_uc, c_sys5_hy_uc; name = "UC", optimizer = HiGHS_optimizer, ), DecisionModel( template_ed, c_sys5_hy_ed; name = "ED", optimizer = HiGHS_optimizer, ), ], ) sequence = SimulationSequence(; models = models, feedforwards = Dict( "ED" => [ SemiContinuousFeedforward(; component_type = ThermalStandard, source = OnVariable, affected_values = [ActivePowerVariable], ), ], ), ini_cond_chronology = InterProblemChronology(), ) sim_not_built = Simulation(; name = "printing_sim", steps = 2, models = models, sequence = sequence, simulation_folder = mktempdir(; cleanup = true), ) sim = Simulation(; name = "printing_sim", steps = 2, models = models, sequence = sequence, simulation_folder = mktempdir(; cleanup = true), ) build!(sim) execute!(sim) results = SimulationResults(sim) results_uc = get_decision_problem_results(results, "UC") list = [models, sequence, template_uc, template_ed, sim, sim_not_built, results_uc] _test_plain_print_methods(list) _test_html_print_methods(list) end ================================================ FILE: test/test_problem_template.jl ================================================ # This file is WIP while the interface for templates is finalized @testset "Manual Operations Template" begin template = ProblemTemplate(CopperPlatePowerModel) set_device_model!(template, PowerLoad, StaticPowerLoad) set_device_model!(template, ThermalStandard, ThermalStandardUnitCommitment) set_device_model!(template, Line, StaticBranchUnbounded) @test !isempty(template.devices) @test !isempty(template.branches) @test isempty(template.services) end @testset "Operations Template Overwrite" begin template = ProblemTemplate(CopperPlatePowerModel) set_device_model!(template, PowerLoad, StaticPowerLoad) set_device_model!(template, ThermalStandard, ThermalStandardUnitCommitment) @test_logs (:warn, "Overwriting ThermalStandard existing model") set_device_model!( template, DeviceModel(ThermalStandard, ThermalBasicUnitCommitment), ) @test PSI.get_formulation(template.devices[:ThermalStandard]) == ThermalBasicUnitCommitment end @testset "Provided Templates Tests" begin uc_template = template_unit_commitment() @test !isempty(uc_template.devices) @test PSI.get_formulation(uc_template.devices[:ThermalStandard]) == ThermalBasicUnitCommitment uc_template = template_unit_commitment(; network = DCPPowerModel) @test get_network_formulation(uc_template) == DCPPowerModel @test !isempty(uc_template.branches) @test !isempty(uc_template.services) ed_template = template_economic_dispatch() @test !isempty(ed_template.devices) @test PSI.get_formulation(ed_template.devices[:ThermalStandard]) == ThermalBasicDispatch ed_template = template_economic_dispatch(; network = ACPPowerModel) @test get_network_formulation(ed_template) == ACPPowerModel @test !isempty(ed_template.branches) @test !isempty(ed_template.services) end ================================================ FILE: test/test_recorder_events.jl ================================================ @testset "Show recorder events in EmulationModel" begin template = get_thermal_standard_uc_template() c_sys5_uc_re = PSB.build_system( PSITestSystems, "c_sys5_uc_re"; add_single_time_series = true, force_build = true, ) 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)) == 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) @test !isempty(events) events = list_recorder_events(PSI.InitialConditionUpdateEvent, recorder_log) @test !isempty(events) for wall_time in (true, false) show_recorder_events( devnull, PSI.InitialConditionUpdateEvent, recorder_log; wall_time = wall_time, ) end end ================================================ FILE: test/test_services_constructor.jl ================================================ @testset "Test Reserves from Thermal Dispatch" begin template = get_thermal_dispatch_template_network(CopperPlatePowerModel) set_service_model!( template, ServiceModel(VariableReserve{ReserveUp}, RangeReserve, "Reserve1"), ) set_service_model!( template, ServiceModel(VariableReserve{ReserveUp}, RangeReserve, "Reserve11"), ) set_service_model!( template, ServiceModel(VariableReserve{ReserveDown}, RangeReserve, "Reserve2"), ) set_service_model!( template, ServiceModel(ReserveDemandCurve{ReserveUp}, StepwiseCostReserve, "ORDC1"), ) 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.ModelBuildStatus.BUILT moi_tests(model, 624, 0, 216, 216, 48, false) reserve_variables = [ :ActivePowerReserveVariable__VariableReserve__ReserveUp__Reserve1 :ActivePowerReserveVariable__ReserveDemandCurve__ReserveUp__ORDC1 :ActivePowerReserveVariable__VariableReserve__ReserveDown__Reserve2 :ActivePowerReserveVariable__VariableReserve__ReserveUp__Reserve11 ] found_vars = 0 for (k, var_array) in PSI.get_optimization_container(model).variables if ISOPT.encode_key(k) in reserve_variables for var in var_array @test JuMP.has_lower_bound(var) @test JuMP.lower_bound(var) == 0.0 end found_vars += 1 end end @test found_vars == 4 end @testset "Test Ramp Reserves from Thermal Dispatch" begin template = get_thermal_dispatch_template_network(CopperPlatePowerModel) set_service_model!( template, ServiceModel(VariableReserve{ReserveUp}, RampReserve, "Reserve1"), ) set_service_model!( template, ServiceModel(VariableReserve{ReserveUp}, RampReserve, "Reserve11"), ) set_service_model!( template, ServiceModel(VariableReserve{ReserveDown}, RampReserve, "Reserve2"), ) 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.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 PSI.get_optimization_container(model).variables if ISOPT.encode_key(k) in reserve_variables for var in var_array @test JuMP.has_lower_bound(var) @test JuMP.lower_bound(var) == 0.0 end end end end @testset "Test Reserves from Thermal Standard UC" begin template = get_thermal_standard_uc_template() set_service_model!( template, ServiceModel(VariableReserve{ReserveUp}, RangeReserve, "Reserve1"), ) set_service_model!( template, ServiceModel(VariableReserve{ReserveUp}, RangeReserve, "Reserve11"), ) set_service_model!( template, ServiceModel(VariableReserve{ReserveDown}, RangeReserve, "Reserve2"), ) set_service_model!( template, ServiceModel(ReserveDemandCurve{ReserveUp}, StepwiseCostReserve, "ORDC1"), ) c_sys5_uc = PSB.build_system(PSITestSystems, "c_sys5_uc"; add_reserves = true) model = DecisionModel(template, c_sys5_uc; optimizer = HiGHS_optimizer) @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 template = get_thermal_standard_uc_template() set_device_model!( template, DeviceModel(ThermalMultiStart, ThermalStandardUnitCommitment), ) set_service_model!( template, ServiceModel(VariableReserveNonSpinning, NonSpinningReserve, "NonSpinningReserve"), ) 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.ModelBuildStatus.BUILT moi_tests(model, 1032, 0, 888, 192, 288, true) end @testset "Test Upwards Reserves from Renewable Dispatch" begin template = ProblemTemplate(CopperPlatePowerModel) set_device_model!(template, PowerLoad, StaticPowerLoad) set_device_model!(template, RenewableDispatch, RenewableFullDispatch) set_service_model!( template, ServiceModel(VariableReserve{ReserveUp}, RangeReserve, "Reserve3"), ) set_service_model!( template, ServiceModel(ReserveDemandCurve{ReserveUp}, StepwiseCostReserve, "ORDC1"), ) 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.ModelBuildStatus.BUILT moi_tests(model, 336, 0, 168, 120, 48, false) end @testset "Test Reserves from Hydro" begin template = ProblemTemplate(CopperPlatePowerModel) set_device_model!(template, PowerLoad, StaticPowerLoad) set_device_model!(template, HydroTurbine, HydroTurbineEnergyDispatch) set_device_model!(template, HydroReservoir, HydroEnergyModelReservoir) set_service_model!( template, ServiceModel(VariableReserve{ReserveUp}, RangeReserve, "Reserve5"), ) set_service_model!( template, ServiceModel(VariableReserve{ReserveDown}, RangeReserve, "Reserve6"), ) set_service_model!( template, ServiceModel(ReserveDemandCurve{ReserveUp}, StepwiseCostReserve, "ORDC1"), ) 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.ModelBuildStatus.BUILT moi_tests(model, 312, 0, 120, 96, 72, false) end @testset "Test Reserves from with slack variables" begin template = get_thermal_dispatch_template_network( NetworkModel(CopperPlatePowerModel; use_slacks = true), ) set_service_model!( template, ServiceModel( VariableReserve{ReserveUp}, RangeReserve, "Reserve1"; use_slacks = true, ), ) set_service_model!( template, ServiceModel( VariableReserve{ReserveUp}, RangeReserve, "Reserve11"; use_slacks = true, ), ) set_service_model!( template, ServiceModel( VariableReserve{ReserveDown}, RangeReserve, "Reserve2"; use_slacks = true, ), ) 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.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) template_agc = template_agc_reserve_deployment() 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.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() set_service_model!( template, ServiceModel(VariableReserve{ReserveUp}, RangeReserve, "Reserve1"), ) set_service_model!( template, ServiceModel(VariableReserve{ReserveUp}, RangeReserve, "Reserve11"), ) set_service_model!( template, ServiceModel(VariableReserve{ReserveDown}, RangeReserve, "Reserve2"), ) set_service_model!( template, ServiceModel(ReserveDemandCurve{ReserveUp}, StepwiseCostReserve, "ORDC1"), ) set_service_model!( template, ServiceModel(ConstantReserveGroup{ReserveDown}, GroupReserve, "init"), ) c_sys5_uc = PSB.build_system(PSITestSystems, "c_sys5_uc"; add_reserves = true) services = get_components(Service, c_sys5_uc) contributing_services = Vector{Service}() for service in services if !(typeof(service) <: PSY.ReserveDemandCurve) push!(contributing_services, service) end end groupservice = ConstantReserveGroup{ReserveDown}(; name = "init", available = true, requirement = 0.0, ext = Dict{String, Any}(), ) add_service!(c_sys5_uc, groupservice, contributing_services) model = DecisionModel(template, c_sys5_uc) @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 template = get_thermal_dispatch_template_network() set_service_model!(template, ServiceModel(VariableReserve{ReserveUp}, RangeReserve)) set_service_model!(template, ServiceModel(VariableReserve{ReserveDown}, RangeReserve)) set_service_model!( template, ServiceModel(ReserveDemandCurve{ReserveUp}, StepwiseCostReserve), ) set_service_model!( template, ServiceModel(ConstantReserveGroup{ReserveDown}, GroupReserve), ) c_sys5_uc = PSB.build_system(PSITestSystems, "c_sys5_uc"; add_reserves = true) services = get_components(Service, c_sys5_uc) contributing_services = Vector{Service}() for service in services if !(typeof(service) <: PSY.ReserveDemandCurve) push!(contributing_services, service) end end groupservice = ConstantReserveGroup{ReserveDown}(; name = "init", available = true, requirement = 0.0, ext = Dict{String, Any}(), ) add_service!(c_sys5_uc, groupservice, contributing_services) off_service = VariableReserve{ReserveUp}("Reserveoff", true, 0.6, 10) push!(groupservice.contributing_services, off_service) model = DecisionModel(template, c_sys5_uc) @test build!( model; output_dir = mktempdir(; cleanup = true), console_level = Logging.AboveMaxLevel, ) == PSI.ModelBuildStatus.FAILED end @testset "Test ConstantReserve" begin template = get_thermal_dispatch_template_network() set_service_model!( template, ServiceModel(ConstantReserve{ReserveUp}, RangeReserve, "Reserve3"), ) c_sys5_uc = PSB.build_system(PSITestSystems, "c_sys5_uc") 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.ModelBuildStatus.BUILT @test typeof(model) <: DecisionModel{<:PSI.DecisionProblem} end =# @testset "Test Reserves with Feedforwards" begin template = get_thermal_dispatch_template_network() service_model = ServiceModel(VariableReserve{ReserveUp}, RangeReserve, "Reserve1") ff_lb = LowerBoundFeedforward(; component_type = VariableReserve{ReserveUp}, source = ActivePowerReserveVariable, affected_values = [ActivePowerReserveVariable], meta = "Reserve1", ) PSI.attach_feedforward!(service_model, ff_lb) set_service_model!(template, service_model) 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 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 @testset "Test Reserves with Participation factor limits" begin c_sys5_uc = PSB.build_system(PSITestSystems, "c_sys5_uc"; add_reserves = true) for service in get_components(Reserve, c_sys5_uc) set_max_participation_factor!(service, 0.8) end template = get_thermal_dispatch_template_network(CopperPlatePowerModel) set_service_model!( template, ServiceModel(VariableReserve{ReserveUp}, RangeReserve, "Reserve1"), ) set_service_model!( template, ServiceModel(VariableReserve{ReserveUp}, RangeReserve, "Reserve11"), ) set_service_model!( template, ServiceModel(VariableReserve{ReserveDown}, RangeReserve, "Reserve2"), ) set_service_model!( template, ServiceModel(ReserveDemandCurve{ReserveUp}, StepwiseCostReserve, "ORDC1"), ) model = DecisionModel(template, c_sys5_uc) @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 :ActivePowerReserveVariable__VariableReserve__ReserveDown__Reserve2 :ActivePowerReserveVariable__VariableReserve__ReserveUp__Reserve11 ] found_vars = 0 for (k, var_array) in PSI.get_optimization_container(model).variables if ISOPT.encode_key(k) in reserve_variables for var in var_array @test JuMP.has_lower_bound(var) @test JuMP.lower_bound(var) == 0.0 end found_vars += 1 end end @test found_vars == 4 participation_constraints = [ :ParticipationFractionConstraint__VariableReserve__ReserveUp__Reserve11, :ParticipationFractionConstraint__VariableReserve__ReserveDown__Reserve2, ] found_constraints = 0 for (k, _) in PSI.get_optimization_container(model).constraints if ISOPT.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, 168, 0, 288, 288, 24, false) #= TODO: Implement Interfaces in AC template = get_thermal_dispatch_template_network(ACPPowerModel) 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, 168, 0, 288, 288, 24, false) end @testset "Test Transmission Interface with Feedforwards" 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) c_sys5_uc2 = PSB.build_system(PSITestSystems, "c_sys5_uc"; add_reserves = true) interface2 = TransmissionInterface(; name = "west_east", available = true, active_power_flow_limits = (min = 0.0, max = 400.0), ) interface_lines2 = [ get_component(Line, c_sys5_uc2, "1"), get_component(Line, c_sys5_uc2, "2"), get_component(Line, c_sys5_uc2, "6"), ] add_service!(c_sys5_uc2, interface2, interface_lines2) template = get_thermal_dispatch_template_network(DCPPowerModel) set_service_model!( template, ServiceModel(TransmissionInterface, ConstantMaxInterfaceFlow; use_slacks = true), ) models = SimulationModels(; decision_models = [ DecisionModel(template, c_sys5_uc; optimizer = HiGHS_optimizer, name = "Sys1"), DecisionModel(template, c_sys5_uc2; optimizer = HiGHS_optimizer, name = "Sys2"), ], ) feedforward = Dict( "Sys2" => [ FixValueFeedforward(; component_type = TransmissionInterface, source = PSI.FlowActivePowerVariable, affected_values = [PSI.FlowActivePowerVariable], ), ], ) sequence = SimulationSequence(; models = models, ini_cond_chronology = InterProblemChronology(), feedforwards = feedforward, ) sim = Simulation(; name = "interface-fail", steps = 2, models = models, sequence = sequence, simulation_folder = mktempdir(; cleanup = true), ) @test_throws ArgumentError build!(sim; console_level = Logging.AboveMaxLevel) end @testset "2 Areas AreaBalance With Transmission Interface" 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"; table_format = TableFormat.WIDE, ) # 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"; table_format = TableFormat.WIDE, ) thermal_gen = read_variable( results, "ActivePowerVariable__ThermalStandard"; table_format = TableFormat.WIDE, ) 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 "Test Interfaces on Interchanges with AreaBalance" begin sys_rts_da = build_system(PSISystems, "modified_RTS_GMLC_DA_sys") transform_single_time_series!(sys_rts_da, Hour(24), Hour(1)) interchange1 = AreaInterchange(; name = "interchange1_2", available = true, active_power_flow = 100.0, flow_limits = (from_to = 1.0, to_from = 1.0), from_area = get_component(Area, sys_rts_da, "1"), to_area = get_component(Area, sys_rts_da, "2"), ) interchange2 = AreaInterchange(; name = "interchange1_3", available = true, active_power_flow = 100.0, flow_limits = (from_to = 1.0, to_from = 1.0), from_area = get_component(Area, sys_rts_da, "1"), to_area = get_component(Area, sys_rts_da, "3"), ) interchange3 = AreaInterchange(; name = "interchange3_2", available = true, active_power_flow = 100.0, flow_limits = (from_to = 1.0, to_from = 1.0), from_area = get_component(Area, sys_rts_da, "3"), to_area = get_component(Area, sys_rts_da, "2"), ) add_components!( sys_rts_da, [interchange1, interchange2, interchange3], ) # This interface is limiting all the flows into 1 interface = TransmissionInterface(; name = "interface1_2_3", available = true, active_power_flow_limits = (min = 0.0, max = 1.0), violation_penalty = 1000.0, direction_mapping = Dict("interchange1_2" => 1, "interchange1_3" => -1, ), ) add_service!( sys_rts_da, interface, [interchange1, interchange2], ) template = ProblemTemplate(NetworkModel(AreaBalancePowerModel)) set_device_model!(template, ThermalStandard, ThermalDispatchNoMin) set_device_model!(template, RenewableDispatch, RenewableFullDispatch) set_device_model!(template, PowerLoad, StaticPowerLoad) set_device_model!(template, RenewableNonDispatch, FixedOutput) set_device_model!(template, HydroDispatch, HydroDispatchRunOfRiver) set_device_model!(template, AreaInterchange, StaticBranch) set_service_model!( template, ServiceModel(TransmissionInterface, ConstantMaxInterfaceFlow), ) ps_model = DecisionModel( template, sys_rts_da; resolution = Hour(1), optimizer = HiGHS_optimizer, store_variable_names = true, ) @test build!(ps_model; output_dir = mktempdir(; cleanup = true)) == PSI.ModelBuildStatus.BUILT @test solve!(ps_model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED moi_tests(ps_model, 8568, 0, 2136, 1416, 2664, 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) == (3, 24) interchange_constraints_ub = PSI.get_constraint(opt_container, InterfaceFlowLimit(), TransmissionInterface, "ub") interchange_constraints_lb = PSI.get_constraint(opt_container, InterfaceFlowLimit(), TransmissionInterface, "lb") @test size(interchange_constraints_ub) == (1, 24) @test size(interchange_constraints_lb) == (1, 24) interchange_constraints_ub["interface1_2_3", 1] # Obj Function test is disabled because with this constraints # the model goes into the slacks and the cost is larger than 1e6 # psi_checksolve_test(ps_model, [MOI.OPTIMAL], 482055, 1) results = OptimizationProblemResults(ps_model) interface_results = read_expression( results, "InterfaceTotalFlow__TransmissionInterface"; table_format = TableFormat.WIDE, ) for i in 1:24 @test interface_results[!, "interface1_2_3"][i] <= 100.0 + PSI.ABSOLUTE_TOLERANCE end end @testset "Test Interfaces on Interchanges and Double Circuits with AreaPTDFPowerModel" begin sys_rts_da = build_system(PSISystems, "modified_RTS_GMLC_DA_sys") transform_single_time_series!(sys_rts_da, Hour(24), Hour(1)) interchange1 = AreaInterchange(; name = "interchange1_2", available = true, active_power_flow = 100.0, flow_limits = (from_to = 1.0, to_from = 1.0), from_area = get_component(Area, sys_rts_da, "1"), to_area = get_component(Area, sys_rts_da, "2"), ) interchange2 = AreaInterchange(; name = "interchange1_3", available = true, active_power_flow = 100.0, flow_limits = (from_to = 1.0, to_from = 1.0), from_area = get_component(Area, sys_rts_da, "1"), to_area = get_component(Area, sys_rts_da, "3"), ) interchange3 = AreaInterchange(; name = "interchange3_2", available = true, active_power_flow = 100.0, flow_limits = (from_to = 1.0, to_from = 1.0), from_area = get_component(Area, sys_rts_da, "3"), to_area = get_component(Area, sys_rts_da, "2"), ) add_components!( sys_rts_da, [interchange1, interchange2, interchange3], ) # This interface is limiting all the flows into 1 interface1 = TransmissionInterface(; name = "interface1_2_3", available = true, active_power_flow_limits = (min = 0.0, max = 1.0), violation_penalty = 1000.0, direction_mapping = Dict("interchange1_2" => 1, "interchange1_3" => -1, ), ) add_service!( sys_rts_da, interface1, [interchange1, interchange2], ) #Add an interface on a double circuit: double_circuit_1 = get_component(Line, sys_rts_da, "A33-1") double_circuit_2 = get_component(Line, sys_rts_da, "A33-2") interface2 = TransmissionInterface(; name = "interface_double_circuit", available = true, active_power_flow_limits = (min = 0.0, max = 1.0), violation_penalty = 1000.0, direction_mapping = Dict("A33-1" => 1, "A33-2" => 1), ) add_service!( sys_rts_da, interface2, [double_circuit_1, double_circuit_2], ) template = ProblemTemplate(NetworkModel(AreaPTDFPowerModel; use_slacks = true)) set_device_model!(template, ThermalStandard, ThermalDispatchNoMin) set_device_model!(template, RenewableDispatch, RenewableFullDispatch) set_device_model!(template, PowerLoad, StaticPowerLoad) set_device_model!(template, RenewableNonDispatch, FixedOutput) set_device_model!(template, HydroDispatch, HydroDispatchRunOfRiver) set_device_model!(template, Line, StaticBranchUnbounded) set_device_model!( template, DeviceModel(AreaInterchange, StaticBranchUnbounded; use_slacks = false), ) set_service_model!( template, ServiceModel(TransmissionInterface, ConstantMaxInterfaceFlow), ) ps_model = DecisionModel( template, sys_rts_da; resolution = Hour(1), optimizer = HiGHS_optimizer, store_variable_names = true, optimizer_solve_log_print = true, ) @test build!(ps_model; output_dir = mktempdir(; cleanup = true)) == PSI.ModelBuildStatus.BUILT @test solve!(ps_model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED moi_tests(ps_model, 8712, 0, 2160, 1440, 2664, 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) == (3, 24) interchange_constraints_ub = PSI.get_constraint(opt_container, InterfaceFlowLimit(), TransmissionInterface, "ub") interchange_constraints_lb = PSI.get_constraint(opt_container, InterfaceFlowLimit(), TransmissionInterface, "lb") @test size(interchange_constraints_ub) == (2, 24) @test size(interchange_constraints_lb) == (2, 24) interchange_constraints_ub["interface1_2_3", 1] # Obj Function test is disabled because with this constraints # the model goes into the slacks and the cost is larger than 1e6 # psi_checksolve_test(ps_model, [MOI.OPTIMAL], 482055, 1) results = OptimizationProblemResults(ps_model) interface_results = read_expression( results, "InterfaceTotalFlow__TransmissionInterface"; table_format = TableFormat.WIDE, ) for i in 1:24 @test interface_results[!, "interface1_2_3"][i] <= 100.0 + PSI.ABSOLUTE_TOLERANCE end end @testset "Test bad data for interfaces with reductions" begin sys_rts_da = build_system(PSISystems, "modified_RTS_GMLC_DA_sys") transform_single_time_series!(sys_rts_da, Hour(24), Hour(1)) # Add an interface on a double circuit: double_circuit_1 = get_component(Line, sys_rts_da, "A33-1") double_circuit_2 = get_component(Line, sys_rts_da, "A33-2") interface_double_circuit = TransmissionInterface(; name = "interface_double_circuit", available = true, active_power_flow_limits = (min = 0.0, max = 1.0), violation_penalty = 1000.0, direction_mapping = Dict("A33-1" => 1, "A33-2" => 1), ) add_service!( sys_rts_da, interface_double_circuit, [double_circuit_1, double_circuit_2], ) # Add interface on a series chain: series_chain_1 = get_component(Line, sys_rts_da, "CA-1") series_chain_2 = get_component(Line, sys_rts_da, "C35") interface_series_chain = TransmissionInterface(; name = "interface_series_chain", available = true, active_power_flow_limits = (min = 0.0, max = 1.0), violation_penalty = 1000.0, direction_mapping = Dict("CA-1" => -1, "C35" => -1), ) # Order matters here; must compute the ptdf before adding the service for the lines that # are included in the interface to be reduced (in order to test the bad data checking) ptdf = PTDF(sys_rts_da; network_reductions = NetworkReduction[DegreeTwoReduction()]) add_service!( sys_rts_da, interface_series_chain, [series_chain_1, series_chain_2], ) template = ProblemTemplate( NetworkModel( AreaPTDFPowerModel; PTDF_matrix = ptdf, reduce_degree_two_branches = true, use_slacks = true, ), ) set_device_model!(template, ThermalStandard, ThermalDispatchNoMin) set_device_model!(template, RenewableDispatch, RenewableFullDispatch) set_device_model!(template, PowerLoad, StaticPowerLoad) set_device_model!(template, RenewableNonDispatch, FixedOutput) set_device_model!(template, HydroDispatch, HydroDispatchRunOfRiver) set_device_model!(template, Line, StaticBranchUnbounded) set_device_model!( template, DeviceModel(AreaInterchange, StaticBranchUnbounded; use_slacks = false), ) set_service_model!( template, ServiceModel(TransmissionInterface, ConstantMaxInterfaceFlow), ) ps_model = DecisionModel( template, sys_rts_da; resolution = Hour(1), optimizer = HiGHS_optimizer, store_variable_names = true, optimizer_solve_log_print = true, ) @test build!(ps_model; output_dir = mktempdir(; cleanup = true)) == PSI.ModelBuildStatus.BUILT # Test bad direction data for interface on series chain : set_direction_mapping!(interface_series_chain, Dict("CA-1" => 1, "C35" => -1)) ps_model = DecisionModel( template, sys_rts_da; resolution = Hour(1), optimizer = HiGHS_optimizer, store_variable_names = true, ) @test build!( ps_model; console_level = Logging.AboveMaxLevel, # Ignore expected errors. output_dir = mktempdir(; cleanup = true), ) == PSI.ModelBuildStatus.FAILED # Test bad direction data for interface on double circuit: set_direction_mapping!(interface_series_chain, Dict("CA-1" => 1, "C35" => 1)) set_direction_mapping!(interface_double_circuit, Dict("A33-1" => 1, "A33-2" => -1)) ps_model = DecisionModel( template, sys_rts_da; resolution = Hour(1), optimizer = HiGHS_optimizer, store_variable_names = true, optimizer_solve_log_print = true, ) @test build!( ps_model; console_level = Logging.AboveMaxLevel, # Ignore expected errors. output_dir = mktempdir(; cleanup = true), ) == PSI.ModelBuildStatus.FAILED set_direction_mapping!(interface_double_circuit, Dict("A33-1" => 1, "A33-2" => 1)) # Test only including part of a double circuit in an interface: pop!(get_services(double_circuit_1)) ps_model = DecisionModel( template, sys_rts_da; resolution = Hour(1), optimizer = HiGHS_optimizer, store_variable_names = true, ) @test build!( ps_model; console_level = Logging.AboveMaxLevel, # Ignore expected errors. output_dir = mktempdir(; cleanup = true), ) == PSI.ModelBuildStatus.FAILED # Test only including part of a series chain in an interface: push!(get_services(double_circuit_1), interface_double_circuit) pop!(get_services(series_chain_1)) ps_model = DecisionModel( template, sys_rts_da; resolution = Hour(1), optimizer = HiGHS_optimizer, store_variable_names = true, ) @test build!( ps_model; console_level = Logging.AboveMaxLevel, # Ignore expected errors. output_dir = mktempdir(; cleanup = true), ) == PSI.ModelBuildStatus.FAILED end ================================================ FILE: test/test_simulation_build.jl ================================================ @testset "Simulation Build Tests" begin models = create_simulation_build_test_problems(get_template_basic_uc_simulation()) sequence = SimulationSequence(; models = models, feedforwards = Dict( "ED" => [ SemiContinuousFeedforward(; component_type = ThermalStandard, source = OnVariable, affected_values = [ActivePowerVariable], ), ], ), ini_cond_chronology = InterProblemChronology(), ) sim = Simulation(; name = "test", steps = 1, models = models, sequence = sequence, simulation_folder = mktempdir(; cleanup = true), ) build_out = build!(sim) @test build_out == PSI.SimulationBuildStatus.BUILT for field in fieldnames(SimulationSequence) if fieldtype(SimulationSequence, field) == Union{Dates.DateTime, Nothing} @test getfield(sim.sequence, field) !== nothing end end @test length(findall(x -> x == 2, sequence.execution_order)) == 24 @test length(findall(x -> x == 1, sequence.execution_order)) == 1 state = PSI.get_simulation_state(sim) uc_vars = [OnVariable, StartVariable, StopVariable] ed_vars = [ActivePowerVariable] for (key, data) in state.decision_states.variables if PSI.get_entry_type(key) ∈ uc_vars _, count = size(data.values) @test count == 24 elseif PSI.get_entry_type(key) ∈ ed_vars _, count = size(data.values) @test count == 288 end end end @testset "Simulation with provided initial time" begin models = create_simulation_build_test_problems(get_template_basic_uc_simulation()) sequence = SimulationSequence(; models = models, feedforwards = Dict( "ED" => [ SemiContinuousFeedforward(; component_type = ThermalStandard, source = OnVariable, affected_values = [ActivePowerVariable], ), ], ), ini_cond_chronology = InterProblemChronology(), ) second_day = DateTime("1/1/2024 23:00:00", "d/m/y H:M:S") + Hour(1) sim = Simulation(; name = "test", steps = 1, models = models, sequence = sequence, simulation_folder = mktempdir(; cleanup = true), initial_time = second_day, ) build_out = build!(sim) @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 end for field in fieldnames(SimulationSequence) if fieldtype(SimulationSequence, field) == Union{Dates.DateTime, Nothing} @test getfield(sim.sequence, field) !== nothing end end @test length(findall(x -> x == 2, sequence.execution_order)) == 24 @test length(findall(x -> x == 1, sequence.execution_order)) == 1 end @testset "Negative Tests (Bad Parametrization)" begin models = create_simulation_build_test_problems(get_template_basic_uc_simulation()) sequence = SimulationSequence(; models = models, feedforwards = Dict( "ED" => [ SemiContinuousFeedforward(; component_type = ThermalStandard, source = OnVariable, affected_values = [ActivePowerVariable], ), ], ), ini_cond_chronology = InterProblemChronology(), ) @test_throws UndefKeywordError sim = Simulation(; name = "test", steps = 1) sim = Simulation(; name = "test", steps = 1, models = models, sequence = sequence, simulation_folder = mktempdir(; cleanup = true), initial_time = Dates.now(), ) @test_throws IS.ConflictingInputsError build!( sim, console_level = Logging.AboveMaxLevel, ) sim = Simulation(; name = "fake_path", steps = 1, models = models, sequence = sequence, simulation_folder = "fake_path", ) @test_throws IS.ConflictingInputsError PSI._check_folder(sim) end @testset "Test SemiContinuous Feedforward with Active and Reactive Power variables" begin template_uc = get_template_basic_uc_simulation() set_device_model!(template_uc, Line, StaticBranchUnbounded) set_network_model!(template_uc, NetworkModel(DCPPowerModel; use_slacks = true)) # network slacks added because of data issues template_ed = get_template_nomin_ed_simulation(NetworkModel(ACPPowerModel; use_slacks = true)) set_device_model!(template_ed, Line, StaticBranchUnbounded) c_sys5_hy_uc = PSB.build_system(PSITestSystems, "c_sys5_hy_uc") c_sys5_hy_ed = PSB.build_system(PSITestSystems, "c_sys5_hy_ed") models = SimulationModels(; decision_models = [ DecisionModel( template_uc, c_sys5_hy_uc; name = "UC", optimizer = HiGHS_optimizer, initialize_model = false, ), DecisionModel( template_ed, c_sys5_hy_ed; name = "ED", optimizer = ipopt_optimizer, initialize_model = false, ), ], ) sequence = SimulationSequence(; models = models, feedforwards = Dict( "ED" => [ SemiContinuousFeedforward(; component_type = ThermalStandard, source = OnVariable, affected_values = [ActivePowerVariable, ReactivePowerVariable], ), ], ), ini_cond_chronology = InterProblemChronology(), ) sim = Simulation(; name = "reactive_feedforward", steps = 2, models = models, sequence = sequence, simulation_folder = mktempdir(; cleanup = true), ) build_out = build!(sim) @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), FeedforwardSemiContinuousConstraint(), ThermalStandard, "ActivePowerVariable_ub", ) @test !isempty(c) c = PSI.get_constraint( PSI.get_optimization_container(ac_power_model), FeedforwardSemiContinuousConstraint(), ThermalStandard, "ActivePowerVariable_lb", ) @test !isempty(c) c = PSI.get_constraint( PSI.get_optimization_container(ac_power_model), FeedforwardSemiContinuousConstraint(), ThermalStandard, "ReactivePowerVariable_ub", ) @test !isempty(c) c = PSI.get_constraint( PSI.get_optimization_container(ac_power_model), FeedforwardSemiContinuousConstraint(), ThermalStandard, "ReactivePowerVariable_lb", ) @test !isempty(c) end @testset "Test Upper/Lower Bound Feedforwards" begin template_uc = get_template_basic_uc_simulation() set_network_model!(template_uc, NetworkModel(PTDFPowerModel; use_slacks = true)) set_device_model!(template_uc, DeviceModel(Line, StaticBranchBounds)) template_ed = get_template_nomin_ed_simulation(NetworkModel(PTDFPowerModel; use_slacks = true)) set_device_model!(template_ed, DeviceModel(Line, StaticBranchBounds)) c_sys5_hy_uc = PSB.build_system(PSITestSystems, "c_sys5_hy_uc") c_sys5_hy_ed = PSB.build_system(PSITestSystems, "c_sys5_hy_ed") models = SimulationModels(; decision_models = [ DecisionModel( template_uc, c_sys5_hy_uc; name = "UC", optimizer = HiGHS_optimizer, initialize_model = false, ), DecisionModel( template_ed, c_sys5_hy_ed; name = "ED", optimizer = ipopt_optimizer, initialize_model = false, ), ], ) sequence = SimulationSequence(; models = models, feedforwards = Dict( "ED" => [ SemiContinuousFeedforward(; component_type = ThermalStandard, source = OnVariable, affected_values = [ActivePowerVariable], ), LowerBoundFeedforward(; component_type = Line, source = FlowActivePowerVariable, affected_values = [FlowActivePowerVariable], add_slacks = true, ), UpperBoundFeedforward(; component_type = Line, source = FlowActivePowerVariable, affected_values = [FlowActivePowerVariable], add_slacks = true, ), ], ), ini_cond_chronology = InterProblemChronology(), ) sim = Simulation(; name = "reactive_feedforward", steps = 2, models = models, sequence = sequence, simulation_folder = mktempdir(; cleanup = true), ) build_out = build!(sim) @test build_out == PSI.SimulationBuildStatus.BUILT ed_power_model = PSI.get_simulation_model(PSI.get_models(sim), :ED) c = PSI.get_constraint( PSI.get_optimization_container(ed_power_model), FeedforwardSemiContinuousConstraint(), ThermalStandard, "ActivePowerVariable_ub", ) @test !isempty(c) c = PSI.get_constraint( PSI.get_optimization_container(ed_power_model), FeedforwardSemiContinuousConstraint(), ThermalStandard, "ActivePowerVariable_lb", ) @test !isempty(c) c = PSI.get_constraint( PSI.get_optimization_container(ed_power_model), FeedforwardLowerBoundConstraint(), Line, "FlowActivePowerVariablelb", ) @test !isempty(c) c = PSI.get_constraint( PSI.get_optimization_container(ed_power_model), FeedforwardUpperBoundConstraint(), Line, "FlowActivePowerVariableub", ) @test !isempty(c) c = PSI.get_variable( PSI.get_optimization_container(ed_power_model), UpperBoundFeedForwardSlack(), Line, "FlowActivePowerVariable", ) @test !isempty(c) c = PSI.get_variable( PSI.get_optimization_container(ed_power_model), LowerBoundFeedForwardSlack(), Line, "FlowActivePowerVariable", ) @test !isempty(c) end @testset "Test FixValue Feedforwards" begin template_uc = get_template_basic_uc_simulation() set_network_model!(template_uc, NetworkModel(PTDFPowerModel; use_slacks = true)) set_device_model!(template_uc, DeviceModel(Line, StaticBranchUnbounded)) set_service_model!(template_uc, ServiceModel(VariableReserve{ReserveUp}, RangeReserve)) template_ed = get_template_nomin_ed_simulation(NetworkModel(PTDFPowerModel; use_slacks = true)) set_device_model!(template_ed, DeviceModel(Line, StaticBranchUnbounded)) set_service_model!(template_ed, ServiceModel(VariableReserve{ReserveUp}, RangeReserve)) c_sys5_hy_uc = PSB.build_system(PSITestSystems, "c_sys5_uc"; add_reserves = true) c_sys5_hy_ed = PSB.build_system(PSITestSystems, "c_sys5_ed"; add_reserves = true) models = SimulationModels(; decision_models = [ DecisionModel( template_uc, c_sys5_hy_uc; name = "UC", optimizer = HiGHS_optimizer, initialize_model = false, ), DecisionModel( template_ed, c_sys5_hy_ed; name = "ED", optimizer = ipopt_optimizer, initialize_model = false, ), ], ) sequence = SimulationSequence(; models = models, feedforwards = Dict( "ED" => [ SemiContinuousFeedforward(; component_type = ThermalStandard, source = OnVariable, affected_values = [ActivePowerVariable], ), FixValueFeedforward(; component_type = VariableReserve{ReserveUp}, source = ActivePowerReserveVariable, affected_values = [ActivePowerReserveVariable], ), ], ), ini_cond_chronology = InterProblemChronology(), ) sim = Simulation(; name = "reserve_feedforward", steps = 2, models = models, sequence = sequence, simulation_folder = mktempdir(; cleanup = true), ) build_out = build!(sim) @test build_out == PSI.SimulationBuildStatus.BUILT ed_power_model = PSI.get_simulation_model(PSI.get_models(sim), :ED) c = PSI.get_parameter( PSI.get_optimization_container(ed_power_model), FixValueParameter(), VariableReserve{ReserveUp}, "Reserve1", ) @test !isempty(c.multiplier_array) @test !isempty(c.parameter_array) c = PSI.get_parameter( PSI.get_optimization_container(ed_power_model), FixValueParameter(), VariableReserve{ReserveUp}, "Reserve11", ) @test !isempty(c.multiplier_array) @test !isempty(c.parameter_array) end @testset "Build with store_systems_in_results option" begin models = create_simulation_build_test_problems(get_template_basic_uc_simulation()) sequence = SimulationSequence(; models = models, feedforwards = Dict( "ED" => [ SemiContinuousFeedforward(; component_type = ThermalStandard, source = OnVariable, affected_values = [ActivePowerVariable], ), ], ), ini_cond_chronology = InterProblemChronology(), ) # Test store_systems_in_results = true (default) sim_with = Simulation(; name = "test_with_systems", steps = 1, models = models, sequence = sequence, simulation_folder = mktempdir(; cleanup = true), ) build_out = build!(sim_with; store_systems_in_results = true) @test build_out == PSI.SimulationBuildStatus.BUILT PSI.open_store(PSI.HdfSimulationStore, PSI.get_store_dir(sim_with), "r") do store root = store.file["simulation"] @test haskey(root, "systems") @test length(keys(root["systems"])) > 0 end # Test store_systems_in_results = false models2 = create_simulation_build_test_problems(get_template_basic_uc_simulation()) sequence2 = SimulationSequence(; models = models2, feedforwards = Dict( "ED" => [ SemiContinuousFeedforward(; component_type = ThermalStandard, source = OnVariable, affected_values = [ActivePowerVariable], ), ], ), ini_cond_chronology = InterProblemChronology(), ) sim_without = Simulation(; name = "test_without_systems", steps = 1, models = models2, sequence = sequence2, simulation_folder = mktempdir(; cleanup = true), ) build_out = build!(sim_without; store_systems_in_results = false) @test build_out == PSI.SimulationBuildStatus.BUILT PSI.open_store(PSI.HdfSimulationStore, PSI.get_store_dir(sim_without), "r") do store root = store.file["simulation"] @test !haskey(root, "systems") end end ================================================ FILE: test/test_simulation_execute.jl ================================================ function test_single_stage_sequential(in_memory, rebuild, export_model) tmp_dir = mktempdir(; cleanup = true) template_ed = get_template_nomin_ed_simulation() c_sys = PSB.build_system(PSITestSystems, "c_sys5_uc") models = SimulationModels([ DecisionModel( template_ed, c_sys; name = "ED", optimizer = ipopt_optimizer, rebuild_model = rebuild, export_optimization_model = export_model, ), ]) test_sequence = SimulationSequence(; models = models, ini_cond_chronology = InterProblemChronology(), ) sim_single = Simulation(; name = "consecutive", steps = 2, models = models, sequence = test_sequence, simulation_folder = tmp_dir, ) build_out = build!(sim_single) @test build_out == PSI.SimulationBuildStatus.BUILT execute_out = execute!(sim_single; in_memory = in_memory) @test execute_out == PSI.RunStatus.SUCCESSFULLY_FINALIZED return tmp_dir end @testset "Single stage sequential tests" begin for in_memory in (true, false), rebuild in (true, false) test_single_stage_sequential(in_memory, rebuild, false) end end @testset "Test model export at each solve" begin folder = test_single_stage_sequential(true, false, true) test_path = joinpath(folder, "consecutive", "problems", "ED", "optimization_model_exports") @test ispath(test_path) @test length(readdir(test_path)) == 4 end function test_2_stage_decision_models_with_feedforwards(in_memory) template_uc = get_template_basic_uc_simulation() template_ed = get_template_nomin_ed_simulation() set_device_model!(template_ed, InterruptiblePowerLoad, StaticPowerLoad) set_network_model!( template_uc, NetworkModel( CopperPlatePowerModel; duals = [CopperPlateBalanceConstraint], ), ) set_network_model!( template_ed, NetworkModel( CopperPlatePowerModel; duals = [CopperPlateBalanceConstraint], use_slacks = true, ), ) c_sys5_hy_uc = PSB.build_system(PSITestSystems, "c_sys5_hy_uc") c_sys5_hy_ed = PSB.build_system(PSITestSystems, "c_sys5_hy_ed") models = SimulationModels(; decision_models = [ DecisionModel( template_uc, c_sys5_hy_uc; name = "UC", optimizer = HiGHS_optimizer), DecisionModel( template_ed, c_sys5_hy_ed; name = "ED", optimizer = ipopt_optimizer, ), ], ) sequence = SimulationSequence(; models = models, feedforwards = Dict( "ED" => [ SemiContinuousFeedforward(; component_type = ThermalStandard, source = OnVariable, affected_values = [ActivePowerVariable], ), ], ), ini_cond_chronology = InterProblemChronology(), ) sim = Simulation(; name = "no_cache", steps = 2, models = models, sequence = sequence, simulation_folder = mktempdir(; cleanup = true), ) build_out = build!(sim; console_level = Logging.Error) @test build_out == PSI.SimulationBuildStatus.BUILT execute_out = execute!(sim; in_memory = in_memory) @test execute_out == PSI.RunStatus.SUCCESSFULLY_FINALIZED end @testset "2-Stage Decision Models with FeedForwards" begin for in_memory in (true, false) test_2_stage_decision_models_with_feedforwards(in_memory) end end @testset "Test Simulation Utils" begin template_uc = get_template_basic_uc_simulation() set_device_model!(template_uc, ThermalStandard, ThermalStandardUnitCommitment) set_network_model!( template_uc, NetworkModel( CopperPlatePowerModel; duals = [CopperPlateBalanceConstraint], ), ) template_ed = get_template_nomin_ed_simulation( NetworkModel( CopperPlatePowerModel; # Added because of data issues use_slacks = true, duals = [CopperPlateBalanceConstraint], ), ) set_device_model!(template_ed, InterruptiblePowerLoad, StaticPowerLoad) c_sys5_hy_uc = PSB.build_system(PSITestSystems, "c_sys5_hy_uc") c_sys5_hy_ed = PSB.build_system(PSITestSystems, "c_sys5_hy_ed") models = SimulationModels(; decision_models = [ DecisionModel( template_uc, c_sys5_hy_uc; name = "UC", optimizer = HiGHS_optimizer, ), DecisionModel( template_ed, c_sys5_hy_ed; name = "ED", optimizer = ipopt_optimizer, ), ], ) sequence = SimulationSequence(; models = models, feedforwards = Dict( "ED" => [ SemiContinuousFeedforward(; component_type = ThermalStandard, source = OnVariable, affected_values = [ActivePowerVariable], ), ], ), ini_cond_chronology = InterProblemChronology(), ) sim = Simulation(; name = "aggregation", steps = 2, models = models, sequence = sequence, simulation_folder = mktempdir(; cleanup = false), ) build_out = build!(sim; console_level = Logging.Error) @test build_out == PSI.SimulationBuildStatus.BUILT execute_out = execute!(sim) @test execute_out == PSI.RunStatus.SUCCESSFULLY_FINALIZED @testset "Verify simulation events" begin file = joinpath(PSI.get_simulation_dir(sim), "recorder", "simulation_status.log") @test isfile(file) events = PSI.list_simulation_events( PSI.InitialConditionUpdateEvent, PSI.get_simulation_dir(sim); step = 1, ) @test length(events) == 23 events = PSI.list_simulation_events( PSI.InitialConditionUpdateEvent, PSI.get_simulation_dir(sim); step = 2, ) # This value needs to be checked @test length(events) == 46 PSI.show_simulation_events( devnull, PSI.InitialConditionUpdateEvent, PSI.get_simulation_dir(sim), ; step = 2, ) events = PSI.list_simulation_events( PSI.InitialConditionUpdateEvent, PSI.get_simulation_dir(sim); step = 1, model_name = "UC", ) @test length(events) == 0 events = PSI.list_simulation_events( PSI.InitialConditionUpdateEvent, PSI.get_simulation_dir(sim), ; step = 2, model_name = "UC", ) @test length(events) == 22 PSI.show_simulation_events( devnull, PSI.InitialConditionUpdateEvent, PSI.get_simulation_dir(sim), ; step = 2, model_name = "UC", ) end end @testset "Test simulation with VariableReserve" begin template_uc = get_template_basic_uc_simulation() set_network_model!(template_uc, NetworkModel(PTDFPowerModel; use_slacks = true)) set_device_model!(template_uc, DeviceModel(Line, StaticBranchUnbounded)) set_service_model!(template_uc, ServiceModel(VariableReserve{ReserveUp}, RangeReserve)) template_ed = get_template_nomin_ed_simulation(NetworkModel(PTDFPowerModel; use_slacks = true)) set_device_model!(template_ed, DeviceModel(Line, StaticBranchUnbounded)) set_service_model!(template_ed, ServiceModel(VariableReserve{ReserveUp}, RangeReserve)) c_sys5_hy_uc = PSB.build_system(PSITestSystems, "c_sys5_uc"; add_reserves = true) c_sys5_hy_ed = PSB.build_system(PSITestSystems, "c_sys5_ed"; add_reserves = true) models = SimulationModels(; decision_models = [ DecisionModel( template_uc, c_sys5_hy_uc; name = "UC", optimizer = HiGHS_optimizer, initialize_model = false, ), DecisionModel( template_ed, c_sys5_hy_ed; name = "ED", optimizer = ipopt_optimizer, initialize_model = false, ), ], ) sequence = SimulationSequence(; models = models, feedforwards = Dict( "ED" => [ SemiContinuousFeedforward(; component_type = ThermalStandard, source = OnVariable, affected_values = [ActivePowerVariable], ), FixValueFeedforward(; component_type = VariableReserve{ReserveUp}, source = ActivePowerReserveVariable, affected_values = [ActivePowerReserveVariable], ), ], ), ini_cond_chronology = InterProblemChronology(), ) sim = Simulation(; name = "reserve_feedforward", steps = 2, models = models, sequence = sequence, simulation_folder = mktempdir(; cleanup = true), ) build_out = build!(sim) @test build_out == PSI.SimulationBuildStatus.BUILT execute_out = execute!(sim) @test execute_out == PSI.RunStatus.SUCCESSFULLY_FINALIZED results = SimulationResults(sim) for name in list_decision_problems(results) res = get_decision_problem_results(results, name) parameters = read_realized_parameters(res) @test !isempty(parameters) for (key, df1) in parameters df2 = read_realized_parameter(res, key) @test df1 == df2 end end end 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") sys_rts_ha = deepcopy(sys_rts_rt) PSY.transform_single_time_series!(sys_rts_da, Hour(36), Hour(24)) PSY.transform_single_time_series!(sys_rts_ha, Hour(2), Hour(1)) PSY.transform_single_time_series!(sys_rts_rt, Hour(1), Hour(1)) template_uc = get_template_standard_uc_simulation() set_network_model!(template_uc, NetworkModel(CopperPlatePowerModel)) template_ha = deepcopy(template_uc) # network slacks added because of data issues template_ed = get_thermal_dispatch_template_network( NetworkModel(CopperPlatePowerModel; use_slacks = true), ) models = SimulationModels(; decision_models = [ DecisionModel( template_uc, sys_rts_da; name = "UC", optimizer = HiGHS_optimizer, initialize_model = true, ), DecisionModel( template_ha, sys_rts_ha; name = "HA", optimizer = HiGHS_optimizer, initialize_model = true, ), DecisionModel( template_ed, sys_rts_rt; name = "ED", optimizer = HiGHS_optimizer, initialize_model = true, ), ], ) sequence = SimulationSequence(; models = models, feedforwards = Dict( "ED" => [ SemiContinuousFeedforward(; component_type = ThermalStandard, source = OnVariable, affected_values = [ActivePowerVariable], ), ], ), ini_cond_chronology = InterProblemChronology(), ) sim = Simulation(; name = "3stage_feedforward", steps = 1, models = models, sequence = sequence, simulation_folder = mktempdir(; cleanup = true), ) build_out = build!(sim) @test build_out == PSI.SimulationBuildStatus.BUILT execute_out = execute!(sim; in_memory = in_memory) @test execute_out == PSI.RunStatus.SUCCESSFULLY_FINALIZED end @testset "Test 3 stage simulation with FeedForwards" begin for in_memory in (true, false) test_3_stage_simulation_with_feedforwards(in_memory) end end ================================================ FILE: test/test_simulation_models.jl ================================================ @testset "Test Simulation Models" begin models = SimulationModels( [ DecisionModel( MockOperationProblem; horizon = Hour(48), interval = Hour(24), steps = 2, name = "DAUC", ), DecisionModel( MockOperationProblem; horizon = Hour(24), interval = Hour(1), steps = 2 * 24, name = "HAUC", ), DecisionModel( MockOperationProblem; horizon = Hour(12), interval = Minute(5), steps = 2 * 24 * 12, name = "ED", ), ], EmulationModel(MockEmulationProblem; resolution = Minute(1), name = "AGC"), ) @test length(PSI.get_decision_models(models)) == 3 @test PSI.get_emulation_model(models) !== nothing @test_throws ErrorException SimulationModels( [ DecisionModel( MockOperationProblem; horizon = Hour(48), interval = Hour(24), steps = 2, name = "DAUC", ), DecisionModel( MockOperationProblem; horizon = Hour(24), interval = Hour(1), steps = 2 * 24, name = "DAUC", ), DecisionModel( MockOperationProblem; horizon = Hour(12), interval = Minute(5), steps = 2 * 24 * 12, name = "ED", ), ], EmulationModel(MockEmulationProblem; resolution = Minute(1), name = "AGC"), ) end ================================================ FILE: test/test_simulation_partitions.jl ================================================ @testset "Test partitions and step ranges" begin partitions = SimulationPartitions(2, 1, 0) @test PSI.get_absolute_step_range(partitions, 1) == 1:1 @test PSI.get_valid_step_offset(partitions, 1) == 1 @test PSI.get_valid_step_length(partitions, 1) == 1 @test PSI.get_absolute_step_range(partitions, 2) == 2:2 @test PSI.get_valid_step_offset(partitions, 2) == 1 @test PSI.get_valid_step_length(partitions, 2) == 1 partitions = SimulationPartitions(365, 7, 1) @test get_num_partitions(partitions) == 53 @test PSI.get_absolute_step_range(partitions, 1) == 1:7 @test PSI.get_valid_step_offset(partitions, 1) == 1 @test PSI.get_valid_step_length(partitions, 1) == 7 @test PSI.get_absolute_step_range(partitions, 2) == 7:14 @test PSI.get_valid_step_offset(partitions, 2) == 2 @test PSI.get_valid_step_length(partitions, 2) == 7 @test PSI.get_absolute_step_range(partitions, 52) == 357:364 @test PSI.get_valid_step_offset(partitions, 52) == 2 @test PSI.get_valid_step_length(partitions, 52) == 7 @test PSI.get_absolute_step_range(partitions, 53) == 364:365 @test PSI.get_valid_step_offset(partitions, 53) == 2 @test PSI.get_valid_step_length(partitions, 53) == 1 @test_throws ErrorException PSI.get_absolute_step_range(partitions, -1) @test_throws ErrorException PSI.get_absolute_step_range(partitions, 54) end @testset "Test simulation partitions" begin sim_dir = mktempdir() script = joinpath(BASE_DIR, "test", "run_partitioned_simulation.jl") include(script) partition_name = "partitioned" run_parallel_simulation( build_simulation, execute_simulation; script = script, output_dir = sim_dir, name = partition_name, num_steps = 3, period = 1, num_overlap_steps = 1, # Running multiple processes in CI can kill the VM. num_parallel_processes = haskey(ENV, "CI") ? 1 : 3, exeflags = "--project=test", force = true, ) regular_name = "regular" regular_sim = build_simulation( sim_dir, regular_name; initial_time = DateTime("2024-01-02T00:00:00"), num_steps = 1, HiGHS_optimizer = HiGHS_optimizer, ) @test execute_simulation(regular_sim) == PSI.RunStatus.SUCCESSFULLY_FINALIZED regular_results = SimulationResults(sim_dir, regular_name) partitioned_results = SimulationResults(sim_dir, partition_name) functions = ( read_realized_aux_variables, read_realized_expressions, read_realized_parameters, read_realized_variables, ) key_strings_to_skip = ("Flow", "On", "Off", "Shut", "Start", "Stop") for name in ("ED", "UC") regular_model_results = get_decision_problem_results(regular_results, name) partitioned_model_results = get_decision_problem_results(partitioned_results, name) for func in functions regular = func(regular_model_results; table_format = TableFormat.WIDE) partitioned = func(partitioned_model_results; table_format = TableFormat.WIDE) @test sort(collect(keys(regular))) == sort(collect(keys(partitioned))) for key in keys(regular) t_start = regular[key][1, 1] t_end = regular[key][end, 1] rdf = regular[key] pdf = partitioned[key] pdf = pdf[(pdf.DateTime .>= t_start) .& (pdf.DateTime .<= t_end), :] @test nrow(rdf) == nrow(pdf) @test ncol(rdf) == ncol(pdf) skip = false for key_string_to_skip in key_strings_to_skip if occursin(key_string_to_skip, key) skip = true break end end skip && continue r_sum = 0 p_sum = 0 atol = if ( occursin("ProductionCostExpression", key) || occursin("FuelCostExpression__ThermalStandard", key) ) 11000 else 1e-6 end for i in 2:ncol(rdf) r_sum += sum(rdf[!, i]) p_sum += sum(pdf[!, i]) end if !isapprox(r_sum, p_sum; atol = atol) @error "Mismatch" r_sum p_sum key end @test isapprox(r_sum, p_sum, atol = atol) end end end # TODO: Can emulation model results be validated? end ================================================ FILE: test/test_simulation_results.jl ================================================ using Base: COMPILETIME_PREFERENCES # Read the actual data of a result to see what the timestamps are actual_timestamps(result) = result |> values |> first |> x -> x.data |> keys |> collect # Test that a particular call to _read_results reads from outside the cache; pass through the results macro test_no_cache(expr) :(@test_logs( match_mode = :any, (:debug, r"reading results from data store"), min_level = Logging.Debug, $(esc(expr)))) end @test_no_cache((@debug "reading results from data store"; @debug "msg 2")) # Test that a particular call to _read_results reads from the cache; pass through the results macro test_yes_cache(expr) :(@test_logs( match_mode = :any, (:debug, r"reading results from SimulationsResults cache"), min_level = Logging.Debug, $(esc(expr)))) end @test_yes_cache((@debug "reading results from SimulationsResults cache"; @debug "msg 2")) ED_EXPECTED_VARS = [ "ActivePowerVariable__HydroTurbine", "EnergyVariable__HydroReservoir", "WaterSpillageVariable__HydroReservoir", "HydroEnergySurplusVariable__HydroReservoir", "HydroEnergyShortageVariable__HydroReservoir", "ActivePowerVariable__RenewableDispatch", "ActivePowerVariable__ThermalStandard", "SystemBalanceSlackDown__System", "SystemBalanceSlackUp__System", ] UC_EXPECTED_VARS = [ "ActivePowerVariable__HydroTurbine", "EnergyVariable__HydroReservoir", "WaterSpillageVariable__HydroReservoir", "HydroEnergySurplusVariable__HydroReservoir", "HydroEnergyShortageVariable__HydroReservoir", "ActivePowerVariable__RenewableDispatch", "ActivePowerVariable__ThermalStandard", "OnVariable__ThermalStandard", "StartVariable__ThermalStandard", "StopVariable__ThermalStandard", ] function verify_export_results(results, export_path) exports = SimulationResultsExport( make_export_all(keys(results.decision_problem_results)), results.params, ) export_results(results, exports) for problem_results in values(results.decision_problem_results) rpath = problem_results.results_output_folder problem = problem_results.problem for timestamp in get_timestamps(problem_results) for name in list_dual_names(problem_results) @test compare_results(rpath, export_path, problem, "duals", name, timestamp) end for name in list_parameter_names(problem_results) @test compare_results( rpath, export_path, problem, "parameters", name, timestamp, ) end for name in list_variable_names(problem_results) @test compare_results( rpath, export_path, problem, "variables", name, timestamp, ) end for name in list_aux_variable_names(problem_results) @test compare_results( rpath, export_path, problem, "aux_variables", name, timestamp, ) end end # This file is not currently exported during the simulation. @test isfile( joinpath( problem_results.results_output_folder, problem_results.problem, "optimizer_stats.csv", ), ) end end NATURAL_UNITS_VALUES = [ "ActivePowerVariable__HydroTurbine", "ActivePowerVariable__RenewableDispatch", "ActivePowerVariable__ThermalStandard", "EnergyVariable__HydroReservoir", "ActivePowerTimeSeriesParameter__PowerLoad", "ActivePowerTimeSeriesParameter__HydroTurbine", "ActivePowerTimeSeriesParameter__RenewableDispatch", "ActivePowerTimeSeriesParameter__InterruptiblePowerLoad", "SystemBalanceSlackDown__System", "SystemBalanceSlackUp__System", ] function compare_results(rpath, epath, model, field, name, timestamp) 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) df2 = PSI.read_dataframe(ep) # TODO: Store the tables in the same format. measure_vars = [x for x in names(df2) if x != "DateTime"] df2_long = DataFrames.stack( df2, measure_vars; variable_name = :name, value_name = :value, ) if name ∈ NATURAL_UNITS_VALUES df2_long[!, :value] .*= 100.0 end names1 = names(df1) names2 = names(df2_long) names1 != names2 && return false size(df1) != size(df2_long) && return false if !isapprox(df1.value, df2_long.value) @error "File mismatch" rp ep row1 row2 return false end return true end function make_export_all(problems) return [ OptimizationProblemResultsExport( x; store_all_duals = true, store_all_variables = true, store_all_aux_variables = true, store_all_parameters = true, ) for x in problems ] end function test_simulation_results( file_path::String, export_path; in_memory = false, ) @testset "Test simulation results in_memory = $in_memory" begin c_sys5_hy_uc = PSB.build_system(PSITestSystems, "c_sys5_hy_uc") c_sys5_hy_ed = PSB.build_system(PSITestSystems, "c_sys5_hy_ed") sim = run_simulation( c_sys5_hy_uc, c_sys5_hy_ed, file_path, export_path; in_memory = in_memory, ) 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") @test !isempty(results_ed) @test !isempty(results) empty!(results) @test isempty(results_ed) @test isempty(results) verify_export_results(results, export_path) exported = readdir(export_realized_results(results_ed)) @test length(exported) >= 22 @test any(contains.(exported, "ProductionCostExpression")) @test any(contains.(exported, "FuelCostExpression")) @test any(contains.(exported, "StartUpCostExpression")) # Test that you can't read a failed simulation. PSI.set_simulation_status!(sim, PSI.RunStatus.FAILED) PSI.serialize_status(sim) @test PSI.deserialize_status(sim) == PSI.RunStatus.FAILED @test_throws ErrorException SimulationResults(sim) @test_logs( match_mode = :any, (:warn, r"Results may not be valid"), SimulationResults(sim, ignore_status = true), ) if in_memory @test !isempty( sim.internal.store.dm_data[:ED].variables[PSI.VariableKey( ActivePowerVariable, ThermalStandard, )], ) @test !isempty(sim.internal.store.dm_data[:ED].optimizer_stats) empty!(sim.internal.store) @test isempty(sim.internal.store.dm_data[:ED].variables) @test isempty(sim.internal.store.dm_data[:ED].optimizer_stats) end end end function test_decision_problem_results_values( results_ed, results_uc, c_sys5_hy_ed, c_sys5_hy_uc, ) @test IS.get_uuid(get_system(results_uc)) === IS.get_uuid(c_sys5_hy_uc) @test IS.get_uuid(get_system(results_ed)) === IS.get_uuid(c_sys5_hy_ed) # Temporarily mark some stuff unavailable unav_uc = first(PSY.get_available_components(ThermalStandard, get_system(results_uc))) PSY.set_available!(unav_uc, false) unav_ed = first(PSY.get_available_components(ThermalStandard, get_system(results_ed))) PSY.set_available!(unav_ed, false) sel = PSY.make_selector(ThermalStandard; groupby = :each) @test collect(get_components(ThermalStandard, results_uc)) == collect(get_available_components(ThermalStandard, get_system(results_uc))) @test collect(get_components(ThermalStandard, results_ed)) == collect(get_available_components(ThermalStandard, get_system(results_ed))) @test collect(get_groups(sel, results_uc)) == collect(get_available_groups(sel, get_system(results_uc))) @test collect(get_groups(sel, results_ed)) == collect(get_available_groups(sel, get_system(results_ed))) PSY.set_available!(unav_uc, true) PSY.set_available!(unav_ed, true) @test isempty(setdiff(UC_EXPECTED_VARS, list_variable_names(results_uc))) @test isempty(setdiff(ED_EXPECTED_VARS, list_variable_names(results_ed))) p_thermal_standard_ed = read_variable(results_ed, ActivePowerVariable, ThermalStandard) @test length(keys(p_thermal_standard_ed)) == 48 for v in values(p_thermal_standard_ed) @test length(unique(v.DateTime)) == 12 @test length(unique(v.name)) == 5 end p_thermal_standard_ed_wide = read_variable( results_ed, ActivePowerVariable, ThermalStandard; table_format = TableFormat.WIDE, ) @test length(keys(p_thermal_standard_ed)) == 48 for v in values(p_thermal_standard_ed_wide) @test size(v) == (12, 6) end ren_dispatch_params = read_parameter(results_ed, ActivePowerTimeSeriesParameter, RenewableDispatch) @test length(keys(ren_dispatch_params)) == 48 for v in values(ren_dispatch_params) @test length(unique(v.DateTime)) == 12 @test length(unique(v.name)) == 3 end network_duals = read_dual(results_ed, CopperPlateBalanceConstraint, PSY.System) @test length(keys(network_duals)) == 48 for v in values(network_duals) @test length(unique(v.DateTime)) == 12 @test length(unique(v.name)) == 1 end expression = read_expression(results_ed, PSI.ProductionCostExpression, ThermalStandard) @test length(keys(expression)) == 48 for v in values(expression) @test length(unique(v.DateTime)) == 12 @test length(unique(v.name)) == 5 end for var_key in ((ActivePowerVariable, RenewableDispatch), (ActivePowerVariable, ThermalStandard)) variable_by_initial_time = read_variable(results_uc, var_key...) for df in values(variable_by_initial_time) @test length(unique(df.DateTime)) == 24 end end realized_variable_uc = read_realized_variables(results_uc) @test length(keys(realized_variable_uc)) == length(UC_EXPECTED_VARS) for var in values(realized_variable_uc) @test length(unique(var.DateTime)) == 48 end compare_long_and_wide_realized_results_2d(read_realized_variables, results_uc) realized_variable_uc = read_realized_variables(results_uc, [(ActivePowerVariable, ThermalStandard)]) @test realized_variable_uc == read_realized_variables(results_uc, ["ActivePowerVariable__ThermalStandard"]) @test length(keys(realized_variable_uc)) == 1 for var in values(realized_variable_uc) @test length(unique(var.DateTime)) == 48 end # Test custom indexing. realized_variable_uc2 = read_realized_variables( results_uc, [(ActivePowerVariable, ThermalStandard)]; start_time = Dates.DateTime("2024-01-01T01:00:00"), len = 47, ) @test unique(realized_variable_uc["ActivePowerVariable__ThermalStandard"].DateTime)[2:end] == unique(realized_variable_uc2["ActivePowerVariable__ThermalStandard"].DateTime) @test realized_variable_uc2["ActivePowerVariable__ThermalStandard"] == @rsubset( realized_variable_uc["ActivePowerVariable__ThermalStandard"], :DateTime > Dates.DateTime("2024-01-01T00:00:00") ) compare_long_and_wide_realized_results_2d( read_realized_variables, results_uc, [(ActivePowerVariable, ThermalStandard)]; start_time = Dates.DateTime("2024-01-01T01:00:00"), len = 47, ) compare_long_and_wide_realized_results_2d( read_realized_variables, results_uc, [(ActivePowerVariable, ThermalStandard)]; start_time = Dates.DateTime("2024-01-02T12:00:00"), len = 10, ) realized_param_uc = read_realized_parameters(results_uc) @test length(keys(realized_param_uc)) == 3 for param in values(realized_param_uc) @test length(unique(param.DateTime)) == 48 end compare_long_and_wide_realized_results_2d(read_realized_parameters, results_uc) realized_param_uc = read_realized_parameters( results_uc, [(ActivePowerTimeSeriesParameter, RenewableDispatch)], ) compare_long_and_wide_realized_results_2d( read_realized_parameters, results_uc, [(ActivePowerTimeSeriesParameter, RenewableDispatch)], ) @test realized_param_uc == read_realized_parameters( results_uc, ["ActivePowerTimeSeriesParameter__RenewableDispatch"], ) @test length(keys(realized_param_uc)) == 1 for param in values(realized_param_uc) @test length(unique(param.DateTime)) == 48 end realized_duals_ed = read_realized_duals(results_ed) @test length(keys(realized_duals_ed)) == 1 for param in values(realized_duals_ed) @test size(param)[1] == 576 end realized_duals_ed = read_realized_duals(results_ed, [(CopperPlateBalanceConstraint, System)]) @test realized_duals_ed == read_realized_duals(results_ed, ["CopperPlateBalanceConstraint__System"]) @test length(keys(realized_duals_ed)) == 1 for param in values(realized_duals_ed) @test size(param)[1] == 576 end realized_duals_uc = read_realized_duals(results_uc, [(CopperPlateBalanceConstraint, System)]) @test length(keys(realized_duals_uc)) == 1 for param in values(realized_duals_uc) @test size(param)[1] == 48 end realized_expressions = read_realized_expressions( results_uc, [(PSI.ProductionCostExpression, RenewableDispatch)], ) @test realized_expressions == read_realized_expressions( results_uc, ["ProductionCostExpression__RenewableDispatch"], ) @test length(keys(realized_expressions)) == 1 for exp in values(realized_expressions) @test length(unique(exp.DateTime)) == 48 @test length(unique(exp.name)) == 3 end #request non sync data @test_logs( (:error, r"Requested time does not match available results"), match_mode = :any, @test_throws IS.InvalidValue read_realized_variables( results_ed, [(ActivePowerVariable, ThermalStandard)]; start_time = DateTime("2024-01-01T02:12:00"), len = 3, ) ) # request good window @test length( unique( read_realized_variables( results_ed, [(ActivePowerVariable, ThermalStandard)]; start_time = DateTime("2024-01-02T23:10:00"), len = 10, )["ActivePowerVariable__ThermalStandard"].DateTime), ) == 10 # request bad window @test_logs( (:error, r"Requested time does not match available results"), (@test_throws IS.InvalidValue read_realized_variables( results_ed, [(ActivePowerVariable, ThermalStandard)]; start_time = DateTime("2024-01-02T23:10:00"), len = 11, )) ) # request bad window @test_logs( (:error, r"Requested time does not match available results"), (@test_throws IS.InvalidValue read_realized_variables( results_ed, [(ActivePowerVariable, ThermalStandard)]; start_time = DateTime("2024-01-02T23:10:00"), len = 12, )) ) load_results!( results_ed, 3; initial_time = DateTime("2024-01-01T00:00:00"), variables = [(ActivePowerVariable, ThermalStandard)], ) @test !isempty( PSI.get_cached_variables(results_ed)[PSI.VariableKey( ActivePowerVariable, ThermalStandard, )].data, ) @test length( PSI.get_cached_variables(results_ed)[PSI.VariableKey( ActivePowerVariable, ThermalStandard, )].data, ) == 3 @test length(results_ed) == 3 @test_throws(ErrorException, read_parameter(results_ed, "invalid")) @test_throws(ErrorException, read_variable(results_ed, "invalid")) @test_logs( (:error, r"not stored"), @test_throws( IS.InvalidValue, read_variable( results_uc, ActivePowerVariable, ThermalStandard; initial_time = now(), ) ) ) @test_logs( (:error, r"not stored"), @test_throws( IS.InvalidValue, read_variable(results_uc, ActivePowerVariable, ThermalStandard; count = 25) ) ) empty!(results_ed) @test !haskey( PSI.get_cached_variables(results_ed), PSI.VariableKey(ActivePowerVariable, ThermalStandard), ) initial_time = DateTime("2024-01-01T00:00:00") load_results!( results_ed, 3; initial_time = initial_time, variables = [(ActivePowerVariable, ThermalStandard)], duals = [(CopperPlateBalanceConstraint, System)], parameters = [(ActivePowerTimeSeriesParameter, RenewableDispatch)], ) @test !isempty( PSI.get_cached_variables(results_ed)[PSI.VariableKey( ActivePowerVariable, ThermalStandard, )].data, ) @test !isempty( PSI.get_cached_duals(results_ed)[PSI.ConstraintKey( CopperPlateBalanceConstraint, System, )].data, ) @test !isempty( PSI.get_cached_parameters(results_ed)[PSI.ParameterKey{ ActivePowerTimeSeriesParameter, RenewableDispatch, }( "", )].data, ) # Inspired by https://github.com/Sienna-Platform/PowerSimulations.jl/issues/1072 @testset "Test cache behavior" begin myres = deepcopy(results_ed) initial_time = DateTime("2024-01-01T00:00:00") timestamps = PSI._process_timestamps(myres, initial_time, 3) variable_tuple = (ActivePowerVariable, ThermalStandard) variable_key = PSI.VariableKey(variable_tuple...) empty!(myres) @test isempty(PSI.get_cached_variables(myres)) # With nothing cached, all reads should be from outside the cache read = @test_no_cache PSI._read_results(myres, [variable_key], timestamps, nothing) @test actual_timestamps(read) == timestamps # With 2 result windows cached, reading 2 windows should come from cache and reading 3 should come from outside load_results!(myres, 2; initial_time = initial_time, variables = [variable_tuple]) @test haskey(PSI.get_cached_variables(myres), variable_key) read = @test_yes_cache PSI._read_results( myres, [variable_key], timestamps[1:2], nothing, ) @test actual_timestamps(read) == timestamps[1:2] read = @test_no_cache PSI._read_results(myres, [variable_key], timestamps, nothing) @test actual_timestamps(read) == timestamps # With 3 result windows cached, reading 2 and 3 windows should both come from cache load_results!(myres, 3; initial_time = initial_time, variables = [variable_tuple]) read = @test_yes_cache PSI._read_results( myres, [variable_key], timestamps[1:2], nothing, ) @test actual_timestamps(read) == timestamps[1:2] read = @test_yes_cache PSI._read_results(myres, [variable_key], timestamps, nothing) @test actual_timestamps(read) == timestamps # Caching an additional variable should incur an additional read but not evict the old variable @test_no_cache load_results!( myres, 3; initial_time = initial_time, variables = [(ActivePowerVariable, RenewableDispatch)], ) @test haskey(PSI.get_cached_variables(myres), variable_key) @test haskey( PSI.get_cached_variables(myres), PSI.VariableKey(ActivePowerVariable, RenewableDispatch), ) # Reset back down to 2 windows empty!(myres) @test_no_cache load_results!( myres, 2; initial_time = initial_time, variables = [variable_tuple], ) # Loading a subset of what has already been loaded should not incur additional reads from outside the cache @test_yes_cache load_results!( myres, 2; initial_time = initial_time, variables = [variable_tuple], ) @test_yes_cache load_results!( myres, 1; initial_time = initial_time, variables = [variable_tuple], ) # But loading a superset should @test_no_cache load_results!( myres, 3; initial_time = initial_time, variables = [variable_tuple], ) empty!(myres) # With windows 2-3 cached, reading 2-3 and 3-3 should be from cache, reading 1-2 should be from outside cache @test_no_cache load_results!( myres, 2; initial_time = timestamps[2], variables = [variable_tuple], ) read = @test_yes_cache PSI._read_results( myres, [variable_key], timestamps[2:3], nothing, ) @test actual_timestamps(read) == timestamps[2:3] read = @test_yes_cache PSI._read_results( myres, [variable_key], timestamps[3:3], nothing, ) @test actual_timestamps(read) == timestamps[3:3] read = @test_no_cache PSI._read_results( myres, [variable_key], timestamps[1:2], nothing, ) @test actual_timestamps(read) == timestamps[1:2] 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; table_format = TableFormat.WIDE) @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"], table_format = TableFormat.WIDE, ) @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 compare_long_and_wide_realized_results_2d( PSI.read_results_with_keys, myres, result_keys; cols = ["Park City", "Brighton"], ) res3_df = PSI.read_results_with_keys( myres, result_keys; start_time = timestamps[2], table_format = TableFormat.WIDE, )[first( result_keys, )] @test res3_df[1, "DateTime"] == timestamps[2] res4_df = PSI.read_results_with_keys( myres, result_keys; len = 2, table_format = TableFormat.WIDE, )[first(result_keys)] @test size(res4_df) == (2, 6) end end function test_decision_problem_results( results::SimulationResults, c_sys5_hy_ed, c_sys5_hy_uc, in_memory, ) @test list_decision_problems(results) == ["ED", "UC"] results_uc = get_decision_problem_results(results, "UC") results_ed = get_decision_problem_results(results, "ED") test_decision_problem_results_values(results_ed, results_uc, c_sys5_hy_ed, c_sys5_hy_uc) if !in_memory test_simulation_results_from_file(dirname(results.path), c_sys5_hy_ed, c_sys5_hy_uc) end end function test_emulation_problem_results(results::SimulationResults, in_memory) results_em = get_emulation_problem_results(results) read_realized_aux_variables(results_em) duals_keys = collect(keys(read_realized_duals(results_em))) @test length(duals_keys) == 1 @test duals_keys[1] == "CopperPlateBalanceConstraint__System" duals_inputs = (["CopperPlateBalanceConstraint__System"], [(CopperPlateBalanceConstraint, System)]) for input in duals_inputs duals_value = first(values(read_realized_duals(results_em, input))) @test duals_value isa DataFrames.DataFrame @test length(unique(duals_value.DateTime)) == 576 end expressions_keys = collect(keys(read_realized_expressions(results_em))) @test length(expressions_keys) == 12 expressions_inputs = ( [ # "ProductionCostExpression__HydroEnergyReservoir", "ProductionCostExpression__ThermalStandard", ], [ # (ProductionCostExpression, HydroEnergyReservoir), (ProductionCostExpression, ThermalStandard), ], ) for input in expressions_inputs expressions_value = first(values(read_realized_expressions(results_em, input))) @test expressions_value isa DataFrames.DataFrame @test length(unique(expressions_value.DateTime)) == 576 end @test length( unique( read_realized_expression( results_em, "ProductionCostExpression__ThermalStandard", ).DateTime, ), ) == 576 @test length( unique( read_realized_expression( results_em, ProductionCostExpression, ThermalStandard; len = 10, ).DateTime), ) == 10 parameters_keys = collect(keys(read_realized_parameters(results_em))) @test length(parameters_keys) == 5 parameters_inputs = ( [ "ActivePowerTimeSeriesParameter__PowerLoad", "ActivePowerTimeSeriesParameter__RenewableDispatch", ], [ (ActivePowerTimeSeriesParameter, PowerLoad), (ActivePowerTimeSeriesParameter, RenewableDispatch), ], ) for input in parameters_inputs parameters_value = first(values(read_realized_parameters(results_em, input))) @test parameters_value isa DataFrames.DataFrame @test length(unique(parameters_value.DateTime)) == 576 end @test length( unique( read_realized_parameter( results_em, "ActivePowerTimeSeriesParameter__RenewableDispatch"; len = 10, ).DateTime), ) == 10 expected_vars = union(Set(ED_EXPECTED_VARS), UC_EXPECTED_VARS) @test isempty(setdiff(list_variable_names(results_em), expected_vars)) all_vars = Set(keys(read_realized_variables(results_em))) @test isempty(setdiff(all_vars, expected_vars)) variables_inputs = ( ["ActivePowerVariable__ThermalStandard", "ActivePowerVariable__RenewableDispatch"], [(ActivePowerVariable, ThermalStandard), (ActivePowerVariable, RenewableDispatch)], ) for input in variables_inputs vars = read_realized_variables(results_em, input; table_format = TableFormat.WIDE) var_keys = collect(keys(vars)) @test length(var_keys) == 2 @test first(var_keys) == "ActivePowerVariable__ThermalStandard" @test last(var_keys) == "ActivePowerVariable__RenewableDispatch" for val in values(vars) @test val isa DataFrames.DataFrame @test DataFrames.nrow(val) == 576 end end start_time = first(results_em.timestamps) + Dates.Hour(1) len = 12 @test DataFrames.nrow( read_realized_variable( results_em, ActivePowerVariable, ThermalStandard; start_time = start_time, len = len, table_format = TableFormat.WIDE, ), ) == len vars = read_realized_variables( results_em, variables_inputs[1]; start_time = start_time, len = len, table_format = TableFormat.WIDE, ) df = first(values(vars)) @test length(unique(df.DateTime)) == len @test df[!, "DateTime"][1] == start_time @test_throws IS.InvalidValue read_realized_variables( results_em, variables_inputs[1], start_time = start_time, len = 100000, table_format = TableFormat.WIDE, ) @test_throws IS.InvalidValue read_realized_variables( results_em, variables_inputs[1], start_time = start_time + Dates.Second(1), table_format = TableFormat.WIDE, ) @test_throws IS.InvalidValue read_realized_variables( results_em, variables_inputs[1], start_time = start_time - Dates.Hour(1000), table_format = TableFormat.WIDE, ) @test_throws IS.InvalidValue read_realized_variables( results_em, variables_inputs[1], len = 100000, table_format = TableFormat.WIDE, ) @test isempty(results_em) load_results!( results_em; duals = duals_inputs[2], expressions = expressions_inputs[2], parameters = parameters_inputs[2], variables = variables_inputs[2], ) @test !isempty(results_em) @test length(results_em) == length(duals_inputs[2]) + length(expressions_inputs[2]) + length(parameters_inputs[2]) + length(variables_inputs[2]) # Test that table_format is applied when reading from cache (regression test for GH issue) vars_wide_from_cache = read_realized_variables( results_em, variables_inputs[2]; table_format = TableFormat.WIDE, ) for val in values(vars_wide_from_cache) @test val isa DataFrames.DataFrame @test DataFrames.nrow(val) == 576 @test :DateTime in propertynames(val) @test :name ∉ propertynames(val) @test :value ∉ propertynames(val) @test DataFrames.ncol(val) > 1 # DateTime + at least one component column end empty!(results_em) @test isempty(results_em) export_path = mktempdir(; cleanup = true) export_realized_results(results_em, export_path) var_name = "ActivePowerVariable__ThermalStandard" df = read_realized_variable(results_em, var_name) export_active_power_file = joinpath(export_path, "$(var_name).csv") export_df = PSI.read_dataframe(export_active_power_file) # TODO: results A bug in the code produces NaN after index 48. @test isapprox(df[48, :], export_df[48, :]) end function test_simulation_results_from_file(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") @test !isnothing(get_system(results_uc)) @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") @test !isnothing(get_system(results_uc)) @test !isnothing(get_system(results_ed)) results_ed = get_decision_problem_results(results, "ED"; populate_system = true) @test !isnothing(get_system(results_ed)) @test PSY.get_units_base(get_system(results_ed)) == "NATURAL_UNITS" @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) results_ed = get_decision_problem_results( results, "ED"; populate_system = true, populate_units = IS.UnitSystem.DEVICE_BASE, ) @test !isnothing(PSI.get_system(results_ed)) @test PSY.get_units_base(get_system(results_ed)) == "DEVICE_BASE" @test_throws ArgumentError get_decision_problem_results( results, "ED"; populate_system = false, populate_units = IS.UnitSystem.DEVICE_BASE, ) test_decision_problem_results_values(results_ed, results_uc, c_sys5_hy_ed, c_sys5_hy_uc) end function compare_long_and_wide_realized_results_2d(func, args...; kwargs...) long_results = func(args...; table_format = TableFormat.LONG, kwargs...) wide_results = func(args...; table_format = TableFormat.WIDE, kwargs...) @test sort!(collect(keys(long_results))) == sort!(collect(keys(wide_results))) for (key, long_value) in long_results wide_value = wide_results[key] measure_vars = [x for x in names(wide_value) if x != "DateTime"] wide_converted = DataFrames.stack( wide_value, measure_vars; variable_name = :name, value_name = :value, ) @test @orderby(wide_converted, :DateTime, :name) == @orderby(long_value, :DateTime, :name) end end @testset "Test simulation results" begin for in_memory in (false, true) file_path = mktempdir(; cleanup = true) export_path = mktempdir(; cleanup = true) test_simulation_results(file_path, export_path; in_memory = in_memory) end end @testset "Test simulation results with system from store" begin file_path = mktempdir(; cleanup = true) export_path = mktempdir(; cleanup = true) c_sys5_hy_uc = PSB.build_system(PSITestSystems, "c_sys5_hy_uc") c_sys5_hy_ed = PSB.build_system(PSITestSystems, "c_sys5_hy_ed") in_memory = false sim = run_simulation( c_sys5_hy_uc, c_sys5_hy_ed, file_path, export_path; in_memory = in_memory, ) results = SimulationResults(PSI.get_simulation_folder(sim)) uc = get_decision_problem_results(results, "UC") ed = get_decision_problem_results(results, "ED") sys_uc = get_system!(uc) sys_ed = get_system!(ed) test_decision_problem_results(results, sys_ed, sys_uc, in_memory) test_emulation_problem_results(results, in_memory) end function read_result_names(results, key::PSI.OptimizationContainerKey) result_data = PSI.read_results_with_keys( results, [key]; table_format = TableFormat.LONG, ) first_result = only(values(result_data)) columns_without_datetime = first_result[!, Not(:DateTime)] return Set(names(columns_without_datetime)) end @testset "Test system is automatically populated from HDF5 store on file deserialization" begin file_path = mktempdir(; cleanup = true) export_path = mktempdir(; cleanup = true) c_sys5_hy_uc = PSB.build_system(PSITestSystems, "c_sys5_hy_uc") c_sys5_hy_ed = PSB.build_system(PSITestSystems, "c_sys5_hy_ed") sim = run_simulation( c_sys5_hy_uc, c_sys5_hy_ed, file_path, export_path; in_memory = false, ) results = SimulationResults(PSI.get_simulation_folder(sim)) results_uc = get_decision_problem_results(results, "UC") results_ed = get_decision_problem_results(results, "ED") results_em = get_emulation_problem_results(results) sys_uc = get_system(results_uc) sys_ed = get_system(results_ed) sys_em = get_system(results_em) @test !isnothing(sys_uc) @test !isnothing(sys_ed) @test !isnothing(sys_em) @test IS.get_uuid(sys_uc) == IS.get_uuid(c_sys5_hy_uc) @test IS.get_uuid(sys_ed) == IS.get_uuid(c_sys5_hy_ed) # Note: the system is serialized as a JSON string into the HDF5 store and does not include time series data ts_counts = PSY.get_time_series_counts(sys_uc) @test ts_counts.forecast_count == 0 end @testset "Test system is automatically populated from HDF5 store on file deserialization" begin file_path = mktempdir(; cleanup = true) export_path = mktempdir(; cleanup = true) c_sys5_hy_uc = PSB.build_system(PSITestSystems, "c_sys5_hy_uc") c_sys5_hy_ed = PSB.build_system(PSITestSystems, "c_sys5_hy_ed") sim = run_simulation( c_sys5_hy_uc, c_sys5_hy_ed, file_path, export_path; in_memory = false, store_systems_in_results = false, ) results = SimulationResults(PSI.get_simulation_folder(sim)) @test results isa SimulationResults end ================================================ FILE: test/test_simulation_results_export.jl ================================================ import PowerSimulations: SimulationStoreParams, ModelStoreParams, get_problem_exports, should_export_dual, should_export_parameter, should_export_variable, ISOPT.OptimizationContainerMetadata function _make_params() sim = Dict( "initial_time" => Dates.DateTime("2020-01-01T00:00:00"), "step_resolution" => Dates.Hour(24), "num_steps" => 2, ) problem_defs = OrderedDict( :ED => Dict( "execution_count" => 24, "horizon" => Dates.Hour(12), "interval" => Dates.Hour(1), "resolution" => Dates.Hour(1), "base_power" => 100.0, "system_uuid" => Base.UUID("4076af6c-e467-56ae-b986-b466b2749572"), ), :UC => Dict( "execution_count" => 1, "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 = ISOPT.OptimizationContainerMetadata( Dict( "ActivePowerVariable__ThermalStandard" => PSI.VariableKey(ActivePowerVariable, ThermalStandard), #"EnergyVariable__HydroEnergyReservoir" => # PSI.VariableKey(EnergyVariable, HydroEnergyReservoir), "OnVariable__ThermalStandard" => PSI.VariableKey(OnVariable, ThermalStandard), ), ) problems = OrderedDict{Symbol, ModelStoreParams}() for problem in keys(problem_defs) problem_params = ModelStoreParams( problem_defs[problem]["execution_count"], 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, ) problems[problem] = problem_params end return SimulationStoreParams( sim["initial_time"], sim["step_resolution"], sim["num_steps"], problems, # Emulation Problem Params. Export not implemented yet OrderedDict{Symbol, ModelStoreParams}(), ) end @testset "Test export from JSON" begin params = _make_params() exports = SimulationResultsExport(joinpath(DATA_DIR, "results_export.json"), params) valid = Dates.DateTime("2020-01-01T06:00:00") valid2 = Dates.DateTime("2020-01-02T23:00:00") invalid = Dates.DateTime("2020-01-01T02:00:00") invalid2 = Dates.DateTime("2020-01-03T00:00:00") @test should_export_variable( exports, valid, :ED, PSI.VariableKey(ActivePowerVariable, ThermalStandard), ) @test should_export_variable( exports, valid2, :ED, PSI.VariableKey(ActivePowerVariable, ThermalStandard), ) @test !should_export_variable( exports, invalid, :ED, PSI.VariableKey(ActivePowerVariable, ThermalStandard), ) @test !should_export_variable( exports, invalid2, :ED, PSI.VariableKey(ActivePowerVariable, ThermalStandard), ) @test !should_export_variable( exports, valid, :ED, PSI.VariableKey(ActivePowerVariable, RenewableNonDispatch), ) @test should_export_parameter( exports, valid, :ED, PSI.ParameterKey(ActivePowerTimeSeriesParameter, ThermalStandard), ) @test !should_export_dual( exports, valid, :ED, PSI.ConstraintKey(ActivePowerVariableLimitsConstraint, RenewableNonDispatch), ) @test should_export_variable( exports, valid, :UC, PSI.VariableKey(OnVariable, ThermalStandard), ) @test !should_export_variable( exports, valid, :UC, PSI.VariableKey(ActivePowerVariable, RenewableNonDispatch), ) @test should_export_parameter( exports, valid, :UC, PSI.ParameterKey(ActivePowerTimeSeriesParameter, ThermalStandard), ) @test should_export_dual( exports, valid, :UC, PSI.ConstraintKey(ActivePowerVariableLimitsConstraint, RenewableNonDispatch), ) @test exports.path == "export_path" @test exports.format == "csv" @test "csv" in list_supported_formats(SimulationResultsExport) end @testset "Invalid exports" begin params = _make_params() valid = Dates.DateTime("2020-01-01T00:00:00") invalid = Dates.DateTime("2020-01-03T00:00:00") # Invalid start_time @test_throws IS.InvalidValue SimulationResultsExport( Dict("start_time" => invalid, "models" => [Dict("name" => "ED")]), params, ) # Invalid end_time @test_throws IS.InvalidValue SimulationResultsExport( Dict("end_time" => invalid, "models" => [Dict("name" => "ED")]), params, ) # Invalid format @test_throws IS.InvalidValue SimulationResultsExport( Dict("format" => "invalid", "models" => [Dict("name" => "ED")]), params, ) # Missing name @test_throws IS.InvalidValue SimulationResultsExport( Dict("models" => [Dict("variables" => ["ActivePowerVariable__ThermalStandard"])]), params, ) end ================================================ FILE: test/test_simulation_sequence.jl ================================================ @testset "Simulation Sequence Correct Execution Order" begin models_array = [ DecisionModel( MockOperationProblem; horizon = Hour(48), interval = Hour(24), resolution = Hour(1), steps = 2, name = "DAUC", ), DecisionModel( MockOperationProblem; horizon = Hour(24), resolution = Minute(5), interval = Hour(1), steps = 2 * 24, name = "HAUC", ), DecisionModel( MockOperationProblem; horizon = Hour(12), resolution = Minute(5), interval = Minute(5), steps = 2 * 24 * 12, name = "ED", ), ] set_device_model!( PSI.get_template(models_array[3]), ThermalStandard, ThermalBasicDispatch, ) models = SimulationModels( models_array, EmulationModel( MockEmulationProblem; interval = Minute(1), resolution = Minute(1), name = "AGC", ), ) test_sequence = SimulationSequence(; models = models, feedforwards = Dict( "ED" => [ SemiContinuousFeedforward(; component_type = ThermalStandard, source = OnVariable, affected_values = [ActivePowerVariable], ), ], ), ini_cond_chronology = InterProblemChronology(), ) @test !isempty( PSI.get_model(PSI.get_template(models_array[3]), ThermalStandard).feedforwards, ) @test length(findall(x -> x == 4, test_sequence.execution_order)) == 24 * 60 @test length(findall(x -> x == 3, test_sequence.execution_order)) == 24 * 12 @test length(findall(x -> x == 2, test_sequence.execution_order)) == 24 @test length(findall(x -> x == 1, test_sequence.execution_order)) == 1 for model in PSI.get_decision_models(models) @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 = Hour(48), name = "DAUC")]), ini_cond_chronology = InterProblemChronology(), ) # Disabled temporarily # @test isa(test_sequence.ini_cond_chronology, IntraProblemChronology) @test test_sequence.execution_order == [1] end @testset "Simulation Sequence invalid sequences" begin models = SimulationModels([ DecisionModel( MockOperationProblem; horizon = Hour(48), interval = Hour(24), resolution = Hour(1), steps = 2, name = "DAUC", ), DecisionModel( MockOperationProblem; horizon = Hour(24), interval = Hour(5), resolution = Minute(5), steps = 2 * 24, name = "HAUC", ), ]) @test_throws IS.ConflictingInputsError SimulationSequence(models = models) models = SimulationModels([ DecisionModel( MockOperationProblem; horizon = Hour(2), interval = Hour(1), resolution = Hour(1), steps = 2, name = "DAUC", ), DecisionModel( MockOperationProblem; horizon = Hour(24), interval = Hour(1), resolution = Minute(5), steps = 2 * 24, name = "HAUC", ), ]) @test_throws IS.ConflictingInputsError SimulationSequence(models = models) models = SimulationModels([ DecisionModel( MockOperationProblem; horizon = Hour(24), interval = Hour(1), resolution = Hour(1), steps = 2, name = "DAUC", ), DecisionModel( MockOperationProblem; horizon = Hour(24), interval = Minute(22), resolution = Hour(1), steps = 2 * 24, name = "HAUC", ), ]) @test_throws IS.ConflictingInputsError SimulationSequence(models = models) end ================================================ FILE: test/test_simulation_store.jl ================================================ import PowerSimulations: open_store, HdfSimulationStore, HDF_FILENAME, SimulationStoreParams, ModelStoreParams, SimulationModelStoreRequirements, CacheFlushRules, KiB, MiB, GiB, STORE_CONTAINER_VARIABLES, initialize_problem_storage!, add_rule!, write_result!, read_result, has_dirty, get_cache_hit_percentage function _initialize!(store, sim, variables, model_defs, cache_rules) models = OrderedDict{Symbol, ModelStoreParams}() model_reqs = Dict{Symbol, SimulationModelStoreRequirements}() num_param_containers = 0 for model in keys(model_defs) 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, 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_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) end models[model] = model_params model_reqs[model] = reqs num_param_containers += length(reqs.variables) end params = SimulationStoreParams( sim["initial_time"], sim["step_resolution"], sim["num_steps"], models, # Emulation Model Store requirements. No tests yet OrderedDict( :Emulator => ModelStoreParams( 100, # Num Executions 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"), ), ), ) em_reqs = SimulationModelStoreRequirements() initialize_problem_storage!(store, params, model_reqs, em_reqs, cache_rules) return end function _run_sim_test(path, sim, variables, model_defs, cache_rules, seed) rng = MersenneTwister(seed) open_store(HdfSimulationStore, path, "w") do store sim_time = sim["initial_time"] _initialize!(store, sim, variables, model_defs, cache_rules) for _ in 1:sim["num_steps"] for model in keys(model_defs) model_time = sim_time for i in 1:model_defs[model]["execution_count"] for key in keys(variables) data = rand(rng, size(model_defs[model]["variables"][key])...) columns = model_defs[model]["names"] write_result!( store, model, key, model_time, model_time, Containers.DenseAxisArray( permutedims(data), columns, 1:size(data)[1], ), ) _verify_data(data, store, model, key, model_time, columns) end model_time += model_defs[model]["resolution"] end end sim_time += sim["step_resolution"] end for output_cache in values(store.cache.data) if PSI.should_keep_in_cache(output_cache) @test get_cache_hit_percentage(output_cache) == 100.0 else @test get_cache_hit_percentage(output_cache) < 100.0 end end flush(store) @test !has_dirty(store.cache) end end function _verify_read_results(path, sim, variables, model_defs, seed) rng = MersenneTwister(seed) open_store(HdfSimulationStore, path, "r") do store sim_time = sim["initial_time"] for _ in 1:sim["num_steps"] for model in keys(model_defs) model_time = sim_time for i in 1:model_defs[model]["execution_count"] for key in keys(variables) data = rand(rng, size(model_defs[model]["variables"][key])...) columns = model_defs[model]["names"] _verify_data(data, store, model, key, model_time, columns) end model_time += model_defs[model]["resolution"] end end sim_time += sim["step_resolution"] end for output_cache in values(store.cache.data) @test get_cache_hit_percentage(output_cache) == 0.0 end end end function _verify_data(expected, store, model, name, time, columns::Tuple{Vector{Symbol}}) expected_df = DataFrames.DataFrame(expected, columns[1]) df = read_result(DataFrames.DataFrame, store, model, name, time) @test expected_df == df end @testset "Test SimulationStore 2-d arrays" begin sim = Dict( "initial_time" => Dates.DateTime("2020-01-01T00:00:00"), "step_resolution" => Dates.Hour(24), "num_steps" => 50, ) variables = Dict( PSI.VariableKey(ActivePowerVariable, ThermalStandard) => Dict("keep_in_cache" => true), PSI.VariableKey(ActivePowerVariable, RenewableDispatch) => Dict("keep_in_cache" => true), PSI.VariableKey(ActivePowerVariable, InterruptiblePowerLoad) => Dict("keep_in_cache" => false), PSI.VariableKey(ActivePowerVariable, RenewableNonDispatch) => Dict("keep_in_cache" => false), ) model_defs = OrderedDict( :ED => Dict( "execution_count" => 24, "horizon" => Hour(12), "names" => ([:dev1, :dev2, :dev3, :dev4, :dev5],), "variables" => Dict(x => ones(12, 5) for x in keys(variables)), "interval" => Dates.Hour(1), "resolution" => Dates.Hour(1), "base_power" => 100.0, "system_uuid" => Base.UUID("4076af6c-e467-56ae-b986-b466b2749572"), ), :UC => Dict( "execution_count" => 1, "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(1), "base_power" => 100.0, "system_uuid" => Base.UUID("4076af6c-e467-56ae-b986-b466b2749572"), ), ) cache_rules = CacheFlushRules(; max_size = 1 * MiB, min_flush_size = 4 * KiB) path = mktempdir() # Use this seed to produce the same randomly generated arrays for write and verify. seed = 1234 _run_sim_test(path, sim, variables, model_defs, cache_rules, seed) _verify_read_results(path, sim, variables, model_defs, seed) end @testset "Test OptimizationOutputCache" begin key = PSI.OptimizationResultCacheKey( :ED, PSI.VariableKey(ActivePowerVariable, InterruptiblePowerLoad), ) cache = PSI.OptimizationOutputCache(key, PSI.CacheFlushRule(true)) @test !PSI.has_clean(cache) @test !PSI.is_dirty(cache, Dates.now()) timestamp1 = Dates.DateTime("2020-01-01T00:00:00") timestamp2 = Dates.DateTime("2020-01-01T01:00:00") timestamp3 = Dates.DateTime("2020-01-01T02:00:00") timestamp4 = Dates.DateTime("2020-01-01T03:00:00") PSI.add_result!(cache, timestamp1, ones(2), false) @test PSI.is_dirty(cache, timestamp1) PSI.add_result!(cache, timestamp2, ones(2), false) @test PSI.is_dirty(cache, timestamp2) @test_throws IS.InvalidValue PSI.add_result!(cache, timestamp2, ones(2), false) @test length(cache.data) == 2 @test length(cache.dirty_timestamps) == 2 popfirst!(cache.dirty_timestamps) @test !PSI.is_dirty(cache, timestamp1) @test PSI.has_clean(cache) @test length(cache.data) == 2 @test length(cache.dirty_timestamps) == 1 PSI.add_result!(cache, timestamp3, ones(2), false) @test length(cache.data) == 3 @test length(cache.dirty_timestamps) == 2 PSI.add_result!(cache, timestamp4, ones(2), true) @test length(cache.data) == 3 @test length(cache.dirty_timestamps) == 3 popfirst!(cache.dirty_timestamps) @test PSI.has_clean(cache) empty!(cache.dirty_timestamps) empty!(cache) @test isempty(cache.data) @test isempty(cache.dirty_timestamps) PSI.add_result!(cache, timestamp1, ones(2), false) PSI.add_result!(cache, timestamp2, ones(2), false) PSI.discard_results!(cache, [timestamp1, timestamp2]) @test isempty(cache.data) end # TODO: test optimizer stats # TODO: unit tests of individual functions, size checks # TODO: profiling of memory performance and GC ================================================ FILE: test/test_utils/add_components_to_system.jl ================================================ function get_copied_line( line::PSY.Line, ) copied_line = Line(; name = PSY.get_name(line) * "_copy", available = PSY.get_available(line), active_power_flow = PSY.get_active_power_flow(line), reactive_power_flow = PSY.get_reactive_power_flow(line), arc = PSY.get_arc(line), r = PSY.get_r(line), x = PSY.get_x(line), b = PSY.get_b(line), rating = PSY.get_rating(line), angle_limits = PSY.get_angle_limits(line), rating_b = PSY.get_rating_b(line), rating_c = PSY.get_rating_c(line), g = PSY.get_g(line), services = PSY.get_services(line), ext = PSY.get_ext(line), ) return copied_line end function get_copied_bus( bus::PSY.ACBus, ) copied_bus = ACBus(; number = PSY.get_number(bus) + 1000, #Add 1000 to avoid name conflicts name = PSY.get_name(bus) * "_copy", available = PSY.get_available(bus), bustype = ACBusTypes.PQ, angle = PSY.get_angle(bus), magnitude = PSY.get_magnitude(bus), voltage_limits = PSY.get_voltage_limits(bus), base_voltage = PSY.get_base_voltage(bus), area = PSY.get_area(bus), load_zone = PSY.get_load_zone(bus), ) return copied_bus end function add_equivalent_ac_transmission_with_series_parallel_circuits!( sys::System, ac_transmission::PSY.Line, ::Type{T}, ) where {T <: PSY.Line} #Create intermediate Bus old_arc = PSY.get_arc(ac_transmission) original_bus_to = PSY.get_to(old_arc) original_bus_from = PSY.get_from(old_arc) intermediate_bus = get_copied_bus(original_bus_to) add_component!(sys, intermediate_bus) #Remove old arc remove_component!(sys, old_arc) #add new Arcs arc1 = Arc(; from = original_bus_from, to = intermediate_bus) arc2 = Arc(; from = intermediate_bus, to = original_bus_to) add_component!(sys, arc1) add_component!(sys, arc2) #Update Arc original Line set_arc!(ac_transmission, arc1) #make Parallel circuits original_rating = PSY.get_rating(ac_transmission) original_r = PSY.get_r(ac_transmission) original_x = PSY.get_x(ac_transmission) rating_new_parallel = PSY.get_rating(ac_transmission) / 2 new_r_parallel = PSY.get_r(ac_transmission) * 2 new_x_parallel = PSY.get_x(ac_transmission) * 2 ac_transmission_copy_parallel = get_copied_line(ac_transmission) ac_transmission_copy_series = get_copied_line(ac_transmission_copy_parallel) set_rating!(ac_transmission, rating_new_parallel) set_rating!(ac_transmission_copy_parallel, rating_new_parallel) set_r!(ac_transmission, original_r) set_r!(ac_transmission_copy_parallel, new_r_parallel) set_x!(ac_transmission, new_x_parallel) set_x!(ac_transmission_copy_parallel, new_x_parallel) add_component!(sys, ac_transmission_copy_parallel) #Add new series Line with same parameters set_arc!(ac_transmission_copy_series, arc2) add_component!(sys, ac_transmission_copy_series) set_x!(ac_transmission_copy_series, 1e-9) set_r!(ac_transmission_copy_series, 1e-9) end function add_equivalent_ac_transmission_with_parallel_circuits!( sys::System, ac_transmission::PSY.Line, ::Type{T}, ) where {T <: PSY.Line} rating_new = PSY.get_rating(ac_transmission) / 2 x_new = PSY.get_x(ac_transmission) * 2 r_new = PSY.get_r(ac_transmission) * 2 ac_transmission_copy_parallel = get_copied_line(ac_transmission) #Set ratings the half so the case remains equivalent to the original set_rating!(ac_transmission, rating_new) set_rating!(ac_transmission_copy_parallel, rating_new) set_x!(ac_transmission, x_new) set_x!(ac_transmission_copy_parallel, x_new) set_r!(ac_transmission, r_new) set_r!(ac_transmission_copy_parallel, r_new) add_component!(sys, ac_transmission_copy_parallel) end function add_equivalent_ac_transmission_with_parallel_circuits!( sys::System, ac_transmission::PSY.ACTransmission, ::Type{T}, ::Type{PSY.MonitoredLine}, ) where {T <: PSY.Line} rating_new = PSY.get_rating(ac_transmission) / 2 x_new = PSY.get_x(ac_transmission) * 2 r_new = PSY.get_r(ac_transmission) * 2 ac_transmission_copy = MonitoredLine(; name = PSY.get_name(ac_transmission) * "_copy", available = PSY.get_available(ac_transmission), active_power_flow = PSY.get_active_power_flow(ac_transmission), reactive_power_flow = PSY.get_reactive_power_flow(ac_transmission), arc = PSY.get_arc(ac_transmission), r = PSY.get_r(ac_transmission), x = PSY.get_x(ac_transmission), b = PSY.get_b(ac_transmission), flow_limits = ( from_to = rating_new, to_from = rating_new, ), rating = rating_new, angle_limits = PSY.get_angle_limits(ac_transmission), rating_b = PSY.get_rating_b(ac_transmission), rating_c = PSY.get_rating_c(ac_transmission), g = PSY.get_g(ac_transmission), services = PSY.get_services(ac_transmission), ext = PSY.get_ext(ac_transmission)) #Set ratings the half so the case remains equivalent to the original set_rating!(ac_transmission, rating_new) set_x!(ac_transmission, x_new) set_r!(ac_transmission, r_new) add_component!(sys, ac_transmission_copy) end function add_reserve_product_without_requirement_time_series!( sys::PSY.System, name::String, direction::String, contributing_devices::Union{ IS.FlattenIteratorWrapper{<:PSY.Generator}, Vector{<:PSY.Generator}, }, ) AS_DIRECTION_MAP = Dict( "Up" => ReserveUp, "Down" => ReserveDown, ) as_direction = AS_DIRECTION_MAP[direction] reserve_instance = VariableReserve{as_direction}(; name = name, available = true, time_frame = 0.0, requirement = 0.0, sustained_time = 3600, max_output_fraction = 1.0, max_participation_factor = 0.25, deployed_fraction = 0.0, ) add_service!(sys, reserve_instance, contributing_devices) end ================================================ FILE: test/test_utils/add_dlr_ts.jl ================================================ function add_dlr_to_system_branches!( sys::System, branches_dlr::Vector{String}, n_steps::Int, dlr_factors::Vector{Float64}; initial_date::String = "2020-01-01", ) # Add dynamic line ratings to the system for branch_name in branches_dlr branch = get_component(ACTransmission, sys, branch_name) dlr_data = SortedDict{Dates.DateTime, TimeSeries.TimeArray}() data_ts = collect( DateTime("$initial_date 0:00:00", "y-m-d H:M:S"):Hour(1):( DateTime("$initial_date 23:00:00", "y-m-d H:M:S") ), ) for t in 1:n_steps ini_time = data_ts[1] + Day(t - 1) dlr_data[ini_time] = TimeArray( data_ts + Day(t - 1), dlr_factors, ) end PowerSystems.add_time_series!( sys, branch, PowerSystems.Deterministic( "dynamic_line_ratings", dlr_data; scaling_factor_multiplier = get_rating, ), ) end end ================================================ FILE: test/test_utils/add_market_bid_cost.jl ================================================ # WARNING: included in HydroPowerSimulations's tests as well. # If you make changes, run those tests too! """ Add a MarketBidCost object to the selected components, with specified incremental and/or decremental cost curves. """ function add_mbc_inner!( sys::PSY.System, active_components::ComponentSelector; incr_curve::Union{Nothing, PiecewiseIncrementalCurve} = nothing, decr_curve::Union{Nothing, PiecewiseIncrementalCurve} = nothing, ) @assert !isempty(get_components(active_components, sys)) "No components selected" if isnothing(incr_curve) && isnothing(decr_curve) error("At least one of incr_curve or decr_curve must be provided") end mbc = MarketBidCost(; no_load_cost = 0.0, start_up = (hot = 0.0, warm = 0.0, cold = 0.0), shut_down = 0.0, ) if !isnothing(decr_curve) set_decremental_offer_curves!(mbc, CostCurve(decr_curve)) end if !isnothing(incr_curve) set_incremental_offer_curves!(mbc, CostCurve(incr_curve)) end for comp in get_components(active_components, sys) set_operation_cost!(comp, mbc) end end """ Add a MarketBidCost object to the selected components, with an incremental cost curve and/or a decremental cost curve defined by hard-coded values. """ function add_mbc!( sys::PSY.System, active_components::ComponentSelector; incremental::Bool = true, decremental::Bool = false, ) incr_slopes = 100 .* [0.3, 0.5, 0.7] decr_slopes = 100 .* [0.7, 0.5, 0.3] x_coords = [10.0, 30.0, 50.0, 100.0] initial_input = 20.0 if !incremental && !decremental error("At least one of incremental or decremental must be true") end if incremental incr_curve = PiecewiseIncrementalCurve(initial_input, x_coords, incr_slopes) else incr_curve = nothing end if decremental decr_curve = PiecewiseIncrementalCurve(initial_input, x_coords, decr_slopes) else decr_curve = nothing end add_mbc_inner!(sys, active_components; incr_curve = incr_curve, decr_curve = decr_curve) end """ Get a deterministic or DeterministicSingleTimeSeries time series from the system. """ function get_deterministic_ts(sys::PSY.System) for device in get_components(PSY.Device, sys) if has_time_series(device, Union{DeterministicSingleTimeSeries, Deterministic}) for key in PSY.get_time_series_keys(device) ts = get_time_series(device, key) if ts isa DeterministicSingleTimeSeries || ts isa Deterministic return ts end end end end @assert false "No Deterministic or DeterministicSingleTimeSeries found in system" return DeterministicSingleTimeSeries(nothing) end """ Extend the MarketBidCost objects attached to the selected components such that they're determined by a time series. # Arguments: - `initial_varies`: whether the initial input time series should have values that vary over time (as opposed to a time series with constant values over time) - `breakpoints_vary`: whether the breakpoints in the variable cost time series should vary over time - `slopes_vary`: whether the slopes of the variable cost time series should vary over time - `active_components`: a `ComponentSelector` specifying which components should get time series - `initial_input_names_vary`: whether the initial input time series names should vary over components - `variable_cost_names_vary`: whether the variable cost time series names should vary over components """ function extend_mbc!( sys::PSY.System, active_components::ComponentSelector; modify_baseline_pwl = nothing, initial_varies::Bool = false, breakpoints_vary::Bool = false, slopes_vary::Bool = false, initial_input_names_vary::Bool = false, variable_cost_names_vary::Bool = false, zero_cost_at_min::Bool = false, create_extra_tranches::Bool = false, do_override_min_x::Bool = false, ) @assert !isempty(get_components(active_components, sys)) "No components selected" # incremental_initial_input is cost at minimum generation, NOT cost at zero generation for comp in get_components(active_components, sys) op_cost = get_operation_cost(comp) if do_override_min_x && :active_power_limits in fieldnames(typeof(comp)) min_power = with_units_base(sys, UnitSystem.NATURAL_UNITS) do get_active_power_limits(comp).min end else min_power = nothing end @assert op_cost isa MarketBidCost for (getter, setter_initial, setter_curves, incr_or_decr) in ( ( get_incremental_offer_curves, set_incremental_initial_input!, set_incremental_offer_curves!, "incremental", ), ( get_decremental_offer_curves, set_decremental_initial_input!, set_decremental_offer_curves!, "decremental", ), ) cost_curve = getter(op_cost) isnothing(cost_curve) && continue baseline = get_value_curve(cost_curve)::PiecewiseIncrementalCurve baseline_initial = get_initial_input(baseline) if zero_cost_at_min baseline_initial = 0.0 end baseline_pwl = get_function_data(baseline) if do_override_min_x && isnothing(min_power) min_power = first(get_x_coords(baseline_pwl)) end !isnothing(modify_baseline_pwl) && (baseline_pwl = modify_baseline_pwl(baseline_pwl)) # primes for easier attribution incr_initial = initial_varies ? (0.11, 0.05) : (0.0, 0.0) incr_x = breakpoints_vary ? (0.02, 0.07, 0.03) : (0.0, 0.0, 0.0) incr_y = slopes_vary ? (0.02, 0.07, 0.03) : (0.0, 0.0, 0.0) name_modifier = "_$(replace(get_name(comp), " " => "_"))_" initial_name = "initial_input $(incr_or_decr)" * (initial_input_names_vary ? name_modifier : "") my_initial_ts = make_deterministic_ts( sys, initial_name, baseline_initial, incr_initial...; ) variable_name = "variable_cost $(incr_or_decr)" * (variable_cost_names_vary ? name_modifier : "") my_pwl_ts = make_deterministic_ts( sys, variable_name, baseline_pwl, incr_x, incr_y; create_extra_tranches = create_extra_tranches, override_min_x = do_override_min_x ? min_power : nothing, ) initial_key = add_time_series!(sys, comp, my_initial_ts) curve_key = add_time_series!(sys, comp, my_pwl_ts) setter_initial(op_cost, initial_key) setter_curves(op_cost, curve_key) end end end """ Make a deterministic time series from a tuple or a float value. See below function for details about the arguments. """ function make_deterministic_ts( name::String, ini_val::T, res_incr::Number, interval_incr::Number, init_time::DateTime, horizon::Period, interval::Period, window_count::Int, resolution::Period, ) where {T <: Union{Number, Tuple}} horizon_count = IS.get_horizon_count(horizon, resolution) ts_data = OrderedDict{DateTime, Vector{T}}() for i in 0:(window_count - 1) if ini_val isa Tuple series = [ ini_val .+ (res_incr * j + i * interval_incr) for j in 0:(horizon_count - 1) ] else series = ini_val .+ res_incr .* (0:(horizon_count - 1)) .+ i * interval_incr end ts_data[init_time + i * interval] = series end return Deterministic(; name = name, data = ts_data, resolution = resolution, interval = interval, ) end """ Create a deterministic time series with increments to the initial values, breakpoints, and slopes. Here, the elements of `incrs_x` and `incrs_y` are tuples of three values, corresponding to: `tranche_incr`: increment between tranche breakpoints. `res_incr`: increment within the forecast horizon window. `interval_incr`: increment in baseline, between horizon windows. `override_min_x`: if provided, overrides the minimum x value in all piecewise curves. `create_extra_tranches`: if true, split the first tranche of the first timestep into two; split the last tranche of the last timestep of into three. """ function make_deterministic_ts( name::String, ini_val::PiecewiseStepData, incrs_x::NTuple{3, Float64}, incrs_y::NTuple{3, Float64}, init_time::DateTime, horizon::Period, interval::Period, count::Int, resolution::Period; override_min_x = nothing, override_max_x = nothing, create_extra_tranches = false, ) (tranche_incr_x, res_incr_x, interval_incr_x) = incrs_x (tranche_incr_y, res_incr_y, interval_incr_y) = incrs_y horizon_count = IS.get_horizon_count(horizon, resolution) # Perturb the baseline curves by the tranche increments xs1, ys1 = deepcopy(get_x_coords(ini_val)), deepcopy(get_y_coords(ini_val)) xs1 .+= [i * tranche_incr_x for i in 0:(length(xs1) - 1)] ys1 .+= [i * tranche_incr_y for i in 0:(length(ys1) - 1)] ts_data = OrderedDict{DateTime, Vector{PiecewiseStepData}}() for i in 0:(count - 1) xs = [deepcopy(xs1) .+ i * interval_incr_x for _ in 1:horizon_count] ys = [deepcopy(ys1) .+ i * interval_incr_y for _ in 1:horizon_count] for j in 1:horizon_count xs[j] .+= (j - 1) * res_incr_x ys[j] .+= (j - 1) * res_incr_y end if !isnothing(override_min_x) for j in 1:horizon_count xs[j][1] = override_min_x end end if !isnothing(override_max_x) for j in 1:horizon_count xs[j][end] = override_max_x end end if i == 0 && create_extra_tranches xs[1] = [xs[1][1], (xs[1][1] + xs[1][2]) / 2, xs[1][2:end]...] ys[1] = [ys[1][1], ys[1][1], ys[1][2:end]...] elseif i == count - 1 && create_extra_tranches xs[end] = [ xs[end][1:(end - 1)]..., (2 * xs[end][end - 1] + xs[end][end]) / 3, (xs[end][end - 1] + 2 * xs[end][end]) / 3, xs[end][end], ] ys[end] = [ys[end][1:(end - 1)]..., ys[end][end], ys[end][end], ys[end][end]] end ts_data[init_time + i * interval] = PiecewiseStepData.(xs, ys) end return Deterministic(; name = name, data = ts_data, resolution = resolution, interval = interval, ) end """ Create a deterministic time series as above, with the same horizon, count, and interval as an existing time series. """ function make_deterministic_ts( sys::PSY.System, args...; kwargs..., ) @assert all( PSY.get_time_series_resolutions(sys) .== first(PSY.get_time_series_resolutions(sys)), ) return make_deterministic_ts( args..., first(PSY.get_forecast_initial_times(sys)), PSY.get_forecast_horizon(sys), PSY.get_forecast_interval(sys), PSY.get_forecast_window_count(sys), first(PSY.get_time_series_resolutions(sys)); kwargs..., ) end ================================================ FILE: test/test_utils/common_operation_model.jl ================================================ const _DESERIALIZE_MESSAGE = "Deserialized initial_conditions_data" const _MAKE_IC_MESSAGE = "Make Initial Conditions Model" const _SKIP_IC_MESSAGE = "Skip build of initial conditions" function test_ic_serialization_outputs(model::PSI.OperationModel; ic_file_exists, message) ic_file = PSI.get_initial_conditions_file(model) log_file = PSI.get_log_file(model) @test isfile(ic_file) == ic_file_exists if ic_file_exists @test Serialization.deserialize(ic_file) isa PSI.InitialConditionsData end make = false deserialize = false skip = false if message == "make" make = true elseif message == "deserialize" deserialize = true elseif message == "skip" skip = true else error("invalid: $message") end text = read(log_file, String) @test make == occursin(_MAKE_IC_MESSAGE, text) @test deserialize == occursin(_DESERIALIZE_MESSAGE, text) @test skip == occursin(_SKIP_IC_MESSAGE, text) end ================================================ FILE: test/test_utils/events_simulation_utils.jl ================================================ # Note: this function is used in HydroPowerSimulations.jl and StorageSystemsSimulations.jl as well for testing of events function run_fixed_forced_outage_sim_with_timeseries(; sys, networks, optimizers, outage_status_timeseries, device_type, device_names, renewable_formulation, ) sys_em = deepcopy(sys) sys_d1 = deepcopy(sys) sys_d2 = deepcopy(sys) transform_single_time_series!(sys_d1, Day(2), Day(1)) transform_single_time_series!(sys_d2, Hour(4), Hour(1)) event_model = EventModel( FixedForcedOutage, PSI.ContinuousCondition(); timeseries_mapping = Dict( :outage_status => "outage_profile_1", ), ) template_d1 = get_template_basic_uc_simulation() set_network_model!(template_d1, NetworkModel(networks[1])) template_d2 = get_template_basic_uc_simulation() set_network_model!(template_d2, NetworkModel(networks[2])) template_em = get_template_nomin_ed_simulation(networks[3]) set_device_model!(template_d1, RenewableDispatch, renewable_formulation) set_device_model!(template_d2, RenewableDispatch, renewable_formulation) set_device_model!(template_em, RenewableDispatch, renewable_formulation) set_device_model!(template_em, ThermalStandard, ThermalBasicDispatch) set_service_model!(template_d1, ServiceModel(ConstantReserve{ReserveUp}, RangeReserve)) set_service_model!(template_d2, ServiceModel(ConstantReserve{ReserveUp}, RangeReserve)) set_device_model!(template_em, InterruptiblePowerLoad, PowerLoadDispatch) set_device_model!(template_d1, InterruptiblePowerLoad, PowerLoadDispatch) set_device_model!(template_d2, InterruptiblePowerLoad, PowerLoadDispatch) set_device_model!(template_d1, Line, StaticBranch) set_device_model!(template_d2, Line, StaticBranch) set_device_model!(template_em, Line, StaticBranch) for sys in [sys_d1, sys_d2, sys_em] for name in device_names g = get_component(device_type, sys, name) transition_data = PSY.FixedForcedOutage(; outage_status = 0.0, ) add_supplemental_attribute!(sys, g, transition_data) PSY.add_time_series!( sys, transition_data, PSY.SingleTimeSeries("outage_profile_1", outage_status_timeseries), ) end end models = SimulationModels(; decision_models = [ DecisionModel( template_d1, sys_d1; name = "D1", initialize_model = false, optimizer = optimizers[1], ), DecisionModel( template_d2, sys_d2; name = "D2", initialize_model = false, optimizer = optimizers[2], store_variable_names = true, ), ], emulation_model = EmulationModel( template_em, sys_em; name = "EM", optimizer = optimizers[3], calculate_conflict = true, store_variable_names = true, ), ) sequence = SimulationSequence(; models = models, ini_cond_chronology = InterProblemChronology(), feedforwards = Dict( "EM" => [# This FeedForward will force the commitment to be kept in the emulator SemiContinuousFeedforward(; component_type = ThermalStandard, source = OnVariable, affected_values = [ActivePowerVariable], ), ], ), events = [event_model], ) sim = Simulation(; name = "no_cache", steps = 1, models = models, sequence = sequence, simulation_folder = mktempdir(; cleanup = true), ) build_out = build!(sim; console_level = Logging.Error) @test build_out == PSI.SimulationBuildStatus.BUILT execute_out = execute!(sim; in_memory = true) @test execute_out == PSI.RunStatus.SUCCESSFULLY_FINALIZED results = SimulationResults(sim; ignore_status = true) return results end function run_events_simulation(; sys_emulator, networks, optimizers, outage_time, #DateTime outage_length, #hrs uc_formulation, # ed_formulation, feedforward, in_memory, ) sys_em = deepcopy(sys_emulator) sys_d1 = build_system(PSITestSystems, "c_sys5_events") transform_single_time_series!(sys_d1, Day(2), Day(1)) sys_d2 = build_system(PSITestSystems, "c_sys5_events") transform_single_time_series!(sys_d2, Hour(4), Hour(1)) event_model = EventModel( GeometricDistributionForcedOutage, PSI.PresetTimeCondition([outage_time]), ) if uc_formulation == "basic" template_d1 = get_template_basic_uc_simulation() set_network_model!(template_d1, NetworkModel(networks[1])) template_d2 = get_template_basic_uc_simulation() set_network_model!(template_d2, NetworkModel(networks[2])) elseif uc_formulation == "standard" template_d1 = get_template_standard_uc_simulation() set_network_model!(template_d1, NetworkModel(networks[1])) template_d2 = get_template_standard_uc_simulation() set_network_model!(template_d2, NetworkModel(networks[2])) else @error "invalid uc formulation: $(uc_formulation). Must be basic or standard" end template_em = get_template_nomin_ed_simulation(networks[3]) if ed_formulation == "basic" set_device_model!(template_em, ThermalStandard, ThermalBasicDispatch) elseif ed_formulation == "nomin" else @error "invalid ed formulation: $(ed). Must be basic or nomin" end set_device_model!(template_d1, Line, StaticBranch) set_device_model!(template_d2, Line, StaticBranch) set_device_model!(template_em, Line, StaticBranch) set_service_model!(template_d1, ServiceModel(ConstantReserve{ReserveUp}, RangeReserve)) set_service_model!(template_d2, ServiceModel(ConstantReserve{ReserveUp}, RangeReserve)) for sys in [sys_d1, sys_d2, sys_em] outage_gens = ["Alta"] for name in outage_gens g = get_component(ThermalStandard, sys, name) transition_data = PSY.GeometricDistributionForcedOutage(; mean_time_to_recovery = outage_length, outage_transition_probability = 1.0, ) add_supplemental_attribute!(sys, g, transition_data) end end models = SimulationModels(; decision_models = [ DecisionModel( template_d1, sys_d1; name = "D1", initialize_model = false, optimizer = optimizers[1], ), DecisionModel( template_d2, sys_d2; name = "D2", initialize_model = false, optimizer = optimizers[2], store_variable_names = true, ), ], emulation_model = EmulationModel( template_em, sys_em; name = "EM", optimizer = optimizers[3], calculate_conflict = true, store_variable_names = true, ), ) if feedforward sequence = SimulationSequence(; models = models, ini_cond_chronology = InterProblemChronology(), feedforwards = Dict( "EM" => [# This FeedForward will force the commitment to be kept in the emulator SemiContinuousFeedforward(; component_type = ThermalStandard, source = OnVariable, affected_values = [ActivePowerVariable], # add_slacks = false, ), ], ), events = [event_model], ) else sequence = SimulationSequence(; models = models, ini_cond_chronology = InterProblemChronology(), events = [event_model], ) end sim = Simulation(; name = "no_cache", steps = 1, models = models, sequence = sequence, simulation_folder = mktempdir(; cleanup = true), ) build_out = build!(sim; console_level = Logging.Error) @test build_out == PSI.SimulationBuildStatus.BUILT execute_out = execute!(sim; in_memory = in_memory) @test execute_out == PSI.RunStatus.SUCCESSFULLY_FINALIZED results = SimulationResults(sim; ignore_status = true) return results end function test_event_results(; res, outage_time, outage_length, expected_power_recovery, expected_on_variable_recovery, test_reactive_power = false, ) em = get_emulation_problem_results(res) p = read_realized_variable( em, "ActivePowerVariable__ThermalStandard"; table_format = TableFormat.WIDE, ) count = read_realized_variable( em, "AvailableStatusChangeCountdownParameter__ThermalStandard"; table_format = TableFormat.WIDE, ) on = read_realized_variable( em, "OnVariable__ThermalStandard"; table_format = TableFormat.WIDE, ) status = read_realized_variable( em, "AvailableStatusParameter__ThermalStandard"; table_format = TableFormat.WIDE, ) outage_ix = indexin([outage_time], p[!, :DateTime])[1] outage_length_ix = Int64((Hour(1) / em.resolution) * outage_length) on_recover_ix = indexin([expected_on_variable_recovery], p[!, :DateTime])[1] p_recover_ix = indexin([expected_power_recovery], p[!, :DateTime])[1] #Test condition at time of outage @test count[outage_ix, "Alta"] == 0.0 @test status[outage_ix, "Alta"] == 1.0 @test on[outage_ix, "Alta"] == 1.0 @test p[outage_ix, "Alta"] != 0.0 #Test condition during outage @test count[(outage_ix + 1):(outage_ix + outage_length_ix), "Alta"] == outage_length_ix:-1.0:1.0 @test status[(outage_ix + 1):(outage_ix + outage_length_ix), "Alta"] == zeros(outage_length_ix) @test on[(on_recover_ix - 1), "Alta"] == 0.0 #on variable not necessarily zero for full time; possibly updated later length_p_zero = p_recover_ix - outage_ix - 1 @test isapprox( p[(outage_ix + 1):(p_recover_ix - 1), "Alta"], zeros(length_p_zero); atol = 1e-5, ) #Test condition after outage @test count[(outage_ix + outage_length_ix + 1), "Alta"] == 0.0 @test status[(outage_ix + outage_length_ix + 1), "Alta"] == 1.0 @test isapprox(on[on_recover_ix, "Alta"], 1.0; atol = 1e-5) @test !isapprox(p[p_recover_ix, "Alta"], 0.0; atol = 1e-5) if test_reactive_power == true q = read_realized_variable( em, "ReactivePowerVariable__ThermalStandard"; table_format = TableFormat.WIDE, ) @test isapprox( q[(outage_ix + 1):(p_recover_ix - 1), "Alta"], zeros(length_p_zero); atol = 5e-2, ) @test q[p_recover_ix, "Alta"] != 0.0 @test !isapprox(q[p_recover_ix, "Alta"], 0.0; atol = 5e-2) end return end ================================================ FILE: test/test_utils/iec_simulation_utils.jl ================================================ const IECComponentType = Source const IEC_COMPONENT_NAME = "source" const SEL_IEC = make_selector(IECComponentType, IEC_COMPONENT_NAME) function make_5_bus_with_import_export(; add_single_time_series::Bool = false, name = nothing, ) sys = build_system( PSITestSystems, "c_sys5_uc"; add_single_time_series = add_single_time_series, ) source = IECComponentType(; name = IEC_COMPONENT_NAME, available = true, bus = get_component(ACBus, sys, "nodeC"), active_power = 0.0, reactive_power = 0.0, active_power_limits = (min = -2.0, max = 2.0), reactive_power_limits = (min = -2.0, max = 2.0), R_th = 0.01, X_th = 0.02, internal_voltage = 1.0, internal_angle = 0.0, base_power = 100.0, ) import_curve = make_import_curve( [0.0, 100.0, 105.0, 120.0, 200.0], [5.0, 10.0, 20.0, 40.0], ) export_curve = make_export_curve( [0.0, 100.0, 105.0, 120.0, 200.0], [12.0, 8.0, 4.0, 1.0], # elsewhere the final slope is 0.0 but that's problematic here ) ie_cost = ImportExportCost(; import_offer_curves = import_curve, export_offer_curves = export_curve, ancillary_service_offers = Vector{Service}(), energy_import_weekly_limit = 1e6, energy_export_weekly_limit = 1e6, ) set_operation_cost!(source, ie_cost) add_component!(sys, source) @assert get_component(SEL_IEC, sys) == source isnothing(name) || set_name!(sys, name) return sys end function make_5_bus_with_ie_ts( import_breakpoints_vary::Bool, import_slopes_vary::Bool, export_breakpoints_vary::Bool, export_slopes_vary::Bool; zero_min_power::Bool = true, unperturb_max_power::Bool = false, add_single_time_series::Bool = false, import_scalar = 1.0, export_scalar = 1.0, name = nothing) im_incr_x = import_breakpoints_vary ? (0.02, 0.11, 0.05) : (0.0, 0.0, 0.0) im_incr_y = import_slopes_vary ? (0.02, 0.11, 0.05) : (0.0, 0.0, 0.0) ex_incr_x = export_breakpoints_vary ? (0.03, 0.13, 0.07) : (0.0, 0.0, 0.0) ex_incr_y = export_slopes_vary ? (0.03, 0.13, 0.07) : (0.0, 0.0, 0.0) sys = make_5_bus_with_import_export(; add_single_time_series = add_single_time_series, name = name, ) source = get_component(SEL_IEC, sys) oc = get_operation_cost(source)::ImportExportCost im_oc = get_import_offer_curves(oc) ex_oc = get_export_offer_curves(oc) im_fd = get_function_data(im_oc) * import_scalar ex_fd = get_function_data(ex_oc) * export_scalar im_ts = make_deterministic_ts( sys, "variable_cost_import", im_fd, im_incr_x, im_incr_y; override_min_x = zero_min_power ? 0.0 : nothing, override_max_x = unperturb_max_power ? last(get_x_coords(im_fd)) : nothing, ) ex_ts = make_deterministic_ts( sys, "variable_cost_export", ex_fd, ex_incr_x, ex_incr_y; override_min_x = zero_min_power ? 0.0 : nothing, override_max_x = unperturb_max_power ? last(get_x_coords(ex_fd)) : nothing, ) im_key = add_time_series!(sys, source, im_ts) ex_key = add_time_series!(sys, source, ex_ts) set_import_offer_curves!(oc, im_key) set_export_offer_curves!(oc, ex_key) return sys end # Analogous to run_mbc_obj_fun_test in test_utils/mbc_simulation_utils.jl function run_iec_obj_fun_test(sys1, sys2, comp_name::String, ::Type{T}; simulation = true, in_memory_store = false, reservation = false, ) where {T <: PSY.Component} _, res1, decisions1, nullable_decisions1 = run_iec_sim(sys1, comp_name, T; simulation = simulation, in_memory_store = in_memory_store, reservation = reservation, ) _, res2, decisions2, nullable_decisions2 = run_iec_sim(sys2, comp_name, T; simulation = simulation, in_memory_store = in_memory_store, reservation = reservation, ) all_decisions1 = (decisions1..., nullable_decisions1...) all_decisions2 = (decisions2..., nullable_decisions2...) if !all(isapprox.(all_decisions1, all_decisions2)) @error all_decisions1 @error all_decisions2 end @assert all(isapprox.(all_decisions1, all_decisions2)) ground_truth_1 = cost_due_to_time_varying_iec(sys1, res1, T) ground_truth_2 = cost_due_to_time_varying_iec(sys2, res2, T) success = obj_fun_test_helper(ground_truth_1, ground_truth_2, res1, res2) return decisions1, decisions2 end function run_iec_sim(sys::System, comp_name::String, ::Type{T}; simulation = true, in_memory_store = false, reservation = false, ) where {T <: PSY.Component} device_to_formulation = FormulationDict( Source => DeviceModel( Source, ImportExportSourceModel; attributes = Dict("reservation" => reservation), ), ) model, res = if simulation run_generic_mbc_sim( sys; in_memory_store = in_memory_store, device_to_formulation = device_to_formulation, ) else run_generic_mbc_prob(sys; device_to_formulation = device_to_formulation) end # Test that breakpoint and slope parameters read from results match the # ground truth from the system's offer curve time series. # We can compare raw PiecewiseStepData values directly because time-variant offer curve # time series are always in natural units and the parameter multiplier is 1.0 # (see the analogous comment in mbc_simulation_utils.jl). for (is_decremental, oc_getter) in ( (false, PSY.get_import_offer_curves), (true, PSY.get_export_offer_curves), ) bp_param_type = if is_decremental PSI.DecrementalPiecewiseLinearBreakpointParameter else PSI.IncrementalPiecewiseLinearBreakpointParameter end sl_param_type = if is_decremental PSI.DecrementalPiecewiseLinearSlopeParameter else PSI.IncrementalPiecewiseLinearSlopeParameter end bp_param = _maybe_upgrade_to_dict(read_parameter(res, bp_param_type, T)) sl_param = _maybe_upgrade_to_dict(read_parameter(res, sl_param_type, T)) for (step_dt, bp_step_df) in pairs(bp_param) sl_step_df = sl_param[step_dt] for gen_name in unique(bp_step_df.name) comp = get_component(T, sys, gen_name) cost = PSY.get_operation_cost(comp) oc_ts = oc_getter(comp, cost; start_time = step_dt) gen_bp = @rsubset(bp_step_df, :name == gen_name) gen_sl = @rsubset(sl_step_df, :name == gen_name) for (ts, psd) in zip(TimeSeries.timestamp(oc_ts), TimeSeries.values(oc_ts)) expected_bp = get_x_coords(psd) expected_sl = get_y_coords(psd) actual_bp = sort(@rsubset(gen_bp, :DateTime == ts), :name2).value actual_sl = sort(@rsubset(gen_sl, :DateTime == ts), :name2).value # actual may be longer than expected due to padding (see _unwrap_for_param) @test length(actual_bp) >= length(expected_bp) @test length(actual_sl) >= length(expected_sl) @test all(isapprox.(actual_bp[1:length(expected_bp)], expected_bp)) @test all(isapprox.(actual_sl[1:length(expected_sl)], expected_sl)) end end end end decisions = ( _read_one_value(res, PSI.ActivePowerOutVariable, T, comp_name), _read_one_value(res, PSI.ActivePowerInVariable, T, comp_name), ) output_var = read_variable_dict(res, PSI.ActivePowerOutVariable, T) input_var = read_variable_dict(res, PSI.ActivePowerInVariable, T) for key in keys(output_var) output_on = output_var[key][!, "value"] .> PSI.COST_EPSILON input_on = input_var[key][!, "value"] .> PSI.COST_EPSILON if reservation @test all(.~(output_on .& input_on)) # no simultaneous import/export else @test any(output_on .& input_on) # some simultaneous import/export end end return model, res, decisions, () # return format follows the MBC run_startup_shutdown_test convention end # Analogous to cost_due_to_time_varying_mbc in test_utils/mbc_simulation_utils.jl # TODO deduplicate after initial time-sensitive merge function cost_due_to_time_varying_iec( sys::System, res::IS.Results, ::Type{T}, ) where {T <: PSY.Component} power_in_vars = read_variable_dict(res, PSI.ActivePowerInVariable, T) power_out_vars = read_variable_dict(res, PSI.ActivePowerOutVariable, T) result = SortedDict{DateTime, DataFrame}() for step_dt in keys(power_in_vars) power_in_df = power_in_vars[step_dt] step_df = DataFrame(:DateTime => unique(power_in_df.DateTime)) gen_names = unique(power_in_df.name) @assert !isempty(gen_names) power_out_df = power_out_vars[step_dt] @assert names(power_in_df) == names(power_out_df) @assert all(power_in_df.DateTime .== power_out_df.DateTime) @assert any([ get_operation_cost(comp) isa ImportExportCost for comp in get_components(T, sys) ]) for gen_name in gen_names comp = get_component(T, sys, gen_name) cost = PSY.get_operation_cost(comp) (cost isa ImportExportCost) || continue step_df[!, gen_name] .= 0.0 # imports = addition of power = power flowing out of the device # exports = reduction of power = power flowing into the device for (multiplier, power_df, getter) in ( (1.0, power_out_df, PSY.get_import_offer_curves), (-1.0, power_in_df, PSY.get_export_offer_curves), ) offer_curves = getter(cost) if PSI.is_time_variant(offer_curves) vc_ts = getter(comp, cost; start_time = step_dt) @assert all(unique(power_df.DateTime) .== TimeSeries.timestamp(vc_ts)) step_df[!, gen_name] .+= multiplier * _calc_pwi_cost.( @rsubset(power_df, :name == gen_name).value, TimeSeries.values(vc_ts), ) end end end measure_vars = [x for x in names(step_df) if x != "DateTime"] # rows represent: [time, component, time-varying MBC cost for {component} at {time}] result[step_dt] = DataFrames.stack( step_df, measure_vars; variable_name = :name, value_name = :value, ) end return result end function iec_obj_fun_test_wrapper(sys_constant, sys_varying; reservation = false) for use_simulation in (false, true) for in_memory_store in (use_simulation ? (false, true) : (false,)) decisions1, decisions2 = run_iec_obj_fun_test( sys_constant, sys_varying, IEC_COMPONENT_NAME, IECComponentType; simulation = use_simulation, in_memory_store = in_memory_store, reservation = reservation, ) if !all(isapprox.(decisions1, decisions2)) @error decisions1 @error decisions2 end @assert all(approx_geq_1.(decisions1)) end end end ================================================ FILE: test/test_utils/mbc_simulation_utils.jl ================================================ # WARNING: included in HydroPowerSimulations's tests as well. # If you make changes, run those tests too! const TIME1 = DateTime("2024-01-01T00:00:00") test_path = mktempdir() const FormulationDict = Dict{Type{<:PSY.Device}, Union{DeviceModel, Type{<:PSI.AbstractDeviceFormulation}}} # TODO could replace with PSI's defaults, template_unit_commitment const DEFAULT_FORMULATIONS = FormulationDict( ThermalStandard => ThermalBasicUnitCommitment, PowerLoad => StaticPowerLoad, InterruptiblePowerLoad => PowerLoadInterruption, RenewableDispatch => RenewableFullDispatch, # I include this file in the tests of SSS and HPS, which error on these formulations. # HydroDispatch => HydroCommitmentRunOfRiver, # EnergyReservoirStorage => StorageDispatchWithReserves, ) # debugging code for inspecting objective functions -- ignore function format_objective_function_file(filepath::String) if !isfile(filepath) println("Error: File '$filepath' does not exist.") exit(1) end try content = read(filepath, String) content = replace(content, "+" => "+\n") content = replace(content, "-" => "-\n") write(filepath, content) catch e println("Error processing file '$filepath': $e") exit(1) end end function save_objective_function(model::DecisionModel, filepath::String) open(filepath, "w") do file println(file, "invariant_terms:") println(file, model.internal.container.objective_function.invariant_terms) println(file, "variant_terms:") println(file, model.internal.container.objective_function.variant_terms) end format_objective_function_file(filepath) end function save_constraints(model::DecisionModel, filepath::String) open(filepath, "w") do file for (k, v) in model.internal.container.constraints println(file, "Constraint Type: $(k)") println(file, v) end end end # end debugging code function set_formulations!(template::ProblemTemplate, sys::PSY.System, device_to_formulation::FormulationDict, ) for (device, formulation) in device_to_formulation if !isempty(get_components(device, sys)) _set_formulations_helper(template, device, formulation) end end for (device, formulation) in DEFAULT_FORMULATIONS if !haskey(device_to_formulation, device) && !isempty(get_components(device, sys)) _set_formulations_helper(template, device, formulation) end end end _set_formulations_helper( template::ProblemTemplate, device::Type{<:PSY.Device}, formulation::Type{<:PSI.AbstractDeviceFormulation}, ) = set_device_model!(template, device, formulation) _set_formulations_helper(template::ProblemTemplate, _, device_model::DeviceModel) = set_device_model!(template, device_model) # Layer of indirection to upgrade problem results to look like simulation results _maybe_upgrade_to_dict(input::AbstractDict) = input _maybe_upgrade_to_dict(input::DataFrame) = SortedDict{DateTime, DataFrame}(first(input[!, :DateTime]) => input) read_variable_dict( res::IS.Results, var_name::Type{<:PSI.VariableType}, comp_type::Type{<:PSY.Component}, ) = _maybe_upgrade_to_dict(read_variable(res, var_name, comp_type)) read_parameter_dict( res::IS.Results, par_name::Type{<:PSI.ParameterType}, comp_type::Type{<:PSY.Component}, ) = _maybe_upgrade_to_dict(read_parameter(res, par_name, comp_type)) function _read_one_value(res, var_name, gentype, unit_name) df = @chain begin vcat(values(read_variable_dict(res, var_name, gentype))...) @rsubset(:name == unit_name) @combine(:value = sum(:value)) end return df[1, 1] end function build_generic_mbc_model(sys::System; multistart::Bool = false, standard::Bool = false, device_to_formulation = FormulationDict(), ) template = ProblemTemplate( NetworkModel( CopperPlatePowerModel; duals = [CopperPlateBalanceConstraint], ), ) set_formulations!( template, sys, device_to_formulation, ) if standard set_device_model!(template, ThermalStandard, ThermalStandardUnitCommitment) end if multistart set_device_model!(template, ThermalMultiStart, ThermalMultiStartUnitCommitment) end model = DecisionModel( template, sys; name = "UC", store_variable_names = true, optimizer = HiGHS_optimizer_small_gap, ) return model end function run_generic_mbc_prob( sys::System; multistart::Bool = false, standard = false, test_success = true, filename::Union{String, Nothing} = nothing, is_decremental::Bool = false, device_to_formulation = FormulationDict(), ) model = build_generic_mbc_model( sys; multistart = multistart, standard = standard, device_to_formulation = device_to_formulation, ) test_path = mktempdir() build_result = build!(model; output_dir = test_path) test_success && @test build_result == PSI.ModelBuildStatus.BUILT solve_result = solve!(model) test_success && @test solve_result == PSI.RunStatus.SUCCESSFULLY_FINALIZED res = OptimizationProblemResults(model) if !isnothing(filename) adj = is_decremental ? "decr" : "incr" save_objective_function( model, "$(filename)_$(adj)_prob_objective_function.txt", ) save_constraints( model, "$(filename)_$(adj)_prob_constraints.txt", ) end return model, res end function run_generic_mbc_sim( sys::System; multistart::Bool = false, in_memory_store::Bool = false, standard::Bool = false, test_success = true, filename::Union{String, Nothing} = nothing, is_decremental::Bool = false, device_to_formulation = FormulationDict(), ) model = build_generic_mbc_model( sys; multistart = multistart, standard = standard, device_to_formulation = device_to_formulation, ) models = SimulationModels(; decision_models = [ model, ], ) sequence = SimulationSequence(; models = models, feedforwards = Dict( ), ini_cond_chronology = InterProblemChronology(), ) sim = Simulation(; name = "compact_sim", steps = 2, models = models, sequence = sequence, initial_time = TIME1, simulation_folder = mktempdir(), ) test_success && @test build!(sim) == PSI.SimulationBuildStatus.BUILT test_success && @test execute!(sim; in_memory = in_memory_store) == PSI.RunStatus.SUCCESSFULLY_FINALIZED sim_res = SimulationResults(sim) res = get_decision_problem_results(sim_res, "UC") if !isnothing(filename) adj = is_decremental ? "decr" : "incr" save_objective_function( model, "$(filename)_$(adj)_sim_objective_function.txt", ) save_constraints( model, "$(filename)_$(adj)_sim_constraints.txt", ) end return model, res end """ Run a simple simulation with the system and return information useful for testing time-varying startup and shutdown functionality. Pass `simulation = false` to use a single decision model, `true` for a full simulation. """ function run_mbc_sim( sys::System, comp_name::String, ::Type{T}; has_initial_input::Bool = true, is_decremental::Bool = false, simulation = true, in_memory_store = false, standard = false, filename::Union{String, Nothing} = nothing, device_to_formulation = FormulationDict(), ) where {T <: PSY.Component} model, res = if simulation run_generic_mbc_sim( sys; in_memory_store = in_memory_store, standard = standard, filename = filename, is_decremental = is_decremental, device_to_formulation = device_to_formulation, ) else run_generic_mbc_prob( sys; standard = standard, filename = filename, is_decremental = is_decremental, device_to_formulation = device_to_formulation, ) end # TODO make this more general as to which variables we're reading. # e.g. hydro. # Test that breakpoint and slope parameters read from results match the # ground truth from the system's offer curve time series. # The PowerLoadDispatch device formulation doesn't have # DecrementalCostAtMinParameter nor OnVariable. if is_decremental bp_param_type = PSI.DecrementalPiecewiseLinearBreakpointParameter sl_param_type = PSI.DecrementalPiecewiseLinearSlopeParameter oc_getter = get_decremental_offer_curves else bp_param_type = PSI.IncrementalPiecewiseLinearBreakpointParameter sl_param_type = PSI.IncrementalPiecewiseLinearSlopeParameter oc_getter = get_incremental_offer_curves end # Both bp_param and sl_param are SortedDict{DateTime, DataFrame}. # Columns: :DateTime, :name ("Test Unit1"), :name2 ("tranche_1"), # and :value (breakpoints for bp_param, slopes for sl_param). bp_param = _maybe_upgrade_to_dict(read_parameter(res, bp_param_type, T)) sl_param = _maybe_upgrade_to_dict(read_parameter(res, sl_param_type, T)) # We can compare the raw PiecewiseStepData values directly against read_parameter # results because: (1) time-variant offer curve time series are always in natural units # (see PSY's cost_function_timeseries.jl), and (2) the parameter multiplier is 1.0 for # both slopes and breakpoints (see default_interface_methods.jl). Unit conversion via # get_piecewise_curve_per_system_unit only happens later when building expressions. for (step_dt, bp_step_df) in pairs(bp_param) sl_step_df = sl_param[step_dt] for gen_name in unique(bp_step_df.name) comp = get_component(T, sys, gen_name) cost = PSY.get_operation_cost(comp) oc_ts = oc_getter(comp, cost; start_time = step_dt) gen_bp = @rsubset(bp_step_df, :name == gen_name) gen_sl = @rsubset(sl_step_df, :name == gen_name) for (ts, psd) in zip(TimeSeries.timestamp(oc_ts), TimeSeries.values(oc_ts)) expected_bp = get_x_coords(psd) expected_sl = get_y_coords(psd) actual_bp = sort(@rsubset(gen_bp, :DateTime == ts), :name2).value actual_sl = sort(@rsubset(gen_sl, :DateTime == ts), :name2).value # actual may be longer than expected due to padding (see _unwrap_for_param) @test length(actual_bp) >= length(expected_bp) @test length(actual_sl) >= length(expected_sl) @test all(isapprox.(actual_bp[1:length(expected_bp)], expected_bp)) @test all(isapprox.(actual_sl[1:length(expected_sl)], expected_sl)) end end end if has_initial_input if is_decremental param_type = PSI.DecrementalCostAtMinParameter initial_getter = get_decremental_initial_input else # Default to incremental for ThermalStandard and other types param_type = PSI.IncrementalCostAtMinParameter initial_getter = get_incremental_initial_input end init_param = read_parameter_dict(res, param_type, T) for (step_dt, step_df) in pairs(init_param) for gen_name in unique(step_df.name) comp = get_component(T, sys, gen_name) ii_comp = initial_getter( comp, PSY.get_operation_cost(comp); start_time = step_dt, ) @test all(step_df[!, :DateTime] .== TimeSeries.timestamp(ii_comp)) @test all( isapprox.( @rsubset(step_df, :name == gen_name).value, TimeSeries.values(ii_comp), ), ) end end end # NOTE this could be rewritten nicely using PowerAnalytics # Select component based on comp_type - fallback to legacy behavior if needed sel = make_selector(T, comp_name) @assert !isnothing(first(get_components(sel, sys))) if has_initial_input decisions = ( _read_one_value(res, PSI.OnVariable, T, comp_name), _read_one_value(res, PSI.ActivePowerVariable, T, comp_name), ) else decisions = ( 1.0, # placeholder so return type is consistent. _read_one_value(res, PSI.ActivePowerVariable, T, comp_name), ) end return model, res, decisions, () end function cost_due_to_time_varying_mbc( sys::System, res::IS.Results, ::Type{T}; is_decremental = false, has_initial_input = true, device_to_formulation::Any, #unused ) where {T <: PSY.Device} power_vars = read_variable_dict(res, PSI.ActivePowerVariable, T) result = SortedDict{DateTime, DataFrame}() if has_initial_input on_vars = read_variable_dict(res, PSI.OnVariable, T) @assert all(keys(on_vars) .== keys(power_vars)) @assert !isempty(keys(on_vars)) end for step_dt in keys(power_vars) power_df = power_vars[step_dt] step_df = DataFrame(:DateTime => unique(power_df.DateTime)) gen_names = unique(power_df.name) @assert !isempty(gen_names) @assert any([ get_operation_cost(comp) isa MarketBidCost for comp in get_components(T, sys) ]) if has_initial_input on_df = on_vars[step_dt] @assert names(on_df) == names(power_df) @assert on_df[!, :DateTime] == power_df[!, :DateTime] else # assumption: all devices are on. on_df = DataFrame( :DateTime => power_df.DateTime, :name => power_df.name, :value => ones(nrow(power_df)), ) end for gen_name in gen_names comp = get_component(T, sys, gen_name) cost = PSY.get_operation_cost(comp) (cost isa MarketBidCost) || continue step_df[!, gen_name] .= 0.0 ii_getter = if is_decremental get_decremental_initial_input else get_incremental_initial_input end if PSI.is_time_variant(ii_getter(cost)) # initial cost: initial input time series multiplied by OnVariable value. ii_ts = ii_getter(comp, cost; start_time = step_dt) @assert all(unique(on_df.DateTime) .== TimeSeries.timestamp(ii_ts)) step_df[!, gen_name] .+= @rsubset(on_df, :name == gen_name).value .* TimeSeries.values(ii_ts) end oc_getter = is_decremental ? get_decremental_offer_curves : get_incremental_offer_curves if PSI.is_time_variant(oc_getter(cost)) vc_ts = oc_getter(comp, cost; start_time = step_dt) @assert all(unique(power_df.DateTime) .== TimeSeries.timestamp(vc_ts)) # variable cost: cost function time series evaluated at ActivePowerVariable value. step_df[!, gen_name] .+= _calc_pwi_cost.( @rsubset(power_df, :name == gen_name).value, TimeSeries.values(vc_ts), ) # could replace with direct evaluation, now that it is implemented in IS. (https://github.com/Sienna-Platform/PowerSimulations.jl/issues/1430) end end measure_vars = [x for x in names(step_df) if x != "DateTime"] # rows represent: [time, component, time-varying MBC cost for {component} at {time}] result[step_dt] = DataFrames.stack( step_df, measure_vars; variable_name = :name, value_name = :value, ) end return result end # See run_startup_shutdown_obj_fun_test for explanation function obj_fun_test_helper( ground_truth_1, ground_truth_2, res1, res2; is_decremental = false, ) @assert all(keys(ground_truth_1) .== keys(ground_truth_2)) # total cost due to time-varying MBCs in each scenario total1 = [only(@combine(df, :total = sum(:value)).total) for df in values(ground_truth_1)] total2 = [only(@combine(df, :total = sum(:value)).total) for df in values(ground_truth_2)] if !is_decremental ground_truth_diff = total2 .- total1 # How much did the cost increase between simulation 1 and simulation 2 for each step else # objective = cost - benefit. higher load prices => more willing to pay, more benefit. # so we get an extra negative sign, since we're increasing benefit, not cost. ground_truth_diff = total1 .- total2 end obj1 = PSI.read_optimizer_stats(res1)[!, "objective_value"] obj2 = PSI.read_optimizer_stats(res2)[!, "objective_value"] obj_diff = obj2 .- obj1 # Make sure there is some real difference between the two scenarios @assert !any(isapprox.(ground_truth_diff, 0.0; atol = 0.0001)) # Make sure the difference is reflected correctly in the objective value if !all(isapprox.(obj_diff, ground_truth_diff; atol = 0.0001)) @error obj_diff @error ground_truth_diff end comparison_passes = all(isapprox.(obj_diff, ground_truth_diff; atol = 0.0001)) # An assumption in this line of testing is that our perturbations are small enough that # they don't actually change the decisions, just slightly alter the cost. If the # comparison isn't passing, one reason could be that this assumption is violated. In # that case, we want an `AssertionError` rather than a test failure. if !comparison_passes @assert isapprox(obj1, obj2; atol = 10, rtol = 0.01) "obj1 ($obj1) and obj2 ($obj2) are supposed to differ, but they differ by an improbably large amount ($obj_diff) -- the perturbations are likely affecting the decisions. Ground truth difference is $ground_truth_diff; the test would fail, but we have some reason to believe it is instead broken." end # At this point, we've eliminiated many 'innocent' sources of error, so if the # comparison isn't passing here we can have some confidence that it's actually pointing # to a bug in the implementation @test comparison_passes return comparison_passes end # See run_startup_shutdown_obj_fun_test for explanation function run_mbc_obj_fun_test( sys1, sys2, comp_name::String, comp_type::Type{T}; is_decremental::Bool = false, has_initial_input::Bool = true, simulation = true, in_memory_store = false, filename::Union{String, Nothing} = nothing, device_to_formulation = FormulationDict(), ) where {T <: PSY.Component} # at the moment, nullable_decisions are empty tuples, but keep them for future-proofing. # look at run_startup_shutdown_test for explanation: non-nullable should be approx_geq_1. kwargs = Dict( :is_decremental => is_decremental, :has_initial_input => has_initial_input, :simulation => simulation, :in_memory_store => in_memory_store, :filename => filename, :device_to_formulation => device_to_formulation, ) filename_in = get(kwargs, :filename, nothing) if !isnothing(filename_in) kwargs[:filename] = filename_in * get_name(sys1) end _, res1, decisions1, nullable_decisions1 = run_mbc_sim( sys1, comp_name, comp_type; kwargs..., ) if !isnothing(filename_in) kwargs[:filename] = filename_in * get_name(sys2) end _, res2, decisions2, nullable_decisions2 = run_mbc_sim( sys2, comp_name, comp_type; kwargs..., ) all_decisions1 = (decisions1..., nullable_decisions1...) all_decisions2 = (decisions2..., nullable_decisions2...) if !all(isapprox.(all_decisions1, all_decisions2)) @error all_decisions1 @error all_decisions2 end @assert all(isapprox.(all_decisions1, all_decisions2)) ground_truth_1 = cost_due_to_time_varying_mbc(sys1, res1, T; is_decremental = is_decremental, has_initial_input = has_initial_input, device_to_formulation = device_to_formulation) ground_truth_2 = cost_due_to_time_varying_mbc(sys2, res2, T; is_decremental = is_decremental, has_initial_input = has_initial_input, device_to_formulation = device_to_formulation) success = obj_fun_test_helper( ground_truth_1, ground_truth_2, res1, res2; is_decremental = is_decremental, ) #= if !success @error ground_truth_1 @error ground_truth_2 obj1 = PSI.read_optimizer_stats(res1)[!, "objective_value"] obj2 = PSI.read_optimizer_stats(res2)[!, "objective_value"] @error obj1 @error obj2 end=# return decisions1, decisions2 end # TODO for https://github.com/Sienna-Platform/PowerSimulations.jl/issues/1430, reimplement this # by converting the implied IncrementalCurve into an InputOutputCurve and then evaluating # *its* `FunctionData` function _calc_pwi_cost(active_power::Float64, pwi::PiecewiseStepData) isapprox(active_power, 0.0) && return 0.0 breakpoints = get_x_coords(pwi) slopes = get_y_coords(pwi) above_min = isapprox(active_power, first(breakpoints)) || active_power > first(breakpoints) below_max = isapprox(active_power, last(breakpoints)) || active_power < last(breakpoints) @assert above_min && below_max "Active power ($active_power) is outside the range of breakpoints ($(first(breakpoints)) to $(last(breakpoints))) for the piecewise step data." active_power = clamp(active_power, first(breakpoints), last(breakpoints)) i_leq = findlast(<=(active_power), breakpoints) cost = sum(slopes[1:(i_leq - 1)] .* (breakpoints[2:i_leq] .- breakpoints[1:(i_leq - 1)])) (active_power > breakpoints[i_leq]) && (cost += slopes[i_leq] * (active_power - breakpoints[i_leq])) return cost end "Test that the two systems (typically one without time series and one with constant time series) simulate the same" function test_generic_mbc_equivalence(sys0, sys1; kwargs...) for runner in (run_generic_mbc_prob, run_generic_mbc_sim) # test with both a single problem and a full simulation filename_in = get(kwargs, :filename, nothing) # Create a mutable copy of kwargs kwargs_dict = Dict(kwargs) if !isnothing(filename_in) kwargs_dict[:filename] = filename_in * get_name(sys0) end _, res0 = runner(sys0; kwargs_dict...) if !isnothing(filename_in) kwargs_dict[:filename] = filename_in * get_name(sys1) end _, res1 = runner(sys1; kwargs_dict...) obj_val_0 = PSI.read_optimizer_stats(res0)[!, "objective_value"] obj_val_1 = PSI.read_optimizer_stats(res1)[!, "objective_value"] @test isapprox(obj_val_0, obj_val_1; atol = 0.0001) end end approx_geq_1(x; kwargs...) = (x >= 1.0) || isapprox(x, 1.0; kwargs...) ================================================ FILE: test/test_utils/mbc_system_utils.jl ================================================ # WARNING: included in HydroPowerSimulations's tests as well. # If you make changes, run those tests too! const SEL_INCR = make_selector(ThermalStandard, "Test Unit1") const SEL_DECR = make_selector(InterruptiblePowerLoad, "Bus1_interruptible") const SEL_MULTISTART = make_selector(ThermalMultiStart, "115_STEAM_1") # functions for replacing components in the system function replace_with_renewable!( sys::PSY.System, unit1::PSY.Generator; use_thermal_max_power = false, magnitude = 1.0, random_variation = 0.1, ) rg1 = PSY.RenewableDispatch(; name = "RG1", available = true, bus = get_bus(unit1), active_power = get_active_power(unit1), reactive_power = get_reactive_power(unit1), rating = get_rating(unit1), prime_mover_type = PSY.PrimeMovers.PVe, reactive_power_limits = get_reactive_power_limits(unit1), power_factor = 0.9, # the start up, shunt down, and no-load cost of renewables should be zero, # but we'll use the unit's operation cost as-is for simplicity. operation_cost = deepcopy(get_operation_cost(unit1)), base_power = get_base_power(unit1), ) add_component!(sys, rg1) transfer_mbc!(rg1, unit1, sys) remove_component!(sys, unit1) zero_out_startup_shutdown_costs!(rg1) # add a max_active_power time series to the component load = first(PSY.get_components(PSY.PowerLoad, sys)) load_ts = get_time_series(Deterministic, load, "max_active_power") num_windows = length(get_data(load_ts)) num_forecast_steps = floor(Int, get_horizon(load_ts) / get_interval(load_ts)) total_steps = num_windows + num_forecast_steps - 1 dates = range( get_initial_timestamp(load_ts); step = get_interval(load_ts), length = total_steps, ) if use_thermal_max_power rg_data = fill(get_active_power_limits(unit1).max, total_steps) else rg_data = magnitude .* ones(total_steps) .+ random_variation .* rand(total_steps) end rg_ts = SingleTimeSeries("max_active_power", TimeArray(dates, rg_data)) add_time_series!(sys, rg1, rg_ts) transform_single_time_series!( sys, get_horizon(load_ts), get_interval(load_ts), ) end function replace_load_with_interruptible!(sys::System) @assert !isempty(get_components(PSY.PowerLoad, sys)) load1 = first(get_components(PSY.PowerLoad, sys)) interruptible_load = PSY.InterruptiblePowerLoad(; name = get_name(load1) * "_interruptible", bus = get_bus(load1), available = get_available(load1), active_power = get_active_power(load1), reactive_power = get_reactive_power(load1), max_active_power = get_max_active_power(load1), max_reactive_power = get_max_reactive_power(load1), operation_cost = PSY.LoadCost(nothing), base_power = get_base_power(load1), conformity = get_conformity(load1), ) add_component!(sys, interruptible_load) for ts_key in get_time_series_keys(load1) ts = get_time_series(load1, ts_key) add_time_series!( sys, interruptible_load, ts, ) end remove_component!(sys, load1) end # functions for adjusting power/cost curves and manipulating time series """ Helper function to tweak load powers, non-MBC generator powers, and non-MBC generator costs to exercise the generators we want to test. Multiplies {} for {} by {}: - max active power, all loads, load_pow_mult - active power limits, non-MBC ThermalStandard, therm_pow_mult - operational costs, non-MBC ThermalStandard, therm_price_mult """ function tweak_system!(sys::System, load_pow_mult, therm_pow_mult, therm_price_mult) for load in get_components(PowerLoad, sys) set_max_active_power!(load, get_max_active_power(load) * load_pow_mult) end # replace with type of component? for therm in get_components(ThermalStandard, sys) op_cost = get_operation_cost(therm) op_cost isa MarketBidCost && continue with_units_base(sys, UnitSystem.DEVICE_BASE) do old_limits = get_active_power_limits(therm) new_limits = (min = old_limits.min, max = old_limits.max * therm_pow_mult) set_active_power_limits!(therm, new_limits) end if get_variable(op_cost) isa CostCurve{LinearCurve} || get_variable(op_cost) isa CostCurve{QuadraticCurve} prop = get_proportional_term(get_value_curve(get_variable(op_cost))) set_variable!(op_cost, CostCurve(LinearCurve(prop * therm_price_mult))) elseif get_variable(op_cost) isa CostCurve{PiecewiseIncrementalCurve} pwl = get_value_curve(get_variable(op_cost)) new_pwl = PiecewiseIncrementalCurve( therm_price_mult * get_initial_input(pwl), get_x_coords(pwl), therm_price_mult * get_slopes(pwl), ) set_variable!(op_cost, CostCurve(new_pwl)) else error("Unhandled operation cost variable type $(typeof(get_variable(op_cost)))") end end end tweak_for_startup_shutdown!(sys::System) = tweak_system!(sys::System, 0.8, 1.0, 1.0) tweak_for_decremental_initial!(sys::PSY.System) = tweak_system!(sys, 1.0, 1.2, 0.5) """Transfer the market bid cost from old_comp to new_comp, copying any time series in the process.""" function transfer_mbc!( new_comp::PSY.Device, old_comp::PSY.Device, new_sys::PSY.System, ) mbc = deepcopy(get_operation_cost(old_comp)) @assert mbc isa PSY.MarketBidCost for field in fieldnames(PSY.MarketBidCost) val = getfield(mbc, field) if val isa IS.TimeSeriesKey ts = PSY.get_time_series(old_comp, val) new_ts_key = add_time_series!(new_sys, new_comp, deepcopy(ts)) setfield!(mbc, field, new_ts_key) end end set_operation_cost!(new_comp, mbc) return end function zero_out_startup_shutdown_costs!(comp::PSY.Device) op_cost = get_operation_cost(comp)::MarketBidCost set_start_up!(op_cost, (hot = 0.0, warm = 0.0, cold = 0.0)) set_shut_down!(op_cost, 0.0) end """Set everything except the incremental_offer_curves to zero on the MarketBidCost attached to the unit.""" function zero_out_non_incremental_curve!(sys::PSY.System, unit::PSY.Component) cost = deepcopy(get_operation_cost(unit)::MarketBidCost) set_no_load_cost!(cost, 0.0) set_start_up!(cost, (hot = 0.0, warm = 0.0, cold = 0.0)) set_shut_down!(cost, 0.0) # set minimum generation cost (but not min gen power) to zero. if get_incremental_offer_curves(cost) isa IS.TimeSeriesKey zero_ts = make_deterministic_ts(sys, "initial_input", 0.0, 0.0, 0.0) zero_ts_key = add_time_series!(sys, unit, zero_ts) set_incremental_initial_input!(cost, zero_ts_key) else base_curve = get_value_curve(get_incremental_offer_curves(cost)) x_coords = get_x_coords(base_curve) slopes = get_slopes(base_curve) new_curve = PiecewiseIncrementalCurve(0.0, x_coords, slopes) set_incremental_offer_curves!(cost, CostCurve(new_curve)) end set_operation_cost!(unit, cost) end "Set the no_load_cost to `nothing` and the initial_input to the old no_load_cost. Not designed for time series" function no_load_to_initial_input!(comp::Generator) cost = get_operation_cost(comp)::MarketBidCost no_load = PSY.get_no_load_cost(cost) old_fd = get_function_data( get_value_curve(get_incremental_offer_curves(get_operation_cost(comp))), )::IS.PiecewiseStepData new_vc = PiecewiseIncrementalCurve(old_fd, no_load, nothing) set_incremental_offer_curves!(get_operation_cost(comp), CostCurve(new_vc)) set_no_load_cost!(get_operation_cost(comp), nothing) return end no_load_to_initial_input!( sys::PSY.System, sel = make_selector(x -> get_operation_cost(x) isa MarketBidCost, Generator), ) = no_load_to_initial_input!.(get_components(sel, sys)) "Set all MBC thermal unit min active powers to their min breakpoints" function adjust_min_power!(sys) for comp in get_components(Union{ThermalStandard, ThermalMultiStart}, sys) op_cost = get_operation_cost(comp) op_cost isa MarketBidCost || continue cost_curve = get_incremental_offer_curves(op_cost)::CostCurve baseline = get_value_curve(cost_curve)::PiecewiseIncrementalCurve x_coords = get_x_coords(get_function_data(baseline)) with_units_base(sys, UnitSystem.NATURAL_UNITS) do set_active_power_limits!(comp, (min = first(x_coords), max = last(x_coords))) end end end """ Add startup and shutdown time series to a certain component. `with_increments`: whether the elements should be increasing over time or constant. Version A: designed for `c_fixed_market_bid_cost`. """ function add_startup_shutdown_ts_a!(sys::System, with_increments::Bool) res_incr = with_increments ? 0.05 : 0.0 interval_incr = with_increments ? 0.01 : 0.0 unit1 = get_component(ThermalStandard, sys, "Test Unit1") @assert get_operation_cost(unit1) isa MarketBidCost startup_ts_1 = make_deterministic_ts( sys, "start_up", (1.0, 1.5, 2.0), res_incr, interval_incr, ) set_start_up!(sys, unit1, startup_ts_1) shutdown_ts_1 = make_deterministic_ts(sys, "shut_down", 0.5, res_incr, interval_incr) set_shut_down!(sys, unit1, shutdown_ts_1) return startup_ts_1, shutdown_ts_1 end """ Add startup and shutdown time series to a certain component. `with_increments`: whether the elements should be increasing over time or constant. Version B: designed for `c_sys5_pglib`. """ function add_startup_shutdown_ts_b!(sys::System, with_increments::Bool) res_incr = with_increments ? 0.05 : 0.0 interval_incr = with_increments ? 0.01 : 0.0 unit1 = get_component(ThermalMultiStart, sys, "115_STEAM_1") base_startup = Tuple(get_start_up(get_operation_cost(unit1))) base_shutdown = get_shut_down(get_operation_cost(unit1)) @assert get_operation_cost(unit1) isa MarketBidCost startup_ts_1 = make_deterministic_ts( sys, "start_up", base_startup, res_incr, interval_incr, ) set_start_up!(sys, unit1, startup_ts_1) shutdown_ts_1 = make_deterministic_ts( sys, "shut_down", base_shutdown, res_incr, interval_incr, ) set_shut_down!(sys, unit1, shutdown_ts_1) return startup_ts_1, shutdown_ts_1 end # functions for building the systems: calls the above function load_and_fix_system(args...; kwargs...) sys = Logging.with_logger(Logging.NullLogger()) do build_system(args...; kwargs...) end no_load_to_initial_input!(sys) adjust_min_power!(sys) return sys end """Create a system with for testing fixed market bid costs on thermal get_components.""" function load_sys_incr() # NOTE we are using the fixed one so we can add time series ourselves sys = load_and_fix_system( PSITestSystems, "c_fixed_market_bid_cost", ) tweak_system!(sys, 1.05, 1.0, 1.0) get_y_coords( get_function_data( get_value_curve( get_incremental_offer_curves( get_operation_cost(get_component(ThermalStandard, sys, "Test Unit2")), ), ), ), )[1] *= 0.9 return sys end """ Create a system with initial input and variable cost time series. Lots of options: # Arguments: - `initial_varies`: whether the initial input time series should have values that vary over time (as opposed to a time series with constant values over time) - `breakpoints_vary`: whether the breakpoints in the variable cost time series should vary over time - `slopes_vary`: whether the slopes of the variable cost time series should vary over time - `modify_baseline_pwl`: optional, a function to modify the baseline piecewise linear cost `FunctionData` from which the variable cost time series is calculated - `do_override_min_x`: whether to override the P1 to be equal to the minimum power in all time steps - `create_extra_tranches`: whether to create extra tranches in some time steps by splitting one tranche into two - `active_components`: a `ComponentSelector` specifying which components should get time series - `initial_input_names_vary`: whether the initial input time series names should vary over components - `variable_cost_names_vary`: whether the variable cost time series names should vary over components """ function build_sys_incr( initial_varies::Bool, breakpoints_vary::Bool, slopes_vary::Bool; modify_baseline_pwl = nothing, do_override_min_x = true, create_extra_tranches = false, active_components = SEL_INCR, initial_input_names_vary = false, variable_cost_names_vary = false, ) sys = load_sys_incr() @assert !isempty(get_components(active_components, sys)) "No components selected" extend_mbc!( sys, active_components; initial_varies = initial_varies, breakpoints_vary = breakpoints_vary, slopes_vary = slopes_vary, modify_baseline_pwl = modify_baseline_pwl, do_override_min_x = do_override_min_x, create_extra_tranches = create_extra_tranches, initial_input_names_vary = initial_input_names_vary, variable_cost_names_vary = variable_cost_names_vary, ) return sys end function remove_thermal_mbcs!(sys::PSY.System) for comp in get_components(ThermalStandard, sys) old_cost = get_operation_cost(comp) old_cost isa MarketBidCost || continue new_op_cost = ThermalGenerationCost(; variable = get_incremental_offer_curves(old_cost), start_up = get_start_up(old_cost), shut_down = get_shut_down(old_cost), fixed = 0.0, ) set_operation_cost!(comp, new_op_cost) end end function zero_out_thermal_costs!(sys) for comp in get_components(ThermalStandard, sys) set_operation_cost!( comp, ThermalGenerationCost(; variable = CostCurve( LinearCurve(0.0), ), start_up = (hot = 0.0, warm = 0.0, cold = 0.0), shut_down = 0.0, fixed = 0.0, ), ) end end """Like `load_sys_incr` but for decremental MarketBidCost on ControllableLoad components.""" function load_sys_decr2() sys = load_and_fix_system( PSITestSystems, "c_fixed_market_bid_cost", ) replace_load_with_interruptible!(sys) interruptible_load = first(get_components(PSY.InterruptiblePowerLoad, sys)) selector = make_selector(PSY.InterruptiblePowerLoad, get_name(interruptible_load)) add_mbc!(sys, selector; incremental = false, decremental = true) # replace the MBCs on the thermals with ThermalCost objects. remove_thermal_mbcs!(sys) # makes the objective function/constraints simpler, easier to track down issues, # but not actually needed. zero_out_thermal_costs!(sys) return sys end """Like `build_sys_incr` but for decremental MarketBidCost on ControllableLoad components.""" function build_sys_decr2( initial_varies::Bool, breakpoints_vary::Bool, slopes_vary::Bool; modify_baseline_pwl = nothing, do_override_min_x = true, create_extra_tranches = false, active_components = SEL_DECR, initial_input_names_vary = false, variable_cost_names_vary = false, ) sys = load_sys_decr2() @assert !isempty(get_components(active_components, sys)) "No components selected" extend_mbc!( sys, active_components; initial_varies = initial_varies, breakpoints_vary = breakpoints_vary, slopes_vary = slopes_vary, modify_baseline_pwl = modify_baseline_pwl, do_override_min_x = do_override_min_x, create_extra_tranches = create_extra_tranches, initial_input_names_vary = initial_input_names_vary, variable_cost_names_vary = variable_cost_names_vary, ) # make the max_active_power time series constant. il = first(get_components(PSY.InterruptiblePowerLoad, sys)) for ts_key in get_time_series_keys(il) if get_name(ts_key) == "max_active_power" max_active_power_ts = get_time_series( first(get_components(PSY.InterruptiblePowerLoad, sys)), ts_key, ) max_max_active_power = maximum(maximum(values(max_active_power_ts.data))) remove_time_series!(sys, Deterministic, il, "max_active_power") new_ts = make_deterministic_ts( sys, "max_active_power", max_max_active_power, 0.0, 0.0, ) add_time_series!(sys, il, new_ts) break end end return sys end function create_multistart_sys( with_increments::Bool, load_pow_mult, therm_pow_mult, therm_price_mult; add_ts = true, ) @assert add_ts || !with_increments c_sys5_pglib = load_and_fix_system(PSITestSystems, "c_sys5_pglib") tweak_system!(c_sys5_pglib, load_pow_mult, therm_pow_mult, therm_price_mult) ms_comp = get_component(SEL_MULTISTART, c_sys5_pglib) old_op = get_operation_cost(ms_comp) old_ic = IncrementalCurve(get_value_curve(get_variable(old_op))) new_ii = get_initial_input(old_ic) + get_fixed(old_op) new_ic = IncrementalCurve(get_function_data(old_ic), new_ii, nothing) set_operation_cost!( ms_comp, MarketBidCost(; no_load_cost = nothing, start_up = (hot = 300.0, warm = 450.0, cold = 500.0), shut_down = 100.0, incremental_offer_curves = CostCurve(new_ic), ), ) add_ts && add_startup_shutdown_ts_b!(c_sys5_pglib, with_increments) return c_sys5_pglib end ================================================ FILE: test/test_utils/mock_operation_models.jl ================================================ # NOTE: None of the models and function in this file are functional. All of these are used for testing purposes and do not represent valid examples either to develop custom # models. Please refer to the documentation. struct MockOperationProblem <: PSI.DefaultDecisionProblem end struct MockEmulationProblem <: PSI.DefaultEmulationProblem end function PSI.DecisionModel( ::Type{MockOperationProblem}, ::Type{T}, sys::PSY.System; name = nothing, 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, settings, nothing; name = name, ) end 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_count) end return Deterministic(; name = "mock_forecast", data = timeseries_data, resolution = resolution, ) end function make_mock_singletimeseries(horizon, resolution) init_time = DateTime("2024-01-01") 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 function PSI.DecisionModel(::Type{MockOperationProblem}; name = nothing, kwargs...) sys = System(100.0) add_component!(sys, ACBus(nothing)) l = PowerLoad(nothing) gen = ThermalStandard(nothing) set_bus!(l, get_component(Bus, sys, "init")) set_bus!(gen, get_component(Bus, sys, "init")) add_component!(sys, l) add_component!(sys, gen) forecast = make_mock_forecast( 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, Hour(24)), resolution = get(kwargs, :resolution, Hour(1))) return DecisionModel{MockOperationProblem}( ProblemTemplate(CopperPlatePowerModel), sys, settings, nothing; name = name, ) end function PSI.EmulationModel(::Type{MockEmulationProblem}; name = nothing, kwargs...) sys = System(100.0) add_component!(sys, ACBus(nothing)) l = PowerLoad(nothing) gen = ThermalStandard(nothing) set_bus!(l, get_component(Bus, sys, "init")) set_bus!(gen, get_component(Bus, sys, "init")) add_component!(sys, l) add_component!(sys, gen) single_ts = make_mock_singletimeseries( get(kwargs, :horizon, Hour(24)), get(kwargs, :resolution, Hour(1)), ) add_time_series!(sys, l, single_ts) settings = PSI.Settings(sys; horizon = get(kwargs, :resolution, Hour(1)), resolution = get(kwargs, :resolution, Hour(1))) return EmulationModel{MockEmulationProblem}( ProblemTemplate(CopperPlatePowerModel), sys, settings, nothing; name = name, ) end # Only used for testing function mock_construct_device!( problem::PSI.DecisionModel{MockOperationProblem}, model; built_for_recurrent_solves = false, add_event_model = false, ) if add_event_model device_type = typeof(model).parameters[1] event_device = collect(get_components(device_type, PSI.get_system(problem)))[1] transition_data = PSY.FixedForcedOutage(; outage_status = 0.0) add_supplemental_attribute!(PSI.get_system(problem), event_device, transition_data) mock_event_key = PowerSimulations.EventKey{FixedForcedOutage, device_type}("") mock_event_model = EventModel( FixedForcedOutage, PSI.ContinuousCondition(), ) model.events = Dict(mock_event_key => mock_event_model) end 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.validate_template(problem) PSI.init_optimization_container!( PSI.get_optimization_container(problem), PSI.get_network_model(template), PSI.get_system(problem), ) PSI.get_network_model(template).subnetworks = PNM.find_subnetworks(PSI.get_system(problem)) PSI.get_optimization_container(problem).built_for_recurrent_solves = built_for_recurrent_solves PSI.initialize_system_expressions!( PSI.get_optimization_container(problem), PSI.get_network_model(template), PSI.get_network_model(template).subnetworks, PSI.get_branch_models(template), PSI.get_system(problem), Dict{Int64, Set{Int64}}(), ) PSI.construct_device!( PSI.get_optimization_container(problem), PSI.get_system(problem), PSI.ArgumentConstructStage(), model, PSI.get_network_model(template), ) PSI.construct_device!( PSI.get_optimization_container(problem), PSI.get_system(problem), PSI.ModelConstructStage(), model, PSI.get_network_model(template), ) PSI.check_optimization_container(PSI.get_optimization_container(problem)) JuMP.@objective( PSI.get_jump_model(problem), MOI.MIN_SENSE, PSI.get_objective_expression( PSI.get_optimization_container(problem).objective_function, ) ) return end function mock_construct_network!(problem::PSI.DecisionModel{MockOperationProblem}, model) PSI.set_network_model!(problem.template, model) PSI.construct_network!( PSI.get_optimization_container(problem), PSI.get_system(problem), model, problem.template.branches, ) return end function mock_uc_ed_simulation_problems(uc_horizon, ed_horizon) return SimulationModels([ DecisionModel(MockOperationProblem; horizon = uc_horizon, name = "UC"), DecisionModel( MockOperationProblem; horizon = ed_horizon, resolution = Minute(5), name = "ED", ), ]) end function create_simulation_build_test_problems( template_uc = get_template_standard_uc_simulation(), template_ed = get_template_nomin_ed_simulation(), sys_uc = PSB.build_system(PSITestSystems, "c_sys5_uc"), sys_ed = PSB.build_system(PSITestSystems, "c_sys5_ed"), ) return SimulationModels(; decision_models = [ DecisionModel(template_uc, sys_uc; name = "UC", optimizer = HiGHS_optimizer), DecisionModel(template_ed, sys_ed; name = "ED", optimizer = HiGHS_optimizer), ], ) end struct MockStagesStruct stages::Dict{Int, Int} end function Base.show(io::IO, struct_stages::MockStagesStruct) println(io, "mock problem") return end function setup_ic_model_container!(model::DecisionModel) # This function is only for testing purposes. if !PSI.isempty(model) PSI.reset!(model) end PSI.init_optimization_container!( PSI.get_optimization_container(model), PSI.get_network_model(PSI.get_template(model)), PSI.get_system(model), ) PSI.init_model_store_params!(model) @info "Make Initial Conditions Model" PSI.set_output_dir!(model, mktempdir(; cleanup = true)) PSI.build_initial_conditions!(model) PSI.initialize!(model) return end ================================================ FILE: test/test_utils/model_checks.jl ================================================ const GAEVF = JuMP.GenericAffExpr{Float64, VariableRef} const GQEVF = JuMP.GenericQuadExpr{Float64, VariableRef} function moi_tests( model::DecisionModel, vars::Int, interval::Int, lessthan::Int, greaterthan::Int, equalto::Int, binary::Bool, lessthan_quadratic::Union{Int, Nothing} = nothing, ) JuMPmodel = PSI.get_jump_model(model) @test JuMP.num_variables(JuMPmodel) == vars @test JuMP.num_constraints(JuMPmodel, GAEVF, MOI.Interval{Float64}) == interval @test JuMP.num_constraints(JuMPmodel, GAEVF, MOI.LessThan{Float64}) == lessthan @test JuMP.num_constraints(JuMPmodel, GAEVF, MOI.GreaterThan{Float64}) == greaterthan @test JuMP.num_constraints(JuMPmodel, GAEVF, MOI.EqualTo{Float64}) == equalto @test ((JuMP.VariableRef, MOI.ZeroOne) in JuMP.list_of_constraint_types(JuMPmodel)) == binary !isnothing(lessthan_quadratic) && @test JuMP.num_constraints(JuMPmodel, GQEVF, MOI.LessThan{Float64}) == lessthan_quadratic return end function psi_constraint_test( model::DecisionModel, constraint_keys::Vector{<:PSI.ConstraintKey}, ) constraints = PSI.get_constraints(model) for con in constraint_keys if get(constraints, con, nothing) !== nothing # Ensure constraint container does not have undefined entries: if typeof(constraints[con]) == DenseAxisArray @test all(x -> isassigned(constraints[con], x), eachindex(constraints[con])) else @test true end else @error con @test false end end return end function psi_aux_variable_test( model::DecisionModel, constraint_keys::Vector{<:PSI.AuxVarKey}, ) op_container = PSI.get_optimization_container(model) vars = PSI.get_aux_variables(op_container) for key in constraint_keys @test get(vars, key, nothing) !== nothing end return end function psi_checkbinvar_test( model::DecisionModel, bin_variable_keys::Vector{<:PSI.VariableKey}, ) container = PSI.get_optimization_container(model) for variable in bin_variable_keys for v in PSI.get_variable(container, variable) @test JuMP.is_binary(v) end end return end function psi_checkobjfun_test(model::DecisionModel, exp_type) model = PSI.get_jump_model(model) @test JuMP.objective_function_type(model) == exp_type return end 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 return end function psi_checksolve_test(model::DecisionModel, status) model = PSI.get_jump_model(model) JuMP.optimize!(model) @test termination_status(model) in status end function psi_checksolve_test(model::DecisionModel, status, expected_result, tol = 0.0) res = solve!(model) model = PSI.get_jump_model(model) @test termination_status(model) in status obj_value = JuMP.objective_value(model) @test isapprox(obj_value, expected_result, atol = tol) end function psi_ptdf_lmps(res::OptimizationProblemResults, ptdf) cp_duals = read_dual( res, PSI.ConstraintKey(CopperPlateBalanceConstraint, PSY.System); table_format = TableFormat.WIDE, ) λ = Matrix{Float64}(cp_duals[:, propertynames(cp_duals) .!= :DateTime]) flow_ub = read_dual( res, PSI.ConstraintKey(FlowRateConstraint, PSY.Line, "ub"); table_format = TableFormat.WIDE, ) flow_lb = read_dual( res, PSI.ConstraintKey(FlowRateConstraint, PSY.Line, "lb"); table_format = TableFormat.WIDE, ) arcs = PNM.get_arc_axis(ptdf) nr = PNM.get_network_reduction_data(ptdf) branch_names = [PSY.get_name(nr.direct_branch_map[arc]) for arc in arcs] μ = Matrix{Float64}(flow_ub[:, branch_names]) .+ Matrix{Float64}(flow_lb[:, branch_names]) buses = get_components(Bus, get_system(res)) lmps = OrderedDict() for bus in buses bus_number = get_number(bus) ptdf_col = [ptdf[arc, bus_number] for arc in arcs] lmps[get_name(bus)] = μ * ptdf_col end lmp = λ .+ DataFrames.DataFrame(lmps) return lmp[!, sort(propertynames(lmp))] end function check_variable_unbounded( model::DecisionModel, ::Type{T}, ::Type{U}, ) where {T <: PSI.VariableType, U <: PSY.Component} return check_variable_unbounded(model::DecisionModel, PSI.VariableKey(T, U)) end function check_variable_unbounded(model::DecisionModel, var_key::PSI.VariableKey) psi_cont = PSI.get_optimization_container(model) variable = PSI.get_variable(psi_cont, var_key) for var in variable if JuMP.has_lower_bound(var) || JuMP.has_upper_bound(var) return false end end return true end function check_variable_bounded( model::DecisionModel, ::Type{T}, ::Type{U}, ) where {T <: PSI.VariableType, U <: PSY.Component} return check_variable_bounded(model, PSI.VariableKey(T, U)) end function check_variable_bounded(model::DecisionModel, var_key::PSI.VariableKey) psi_cont = PSI.get_optimization_container(model) variable = PSI.get_variable(psi_cont, var_key) for var in variable if !JuMP.has_lower_bound(var) || !JuMP.has_upper_bound(var) return false end end return true end function check_flow_variable_values( model::DecisionModel, ::Type{T}, ::Type{U}, device_name::String, limit::Float64, ) where {T <: PSI.VariableType, U <: PSY.Component} psi_cont = PSI.get_optimization_container(model) variable = PSI.get_variable(psi_cont, T(), U) for var in variable[device_name, :] if !(PSI.jump_value(var) <= (limit + 1e-2)) @error "$device_name out of bounds $(PSI.jump_value(var))" return false end end return true end function check_flow_variable_values( model::DecisionModel, ::Type{T}, ::Type{U}, device_name::String, limit::Float64, ) where {T <: PSI.FlowActivePowerVariable, U <: PSY.Component} psi_cont = PSI.get_optimization_container(model) template = model.template device_model = PSI.get_model(template, U) dev_formulation = PSI.get_formulation(device_model) net_formulation = PSI.get_network_formulation(template) if dev_formulation <: Union{PSI.StaticBranch, PSI.StaticBranchUnbounded} && net_formulation <: PSI.PTDFPowerModel variable = PSI.get_expression(psi_cont, PSI.PTDFBranchFlow(), U) else variable = PSI.get_variable(psi_cont, T(), U) end for var in variable[device_name, :] if !(PSI.jump_value(var) <= (limit + 1e-2)) @error "$device_name out of bounds $(PSI.jump_value(var))" return false end end return true end function check_flow_variable_values( model::DecisionModel, ::Type{T}, ::Type{U}, device_name::String, limit_min::Float64, limit_max::Float64, ) where {T <: PSI.VariableType, U <: PSY.Component} psi_cont = PSI.get_optimization_container(model) variable = PSI.get_variable(psi_cont, T(), U) for var in variable[device_name, :] if !(PSI.jump_value(var) <= (limit_max + 1e-2)) || !(PSI.jump_value(var) >= (limit_min - 1e-2)) return false end end return true end function check_flow_variable_values( model::DecisionModel, ::Type{T}, ::Type{U}, device_name::String, limit_min::Float64, limit_max::Float64, ) where {T <: PSI.FlowActivePowerVariable, U <: PSY.Component} psi_cont = PSI.get_optimization_container(model) template = model.template device_model = PSI.get_model(template, U) dev_formulation = PSI.get_formulation(device_model) net_formulation = PSI.get_network_formulation(template) if dev_formulation <: Union{PSI.StaticBranch, PSI.StaticBranchUnbounded} && net_formulation <: PSI.PTDFPowerModel variable = PSI.get_expression(psi_cont, PSI.PTDFBranchFlow(), U) else variable = PSI.get_variable(psi_cont, T(), U) end for var in variable[device_name, :] if !(PSI.jump_value(var) <= (limit_max + 1e-2)) || !(PSI.jump_value(var) >= (limit_min - 1e-2)) return false end end return true end function check_flow_variable_values( model::DecisionModel, ::Type{T}, ::Type{U}, ::Type{V}, device_name::String, limit_min::Float64, limit_max::Float64, ) where {T <: PSI.VariableType, U <: PSI.VariableType, V <: PSY.Component} psi_cont = PSI.get_optimization_container(model) time_steps = PSI.get_time_steps(psi_cont) pvariable = PSI.get_variable(psi_cont, T(), V) qvariable = PSI.get_variable(psi_cont, U(), V) for t in time_steps fp = PSI.jump_value(pvariable[device_name, t]) fq = PSI.jump_value(qvariable[device_name, t]) flow = sqrt((fp)^2 + (fq)^2) if !(flow <= (limit_max + 1e-2)^2) || !(flow >= (limit_min - 1e-2)^2) return false end end return true end function check_flow_variable_values( model::DecisionModel, ::Type{T}, ::Type{U}, ::Type{V}, device_name::String, limit::Float64, ) where {T <: PSI.VariableType, U <: PSI.VariableType, V <: PSY.Component} psi_cont = PSI.get_optimization_container(model) time_steps = PSI.get_time_steps(psi_cont) pvariable = PSI.get_variable(psi_cont, T(), V) qvariable = PSI.get_variable(psi_cont, U(), V) for t in time_steps fp = PSI.jump_value(pvariable[device_name, t]) fq = PSI.jump_value(qvariable[device_name, t]) flow = sqrt((fp)^2 + (fq)^2) if !(flow <= (limit + 1e-2)^2) return false end end return true end function PSI.jump_value(int::Int) @warn("This is for testing purposes only.") return int end function _check_constraint_bounds(bounds::PSI.ConstraintBounds, valid_bounds::NamedTuple) @test bounds.coefficient.min == valid_bounds.coefficient.min @test bounds.coefficient.max == valid_bounds.coefficient.max @test bounds.rhs.min == valid_bounds.rhs.min @test bounds.rhs.max == valid_bounds.rhs.max end function _check_variable_bounds(bounds::PSI.VariableBounds, valid_bounds::NamedTuple) @test bounds.bounds.min == valid_bounds.min @test bounds.bounds.max == valid_bounds.max end function check_duration_on_initial_conditions_values( model, ::Type{T}, ) where {T <: PSY.Component} initial_conditions_data = PSI.get_initial_conditions_data(PSI.get_optimization_container(model)) duration_on_data = PSI.get_initial_condition( PSI.get_optimization_container(model), InitialTimeDurationOn(), T, ) for ic in duration_on_data name = PSY.get_name(ic.component) on_var = PSI.get_initial_condition_value(initial_conditions_data, OnVariable(), T)[ name, 1, ] duration_on = PSI.jump_value(PSI.get_value(ic)) if on_var == 1.0 && PSY.get_status(ic.component) @test duration_on == PSY.get_time_at_status(ic.component) elseif on_var == 1.0 && !PSY.get_status(ic.component) @test duration_on == 0.0 end end end function check_duration_off_initial_conditions_values( model, ::Type{T}, ) where {T <: PSY.Component} initial_conditions_data = PSI.get_initial_conditions_data(PSI.get_optimization_container(model)) duration_off_data = PSI.get_initial_condition( PSI.get_optimization_container(model), InitialTimeDurationOff(), T, ) for ic in duration_off_data name = PSY.get_name(ic.component) on_var = PSI.get_initial_condition_value(initial_conditions_data, OnVariable(), T)[ name, 1, ] duration_off = PSI.jump_value(PSI.get_value(ic)) if on_var == 0.0 && !PSY.get_status(ic.component) @test duration_off == PSY.get_time_at_status(ic.component) elseif on_var == 0.0 && PSY.get_status(ic.component) @test duration_off == 0.0 end end end function check_energy_initial_conditions_values(model, ::Type{T}) where {T <: PSY.Component} ic_data = PSI.get_initial_condition( PSI.get_optimization_container(model), InitialEnergyLevel(), 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_storage_capacity_level(d) * PSY.get_storage_capacity(d) * PSY.get_conversion_factor(d) == e_value end end function check_energy_initial_conditions_values(model, ::Type{T}) where {T <: PSY.HydroGen} ic_data = PSI.get_initial_condition( PSI.get_optimization_container(model), InitialEnergyLevel(), T, ) for ic in ic_data name = PSY.get_name(ic.component) e_value = PSI.jump_value(PSI.get_value(ic)) @test PSY.get_initial_storage(ic.component) == e_value end end function check_status_initial_conditions_values(model, ::Type{T}) where {T <: PSY.Component} initial_conditions = PSI.get_initial_condition(PSI.get_optimization_container(model), DeviceStatus(), T) initial_conditions_data = PSI.get_initial_conditions_data(PSI.get_optimization_container(model)) for ic in initial_conditions name = PSY.get_name(ic.component) status = PSI.get_initial_condition_value(initial_conditions_data, OnVariable(), T)[ name, 1, ] @test PSI.jump_value(PSI.get_value(ic)) == status end end function check_active_power_initial_condition_values( model, ::Type{T}, ) where {T <: PSY.Component} initial_conditions = PSI.get_initial_condition(PSI.get_optimization_container(model), DevicePower(), T) initial_conditions_data = PSI.get_initial_conditions_data(PSI.get_optimization_container(model)) for ic in initial_conditions name = PSY.get_name(ic.component) power = PSI.get_initial_condition_value( initial_conditions_data, ActivePowerVariable(), T, )[ name, 1, ] @test PSI.jump_value(PSI.get_value(ic)) == power end end function check_active_power_abovemin_initial_condition_values( model, ::Type{T}, ) where {T <: PSY.Component} initial_conditions = PSI.get_initial_condition( PSI.get_optimization_container(model), PSI.DeviceAboveMinPower(), T, ) initial_conditions_data = PSI.get_initial_conditions_data(PSI.get_optimization_container(model)) for ic in initial_conditions name = PSY.get_name(ic.component) power = PSI.get_initial_condition_value( initial_conditions_data, PSI.PowerAboveMinimumVariable(), T, )[ name, 1, ] @test PSI.jump_value(PSI.get_value(ic)) == power end end function check_initialization_variable_count( model, ::S, ::Type{T}, ) where {S <: PSI.VariableType, T <: PSY.Component} container = PSI.get_optimization_container(model) initial_conditions_data = PSI.get_initial_conditions_data(container) 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_COUNT end function check_variable_count( model, ::S, ::Type{T}, ) where {S <: PSI.VariableType, T <: PSY.Component} no_component = length(PSY.get_components(PSY.get_available, T, model.sys)) time_steps = PSI.get_time_steps(PSI.get_optimization_container(model))[end] variable = PSI.get_variable(PSI.get_optimization_container(model), S(), T) @test length(variable) == no_component * time_steps end function check_initialization_constraint_count( model, ::S, ::Type{T}; filter_func = PSY.get_available, meta = PSI.ISOPT.CONTAINER_KEY_EMPTY_META, ) where {S <: PSI.ConstraintType, T <: PSY.Component} container = ISOPT.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) @test length(constraint) == no_component * time_steps end function check_constraint_count( model, ::S, ::Type{T}; filter_func = PSY.get_available, meta = PSI.ISOPT.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] constraint = PSI.get_constraint(PSI.get_optimization_container(model), S(), T, meta) @test length(constraint) == no_component * time_steps end function check_constraint_count( model, ::PSI.RampConstraint, ::Type{T}, ) where {T <: PSY.Component} container = PSI.get_optimization_container(model) device_name_set = PSY.get_name.( PSI._get_ramp_constraint_devices( container, get_components(PSY.get_available, T, model.sys), ), ) check_constraint_count( model, PSI.RampConstraint(), T; meta = "up", filter_func = x -> x.name in device_name_set, ) check_constraint_count( model, PSI.RampConstraint(), T; meta = "dn", filter_func = x -> x.name in device_name_set, ) return end function check_constraint_count( model, ::PSI.DurationConstraint, ::Type{T}, ) where {T <: PSY.Component} container = PSI.get_optimization_container(model) resolution = PSI.get_resolution(container) steps_per_hour = 60 / Dates.value(Dates.Minute(resolution)) fraction_of_hour = 1 / steps_per_hour duration_devices = filter!( x -> !( PSY.get_time_limits(x).up <= fraction_of_hour && PSY.get_time_limits(x).down <= fraction_of_hour ), collect(get_components(PSY.get_available, T, model.sys)), ) device_name_set = PSY.get_name.(duration_devices) check_constraint_count( model, PSI.DurationConstraint(), T; meta = "up", filter_func = x -> x.name in device_name_set, ) return check_constraint_count( model, PSI.DurationConstraint(), T; meta = "dn", filter_func = x -> x.name in device_name_set, ) end ================================================ FILE: test/test_utils/operations_problem_templates.jl ================================================ 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), ] function get_thermal_standard_uc_template() template = ProblemTemplate(CopperPlatePowerModel) set_device_model!(template, PowerLoad, StaticPowerLoad) set_device_model!(template, ThermalStandard, ThermalStandardUnitCommitment) return template end function get_thermal_dispatch_template_network(network = CopperPlatePowerModel) template = ProblemTemplate(network) set_device_model!(template, ThermalStandard, ThermalBasicDispatch) set_device_model!(template, PowerLoad, StaticPowerLoad) set_device_model!(template, MonitoredLine, StaticBranchBounds) set_device_model!(template, Line, StaticBranch) set_device_model!(template, Transformer2W, StaticBranch) set_device_model!(template, TapTransformer, StaticBranch) set_device_model!(template, TwoTerminalGenericHVDCLine, HVDCTwoTerminalLossless) return template end function get_template_basic_uc_simulation() template = ProblemTemplate(CopperPlatePowerModel) set_device_model!(template, ThermalStandard, ThermalBasicUnitCommitment) set_device_model!(template, RenewableDispatch, RenewableFullDispatch) set_device_model!(template, PowerLoad, StaticPowerLoad) set_device_model!(template, InterruptiblePowerLoad, StaticPowerLoad) set_device_model!(template, HydroTurbine, HydroTurbineEnergyDispatch) set_device_model!(template, HydroReservoir, HydroEnergyModelReservoir) return template end function get_template_standard_uc_simulation() template = get_template_basic_uc_simulation() set_device_model!(template, ThermalStandard, ThermalStandardUnitCommitment) return template end function get_template_nomin_ed_simulation(network = CopperPlatePowerModel) template = ProblemTemplate(network) set_device_model!(template, ThermalStandard, ThermalDispatchNoMin) set_device_model!(template, RenewableDispatch, RenewableFullDispatch) set_device_model!(template, PowerLoad, StaticPowerLoad) set_device_model!(template, InterruptiblePowerLoad, PowerLoadDispatch) set_device_model!(template, HydroTurbine, HydroTurbineEnergyDispatch) set_device_model!(template, HydroReservoir, HydroEnergyModelReservoir) return template end function get_template_hydro_st_uc(network = CopperPlatePowerModel) template = ProblemTemplate(network) set_device_model!(template, ThermalStandard, ThermalStandardUnitCommitment), set_device_model!(template, RenewableDispatch, RenewableFullDispatch), set_device_model!(template, PowerLoad, StaticPowerLoad), set_device_model!(template, InterruptiblePowerLoad, PowerLoadDispatch), set_device_model!(template, HydroTurbine, HydroTurbineEnergyDispatch) set_device_model!(template, HydroReservoir, HydroEnergyModelReservoir) return template end function get_template_hydro_st_ed(network = CopperPlatePowerModel, duals = []) template = ProblemTemplate(network) set_device_model!(template, ThermalStandard, ThermalBasicDispatch) set_device_model!(template, RenewableDispatch, RenewableFullDispatch) set_device_model!(template, PowerLoad, StaticPowerLoad) set_device_model!(template, InterruptiblePowerLoad, PowerLoadDispatch) set_device_model!(template, HydroTurbine, HydroTurbineEnergyDispatch) set_device_model!(template, HydroReservoir, HydroEnergyModelReservoir) return template end function get_template_dispatch_with_network(network = PTDFPowerModel) template = ProblemTemplate(network) set_device_model!(template, PowerLoad, StaticPowerLoad) set_device_model!(template, ThermalStandard, ThermalBasicDispatch) set_device_model!(template, Line, StaticBranch) set_device_model!(template, Transformer2W, StaticBranchBounds) set_device_model!(template, TapTransformer, StaticBranchBounds) set_device_model!(template, TwoTerminalGenericHVDCLine, HVDCTwoTerminalLossless) return template end ================================================ FILE: test/test_utils/run_simulation.jl ================================================ function load_pf_export(root, export_subdir) raw_path, md_path = get_psse_export_paths(export_subdir) sys = System(joinpath(root, raw_path), JSON3.read(joinpath(root, md_path), Dict)) set_units_base_system!(sys, "NATURAL_UNITS") return sys end function run_simulation( c_sys5_hy_uc, c_sys5_hy_ed, file_path::String, export_path; in_memory = false, uc_network_model = nothing, ed_network_model = nothing, store_systems_in_results = true, ) template_uc = get_template_basic_uc_simulation() template_ed = get_template_nomin_ed_simulation() isnothing(uc_network_model) && ( uc_network_model = NetworkModel(CopperPlatePowerModel; duals = [CopperPlateBalanceConstraint]) ) isnothing(ed_network_model) && ( ed_network_model = NetworkModel( CopperPlatePowerModel; duals = [CopperPlateBalanceConstraint], use_slacks = true, ) ) set_device_model!(template_ed, InterruptiblePowerLoad, StaticPowerLoad) set_network_model!( template_uc, uc_network_model, ) set_network_model!( template_ed, ed_network_model, ) models = SimulationModels(; decision_models = [ DecisionModel( template_uc, c_sys5_hy_uc; name = "UC", optimizer = HiGHS_optimizer, ), DecisionModel( template_ed, c_sys5_hy_ed; name = "ED", optimizer = ipopt_optimizer, ), ], ) sequence = SimulationSequence(; models = models, feedforwards = Dict( "ED" => [ SemiContinuousFeedforward(; component_type = ThermalStandard, source = OnVariable, affected_values = [ActivePowerVariable], ), ], ), ini_cond_chronology = InterProblemChronology(), ) sim = Simulation(; name = "no_cache", steps = 2, models = models, sequence = sequence, simulation_folder = file_path, ) build_out = build!( sim; console_level = Logging.Error, store_systems_in_results = store_systems_in_results, ) @test build_out == PSI.SimulationBuildStatus.BUILT exports = Dict( "models" => [ Dict( "name" => "UC", "store_all_variables" => true, "store_all_parameters" => true, "store_all_duals" => true, "store_all_aux_variables" => true, ), Dict( "name" => "ED", "store_all_variables" => true, "store_all_parameters" => true, "store_all_duals" => true, "store_all_aux_variables" => true, ), ], "path" => export_path, "optimizer_stats" => true, ) execute_out = execute!(sim; exports = exports, in_memory = in_memory) @test execute_out == PSI.RunStatus.SUCCESSFULLY_FINALIZED return sim end ================================================ FILE: test/test_utils/solver_definitions.jl ================================================ # Solvers using Ipopt using SCS using HiGHS ipopt_optimizer = JuMP.optimizer_with_attributes(Ipopt.Optimizer, "tol" => 1e-6, "print_level" => 0) fast_ipopt_optimizer = JuMP.optimizer_with_attributes( Ipopt.Optimizer, "print_level" => 0, "max_cpu_time" => 5.0, ) # use default print_level = 5 # set to 0 to disable scs_solver = JuMP.optimizer_with_attributes( SCS.Optimizer, "max_iters" => 100000, "eps_infeas" => 1e-4, "verbose" => 0, ) HiGHS_optimizer = JuMP.optimizer_with_attributes( HiGHS.Optimizer, "time_limit" => 100.0, "random_seed" => 12345, "log_to_console" => false, ) HiGHS_optimizer_small_gap = JuMP.optimizer_with_attributes( HiGHS.Optimizer, "time_limit" => 100.0, "random_seed" => 12345, "mip_rel_gap" => 0.001, "log_to_console" => false, ) ================================================ FILE: test/test_utils.jl ================================================ @testset "test key with value" begin d = Dict("foo" => "bar") @test_throws ErrorException PSI.find_key_with_value(d, "fake") end @testset "Test ProgressMeter" begin @test !PSI.progress_meter_enabled() end @testset "Axis Array to DataFrame" begin # The to_dataframe test the use of the `to_matrix` and `get_column_names` methods one = PSI.DenseAxisArray{Float64}(undef, 1:2) fill!(one, 1.0) mock_key = PSI.VariableKey(ActivePowerVariable, ThermalStandard) one_df = PSI.to_dataframe(one, mock_key) test_df = DataFrames.DataFrame(ISOPT.encode_key(mock_key) => [1.0, 1.0]) @test one_df == test_df two = PSI.DenseAxisArray{Float64}(undef, ["a"], 1:2) fill!(two, 1.0) two_df = PSI.to_dataframe(two, mock_key) test_df = DataFrames.DataFrame(:a => [1.0, 1.0]) @test two_df == test_df three = PSI.DenseAxisArray{Float64}(undef, ["a"], 1:2, 1:3) fill!(three, 1.0) @test_throws MethodError PSI.to_dataframe(three, mock_key) four = PSI.DenseAxisArray{Float64}(undef, ["a"], 1:2, 1:3, 1:5) fill!(three, 1.0) @test_throws MethodError PSI.to_dataframe(four, mock_key) sparse_num = JuMP.Containers.@container([i = 1:10, j = (i + 1):10, t = 1:24], 0.0 + i + j + t) @test_throws MethodError PSI.to_dataframe(sparse_num, mock_key) i_num = 1:10 j_vals = Dict("$i" => string.((i + 1):11) for i in i_num) sparse_valid = JuMP.Containers.@container([i = string.(i_num), j = j_vals[i], t = 1:24], rand()) df = PSI.to_dataframe(sparse_valid, mock_key) @test size(df) == (24, 55) end @testset "Test simulation output directory name" begin tmpdir = mktempdir() name = "simulation" dir1 = mkdir(joinpath(tmpdir, name)) @test PSI._get_output_dir_name(tmpdir, name) == joinpath(tmpdir, name * "-2") dir2 = mkdir(joinpath(tmpdir, name * "-2")) @test PSI._get_output_dir_name(tmpdir, name) == joinpath(tmpdir, name * "-3") end