[
  {
    "path": ".claude/Sienna.md",
    "content": "# Sienna Programming Practices\n\nThis document describes general programming practices and conventions that apply across all Sienna packages (PowerSystems.jl, PowerSimulations.jl, PowerFlows.jl, PowerNetworkMatrices.jl, InfrastructureSystems.jl, etc.).\n\n## Performance Requirements\n\n**Priority:** Critical. See the [Julia Performance Tips](https://docs.julialang.org/en/v1/manual/performance-tips/).\n\n### Anti-Patterns to Avoid\n\n#### Type instability\n\nFunctions must return consistent concrete types. Check with `@code_warntype`.\n\n  - Bad: `f(x) = x > 0 ? 1 : 1.0`\n  - Good: `f(x) = x > 0 ? 1.0 : 1.0`\n\n#### Abstract field types\n\nStruct fields must have concrete types or be parameterized.\n\n  - Bad: `struct Foo; data::AbstractVector; end`\n  - Good: `struct Foo{T<:AbstractVector}; data::T; end`\n\n#### Untyped containers\n\n  - Bad: `Vector{Any}()`, `Vector{Real}()`\n  - Good: `Vector{Float64}()`, `Vector{Int}()`\n\n#### Non-const globals\n\n  - Bad: `THRESHOLD = 0.5`\n  - Good: `const THRESHOLD = 0.5`\n\n#### Unnecessary allocations\n\n  - Use views instead of copies (`@view`, `@views`)\n  - Pre-allocate arrays instead of `push!` in loops\n  - Use in-place operations (functions ending with `!`)\n\n#### Captured variables\n\nAvoid 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).\n\n#### Splatting penalty\n\nAvoid splatting (`...`) in performance-critical code. Splatting containers with known length at compile time (e.g., `Tuple`) is acceptable.\n\n#### Abstract return types\n\nIn 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.\n\n#### Using `isa` in function logic\n\n**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.\n\n  - Bad: `if x isa Float64 ... elseif x isa Int ... end`\n  - Good: Use multiple dispatch with specific type signatures\n  - Bad: `function f(x); if x isa AbstractVector return sum(x) else return x end; end`\n  - Good: `f(x::AbstractVector) = sum(x); f(x::Number) = x`\n\n**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.\n\n### Best Practices\n\n  - Use `@inbounds` when bounds are verified\n  - Use broadcasting (dot syntax) for element-wise operations\n  - Avoid `try-catch` in hot paths\n  - Use function barriers to isolate type instability\n\n> Apply these guidelines with judgment. Not every function is performance-critical. Focus optimization efforts on hot paths and frequently called code.\n\n## Code Conventions\n\nStyle guide: [https://sienna-platform.github.io/InfrastructureSystems.jl/stable/style/](https://sienna-platform.github.io/InfrastructureSystems.jl/stable/style/)\n\nFormatter (JuliaFormatter): Use the formatter script provided in each package.\n\nKey rules:\n\n  - Constructors: use `function Foo()` not `Foo() = ...`\n  - Asserts: prefer `InfrastructureSystems.@assert_op` over `@assert`\n  - Globals: `UPPER_CASE` for constants\n  - Exports: all exports in main module file\n  - Comments: complete sentences, describe why not how\n\n## Documentation Practices and Requirements\n\nFramework: [Diataxis](https://diataxis.fr/)\n\nSienna 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/)\n\nDocstring requirements:\n\n  - Scope: all elements of public interface (IS is selective about exports)\n  - Include: function signatures and arguments list\n  - Automation: `DocStringExtensions.TYPEDSIGNATURES` (`TYPEDFIELDS` used sparingly in IS)\n  - See also: add links for functions with same name (multiple dispatch)\n\nAPI docs:\n\n  - Public: typically in `docs/src/api/public.md` using `@autodocs` with `Public=true, Private=false`\n  - Internals: typically in `docs/src/api/internals.md`\n\n## Design Principles\n\n  - Elegance and concision in both interface and implementation\n  - Fail fast with actionable error messages rather than hiding problems\n  - Validate invariants explicitly in subtle cases\n  - Avoid over-adherence to backwards compatibility for internal helpers\n\n## Contribution Workflow\n\nBranch naming: `feature/description` or `fix/description`\n\n 1. Create feature branch\n 2. Follow style guide and run formatter\n 3. Ensure tests pass\n 4. Submit pull request\n\n## AI Agent Guidance\n\n**Key priorities:** Read existing patterns first, maintain consistency, use concrete types in hot paths, run formatter, add docstrings to public API, ensure tests pass.\n\n**Critical rules:**\n\n  - Always use `julia --project=<env>` (never bare `julia`)\n  - **NEVER use `isa` in function logic** — use multiple dispatch instead. This is absolutely forbidden unless the user explicitly asks for it.\n  - Never edit auto-generated files directly\n  - Verify type stability with `@code_warntype` for performance-critical code\n  - Consider downstream package impact\n\n## Julia Environment Best Practices\n\n**CRITICAL:** Always use `julia --project=<env>` 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.\n\nCommon patterns:\n\n```sh\n# Run tests (using test environment)\njulia --project=test test/runtests.jl\n\n# Run specific test\njulia --project=test test/runtests.jl test_file_name\n\n# Run expression\njulia --project=test -e 'using PackageName; ...'\n\n# Instantiate environment\njulia --project=test -e 'using Pkg; Pkg.instantiate()'\n\n# Build docs (using docs environment)\njulia --project=docs docs/make.jl\n```\n\n**Why this matters:** Running without `--project=<env>` 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.\n\n## Troubleshooting\n\n**Type instability**\n\n  - Symptom: Poor performance, many allocations\n  - Diagnosis: `@code_warntype` on suspect function\n  - Solution: See performance anti-patterns above\n\n**Formatter fails**\n\n  - Symptom: Formatter command returns error\n  - Solution: Run the formatter script provided in the package (e.g., `julia -e 'include(\"scripts/formatter/formatter_code.jl\")'`)\n\n**Test failures**\n\n  - Symptom: Tests fail unexpectedly\n  - Solution: `julia --project=test -e 'using Pkg; Pkg.instantiate()'`\n"
  },
  {
    "path": ".claude/claude.md",
    "content": "# PowerSimulations.jl\n\nPower 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`.\n\n> **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.\n\n## Core Architecture\n\n### Operation Models\n\nThe central abstraction is `OperationModel`, with two concrete types:\n\n  - **`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.\n  - **`EmulationModel{M <: EmulationProblem}`** — Simulates real-time operation with a single time-step horizon. Used for AGC, reserve deployment, and similar fast-timescale problems.\n\nBuilt-in problem types: `GenericOpProblem`, `UnitCommitmentProblem`, `EconomicDispatchProblem`, `AGCReserveDeployment`.\n\n### ProblemTemplate\n\nDefines what a model contains — its network representation and which device/service formulations to use:\n\n```julia\ntemplate = ProblemTemplate(NetworkModel(CopperPlatePowerModel))\nset_device_model!(template, ThermalStandard, ThermalBasicUnitCommitment)\nset_device_model!(template, RenewableDispatch, RenewableFullDispatch)\nset_service_model!(template, VariableReserve{ReserveUp}, RangeReserve)\n```\n\n### Device, Service, and Network Models\n\nThese types bind a PowerSystems component type to a formulation:\n\n  - **`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.\n  - **`ServiceModel{D <: PSY.Service, B <: AbstractServiceFormulation}`** — Same pattern for ancillary services (reserves, AGC).\n  - **`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.\n\n### Formulation Hierarchy\n\nFormulations are organized by device category. The formulation type controls what gets built:\n\n  - **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.\n  - **Renewable**: `RenewableFullDispatch`, `RenewableConstantPowerFactor`\n  - **Load**: `StaticPowerLoad`, `PowerLoadInterruption`, `PowerLoadDispatch`\n  - **Storage**: `BookKeeping`, `BatteryAncillaryServices`\n  - **Branches**: `StaticBranch`, `StaticBranchBounds`, `StaticBranchUnbounded`, `HVDCTwoTerminalDispatch`\n\n### OptimizationContainer\n\nWraps the JuMP model and holds all optimization artifacts in typed containers:\n\n  - **Variables** — decision variables indexed by device and time\n  - **Constraints** — constraint references\n  - **Parameters** — time-varying data (time series, feedforward values) stored as parameter containers\n  - **Expressions** — reusable expressions (e.g., nodal balance) that multiple devices contribute to\n  - **Objective function** — cost components\n\n## Simulation Architecture\n\n### Simulation\n\nOrchestrates multi-model runs across time. A `Simulation` contains:\n\n  - **`SimulationModels`** — Container holding a vector of `DecisionModel`s and an optional `EmulationModel`\n  - **`SimulationSequence`** — Defines execution order, feedforward connections between models, and initial condition chronologies\n  - **`SimulationState`** — Tracks evolving state across the simulation timeline\n\n### SimulationState\n\nMaintains state that flows between models and across time steps:\n\n```\nSimulationState\n├── current_time::Ref{DateTime}          # Current simulation clock\n├── last_decision_model::Ref{Symbol}     # Which model ran last\n├── decision_states::DatasetContainer    # Outputs from decision models\n└── system_states::DatasetContainer      # Actual system state evolution\n```\n\nAfter 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.\n\n### Feedforward Mechanism\n\nFeedforwards transfer values between models in a simulation sequence. They parameterize a downstream model using results from an upstream model:\n\n  - **`UpperBoundFeedforward`** — Constrains variables with upper bounds from source\n  - **`LowerBoundFeedforward`** — Constrains variables with lower bounds from source\n  - **`SemiContinuousFeedforward`** — Passes binary on/off status\n  - **`FixValueFeedforward`** — Fixes variable values from source results\n\nEach feedforward specifies a source model, source variable, and affected component/variable in the target model.\n\n### Initial Conditions\n\nState carried between time steps within or across models:\n\n  - `DevicePower` — Previous generation level\n  - `DeviceStatus` — On/off status\n  - `InitialTimeDurationOn/Off` — Time in current state\n  - `InitialEnergyLevel` — Storage state-of-charge\n  - `AreaControlError` — AGC error state\n\nChronologies control how initial conditions are sourced: `InterProblemChronology` (from a different model's results) or `IntraProblemChronology` (from the same model's previous solve).\n\n### Simulation Execution Loop\n\n 1. Read current state from `SimulationState`\n 2. Update feedforward parameters in the current model from upstream results\n 3. Update initial conditions from state\n 4. Solve the model (`JuMP.optimize!`)\n 5. Write results to `SimulationState` and results store (HDF5 or in-memory)\n 6. Advance to next model in sequence; repeat\n\n## Directory Structure\n\n```\nsrc/\n├── core/                          # Core types: OptimizationContainer, DeviceModel,\n│                                  #   NetworkModel, ServiceModel, formulations,\n│                                  #   variable/constraint/parameter type definitions\n├── operation/                     # DecisionModel, EmulationModel, ProblemTemplate,\n│                                  #   built-in problem templates, model build/solve logic\n├── simulation/                    # Simulation, SimulationModels, SimulationSequence,\n│                                  #   SimulationState, results storage (HDF5, in-memory)\n├── devices_models/\n│   ├── devices/                   # Per-device-type implementations (thermal, renewable,\n│   │                              #   loads, branches, HVDC, storage)\n│   └── device_constructors/       # Build functions that add variables, constraints,\n│                                  #   parameters to OptimizationContainer per formulation\n├── network_models/                # CopperPlate, PTDF, AreaBalance, PowerModels interface\n├── services_models/               # Reserve and transmission interface implementations\n├── feedforward/                   # Feedforward types, argument setup, constraint builders\n├── initial_conditions/            # IC types, chronologies, update logic\n└── parameters/                    # Parameter update mechanisms for time series and state\n```\n\n## Build Flow\n\nWhen `build!(model, system)` is called:\n\n 1. Template specifies device models, service models, and network model\n 2. For each `DeviceModel`, the formulation type dispatches to device-specific constructors that add variables, constraints, parameters, and expressions to the `OptimizationContainer`\n 3. Network model adds power balance and flow constraints; devices contribute to shared nodal balance expressions\n 4. Service models add reserve variables and participation constraints\n 5. Feedforwards (if in simulation context) add linking constraints/parameters\n 6. Objective function assembled from cost components\n 7. Result: a complete JuMP optimization model ready to solve\n"
  },
  {
    "path": ".devcontainer/devcontainer.json",
    "content": "{\n    \"extensions\": [\n        \"julialang.language-julia\"\n    ],\n    \"image\": \"ghcr.io/julia-vscode/julia-devcontainer\"\n}\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "content": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: ''\nlabels: code bug\nassignees: ''\n\n---\n\n**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)**\nOpen a bug report only if you can provide the details below\n\n**Describe the bug**\nA clear and concise description of what the bug is.\n\n**To Reproduce**\nSteps to reproduce the behavior:\nPaste the code we can run to reproduce the error you are seeing\n\n**Expected behavior**\nA clear and concise description of what you expected to happen.\n\n**Screenshots**\nIf applicable, add screenshots to help explain your problem.\n\n**Additional context**\nAdd any other context about the problem here.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "content": "---\nname: Feature request\nabout: Suggest an idea for this project\ntitle: ''\nlabels: feature request\nassignees: ''\n\n---\n\n**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)**\n\n**Is your feature request related to a problem? Please describe.**\nA 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.\n\n**Describe the solution you'd like**\nA clear and concise description of what you want to happen.\n\n**Describe alternatives you've considered**\nA clear and concise description of any alternative solutions or features you've considered.\n\n**Additional context**\nAdd any other context or screenshots about the feature request here.\n"
  },
  {
    "path": ".github/workflows/TagBot.yml",
    "content": "name: TagBot\non:\n  issue_comment:\n    types:\n      - created\n  workflow_dispatch:\njobs:\n  TagBot:\n    if: github.event_name == 'workflow_dispatch' || github.actor == 'JuliaTagBot'\n    runs-on: ubuntu-latest\n    steps:\n      - uses: JuliaRegistries/TagBot@v1\n        with:\n          token: ${{ secrets.GITHUB_TOKEN }}\n          ssh: ${{ secrets.DOCUMENTER_KEY }}\n"
  },
  {
    "path": ".github/workflows/cross-package-test.yml",
    "content": "name: CrossPackageTest\n\non:\n  push:\n    branches: [main]\n    tags: [v*]\n  pull_request:\n\njobs:\n  test:\n    name: Julia v${{ matrix.julia-version }} - ${{ matrix.package_name }}\n    runs-on: ${{ matrix.os }}\n    strategy:\n      matrix:\n        julia-version: [1]\n        os: [ubuntu-latest]\n        package_name: [HydroPowerSimulations, StorageSystemsSimulations, PowerAnalytics]\n    continue-on-error: true\n    steps:\n      - uses: actions/checkout@v2\n      - uses: julia-actions/setup-julia@v1\n        with:\n          version: ${{ matrix.julia-version }}\n          arch: x64\n      - uses: julia-actions/julia-buildpkg@latest\n      - name: Clone ${{matrix.package_name}}\n        uses: actions/checkout@v2\n        with:\n          repository: Sienna-Platform/${{matrix.package_name}}.jl\n          path: downstream\n      - name: Run the tests\n        shell: julia --project=downstream {0}\n        run: |\n          using Pkg\n          try\n            # Force it to use this PR's version of the package\n            Pkg.develop(PackageSpec(path=\".\"))  # resolver may fail with main deps\n            Pkg.update()\n            Pkg.test()  # resolver may fail with test time deps\n          catch err\n            err isa Pkg.Resolve.ResolverError || rethrow()\n            # If we can't resolve that means this is incompatible by SemVer, and this is fine.\n            # It means we marked this as a breaking change, so we don't need to worry about\n            # mistakenly introducing a breaking change as we have intentionally made one.\n            @info \"Not compatible with this release. No problem.\" exception=err\n            exit(0)  # Exit immediately, as a success\n          end\n"
  },
  {
    "path": ".github/workflows/doc-preview-cleanup.yml",
    "content": "name: Doc Preview Cleanup\n\non:\n  pull_request:\n    types: [closed]\n\n# Ensure that only one \"Doc Preview Cleanup\" workflow is force pushing at a time\nconcurrency:\n  group: doc-preview-cleanup\n  cancel-in-progress: false\n\njobs:\n  doc-preview-cleanup:\n    runs-on: ubuntu-latest\n    if: github.event.pull_request.head.repo.fork == false\n    # This workflow pushes to gh-pages; permissions are per-job and independent of docs.yml\n    permissions:\n      contents: write\n    steps:\n      - name: Checkout gh-pages branch\n        uses: actions/checkout@v4\n        with:\n          ref: gh-pages\n      - name: Delete preview and history + push changes\n        run: |\n          if [ -d \"${preview_dir}\" ]; then\n              git config user.name \"Documenter.jl\"\n              git config user.email \"documenter@juliadocs.github.io\"\n              git rm -rf \"${preview_dir}\"\n              git commit -m \"delete preview\"\n              git branch gh-pages-new \"$(echo \"delete history\" | git commit-tree \"HEAD^{tree}\")\"\n              git push --force origin gh-pages-new:gh-pages\n          fi\n        env:\n          preview_dir: previews/PR${{ github.event.number }}\n"
  },
  {
    "path": ".github/workflows/docs.yml",
    "content": "name: Documentation\n\non:\n  push:\n    branches:\n      - main\n      - 'release-'\n    tags: '*'\n  pull_request:\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v2\n      - uses: julia-actions/setup-julia@v1\n        with:\n          version: '1'\n      - name: Install dependencies\n        run: julia --project=docs/ -e 'using Pkg; Pkg.develop(PackageSpec(path=pwd())); Pkg.instantiate()'\n\n      - name: Set DOCUMENTER_CURRENT_VERSION for tutorial download links\n        run: |\n          if [[ \"${{ github.event_name }}\" == \"pull_request\" ]]; then\n            echo \"DOCUMENTER_CURRENT_VERSION=previews/PR${{ github.event.pull_request.number }}\" >> \"$GITHUB_ENV\"\n          elif [[ \"${{ github.ref }}\" == refs/tags/* ]]; then\n            echo \"DOCUMENTER_CURRENT_VERSION=${GITHUB_REF_NAME}\" >> \"$GITHUB_ENV\"\n          elif [[ \"${{ github.ref }}\" == \"refs/heads/main\" ]] || [[ \"${{ github.ref }}\" =~ ^refs/heads/release- ]]; then\n            echo \"DOCUMENTER_CURRENT_VERSION=dev\" >> \"$GITHUB_ENV\"\n          fi\n      - name: Build and deploy\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n          DOCUMENTER_KEY: ${{ secrets.DOCUMENTER_KEY }}\n        run: julia --project=docs --color=yes docs/make.jl\n"
  },
  {
    "path": ".github/workflows/format-check.yml",
    "content": "name: Format Check\n\non:\n  push:\n    branches:\n      - 'main'\n      - 'release-'\n    tags: '*'\n  pull_request:\n\njobs:\n  build:\n    runs-on: ${{ matrix.os }}\n    strategy:\n      matrix:\n        julia-version: [1]\n        julia-arch: [x86]\n        os: [ubuntu-latest]\n    steps:\n      - uses: julia-actions/setup-julia@latest\n        with:\n          version: ${{ matrix.julia-version }}\n\n      - uses: actions/checkout@v2\n      - name: Install JuliaFormatter and format\n        run: |\n          julia  -e 'include(\"scripts/formatter/formatter_code.jl\")'\n      - uses: reviewdog/action-suggester@v1\n        if: github.event_name == 'pull_request'\n        with:\n          tool_name: JuliaFormatter\n          fail_on_error: true\n      - name: Format check\n        run: |\n          julia -e '\n          out = Cmd(`git diff --name-only`) |> read |> String\n          if out == \"\"\n              exit(0)\n          else\n              @error \"Some files have not been formatted !!!\"\n              write(stdout, out)\n              exit(1)\n          end'\n"
  },
  {
    "path": ".github/workflows/main-tests.yml",
    "content": "name: Main - CI\n\non:\n  push:\n    branches:\n      - main\n    schedule:\n      - cron: 0 * * * *\njobs:\n  test:\n    name: Julia ${{ matrix.julia-version }} - ${{ matrix.os }}\n    runs-on: ${{ matrix.os }}\n    strategy:\n      matrix:\n        julia-version: ['1', 'nightly']\n        julia-arch: [x64]\n        os: [ubuntu-latest, windows-latest, macOS-latest]\n\n    steps:\n      - uses: actions/checkout@v2\n      - uses: julia-actions/setup-julia@latest\n        continue-on-error: true\n        with:\n          version: ${{ matrix.julia-version }}\n          arch: ${{ matrix.julia-arch }}\n      - uses: julia-actions/julia-buildpkg@latest\n        env:\n          PYTHON: \"\"\n      - uses: julia-actions/julia-runtest@latest\n        continue-on-error: ${{ matrix.julia-version == 'nightly' }}\n        env:\n          PYTHON: \"\"\n      - uses: julia-actions/julia-processcoverage@v1\n      - uses: codecov/codecov-action@v5\n        with:\n          file: ./lcov.info\n          flags: unittests\n          name: codecov-umbrella\n          fail_ci_if_error: false\n          token: ${{ secrets.CODECOV_TOKEN }}\n"
  },
  {
    "path": ".github/workflows/performance_comparison.yml",
    "content": "name: 'Performance Comparison'\n\non:\n  pull_request:\n\njobs:\n  comparison:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: julia-actions/setup-julia@latest\n      - uses: actions/checkout@v5\n      - name: Run Perfomance Test Main\n        run: |\n          julia --project=test -e 'using Pkg; Pkg.add(PackageSpec(name=\"PowerSimulations\", rev=\"main\")); Pkg.instantiate()'\n          julia -t 4 --project=test test/performance/performance_test.jl \"Main\"\n      - name: Run Perfomance Test Branch\n        run: |\n          julia --project=test -e 'using Pkg; Pkg.develop(PackageSpec(path=pwd())); Pkg.instantiate()'\n          julia -t 4 --project=test test/performance/performance_test.jl \"This Branch\"\n      - name: Read precompile results\n        id: precompile_results\n        run: |\n          body=\"$(cat precompile_time.txt)\"\n          body=\"${body//'%'/'%25'}\"\n          body=\"${body//$'\\n'/'%0A'}\"\n          body=\"${body//$'\\r'/'%0D'}\"\n          echo \"::set-output name=body::$body\"\n      - name: Read build results\n        id: build_results\n        run: |\n          body=\"$(cat build_time.txt)\"\n          body=\"${body//'%'/'%25'}\"\n          body=\"${body//$'\\n'/'%0A'}\"\n          body=\"${body//$'\\r'/'%0D'}\"\n          echo \"::set-output name=body::$body\"\n      - name: Read solve results\n        id: solve_results\n        run: |\n          body=\"$(cat solve_time.txt)\"\n          body=\"${body//'%'/'%25'}\"\n          body=\"${body//$'\\n'/'%0A'}\"\n          body=\"${body//$'\\r'/'%0D'}\"\n          echo \"::set-output name=body::$body\"\n      - name: Find Comment\n        uses: peter-evans/find-comment@v4\n        id: fc\n        with:\n          issue-number: ${{ github.event.pull_request.number }}\n          comment-author: 'github-actions[bot]'\n          body-includes: Performance Results\n      - name: Create comment\n        if: steps.fc.outputs.comment-id == ''\n        uses: peter-evans/create-or-update-comment@v5\n        with:\n          issue-number: ${{ github.event.pull_request.number }}\n          body: |\n            Performance Results\n            | Version      | Precompile Time |\n            | :---        |    :----:   |\n            ${{ steps.precompile_results.outputs.body }}\n\n            | Version      | Build Time |\n            | :---        |    :----:   |\n            ${{ steps.build_results.outputs.body }}\n\n            | Version      | Solve Time |\n            | :---        |    :----:   |\n            ${{ steps.solve_results.outputs.body }}\n      - name: Update comment\n        if: steps.fc.outputs.comment-id != ''\n        uses: peter-evans/create-or-update-comment@v5\n        with:\n          comment-id: ${{ steps.fc.outputs.comment-id }}\n          body: |\n            Performance Results\n            | Version      | Precompile Time |\n            | :---        |    :----:   |\n            ${{ steps.precompile_results.outputs.body }}\n\n            | Version      | Build Time |\n            | :---        |    :----:   |\n            ${{ steps.build_results.outputs.body }}\n\n            | Version      | Build Time |\n            | :---        |    :----:   |\n            ${{ steps.solve_results.outputs.body }}\n\n          edit-mode: replace\n"
  },
  {
    "path": ".github/workflows/pr_testing.yml",
    "content": "name: Test-CI\n\non:\n  pull_request:\n    types: [opened, synchronize, reopened]\n\njobs:\n  test:\n    name: Julia ${{ matrix.julia-version }} - ${{ matrix.os }}\n    runs-on: ${{ matrix.os }}\n    strategy:\n      matrix:\n        julia-version: ['1']\n        julia-arch: [x64]\n        os: [ubuntu-latest, windows-latest, macOS-latest]\n\n    steps:\n      - uses: actions/checkout@v5\n      - uses: julia-actions/setup-julia@latest\n        with:\n          version: ${{ matrix.julia-version }}\n          arch: ${{ matrix.julia-arch }}\n      - uses: julia-actions/julia-buildpkg@latest\n        env:\n          PYTHON: \"\"\n      - uses: julia-actions/julia-runtest@latest\n        env:\n          PYTHON: \"\"\n      - uses: julia-actions/julia-processcoverage@v1\n      - uses: codecov/codecov-action@v5\n        with:\n          file: ./lcov.info\n          flags: unittests\n          name: codecov-umbrella\n          fail_ci_if_error: false\n          token: ${{ secrets.CODECOV_TOKEN }}\n"
  },
  {
    "path": ".gitignore",
    "content": "test_simulation_results/*\n# Claude\nsettings.json\n\n#Files generated by invoking Julia with --code-coverage\n*.jl.cov\n*.jl.*.cov\n*.log\n_*.jl\n# Files generated by invoking Julia with --track-allocation\n*.jl.mem\n\n# System-specific files and directories generated by the BinaryProvider and BinDeps packages\n# They contain absolute paths specific to the host computer, and so should not be committed\ndeps/deps.jl\ndeps/build.log\ndeps/downloads/\ndeps/usr/\ndeps/src/\n\n# Build artifacts for creating documentation generated by the Documenter package\ndocs/build/\ndocs/site/\n\n## Autogenerated code during the documentation process\ngenerated*.md\n\n#Jupyter Ignores\n.ipynb_checkpoints/\n.ipynb_checkpoints\n\n#Mac temp ignores\n.DS_Store\n\n#Figures\n*.pdf\n*.ipynb\n\nManifest.toml\n.vscode\n*.h5\ndata\n\n# profiling results\n**/build_time.txt\n**/precompile_time.txt\n**/solve_time.txt\n\n################################################################################\n#                              Operating systems                               #\n################################################################################\n\n########################################\n#                Linux                 #\n########################################\n\n*~\n\n# temporary files which can be created if a process still has a handle open of\n# a deleted file\n.fuse_hidden*\n\n# KDE directory preferences\n.directory\n\n# Linux trash folder which might appear on any partition or disk\n.Trash-*\n\n# .nfs files are created when an open file is removed but is still being accessed\n.nfs*\n\n########################################\n#                macOS                 #\n########################################\n\n# General\n.DS_Store\n.AppleDouble\n.LSOverride\n\n# Icon must end with two \\r\nIcon\n\n# Thumbnails\n._*\n\n# Files that might appear in the root of a volume\n.DocumentRevisions-V100\n.fseventsd\n.Spotlight-V100\n.TemporaryItems\n.Trashes\n.VolumeIcon.icns\n.com.apple.timemachine.donotpresent\n\n# Directories potentially created on remote AFP share\n.AppleDB\n.AppleDesktop\nNetwork Trash Folder\nTemporary Items\n.apdisk\n\n########################################\n#               Windows                #\n########################################\n\n# Windows thumbnail cache files\nThumbs.db\nehthumbs.db\nehthumbs_vista.db\n\n# Dump file\n*.stackdump\n\n# Folder config file\n[Dd]esktop.ini\n\n# Recycle Bin used on file shares\n$RECYCLE.BIN/\n\n# Windows Installer files\n*.cab\n*.msi\n*.msix\n*.msm\n*.msp\n\n# Windows shortcuts\n*.lnk\n\n\n\n## Acknowledgements\n# Many thanks to `https://gitignore.io/`, written and maintained by Joe Blau, which contributed much material to this gitignore file.\n\n# Claude\nhooks\nsettings.*.json\nsettings.json\n"
  },
  {
    "path": ".pre-commit-config.yaml",
    "content": "repos:\n  - repo: local\n    hooks:\n      - id: julia-formatter\n        name: Run Julia formatter\n        entry: julia scripts/formatter/formatter_code.jl\n        language: system\n        types: [file]\n        pass_filenames: false\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing\n\nCommunity driven development of this package is encouraged. To maintain code quality standards, please adhere to the following guidelines when contributing:\n\n  - To get started, <a href=\"https://www.clahub.com/agreements/NREL/PowerSimulations.jl\">sign the Contributor License Agreement</a>.\n  - Please do your best to adhere to the lengthy [Julia style guide](https://docs.julialang.org/en/latest/manual/style-guide/).\n  - 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/).\n"
  },
  {
    "path": "LICENSE",
    "content": "BSD 3-Clause License\n\nCopyright (c) 2018, 2023 Alliance for Sustainable Energy, LLC and The Regents of the University of California\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n\n* Redistributions of source code must retain the above copyright notice, this\n  list of conditions and the following disclaimer.\n\n* Redistributions in binary form must reproduce the above copyright notice,\n  this list of conditions and the following disclaimer in the documentation\n  and/or other materials provided with the distribution.\n\n* Neither the name of the copyright holder nor the names of its\n  contributors may be used to endorse or promote products derived from\n  this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\nAND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\nIMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE\nFOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\nDAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\nSERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\nCAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\nOR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
  },
  {
    "path": "Project.toml",
    "content": "name = \"PowerSimulations\"\nuuid = \"e690365d-45e2-57bb-ac84-44ba829e73c4\"\nauthors = [\"Jose Daniel Lara\", \"Clayton Barrows\", \"Daniel Thom\", \"Dheepak Krishnamurthy\", \"Sourabh Dalvi\"]\nversion = \"0.34.2\"\n\n[deps]\nCSV = \"336ed68f-0bac-5ca0-87d4-7b16caf5d00b\"\nDataFrames = \"a93c6f00-e57d-5684-b7b6-d8193f3e46c0\"\nDataFramesMeta = \"1313f7d8-7da2-5740-9ea0-a2ca25f37964\"\nDataStructures = \"864edb3b-99cc-5e75-8d2d-829cb0a9cfe8\"\nDates = \"ade2ca70-3891-5945-98fb-dc099432e06a\"\nDistributed = \"8ba89e20-285c-5b6f-9357-94700520ee1b\"\nDistributions = \"31c24e10-a181-5473-b8eb-7969acd0382f\"\nDocStringExtensions = \"ffbed154-4ef7-542d-bbb7-c09d3a79fcae\"\nHDF5 = \"f67ccb44-e63f-5c2f-98bd-6dc0ccc4ba2f\"\nInfrastructureSystems = \"2cd47ed4-ca9b-11e9-27f2-ab636a7671f1\"\nInteractiveUtils = \"b77e0a4c-d291-57a0-90e8-8db25a27a240\"\nJSON3 = \"0f8b85d8-7281-11e9-16c2-39a750bddbf1\"\nJuMP = \"4076af6c-e467-56ae-b986-b466b2749572\"\nLinearAlgebra = \"37e2e46d-f89d-539d-b4ee-838fcccc9c8e\"\nLogging = \"56ddb016-857b-54e1-b83d-db4d58db5568\"\nMathOptInterface = \"b8f27783-ece8-5eb3-8dc8-9495eed66fee\"\nPowerFlows = \"94fada2c-fd9a-4e89-8d82-81405f5cb4f6\"\nPowerModels = \"c36e90e8-916a-50a6-bd94-075b64ef4655\"\nPowerNetworkMatrices = \"bed98974-b02a-5e2f-9fe0-a103f5c450dd\"\nPowerSystems = \"bcd98974-b02a-5e2f-9ee0-a103f5c450dd\"\nPrettyTables = \"08abe8d2-0d0c-5749-adfa-8a2ac140af0d\"\nProgressMeter = \"92933f4c-e287-5a05-a399-4b506db050ca\"\nRandom = \"9a3f8284-a2c9-5f02-9a11-845980a1fd5c\"\nSerialization = \"9e88b42a-f829-5b0c-bbe9-9e923198166b\"\nSparseArrays = \"2f01184e-e22b-5df5-ae63-d93ebab69eaf\"\nTimeSeries = \"9e3dc215-6440-5c97-bce1-76c03772f85e\"\nTimerOutputs = \"a759f4b9-e2f1-59dc-863e-4aeb61b1ea8f\"\n\n[compat]\nCSV = \"~0.10\"\nDataFrames = \"1\"\nDataFramesMeta = \"~0.15\"\nDataStructures = \"~0.18, ~0.19\"\nDates = \"1\"\nDistributed = \"1\"\nDistributions = \"^0.25\"\nDocStringExtensions = \"~v0.9\"\nHDF5 = \"~0.17\"\nInfrastructureSystems = \"^3.5\"\nInteractiveUtils = \"1\"\nJSON3 = \"1\"\nJuMP = \"^1.28\"\nLinearAlgebra = \"1\"\nLogging = \"1\"\nMathOptInterface = \"1\"\nPowerFlows = \"^0.16\"\nPowerModels = \"^0.21.5\"\nPowerNetworkMatrices = \"^0.20\"\nPowerSystems = \"^5.8\"\nPrettyTables = \"2.4, 3.1\"\nProgressMeter = \"^1.5\"\nRandom = \"^1.10\"\nSerialization = \"1\"\nSparseArrays = \"1\"\nTimeSeries = \"~0.25\"\nTimerOutputs = \"~0.5\"\njulia = \"^1.10\"\n"
  },
  {
    "path": "README.md",
    "content": "# PowerSimulations.jl\n\n[![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)\n[![codecov](https://codecov.io/gh/Sienna-Platform/PowerSimulations.jl/branch/main/graph/badge.svg)](https://codecov.io/gh/Sienna-Platform/PowerSimulations.jl)\n[![Documentation](https://github.com/Sienna-Platform/PowerSimulations.jl/workflows/Documentation/badge.svg)](https://sienna-platform.github.io/PowerSimulations.jl/latest)\n[![DOI](https://zenodo.org/badge/109443246.svg)](https://zenodo.org/badge/latestdoi/109443246)\n[<img src=\"https://img.shields.io/badge/slack-@Sienna/PSI-sienna.svg?logo=slack\">](https://join.slack.com/t/nrel-sienna/shared_invite/zt-glam9vdu-o8A9TwZTZqqNTKHa7q3BpQ)\n[![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)\n\n`PowerSimulations.jl` is a Julia package for power system modeling and simulation of Power Systems operations. The objectives of the package are:\n\n  - Provide a flexible modeling framework that can accommodate problems of different complexity and at different time-scales.\n\n  - Streamline the construction of large scale optimization problems to avoid repetition of work when adding/modifying model details.\n  - Exploit Julia's capabilities to improve computational performance of large scale power system quasi-static simulations.\n\nThe 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:\n\n  - _Simulations_ define a set of problems that can be solved using numerical techniques.\n\nFor 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.\n\n## Simulations enabled by PowerSimulations\n\n  - Integrated Resource Planning\n  - Production Cost Modeling\n  - Market Simulations\n\n## Installation\n\n```julia\njulia> ]\n(v1.9) pkg> add PowerSystems\n(v1.9) pkg> add PowerSimulations\n```\n\n## Usage\n\n`PowerSimulations.jl` uses [PowerSystems.jl](https://github.com/Sienna-Platform/PowerSystems.jl) to handle the data used in the simulations.\n\n```julia\nusing PowerSimulations\nusing PowerSystems\n```\n\nFor 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.\n\n## Development\n\nContributions 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.\n\n## License\n\nPowerSimulations 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.\n"
  },
  {
    "path": "codecov.yml",
    "content": "codecov:\n  require_ci_to_pass: yes\n\ncoverage:\n  precision: 2\n  round: down\n  range: \"70...100\"\n\n  status:\n    project:                   # measuring the overall project coverage\n      default:                 # context, you can create multiple ones with custom titles\n        enabled: yes           # must be yes|true to enable this status\n        target: auto           # specify the target coverage for each commit status\n                               #   option: \"auto\" (must increase from parent commit or pull request base)\n                               #   option: \"X%\" a static target percentage to hit\n        threshold: 5        # allowed to drop X% and still result in a \"success\" commit status\n        if_not_found: success  # if parent is not found report status as success, error, or failure\n        if_ci_failed: error    # if ci fails report status as success, error, or failure\n    patch:\n      default:\n        target: 70\n\nparsers:\n  gcov:\n    branch_detection:\n      conditional: yes\n      loop: yes\n      method: no\n      macro: no\n\ncomment:\n  layout: \"reach,diff,flags,tree\"\n  behavior: default\n  require_changes: no\n"
  },
  {
    "path": "docs/Makefile",
    "content": "BRANCH := $(shell git rev-parse --abbrev-ref HEAD)\n\nhtml:\n\tjulia make.jl\n\ngithub: html\n\t-git branch -D gh-pages\n\t-git push origin --delete gh-pages\n\tghp-import -n -b gh-pages -m \"Update documentation\" ./build\n\tgit checkout gh-pages\n\tgit push --set-upstream origin gh-pages\n\tgit checkout ${BRANCH}\n\nall: github\n"
  },
  {
    "path": "docs/Project.toml",
    "content": "[deps]\nCSV = \"336ed68f-0bac-5ca0-87d4-7b16caf5d00b\"\nDataFrames = \"a93c6f00-e57d-5684-b7b6-d8193f3e46c0\"\nDataStructures = \"864edb3b-99cc-5e75-8d2d-829cb0a9cfe8\"\nDocumenter = \"e30172f5-a6a5-5a46-863b-614d45cd2de4\"\nDocumenterInterLinks = \"d12716ef-a0f6-4df4-a9f1-a5a34e75c656\"\nDocumenterTools = \"35a29f4d-8980-5a13-9543-d66fff28ecb8\"\nHiGHS = \"87dc4568-4c63-4d18-b0c0-bb2238e4078b\"\nHydroPowerSimulations = \"fc1677e0-6ad7-4515-bf3a-bd6bf20a0b1b\"\nInfrastructureSystems = \"2cd47ed4-ca9b-11e9-27f2-ab636a7671f1\"\nJuMP = \"4076af6c-e467-56ae-b986-b466b2749572\"\nLatexify = \"23fbe1c1-3f47-55db-b15f-69d7ec21a316\"\nLiterate = \"98b081ad-f1c9-55d3-8b20-4c87d4299306\"\nPowerNetworkMatrices = \"bed98974-b02a-5e2f-9fe0-a103f5c450dd\"\nPowerSimulations = \"e690365d-45e2-57bb-ac84-44ba829e73c4\"\nPowerSystemCaseBuilder = \"f00506e0-b84f-492a-93c2-c0a9afc4364e\"\nPowerSystems = \"bcd98974-b02a-5e2f-9ee0-a103f5c450dd\"\nPrettyTables = \"08abe8d2-0d0c-5749-adfa-8a2ac140af0d\"\nTimeSeries = \"9e3dc215-6440-5c97-bce1-76c03772f85e\"\n\n[compat]\nDocumenter = \"^1.7\"\nInfrastructureSystems = \"3\"\njulia = \"^1.6\"\n"
  },
  {
    "path": "docs/make.jl",
    "content": "using Documenter\nusing PowerSystems\nusing PowerSimulations\nusing DataStructures\nusing DocumenterInterLinks\nusing Literate\n\nlinks = InterLinks(\n    \"Julia\" => \"https://docs.julialang.org/en/v1/\",\n    \"InfrastructureSystems\" => \"https://sienna-platform.github.io/InfrastructureSystems.jl/stable/\",\n    \"PowerSystems\" => \"https://sienna-platform.github.io/PowerSystems.jl/stable/\",\n    \"PowerSimulations\" => \"https://sienna-platform.github.io/PowerSimulations.jl/stable/\",\n    \"StorageSystemsSimulations\" => \"https://sienna-platform.github.io/StorageSystemsSimulations.jl/stable/\",\n    \"HydroPowerSimulations\" => \"https://sienna-platform.github.io/HydroPowerSimulations.jl/dev/\",\n)\n\ninclude(joinpath(@__DIR__, \"make_tutorials.jl\"))\nmake_tutorials()\n\npages = OrderedDict(\n    \"Welcome Page\" => \"index.md\",\n    \"Tutorials\" => Any[\n        \"Single-step Problem\" => \"tutorials/generated_decision_problem.md\",\n        \"Multi-stage Production Cost Simulation\" => \"tutorials/generated_pcm_simulation.md\",\n    ],\n    \"How to...\" => Any[\n        \"...register a variable in a custom operation model\" => \"how_to/register_variable.md\",\n        \"...create a problem template\" => \"how_to/problem_templates.md\",\n        \"...read the simulation results\" => \"how_to/read_results.md\",\n        \"...debug an infeasible model\" => \"how_to/debugging_infeasible_models.md\",\n        \"...configure logging\" => \"how_to/logging.md\",\n        \"...inspect simulation events using the recorder\" => \"how_to/simulation_recorder.md\",\n        \"...run a parallel simulation\" => \"how_to/parallel_simulations.md\",\n    ],\n    \"Explanation\" => Any[\n        \"explanation/psi_structure.md\",\n        \"explanation/feedforward.md\",\n        \"explanation/chronologies.md\",\n        \"explanation/sequencing.md\",\n    ],\n    \"Reference\" => Any[\n        \"Glossary and Acronyms\" => \"api/glossary.md\",\n        \"Public API\" => \"api/PowerSimulations.md\",\n        \"Developers\" => [\"Developer Guidelines\" => \"api/developer.md\",\n            \"Internals\" => \"api/internal.md\"],\n    ],\n    \"Formulation Library\" => Any[\n        \"Introduction\" => \"formulation_library/Introduction.md\",\n        \"General\" => \"formulation_library/General.md\",\n        \"Network\" => \"formulation_library/Network.md\",\n        \"Thermal Generation\" => \"formulation_library/ThermalGen.md\",\n        \"Renewable Generation\" => \"formulation_library/RenewableGen.md\",\n        \"Load\" => \"formulation_library/Load.md\",\n        \"Branch\" => \"formulation_library/Branch.md\",\n        \"Source\" => \"formulation_library/Source.md\",\n        \"Services\" => \"formulation_library/Service.md\",\n        \"Feedforwards\" => \"formulation_library/Feedforward.md\",\n        \"Piecewise Linear Cost\" => \"formulation_library/Piecewise.md\",\n    ],\n)\n\nmakedocs(;\n    modules = [PowerSimulations],\n    format = Documenter.HTML(;\n        prettyurls = haskey(ENV, \"GITHUB_ACTIONS\"),\n        size_threshold = nothing),\n    sitename = \"PowerSimulations.jl\",\n    authors = \"Jose Daniel Lara, Daniel Thom, Kate Doubleday, Rodrigo Henriquez-Auba, and Clayton Barrows\",\n    pages = Any[p for p in pages],\n    plugins = [links],\n)\n\ndeploydocs(;\n    repo = \"github.com/Sienna-Platform/PowerSimulations.jl.git\",\n    target = \"build\",\n    branch = \"gh-pages\",\n    devbranch = \"main\",\n    devurl = \"dev\",\n    push_preview = true,\n    versions = [\"stable\" => \"v^\", \"v#.#\"],\n)\n"
  },
  {
    "path": "docs/make_tutorials.jl",
    "content": "using Pkg\nusing Literate\nusing DataFrames\nusing PrettyTables\n\n# Limit DataFrame rendering during docs generation to avoid huge literal outputs.\n# Notes:\n# - Environment-variable approaches tested (`DATAFRAMES_ROWS`, `DATAFRAMES_COLUMNS`,\n#   `LINES`, `COLUMNS`) did not constrain DataFrames output in this pipeline.\n# - We keep a docs-local Base.show override as a fallback and accept `kwargs...`\n#   so explicit show(...; kwargs) calls do not error on unsupported keywords.\nfunction _env_int(name::String, default::Int)\n    parsed = tryparse(Int, get(ENV, name, string(default)))\n    return something(parsed, default)\nend\n\nconst _DF_MAX_ROWS = _env_int(\"SIENNA_DOCS_DF_MAX_ROWS\", 10)\nconst _DF_MAX_COLS = _env_int(\"SIENNA_DOCS_DF_MAX_COLS\", 80)\n\nfunction Base.show(io::IO, mime::MIME\"text/plain\", df::DataFrame; kwargs...)\n    # Keep docs output bounded while allowing explicit caller kwargs.\n    PrettyTables.pretty_table(io, df;\n        backend = :text,\n        maximum_number_of_rows = _DF_MAX_ROWS,\n        maximum_number_of_columns = _DF_MAX_COLS,\n        show_omitted_cell_summary = true,\n        compact_printing = false,\n        limit_printing = true,\n        kwargs...)\nend\n\nfunction Base.show(io::IO, mime::MIME\"text/html\", df::DataFrame; kwargs...)\n    PrettyTables.pretty_table(io, df;\n        backend = :html,\n        maximum_number_of_rows = _DF_MAX_ROWS,\n        maximum_number_of_columns = _DF_MAX_COLS,\n        show_omitted_cell_summary = true,\n        compact_printing = false,\n        limit_printing = true,\n        kwargs...)\nend\n\n# Remove previously generated tutorial artifacts so a docs build only reflects\n# current source tutorials.\n#\n# Input:\n# - dir: tutorial output directory that can contain generated_*.md/ipynb.\n# Output:\n# - Deletes matching files in-place and logs each deletion.\nfunction clean_old_generated_files(dir::String)\n    if !isdir(dir)\n        @warn \"Directory does not exist: $dir\"\n        return\n    end\n    generated_files = filter(\n        f ->\n            startswith(f, \"generated_\") &&\n                (endswith(f, \".md\") || endswith(f, \".ipynb\")),\n        readdir(dir),\n    )\n    for file in generated_files\n        rm(joinpath(dir, file); force = true)\n        @info \"Removed old generated file: $file\"\n    end\nend\n\n#########################################################\n# Literate post-processing functions for tutorial generation\n#########################################################\n\n# Compute docs base URL from Documenter deploy context.\n#\n# Behavior:\n# - previews/PR123 -> .../previews/PR123\n# - dev (or custom DOCUMENTER_DEVURL) -> .../dev\n# - tagged versions like v0.9 -> .../v0.9\n# - fallback -> .../stable\n#\n# This keeps generated download/view-online links correct across preview, dev,\n# tagged, and stable deployments.\nfunction _compute_docs_base_url()\n    base = \"https://sienna-platform.github.io/PowerSimulations.jl\"\n\n    current_version = get(ENV, \"DOCUMENTER_CURRENT_VERSION\", \"\")\n\n    # Preview builds (e.g. \"previews/PR123\")\n    if startswith(current_version, \"previews/PR\")\n        return \"$base/$current_version\"\n    end\n\n    # Dev builds\n    if current_version == \"dev\"\n        dev_suffix = get(ENV, \"DOCUMENTER_DEVURL\", \"dev\")\n        return \"$base/$dev_suffix\"\n    end\n\n    # Tagged/versioned builds (e.g. \"v0.9\", \"v1.2.3\")\n    if !isempty(current_version) && current_version != \"stable\"\n        return \"$base/$current_version\"\n    end\n\n    # Default to stable\n    return \"$base/stable\"\nend\n\nconst _DOCS_BASE_URL = _compute_docs_base_url()\n\n\"\"\"\nChoose how tutorial download links are written in generated markdown.\n\n- **Absolute** (under `_DOCS_BASE_URL/tutorials/`): CI / Documenter context (`GITHUB_ACTIONS` or\n  non-empty `DOCUMENTER_CURRENT_VERSION`) so previews, `dev`, and versioned URLs match\n  `_compute_docs_base_url()`.\n- **Relative** (bare filenames): local/offline builds; files sit next to `generated_*.md`\n  under `docs/src/tutorials/`.\n\nOverride: `SIENNA_DOCS_DOWNLOAD_LINKS`=`absolute` or `relative`.\n\"\"\"\nfunction _downloads_use_absolute_urls()\n    o = get(ENV, \"SIENNA_DOCS_DOWNLOAD_LINKS\", \"\")\n    o == \"absolute\" && return true\n    o == \"relative\" && return false\n    haskey(ENV, \"GITHUB_ACTIONS\") && return true\n    !isempty(get(ENV, \"DOCUMENTER_CURRENT_VERSION\", \"\")) && return true\n    return false\nend\n\n# Replace APPEND_MARKDOWN(\"path/to/file.md\") placeholders with file contents.\n#\n# Sample input:\n#   \"Before\\nAPPEND_MARKDOWN(\\\"docs/src/tutorials/_snippet.md\\\")\\nAfter\"\n# Sample output:\n#   \"Before\\n<contents of _snippet.md>\\nAfter\"\n#\n# Notes:\n# - Uses a non-greedy-safe capture (`[^\\\"]*`) so multiple placeholders can be\n#   replaced independently.\nfunction insert_md(content)\n    pattern = r\"APPEND_MARKDOWN\\(\\\"([^\\\"]*)\\\"\\)\"\n    if occursin(pattern, content)\n        content = replace(content, pattern => m -> read(m.captures[1], String))\n    end\n    return content\nend\n\n# Default display titles for Documenter admonition types when no custom title is given.\n# See https://documenter.juliadocs.org/stable/showcase/#Admonitions\nconst _ADMONITION_DISPLAY_NAMES = Dict{String, String}(\n    \"note\" => \"Note\",\n    \"info\" => \"Info\",\n    \"tip\" => \"Tip\",\n    \"warning\" => \"Warning\",\n    \"danger\" => \"Danger\",\n    \"compat\" => \"Compat\",\n    \"todo\" => \"TODO\",\n    \"details\" => \"Details\",\n)\n\n# Preprocess Literate source to convert Documenter-style admonitions into Jupyter-friendly\n# blockquotes. Used only for notebook output; markdown keeps `!!! type` and is rendered by\n# Documenter. Admonitions are not recognized by common mark or Jupyter; see\n# https://fredrikekre.github.io/Literate.jl/v2/tips/#admonitions-compatibility\nfunction preprocess_admonitions_for_notebook(str::AbstractString)\n    lines = split(str, '\\n'; keepempty = true)\n    out = String[]\n    i = 1\n    n = length(lines)\n    admonition_start =\n        r\"^# !!! (note|info|tip|warning|danger|compat|todo|details)(?:\\s+\\\"([^\\\"]*)\\\")?\\s*$\"\n    content_line = r\"^#     (.*)$\"  # Documenter admonition body: # then 4 spaces\n    blank_comment = r\"^#\\s*$\"      # # or # with only spaces\n\n    while i <= n\n        line = lines[i]\n        m = match(admonition_start, line)\n        if m !== nothing\n            typ = lowercase(m.captures[1])\n            custom_title = m.captures[2]\n            title = if custom_title !== nothing && !isempty(custom_title)\n                custom_title\n            else\n                get(_ADMONITION_DISPLAY_NAMES, typ, titlecase(typ))\n            end\n            push!(out, \"# > *$(title)*\")\n            push!(out, \"# >\")\n            i += 1\n            # Consume blank comment lines and content lines\n            while i <= n\n                l = lines[i]\n                if match(blank_comment, l) !== nothing\n                    push!(out, \"# >\")\n                    i += 1\n                elseif (cm = match(content_line, l)) !== nothing\n                    push!(out, \"# > \" * cm.captures[1])\n                    i += 1\n                else\n                    break\n                end\n            end\n            continue\n        end\n        push!(out, line)\n        i += 1\n    end\n    return join(out, '\\n')\nend\n\n# Inject a short \"download tutorial files\" sentence after the first markdown\n# heading in generated tutorial pages.\n#\n# Sample input:\n#   \"# Title\\nBody...\"\n# Sample output (conceptual):\n#   \"# Title\\n\\n*To follow along... [Julia script](.../tutorial.jl)...*\\n\\nBody...\"\n#\n# Download links:\n# - **Deployed / CI**: absolute URLs under `_DOCS_BASE_URL` when `_downloads_use_absolute_urls()` is true.\n# - **Local**: bare filenames (siblings of `generated_*.md` in `docs/src/tutorials/`).\nfunction add_download_links(content, jl_file, ipynb_file)\n    script_link, notebook_link = if _downloads_use_absolute_urls()\n        (\"$_DOCS_BASE_URL/tutorials/$(jl_file)\", \"$_DOCS_BASE_URL/tutorials/$(ipynb_file)\")\n    else\n        (jl_file, ipynb_file)\n    end\n    download_section = \"\"\"\n\n*To follow along, you can download this tutorial as a [Julia script (.jl)]($(script_link)) or [Jupyter notebook (.ipynb)]($(notebook_link)).*\n\n\"\"\"\n    # Insert after the first heading (which should be the title)\n    # Match the first heading line and replace it with heading + download section\n    m = match(r\"^(#+ .+)$\"m, content)\n    if m !== nothing\n        heading = m.match\n        content = replace(content, r\"^(#+ .+)$\"m => heading * download_section; count = 1)\n    end\n    return content\nend\n\n# Insert a setup preface and captured `Pkg.status()` into the first markdown\n# cell of a generated notebook, immediately after the first heading.\n#\n# Sample effect:\n# - First markdown cell gains a \"Set up\" blockquote and an embedded code block\n#   containing package versions from the docs build environment.\nfunction add_pkg_status_to_notebook(nb::Dict)\n    cells = get(nb, \"cells\", [])\n    if isempty(cells)\n        return nb\n    end\n\n    # Find the first markdown cell\n    first_markdown_idx = nothing\n    for (i, cell) in enumerate(cells)\n        if get(cell, \"cell_type\", \"\") == \"markdown\"\n            first_markdown_idx = i\n            break\n        end\n    end\n\n    if first_markdown_idx === nothing\n        return nb  # No markdown cell found, return unchanged\n    end\n\n    first_cell = cells[first_markdown_idx]\n    cell_source = get(first_cell, \"source\", [])\n\n    # Convert source array to string to find the first heading\n    source_text = join(cell_source)\n\n    # Find the first heading (lines starting with #)\n    heading_pattern = r\"^(#+\\s+.+?)$\"m\n    heading_match = match(heading_pattern, source_text)\n\n    if heading_match === nothing\n        return nb  # No heading found, return unchanged\n    end\n\n    # Capture Pkg.status() output at build time\n    io = IOBuffer()\n    Pkg.status(; io = io)\n    pkg_status_output = String(take!(io))\n\n    # Create the content to insert: blockquote \"Set up\" with setup instructions and pkg.status()\n    # Blockquote title and body; hyperlinks for IJulia and create an environment\n    preface_lines = [\n        \"\\n\",\n        \"> **Set up**\\n\",\n        \">\\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 <PackageName>` further down.\\n\",\n        \">\\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        \">\\n\",\n    ]\n\n    # Format Pkg.status() output as a code block inside the blockquote\n    pkg_status_lines = split(pkg_status_output, '\\n'; keepempty = true)\n    pkg_status_block = [\" > ```\\n\"]\n    for line in pkg_status_lines\n        push!(pkg_status_block, \" > \" * line * \"\\n\")\n    end\n    push!(pkg_status_block, \" > ```\\n\", \"\\n\")\n\n    # Find the first heading line in the source array\n    heading_line_idx = nothing\n    for (i, line) in enumerate(cell_source)\n        if match(heading_pattern, line) !== nothing\n            heading_line_idx = i\n            break\n        end\n    end\n\n    if heading_line_idx === nothing\n        return nb  # Couldn't find heading line\n    end\n\n    # Build new source array\n    new_source = String[]\n    # Add all lines up to and including the heading line\n    for i in 1:heading_line_idx\n        push!(new_source, cell_source[i])\n    end\n\n    # Add the preface and pkg.status content right after the heading\n    append!(new_source, preface_lines)\n    append!(new_source, pkg_status_block)\n\n    # Add all remaining lines after the heading\n    for i in (heading_line_idx + 1):length(cell_source)\n        push!(new_source, cell_source[i])\n    end\n\n    # Update the cell source\n    first_cell[\"source\"] = new_source\n    cells[first_markdown_idx] = first_cell\n\n    nb[\"cells\"] = cells\n    return nb\nend\n\n# Add italicized \"view online\" comment after each image from ```@raw html ... ``` (or\n# the raw HTML / markdown form Literate writes). Used as a postprocess in Literate.notebook.\n# Literate strips the backtick wrapper and outputs raw HTML; we match that multi-line block.\n# Sample effect:\n# - If a markdown cell contains one or more image fragments, append exactly one\n#   \"view online\" fallback note at the end of that cell.\n# - If the note already exists in the cell, no change is applied.\nfunction add_image_links(nb::Dict, outputfile_base::AbstractString)\n    tutorial_url = \"$_DOCS_BASE_URL/tutorials/$(outputfile_base)/\"\n    msg = \"_If image is not available when viewing in a Jupyter notebook, view the tutorial online [here]($tutorial_url)._\"\n    cells = get(nb, \"cells\", [])\n    for (idx, cell) in enumerate(cells)\n        get(cell, \"cell_type\", \"\") != \"markdown\" && continue\n        source = get(cell, \"source\", [])\n        isempty(source) && continue\n        text = join(source)\n        # Check if this cell already has the \"view online\" message to avoid duplicates\n        contains(text, \"If image is not available when viewing in a Jupyter notebook\") &&\n            continue\n        suffix = \"\\n\\n\" * msg * \"\\n\"\n        # If the cell has any of the image shapes below, we append one \"view online\" note.\n        # We build one alternation pattern from sub-patterns (each line is one case).\n        #\n        # HTML paragraph wrapping an <img> (Literate often emits <p>…<img>…</p>).\n        #   <p[^>]*>     — opening <p> and attributes\n        #   [\\s\\S]*?     — any chars, non-greedy, up to the first <img\n        #   <img…</p>   — from <img through closing </p>\n        p_with_img_pattern = r\"<p[^>]*>[\\s\\S]*?<img[\\s\\S]*?</p>\"\n        # Documenter @raw html chunk that Literate inlines in the notebook (backticks removed in output).\n        #   ```@raw html  — start marker\n        #   [\\s\\S]*?     — block body, non-greedy\n        #   ```          — end fence\n        raw_html_block_pattern = r\"```@raw html[\\s\\S]*?```\"\n        # Standard markdown image: ![alt text](url)\n        #   !\\[…\\]  — alt in brackets;  \\(…\\)  — path in parens\n        markdown_image_pattern = r\"!\\[[^\\]]*\\]\\([^\\)]*\\)\"\n        # A bare <img ...> not already covered by the <p>…<img>…</p> case above.\n        #   <img   — tag start;  [^>]*?  — attributes;  /?>  — self-closing or >\n        standalone_img_pattern = r\"<img[^>]*?/?>\"\n        # Union of the four cases: (?: A | B | C | D )\n        image_fragment_pattern = Regex(\n            \"(?:\" *\n            p_with_img_pattern.pattern * \"|\" *\n            raw_html_block_pattern.pattern * \"|\" *\n            markdown_image_pattern.pattern * \"|\" *\n            standalone_img_pattern.pattern * \")\",\n        )\n        if occursin(image_fragment_pattern, text)\n            text *= suffix\n        end\n        # Convert back to notebook source array (lines, last without trailing \\n if non-empty)\n        lines = split(text, \"\\n\"; keepempty = true)\n        new_source = String[]\n        for i in 1:length(lines)\n            if i < length(lines)\n                push!(new_source, lines[i] * \"\\n\")\n            else\n                isempty(lines[i]) || push!(new_source, lines[i])\n            end\n        end\n        cell[\"source\"] = new_source\n        cells[idx] = cell\n    end\n    nb[\"cells\"] = cells\n    return nb\nend\n\n#########################################################\n# Process tutorials with Literate\n#########################################################\n\n# Generate tutorial markdown + notebook artifacts from literate .jl sources.\n#\n# Pipeline:\n# 1) discover tutorial .jl files (excluding helper files starting with \"_\")\n# 2) generate Documenter-flavored markdown with injected download links\n# 3) generate notebook with admonition conversion, setup preface, and image note\nfunction make_tutorials()\n    tutorials_dir = abspath(joinpath(@__DIR__, \"src\", \"tutorials\"))\n    # Exclude helper scripts that start with \"_\"\n    if isdir(tutorials_dir)\n        tutorial_files =\n            filter(\n                x -> endswith(x, \".jl\") && !startswith(x, \"_\"),\n                readdir(tutorials_dir),\n            )\n        if !isempty(tutorial_files)\n            # Clean up old generated tutorial files\n            tutorial_outputdir = tutorials_dir\n            clean_old_generated_files(tutorial_outputdir)\n\n            for file in tutorial_files\n                @show file\n                infile_path = joinpath(tutorials_dir, file)\n                execute =\n                    if occursin(\"EXECUTE = TRUE\", uppercase(readline(infile_path)))\n                        true\n                    else\n                        false\n                    end\n\n                outputfile = string(\"generated_\", replace(\"$file\", \".jl\" => \"\"))\n\n                # Generate markdown\n                Literate.markdown(infile_path,\n                    tutorial_outputdir;\n                    name = outputfile,\n                    credit = false,\n                    flavor = Literate.DocumenterFlavor(),\n                    documenter = true,\n                    postprocess = (\n                        content -> add_download_links(\n                            insert_md(content),\n                            file,\n                            string(outputfile, \".ipynb\"),\n                        )\n                    ),\n                    execute = execute)\n\n                # Generate notebook (chain add_image_links after add_pkg_status_to_notebook).\n                # preprocess_admonitions_for_notebook converts Documenter admonitions to blockquotes\n                # so they render in Jupyter; markdown output keeps !!! style for Documenter.\n                Literate.notebook(infile_path,\n                    tutorial_outputdir;\n                    name = outputfile,\n                    credit = false,\n                    execute = false,\n                    preprocess = preprocess_admonitions_for_notebook,\n                    postprocess = nb ->\n                        add_image_links(add_pkg_status_to_notebook(nb), outputfile))\n            end\n        end\n    end\nend\n"
  },
  {
    "path": "docs/src/api/PowerSimulations.md",
    "content": "```@meta\nCurrentModule = PowerSimulations\nDocTestSetup  = quote\n    using PowerSimulations\nend\n```\n\n# API Reference\n\n```@contents\nPages = [\"PowerSimulations.md\"]\nDepth = 3\n```\n\n```@raw html\n&nbsp;\n&nbsp;\n```\n\n## Device Models\n\nList of structures and methods for Device models\n\n```@docs\nDeviceModel\n```\n\n### Formulations\n\nRefer to the [Formulations Page](@ref formulation_library) for each Abstract Device Formulation.\n\nHVDC formulations will be moved to its own section in future releases\n\n### HVDC Formulations\n\n```@docs\nTransportHVDCNetworkModel\nVoltageDispatchHVDCNetworkModel\nHVDCTwoTerminalLCC\n```\n\n### Converter Formulations\n\n```@docs\nQuadraticLossConverter\n```\n\n### DC Lines Formulations\n\n```@docs\nDCLossyLine\n```\n\n### Synchronous Condenser Formulations\n\n```@docs\nSynchronousCondenserBasicDispatch\n```\n\n### Problem Templates\n\n```@autodocs\nModules = [PowerSimulations]\nPages   = [\"problem_template.jl\",\n            \"operation_problem_templates.jl\",\n           ]\nOrder = [:type, :function]\nPublic = true\nPrivate = false\n```\n\n```@raw html\n&nbsp;\n&nbsp;\n```\n\n* * *\n\n## Decision Models\n\n```@autodocs\nModules = [PowerSimulations]\nPages   = [\"decision_model.jl\",\n           ]\nOrder = [:type, :function]\nPublic = true\nPrivate = false\n```\n\n```@raw html\n&nbsp;\n```\n\n```@docs\nGenericOpProblem\n```\n\n```@raw html\n&nbsp;\n&nbsp;\n```\n\n* * *\n\n## Emulation Models\n\n```@docs\nEmulationModel\nEmulationModel(::Type{M} where {M <: EmulationProblem}, ::ProblemTemplate, ::PSY.System, ::Union{Nothing, JuMP.Model})\nbuild!(::EmulationModel)\nrun!(::EmulationModel)\nsolve!(::Int, ::EmulationModel{<:EmulationProblem}, ::Dates.DateTime, ::SimulationStore)\n```\n\n```@raw html\n&nbsp;\n&nbsp;\n```\n\n* * *\n\n## Service Models\n\nList of structures and methods for Service models\n\n```@docs\nServiceModel\n```\n\n```@raw html\n&nbsp;\n&nbsp;\n```\n\n* * *\n\n## Simulation Models\n\n```@docs\nInitialCondition\nSimulationModels\nSimulationSequence\nSimulation\nbuild!(::Simulation)\nexecute!(::Simulation)\n```\n\n```@autodocs\nModules = [PowerSimulations]\nPages   = [\"simulation_partitions.jl\",\n           ]\nOrder = [:type, :function]\nPublic = true\nPrivate = false\n```\n\n```@raw html\n&nbsp;\n&nbsp;\n```\n\n## Chronology Models\n\n```@autodocs\nModules = [PowerSimulations]\nPages   = [\"initial_condition_chronologies.jl\",\n           ]\nOrder = [:type, :function]\nPublic = true\nPrivate = false\n```\n\n* * *\n\n## Variables\n\nFor a list of variables for each device refer to its Formulations page.\n\n### Common Variables\n\n```@docs\nActivePowerVariable\nReactivePowerVariable\nPiecewiseLinearCostVariable\nRateofChangeConstraintSlackUp\nRateofChangeConstraintSlackDown\nPostContingencyActivePowerChangeVariable\n```\n\n### Thermal Unit Variables\n\n```@docs\nOnVariable\nStartVariable\nStopVariable\nHotStartVariable\nWarmStartVariable\nColdStartVariable\nPowerAboveMinimumVariable\n```\n\n### Storage Unit Variables\n\n```@docs\nReservationVariable\nEnergyVariable\nActivePowerOutVariable\nActivePowerInVariable\n```\n\n### Load Variables\n\n```@docs\nShiftUpActivePowerVariable\nShiftDownActivePowerVariable\n```\n\n### Branches and Network Variables\n\n```@docs\nFlowActivePowerVariable\nFlowActivePowerSlackUpperBound\nFlowActivePowerSlackLowerBound\nFlowActivePowerFromToVariable\nFlowActivePowerToFromVariable\nFlowReactivePowerFromToVariable\nFlowReactivePowerToFromVariable\nPhaseShifterAngle\nHVDCLosses\nHVDCFlowDirectionVariable\nVoltageMagnitude\nVoltageAngle\n```\n\n### Two Terminal and Multi-Terminal HVDC Variables\n\n```@docs\nInterpolationBinarySquaredCurrentVariable\nSquaredDCVoltage\nDCLineCurrent\nInterpolationSquaredVoltageVariable\nInterpolationBinarySquaredVoltageVariable\nAuxBilinearConverterVariable\nAuxBilinearSquaredConverterVariable\nInterpolationSquaredBilinearVariable\nInterpolationBinarySquaredBilinearVariable\nInterpolationSquaredCurrentVariable\nDCVoltage\nConverterCurrent\nSquaredConverterCurrent\nConverterPositiveCurrent\nConverterNegativeCurrent\nConverterPowerDirection\n```\n\n### Services Variables\n\n```@docs\nActivePowerReserveVariable\nServiceRequirementVariable\nSystemBalanceSlackUp\nSystemBalanceSlackDown\nReserveRequirementSlack\nInterfaceFlowSlackUp\nInterfaceFlowSlackDown\nPostContingencyActivePowerReserveDeploymentVariable\n```\n\n### Feedforward Variables\n\n```@docs\nUpperBoundFeedForwardSlack\nLowerBoundFeedForwardSlack\n```\n\n```@raw html\n&nbsp;\n&nbsp;\n```\n\n* * *\n\n## Auxiliary Variables\n\n### Thermal Unit Auxiliary Variables\n\n```@docs\nTimeDurationOn\nTimeDurationOff\nPowerOutput\n```\n\n### Bus Auxiliary Variables\n\n```@docs\nPowerFlowVoltageAngle\nPowerFlowVoltageMagnitude\nPowerFlowLossFactors\nPowerFlowVoltageStabilityFactors\n```\n\n### Branch Auxiliary Variables\n\n```@docs\nPowerFlowBranchReactivePowerFromTo\nPowerFlowBranchReactivePowerToFrom\nPowerFlowBranchActivePowerFromTo\nPowerFlowBranchActivePowerToFrom\nPowerFlowBranchActivePowerLoss\n```\n\n```@raw html\n&nbsp;\n&nbsp;\n```\n\n* * *\n\n## Constraints\n\n### Common Constraints\n\n```@docs\nPiecewiseLinearCostConstraint\n\n```\n\n### Network Constraints\n\n```@docs\nCopperPlateBalanceConstraint\nNodalBalanceActiveConstraint\nNodalBalanceReactiveConstraint\nAreaParticipationAssignmentConstraint\n```\n\n### Power Variable Limit Constraints\n\n```@docs\nActivePowerVariableLimitsConstraint\nReactivePowerVariableLimitsConstraint\nActivePowerVariableTimeSeriesLimitsConstraint\nInputActivePowerVariableLimitsConstraint\nOutputActivePowerVariableLimitsConstraint\nActivePowerInVariableTimeSeriesLimitsConstraint\nActivePowerOutVariableTimeSeriesLimitsConstraint\n```\n\n### Services Constraints\n\n```@docs\nRequirementConstraint\nParticipationFractionConstraint\nReservePowerConstraint\n```\n\n### Thermal Unit Constraints\n\n```@docs\nActiveRangeICConstraint\nCommitmentConstraint\nDurationConstraint\nRampConstraint\nStartupInitialConditionConstraint\nStartupTimeLimitTemperatureConstraint\n```\n\n### Renewable Unit Constraints\n\n```@docs\nEqualityConstraint\n```\n\n## Source Constraints\n\n```@docs\nImportExportBudgetConstraint\n```\n\n## Load Constraints\n\n```@docs\nShiftDownActivePowerVariableLimitsConstraint\nNonAnticipativityConstraint\nShiftUpActivePowerVariableLimitsConstraint\nRealizedShiftedLoadMinimumBoundConstraint\nShiftedActivePowerBalanceConstraint\n```\n\n### Branches Constraints\n\n```@docs\nFlowLimitConstraint\nFlowRateConstraint\nFlowRateConstraintFromTo\nFlowRateConstraintToFrom\nHVDCPowerBalance\nNetworkFlowConstraint\nPhaseAngleControlLimit\n```\n\n### Two Terminal and Multi-Terminal HVDC Constraints\n\n```@docs\nConverterLossConstraint\nInterpolationVoltageConstraints\nInterpolationCurrentConstraints\nInterpolationBilinearConstraints\nCurrentAbsoluteValueConstraint\nConverterPowerCalculationConstraint\nConverterMcCormickEnvelopes\nDCLineCurrentConstraint\nDCCurrentBalance\n```\n\n### Contingency Constraints\n\n```@docs\nPostContingencyGenerationBalanceConstraint\nPostContingencyActivePowerVariableLimitsConstraint\nPostContingencyActivePowerReserveDeploymentVariableLimitsConstraint\n```\n\n### Market Bid Cost Constraints\n\n```@docs\nPiecewiseLinearBlockIncrementalOfferConstraint\nPiecewiseLinearBlockDecrementalOfferConstraint\n```\n\n### Feedforward Constraints\n\n```@docs\nFeedforwardSemiContinuousConstraint\nFeedforwardUpperBoundConstraint\nFeedforwardLowerBoundConstraint\n```\n\n```@raw html\n&nbsp;\n&nbsp;\n```\n\n* * *\n\n## Parameters\n\n### Time Series Parameters\n\n```@docs\nActivePowerTimeSeriesParameter\nReactivePowerTimeSeriesParameter\nRequirementTimeSeriesParameter\nReactivePowerOffsetParameter\nActivePowerOutTimeSeriesParameter\nActivePowerInTimeSeriesParameter\nFuelCostParameter\nFromToFlowLimitParameter\nToFromFlowLimitParameter\n```\n\n### Variable Value Parameters\n\n```@docs\nUpperBoundValueParameter\nLowerBoundValueParameter\nOnStatusParameter\nFixValueParameter\n```\n\n### Objective Function Parameters\n\n```@docs\nCostFunctionParameter\n```\n\n### Events Parameters\n\n```@docs\nAvailableStatusChangeCountdownParameter\nAvailableStatusParameter\nActivePowerOffsetParameter\nDynamicBranchRatingTimeSeriesParameter\nPostContingencyDynamicBranchRatingTimeSeriesParameter\n```\n\n## Results\n\n### Acessing Optimization Model\n\n```@autodocs\nModules = [PowerSimulations]\nPages   = [\"optimization_container.jl\",\n            \"optimization_debugging.jl\"\n           ]\nOrder = [:type, :function]\nPublic = true\nPrivate = false\n```\n\n### Accessing Problem Results\n\n```@autodocs\nModules = [PowerSimulations]\nPages   = [\"operation/problem_results.jl\",\n           ]\nOrder = [:type, :function]\nPublic = true\nPrivate = false\n```\n\n### Accessing Simulation Results\n\n```@autodocs\nModules = [PowerSimulations]\nPages   = [\"simulation_results.jl\",\n            \"simulation_problem_results.jl\",\n            \"simulation_partition_results.jl\",\n            \"hdf_simulation_store.jl\"\n           ]\nOrder = [:type, :function]\nPublic = true\nPrivate = false\n```\n\n## Simulation Recorder\n\n```@autodocs\nModules = [PowerSimulations]\nPages   = [\"utils/recorder_events.jl\",\n           ]\nOrder = [:type, :function]\nPublic = true\nPrivate = false\n```\n"
  },
  {
    "path": "docs/src/api/developer.md",
    "content": "# Guidelines for Developers\n\nIn order to contribute to `PowerSimulations.jl` repository please read the following sections of\n[`InfrastructureSystems.jl`](https://github.com/Sienna-Platform/InfrastructureSystems.jl)\ndocumentation in detail:\n\n 1. [Style Guide](https://nrel-Sienna.github.io/InfrastructureSystems.jl/stable/style/)\n 2. [Contributing Guidelines](https://github.com/Sienna-Platform/PowerSimulations.jl/blob/main/CONTRIBUTING.md)\n\nPull requests are always welcome to fix bugs or add additional modeling capabilities.\n\n**All the code contributions need to include tests with a minimum coverage of 70%**\n"
  },
  {
    "path": "docs/src/api/glossary.md",
    "content": "# Definitions\n\n## A\n\n  - *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:\n\n```julia\nset_device_model!(\n    template,\n    DeviceModel(\n        GenericBattery,\n        StorageDispatchWithReserves;\n        attributes = Dict{String, Any}(\n            \"reservation\" => false,\n            \"cycling_limits\" => false,\n            \"energy_target\" => false,\n            \"complete_coverage\" => false,\n            \"regularization\" => false,\n        ),\n    ),\n)\n```\n\nChanging the attributes between `true` or `false` can enable/disable multiple aspects of the formulation.\n\n## C\n\n  - *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.\n\n## D\n\n  - *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.\n\n  - *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).\n\n## E\n\n  - *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.\n\n## F\n\n  - *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.\n\n## H\n\n  - *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/)\n\n## I\n\n  - *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/)\n\n## R\n\n  - *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/)\n\n  - *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.\n\n## S\n\n  - *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).\n\n  - *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.\n  - *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.\n\n## T\n\n  - *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).\n"
  },
  {
    "path": "docs/src/api/internal.md",
    "content": "```@meta\nCollapsedDocStrings = true\n```\n\n# Internal API\n\n```@autodocs\nModules = [PowerSimulations]\nPublic = false\n```\n"
  },
  {
    "path": "docs/src/code_base_developer_guide/extending_powersimulations.md",
    "content": "# Extending Source Code Functionalities\n\n## Enable other recorder events\n\nOther types of recorder events can be enabled with a possible performance impact. To do this\npass in the specific recorder names to be enabled when you call build.\n\n```julia\nsim = Simulation(...)\nrecorders = [:execution]\nbuild!(sim; recorders = recorders)\nexecute!(sim)\n```\n\nNow we can examine InitialConditionUpdateEvents for specific steps and stages.\n\n```julia\nshow_simulation_events(\n    PSI.InitialConditionUpdateEvent,\n    \"./output/aggregation/1\",\n    x -> x.initial_condition_type == \"DeviceStatus\";\n    step = 2,\n    stage = 1\n)\n┌─────────────────────────────┬─────────────────────┬────────────────────────┬─────────────────┬─────────────┬─────┬──────────────┐\n│                        name │     simulation_time │ initial_condition_type │     device_type │ device_name │ val │ stage_number │\n├─────────────────────────────┼─────────────────────┼────────────────────────┼─────────────────┼─────────────┼─────┼──────────────┤\n│ InitialConditionUpdateEvent │ 2024-01-02T00:00:00 │           DeviceStatus │ ThermalStandard │    Solitude │ 0.0 │            1 │\n│ InitialConditionUpdateEvent │ 2024-01-02T00:00:00 │           DeviceStatus │ ThermalStandard │   Park City │ 1.0 │            1 │\n│ InitialConditionUpdateEvent │ 2024-01-02T00:00:00 │           DeviceStatus │ ThermalStandard │        Alta │ 1.0 │            1 │\n│ InitialConditionUpdateEvent │ 2024-01-02T00:00:00 │           DeviceStatus │ ThermalStandard │    Brighton │ 1.0 │            1 │\n│ InitialConditionUpdateEvent │ 2024-01-02T00:00:00 │           DeviceStatus │ ThermalStandard │    Sundance │ 0.0 │            1 │\n└─────────────────────────────┴─────────────────────┴────────────────────────┴─────────────────┴─────────────┴─────┴──────────────┘\n```\n\n## Show the wall time with your events\n\nSometimes you might want to see how the events line up with the wall time.\n\n```julia\nshow_simulation_events(\n    PSI.InitialConditionUpdateEvent,\n    \"./output/aggregation/1\",\n    x -> x.initial_condition_type == \"DeviceStatus\";\n    step = 2,\n    stage = 1,\n    wall_time = true\n)\n┌─────────────────────────┬─────────────────────────────┬─────────────────────┬────────────────────────┬─────────────────┬─────────────┬─────┬──────────────┐\n│               timestamp │                        name │     simulation_time │ initial_condition_type │     device_type │ device_name │ val │ stage_number │\n├─────────────────────────┼─────────────────────────────┼─────────────────────┼────────────────────────┼─────────────────┼─────────────┼─────┼──────────────┤\n│ 2020-04-07T15:08:32.711 │ InitialConditionUpdateEvent │ 2024-01-02T00:00:00 │           DeviceStatus │ ThermalStandard │    Solitude │ 0.0 │            1 │\n│ 2020-04-07T15:08:32.711 │ InitialConditionUpdateEvent │ 2024-01-02T00:00:00 │           DeviceStatus │ ThermalStandard │   Park City │ 1.0 │            1 │\n│ 2020-04-07T15:08:32.711 │ InitialConditionUpdateEvent │ 2024-01-02T00:00:00 │           DeviceStatus │ ThermalStandard │        Alta │ 1.0 │            1 │\n│ 2020-04-07T15:08:32.711 │ InitialConditionUpdateEvent │ 2024-01-02T00:00:00 │           DeviceStatus │ ThermalStandard │    Brighton │ 1.0 │            1 │\n│ 2020-04-07T15:08:32.711 │ InitialConditionUpdateEvent │ 2024-01-02T00:00:00 │           DeviceStatus │ ThermalStandard │    Sundance │ 0.0 │            1 │\n└─────────────────────────┴─────────────────────────────┴─────────────────────┴────────────────────────┴─────────────────┴─────────────┴─────┴──────────────┘\n```\n"
  },
  {
    "path": "docs/src/explanation/chronologies.md",
    "content": "# [Chronologies](@id chronologies)\n\nIn PowerSimulations, chronologies define where information is flowing. There are two types\nof chronologies.\n\n  - inter-stage chronologies: Define how information flows between stages. e.g. day-ahead solutions are used to inform economic dispatch problems\n  - 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.\n"
  },
  {
    "path": "docs/src/explanation/feedforward.md",
    "content": "# [Feedforward](@id feedforward)\n\nThe definition of exactly what information is passed using the defined chronologies is accomplished using FeedForwards.\n\nSpecifically, 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.\n\nThe 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.\n"
  },
  {
    "path": "docs/src/explanation/psi_structure.md",
    "content": "# [PowerSimulations.jl Modeling Structure](@id psi_structure)\n\nPowerSimulations enables the simulation of a sequence of power systems optimization problems and provides user control over each aspect of the simulation configuration. Specifically:\n\n  - mathematical formulations can be selected for each component with [`DeviceModel`](@ref) and [`ServiceModel`](@ref)\n  - a problem can be defined by creating model entries in a [Operations `ProblemTemplate`s](@ref op_problem_template)\n  - 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))\n  - [`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.\n\n!!! question \"What is the difference between a Model and a Problem?\"\n    \n    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.\n"
  },
  {
    "path": "docs/src/explanation/sequencing.md",
    "content": "# [Sequencing](@id sequencing)\n\nIn 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.\n\nDepending 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).\n"
  },
  {
    "path": "docs/src/formulation_library/Branch.md",
    "content": "# `PowerSystems.Branch` Formulations\n\n!!! note\n    \n    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.\n\n### Table of contents\n\n 1. [`StaticBranch`](#StaticBranch)\n 2. [`StaticBranchBounds`](#StaticBranchBounds)\n 3. [`StaticBranchUnbounded`](#StaticBranchUnbounded)\n 4. [`HVDCTwoTerminalUnbounded`](#HVDCTwoTerminalUnbounded)\n 5. [`HVDCTwoTerminalLossless`](#HVDCTwoTerminalLossless)\n 6. [`HVDCTwoTerminalDispatch`](#HVDCTwoTerminalDispatch)\n 7. [`PhaseAngleControl`](#PhaseAngleControl)\n 8. [`TwoTerminalLCCLine`](#TwoTerminalLCCLine)\n 9. [Valid configurations](#Valid-configurations)\n\n## `StaticBranch`\n\nFormulation valid for `PTDFPowerModel` Network model\n\n```@docs\nStaticBranch\n```\n\n**Variables:**\n\n  - [`FlowActivePowerVariable`](@ref):\n    \n      + Bounds: ``(-\\infty,\\infty)``\n      + Symbol: ``f``\n        If Slack variables are enabled:\n\n  - [`FlowActivePowerSlackUpperBound`](@ref):\n    \n      + Bounds: [0.0, ]\n      + Default proportional cost: 2e5\n      + Symbol: ``f^\\text{sl,up}``\n  - [`FlowActivePowerSlackLowerBound`](@ref):\n    \n      + Bounds: [0.0, ]\n      + Default proportional cost: 2e5\n      + Symbol: ``f^\\text{sl,lo}``\n\n**Static Parameters**\n\n  - ``R^\\text{max}`` = `PowerSystems.get_rating(branch)`\n\n**Objective:**\n\nAdd 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``\n\n**Expressions:**\n\nNo expressions are used.\n\n**Constraints:**\n\nFor each branch ``b \\in \\{1,\\dots, B\\}`` (in a system with ``N`` buses) the constraints are given by:\n\n```math\n\\begin{aligned}\n&  f_t = \\sum_{i=1}^N \\text{PTDF}_{i,b} \\cdot \\text{Bal}_{i,t}, \\quad \\forall t \\in \\{1,\\dots, T\\}\\\\\n&  f_t - f_t^\\text{sl,up} \\le R^\\text{max},\\quad \\forall t \\in \\{1,\\dots, T\\} \\\\\n&  f_t + f_t^\\text{sl,lo} \\ge -R^\\text{max},\\quad \\forall t \\in \\{1,\\dots, T\\} \n\\end{aligned}\n```\n\non 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``.\n\n* * *\n\n## `StaticBranchBounds`\n\nFormulation valid for `PTDFPowerModel` Network model\n\n```@docs\nStaticBranchBounds\n```\n\n**Variables:**\n\n  - [`FlowActivePowerVariable`](@ref):\n    \n      + Bounds: ``\\left[-R^\\text{max},R^\\text{max}\\right]``\n      + Symbol: ``f``\n\n**Static Parameters**\n\n  - ``R^\\text{max}`` = `PowerSystems.get_rating(branch)`\n\n**Objective:**\n\nNo cost is added to the objective function.\n\n**Expressions:**\n\nNo expressions are used.\n\n**Constraints:**\n\nFor each branch ``b \\in \\{1,\\dots, B\\}`` (in a system with ``N`` buses) the constraints are given by:\n\n```math\n\\begin{aligned}\n&  f_t = \\sum_{i=1}^N \\text{PTDF}_{i,b} \\cdot \\text{Bal}_{i,t}, \\quad \\forall t \\in \\{1,\\dots, T\\}\n\\end{aligned}\n```\n\non 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``.\n\n* * *\n\n## `StaticBranchUnbounded`\n\nFormulation valid for `PTDFPowerModel` Network model\n\n```@docs\nStaticBranchUnbounded\n```\n\n  - [`FlowActivePowerVariable`](@ref):\n    \n      + Bounds: ``(-\\infty,\\infty)``\n      + Symbol: ``f``\n\n**Objective:**\n\nNo cost is added to the objective function.\n\n**Expressions:**\n\nNo expressions are used.\n\n**Constraints:**\n\nFor each branch ``b \\in \\{1,\\dots, B\\}`` (in a system with ``N`` buses) the constraints are given by:\n\n```math\n\\begin{aligned}\n&  f_t = \\sum_{i=1}^N \\text{PTDF}_{i,b} \\cdot \\text{Bal}_{i,t}, \\quad \\forall t \\in \\{1,\\dots, T\\}\n\\end{aligned}\n```\n\non 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``.\n\n* * *\n\n## `HVDCTwoTerminalUnbounded`\n\nFormulation valid for `PTDFPowerModel` Network model\n\n```@docs\nHVDCTwoTerminalUnbounded\n```\n\nThis model assumes that it can transfer power from two AC buses without losses and no limits.\n\n**Variables:**\n\n  - [`FlowActivePowerVariable`](@ref):\n    \n      + Bounds: ``\\left(-\\infty,\\infty\\right)``\n      + Symbol: ``f``\n\n**Objective:**\n\nNo cost is added to the objective function.\n\n**Expressions:**\n\nThe 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.\n\n**Constraints:**\n\nNo constraints are added.\n\n* * *\n\n## `HVDCTwoTerminalLossless`\n\nFormulation valid for `PTDFPowerModel` Network model\n\n```@docs\nHVDCTwoTerminalLossless\n```\n\nThis model assumes that it can transfer power from two AC buses without losses.\n\n**Variables:**\n\n  - [`FlowActivePowerVariable`](@ref):\n    \n      + Bounds: ``\\left(-\\infty,\\infty\\right)``\n      + Symbol: ``f``\n\n**Static Parameters**\n\n  - ``R^\\text{from,min}`` = `PowerSystems.get_active_power_limits_from(branch).min`\n  - ``R^\\text{from,max}`` = `PowerSystems.get_active_power_limits_from(branch).max`\n  - ``R^\\text{to,min}`` = `PowerSystems.get_active_power_limits_to(branch).min`\n  - ``R^\\text{to,max}`` = `PowerSystems.get_active_power_limits_to(branch).max`\n\n**Objective:**\n\nNo cost is added to the objective function.\n\n**Expressions:**\n\nThe 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.\n\n**Constraints:**\n\n```math\n\\begin{align*}\n&  R^\\text{min} \\le f_t  \\le R^\\text{max},\\quad \\forall t \\in \\{1,\\dots, T\\} \\\\\n\\end{align*}\n```\n\nwhere:\n\n```math\n\\begin{align*}\n&  R^\\text{min} = \\begin{cases}\n\t\t\t\\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 \\\\\n      \\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 \\\\\n      R^\\text{from,min},& \\text{if } R^\\text{from,min} \\le 0 \\text{ and } R^\\text{to,min} \\ge 0 \\\\\n      R^\\text{to,min},& \\text{if } R^\\text{from,min} \\ge 0 \\text{ and } R^\\text{to,min} \\le 0\n\t\t \\end{cases}\n\\end{align*}\n```\n\nand\n\n```math\n\\begin{align*}\n&  R^\\text{max} = \\begin{cases}\n\t\t\t\\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 \\\\\n      \\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 \\\\\n      R^\\text{from,max},& \\text{if } R^\\text{from,max} \\le 0 \\text{ and } R^\\text{to,max} \\ge 0 \\\\\n      R^\\text{to,max},& \\text{if } R^\\text{from,max} \\ge 0 \\text{ and } R^\\text{to,max} \\le 0\n\t\t \\end{cases}\n\\end{align*}\n```\n\n* * *\n\n## `HVDCTwoTerminalDispatch`\n\nFormulation valid for `PTDFPowerModel` Network model\n\n```@docs\nHVDCTwoTerminalDispatch\n```\n\n**Variables**\n\n  - [`FlowActivePowerToFromVariable`](@ref):\n    \n      + Symbol: ``f^\\text{to-from}``\n\n  - [`FlowActivePowerFromToVariable`](@ref):\n    \n      + Symbol: ``f^\\text{from-to}``\n  - [`HVDCLosses`](@ref):\n    \n      + Symbol: ``\\ell``\n  - [`HVDCFlowDirectionVariable`](@ref)\n    \n      + Bounds: ``\\{0,1\\}``\n      + Symbol: ``u^\\text{dir}``\n\n**Static Parameters**\n\n  - ``R^\\text{from,min}`` = `PowerSystems.get_active_power_limits_from(branch).min`\n  - ``R^\\text{from,max}`` = `PowerSystems.get_active_power_limits_from(branch).max`\n  - ``R^\\text{to,min}`` = `PowerSystems.get_active_power_limits_to(branch).min`\n  - ``R^\\text{to,max}`` = `PowerSystems.get_active_power_limits_to(branch).max`\n  - ``L_0`` = `PowerSystems.get_loss(branch).l0`\n  - ``L_1`` = `PowerSystems.get_loss(branch).l1`\n\n**Objective:**\n\nNo cost is added to the objective function.\n\n**Expressions:**\n\nEach `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.\n\nIn addition, the `HVDCLosses` are subtracted to the `from` bus in the `ActivePowerBalance` expression.\n\n**Constraints:**\n\n```math\n\\begin{align*}\n&  R^\\text{from,min} \\le f_t^\\text{from-to}  \\le R^\\text{from,max}, \\forall t \\in \\{1,\\dots, T\\} \\\\\n&  R^\\text{to,min} \\le f_t^\\text{to-from}  \\le R^\\text{to,max},\\quad \\forall t \\in \\{1,\\dots, T\\} \\\\\n& 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\\} \\\\\n& 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\\} \\\\\n& 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\\} \\\\\n& 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\\} \\\\\n& f_t^\\text{to-from} - f_t^\\text{from-to} \\le \\ell_t,\\quad \\forall t \\in \\{1,\\dots, T\\} \\\\\n& f_t^\\text{from-to} - f_t^\\text{to-from} \\le \\ell_t,\\quad \\forall t \\in \\{1,\\dots, T\\} \n\\end{align*}\n```\n\n* * *\n\n## `PhaseAngleControl`\n\nFormulation valid for `PTDFPowerModel` Network model\n\n```@docs\nPhaseAngleControl\n```\n\n**Variables:**\n\n  - [`FlowActivePowerVariable`](@ref):\n    \n      + Bounds: ``(-\\infty,\\infty)``\n      + Symbol: ``f``\n\n  - [`PhaseShifterAngle`](@ref):\n    \n      + Symbol: ``\\theta^\\text{shift}``\n\n**Static Parameters**\n\n  - ``R^\\text{max}`` = `PowerSystems.get_rating(branch)`\n  - ``\\Theta^\\text{min}`` = `PowerSystems.get_phase_angle_limits(branch).min`\n  - ``\\Theta^\\text{max}`` = `PowerSystems.get_phase_angle_limits(branch).max`\n  - ``X`` = `PowerSystems.get_x(branch)` (series reactance)\n\n**Objective:**\n\nNo changes to objective function\n\n**Expressions:**\n\nAdds 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.\n\n**Constraints:**\n\nFor each branch ``b \\in \\{1,\\dots, B\\}`` (in a system with ``N`` buses) the constraints are given by:\n\n```math\n\\begin{aligned}\n&  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\\}\\\\\n&  -R^\\text{max} \\le f_t  \\le R^\\text{max},\\quad \\forall t \\in \\{1,\\dots, T\\} \n\\end{aligned}\n```\n\non 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``.\n\n* * *\n\n## `TwoTerminalLCCLine`\n\nFormulation valid for `ACPPowerModel` Network model\n\n**Variables:**\n\n  - [`HVDCRectifierDelayAngleVariable`]:\n    \n        + Bounds: ``(-\\alpha_{r,t}^\\text{min},\\alpha_{r,t}^\\text{max})``\n        + Symbol: ``\\alpha_{r,t}``\n\n  - [`HVDCInverterExtinctionAngleVariable`]:\n    \n        + Bounds: ``(-\\gamma_{i,t}^\\text{min},\\gamma_{i,t}^\\text{max})``\n        + Symbol: ``\\gamma_{i,t}``\n  - [`HVDCRectifierPowerFactorAngleVariable`]:\n    \n        + Bounds: ``\\{0,1\\}``\n        + Symbol: ``\\phi_{r,t}``\n  - [`HVDCInverterPowerFactorAngleVariable`]:\n    \n        + Bounds: ``\\{0,1\\}``\n        + Symbol: ``\\phi_{i,t}``\n  - [`HVDCRectifierOverlapAngleVariable`]:\n    \n        + Bounds: [0.0, ]\n        + Symbol: ``\\mu_{r,t}``\n  - [`HVDCInverterOverlapAngleVariable`]:\n    \n        + Bounds: [0.0, ]\n        + Symbol: ``\\mu_{i,t}``\n  - [`HVDCRectifierTapSettingVariable`]:\n    \n        + Bounds: ``(t_{r,t}^\\text{min},t_{r,t}^\\text{max})``\n        + Symbol: ``t_{r,t}``\n  - [`HVDCInverterTapSettingVariable`]:\n    \n        + Bounds: ``(t_{i,t}^\\text{min},t_{i,t}^\\text{max})``\n        + Symbol: ``t_{i,t}``\n  - [`HVDCRectifierDCVoltageVariable`]:\n    \n        + Bounds: [0.0, ]\n        + Symbol: ``v_{r,t}^\\text{dc}``\n  - [`HVDCInverterDCVoltageVariable`]:\n    \n        + Bounds: [0.0, ]\n        + Symbol: ``v_{i,t}^\\text{dc}``\n  - [`HVDCRectifierACCurrentVariable`]:\n    \n        + Bounds: [0.0, ]\n        + Symbol: ``I_{r,t}^\\text{ac}``\n  - [`HVDCInverterACCurrentVariable`]:\n    \n        + Bounds: [0.0, ]\n        + Symbol: ``I_{i,t}^\\text{ac}``\n  - [`DCLineCurrentFlowVariable`]:\n    \n        + Bounds: [0.0, ]\n        + Symbol: ``I^\\text{dc}``\n  - [`HVDCActivePowerReceivedFromVariable`]:\n    \n        + Bounds: [0.0, ]\n        + Symbol: ``p_{r,t}^\\text{ac}``\n  - [`HVDCActivePowerReceivedToVariable`]:\n    \n        + Bounds: [0.0, ]\n        + Symbol: ``p_{i,t}^\\text{ac}``\n  - [`HVDCReactivePowerReceivedFromVariable`]:\n    \n        + Bounds: [0.0, ]\n        + Symbol: ``q_{r,t}^\\text{ac}``\n  - [`HVDCReactivePowerReceivedToVariable`]:\n    \n        + Bounds: [0.0, ]\n        + Symbol: ``q_{i,t}^\\text{ac}``\n\n**Static Parameters**\n\n  - ``R^\\text{dc}`` = `PowerSystems.get_r(lcc)`\n  - ``N_r`` = `PowerSystems.get_rectifier_bridges(lcc)`\n  - ``N_i`` = `PowerSystems.get_inverter_bridges(lcc)`\n  - ``X_r`` = `PowerSystems.get_rectifier_xc(lcc)`\n  - ``X_i`` = `PowerSystems.get_inverter_xc(lcc)`\n  - ``a_r`` = `PowerSystems.get_rectifier_transformer_ratio(lcc)`\n  - ``a_i`` = `PowerSystems.get_inverter_transformer_ratio(lcc)`\n  - ``t_r`` = `PowerSystems.get_rectifier_tap_setting(lcc)`\n  - ``t_i`` = `PowerSystems.get_inverter_tap_setting(lcc)`\n  - ``t^\\text{min}_r`` = `PowerSystems.get_rectifier_tap_setting(lcc).min`\n  - ``t^\\text{max}_r`` = `PowerSystems.get_rectifier_tap_setting(lcc).max`\n  - ``t^\\text{min}_i`` = `PowerSystems.get_inverter_tap_setting(lcc).min`\n  - ``t^\\text{max}_i`` = `PowerSystems.get_inverter_tap_setting(lcc).max`\n\n**Objective:**\n\nNo changes to objective function\n\n**Expressions:**\n\nThe 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.\n\nThe 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.\n\n**Constraints:**\n\n  - **Rectifier:**\n\n```math\n\\begin{aligned}\n&  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)\\\\\n& \\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}\\\\\n& \\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)\\\\\n\\end{aligned}\n```\n\nWhich can be approximated as:\n\n```math\n\\begin{aligned}\n& \\phi_{r,t} = arccos(\\frac{1}{2}\\cos\\alpha_{r,t} + \\frac{1}{2}\\cos(\\alpha_{r,t} + \\mu_{r,t}))\n\\end{aligned}\n```\n\n```math\n\\begin{aligned}\n& I^\\text{ac}_{r,t} = \\sqrt{6} \\frac{N_r}{\\pi} I^\\text{dc}_t\\\\\n& 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}} \\\\\n& 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}} \\\\\n\\end{aligned}\n```\n\n  - **Inverter:**\n\n```math\n\\begin{aligned}\n&  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)\\\\\n& \\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}\\\\\n& \\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)\\\\\n\\end{aligned}\n```\n\nWhich can be approximated as:\n\n```math\n\\begin{aligned}\n& \\phi_{i,t} = arccos(\\frac{1}{2}\\cos\\gamma_{i,t} + \\frac{1}{2}\\cos(\\gamma_{i,t} + \\mu_{i,t}))\n\\end{aligned}\n```\n\n```math\n\\begin{aligned}\n& I^\\text{ac}_{i,t} = \\sqrt{6} \\frac{N_i}{\\pi} I^\\text{dc}_t\\\\\n& 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}} \\\\\n& 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}} \\\\\n\\end{aligned}\n```\n\n  - **DC Transmission Line:**\n\n```math\n\\begin{aligned}\n&  v^\\text{dc}_{i,t} = v^\\text{dc}_{r,t} - R_\\text{dc}I^\\text{dc}_t\n\\end{aligned}\n```\n\n* * *\n\n## Valid configurations\n\nValid [`DeviceModel`](@ref)s for subtypes of `Branch` include the following:\n\n```@eval\nusing PowerSimulations\nusing PowerSystems\nusing DataFrames\nusing Latexify\ncombos = PowerSimulations.generate_device_formulation_combinations()\nfilter!(x -> (x[\"device_type\"] <: Branch) && (x[\"device_type\"] != TModelHVDCLine), combos)\ncombo_table = DataFrame(\n    \"Valid DeviceModel\" =>\n        [\"`DeviceModel($(c[\"device_type\"]), $(c[\"formulation\"]))`\" for c in combos],\n    \"Device Type\" => [\n        \"[$(c[\"device_type\"])](https://nrel-Sienna.github.io/PowerSystems.jl/stable/model_library/generated_$(c[\"device_type\"])/)\"\n        for c in combos\n    ],\n    \"Formulation\" => [\"[$(c[\"formulation\"])](@ref)\" for c in combos],\n)\nmdtable(combo_table; latex = false)\n```\n"
  },
  {
    "path": "docs/src/formulation_library/DCModels.md",
    "content": "# DC Models formulations\n\n!!! note\n    \n    Multi-terminal DC models are still in early stages of development and future versions will add a more comprehensive list of formulations\n\n* * *\n\n## LosslessLine\n\n`LosslessLine` models are used with `PSY.DCBranch` models.\n\n```@docs\nLosslessLine\n```\n\n**Variables:**\n\n  - [`FlowActivePowerVariable`](@ref):\n    \n      + Bounds: ``(R^\\text{min},R^\\text{max})``\n      + Symbol: ``f``\n\n**Static Parameters**\n\n  - ``R^\\text{from,min}`` = `PowerSystems.get_active_power_limits_from(branch).min`\n  - ``R^\\text{from,max}`` = `PowerSystems.get_active_power_limits_from(branch).max`\n  - ``R^\\text{to,min}`` = `PowerSystems.get_active_power_limits_to(branch).min`\n  - ``R^\\text{to,max}`` = `PowerSystems.get_active_power_limits_to(branch).max`\n\nThen, 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})`\n\n**Objective:**\n\nNo cost is added to the objective function.\n\n**Expressions:**\n\nThe 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.\n\n**Constraints:**\n\nNo constraints are added to the function.\n\n* * *\n\n## LosslessConverter\n\nConverters are used to interface the AC Buses with DC Buses.\n\n```@docs\nLosslessConverter\n```\n\n**Variables:**\n\n  - [`ActivePowerVariable`](@ref):\n    \n      + Bounds: ``(P^\\text{min},P^\\text{max})``\n      + Symbol: ``p``\n\n**Static Parameters:**\n\n  - ``P^\\text{min}`` = `PowerSystems.get_active_power_limits(device).min`\n  - ``P^\\text{max}`` = `PowerSystems.get_active_power_limits(device).max`\n\n**Objective:**\n\nNo cost is added to the objective function.\n\n**Expressions:**\n\nThe 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.\n\n**Constraints:**\n\nNo constraints are added to the function.\n"
  },
  {
    "path": "docs/src/formulation_library/Feedforward.md",
    "content": "# [FeedForward Formulations](@id ff_formulations)\n\n**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.\n\nThe 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.\n\n### Table of contents\n\n 1. [`SemiContinuousFeedforward`](#SemiContinuousFeedForward)\n 2. [`FixValueFeedforward`](#FixValueFeedforward)\n 3. [`UpperBoundFeedforward`](#UpperBoundFeedforward)\n 4. [`LowerBoundFeedforward`](#LowerBoundFeedforward)\n\n* * *\n\n## `SemiContinuousFeedforward`\n\n```@docs\nSemiContinuousFeedforward\n```\n\n**Variables:**\n\nNo variables are created\n\n**Parameters:**\n\n  - ``\\text{on}^\\text{th}`` = `OnStatusParameter` obtained from the source variable, typically the commitment variable of the unit commitment problem ``u^\\text{th}``.\n\n**Objective:**\n\nNo changes to the objective function.\n\n**Expressions:**\n\nAdds ``-\\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.\n\n**Constraints:**\n\nLimits the `ActivePowerRangeExpressionUB` and `ActivePowerRangeExpressionLB` by zero as:\n\n```math\n\\begin{align*}\n&  \\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\\}  \\\\\n&  \\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\\} \n\\end{align*}\n```\n\nThus, 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.\n\n* * *\n\n## `FixValueFeedforward`\n\n```@docs\nFixValueFeedforward\n```\n\n**Variables:**\n\nNo variables are created\n\n**Parameters:**\n\nThe parameter `FixValueParameter` is used to match the result obtained from the source variable (from the simulation state).\n\n**Objective:**\n\nNo changes to the objective function.\n\n**Expressions:**\n\nNo changes on expressions.\n\n**Constraints:**\n\nSet the `VariableType` from the `affected_values` to be equal to the source parameter store in `FixValueParameter`\n\n```math\n\\begin{align*}\n&  \\text{AffectedVariable}_t = \\text{SourceVariableParameter}_t, \\quad \\forall t \\in \\{1,\\dots, T\\}\n\\end{align*}\n```\n\n* * *\n\n## `UpperBoundFeedforward`\n\n```@docs\nUpperBoundFeedforward\n```\n\n**Variables:**\n\nIf slack variables are enabled:\n\n  - [`UpperBoundFeedForwardSlack`](@ref)\n    \n      + Bounds: [0.0, ]\n      + Default proportional cost: 1e6\n      + Symbol: ``p^\\text{ff,ubsl}``\n\n**Parameters:**\n\nThe 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.\n\n**Objective:**\n\nThe slack variable is added to the objective function using its large default cost ``+ p^\\text{ff,ubsl} \\cdot 10^6``\n\n**Expressions:**\n\nNo changes on expressions.\n\n**Constraints:**\n\nSet the `VariableType` from the `affected_values` to be lower than the source parameter store in `UpperBoundValueParameter`.\n\n```math\n\\begin{align*}\n&   \\text{AffectedVariable}_t - p_t^\\text{ff,ubsl} \\le \\text{SourceVariableParameter}_t, \\quad \\forall t \\in \\{1,\\dots, T\\}\n\\end{align*}\n```\n\n* * *\n\n## `LowerBoundFeedforward`\n\n```@docs\nLowerBoundFeedforward\n```\n\n**Variables:**\n\nIf slack variables are enabled:\n\n  - [`LowerBoundFeedForwardSlack`](@ref)\n    \n      + Bounds: [0.0, ]\n      + Default proportional cost: 1e6\n      + Symbol: ``p^\\text{ff,lbsl}``\n\n**Parameters:**\n\nThe 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.\n\n**Objective:**\n\nThe slack variable is added to the objective function using its large default cost ``+ p^\\text{ff,lbsl} \\cdot 10^6``\n\n**Expressions:**\n\nNo changes on expressions.\n\n**Constraints:**\n\nSet the `VariableType` from the `affected_values` to be greater than the source parameter store in `LowerBoundValueParameter`.\n\n```math\n\\begin{align*}\n&   \\text{AffectedVariable}_t + p_t^\\text{ff,lbsl} \\ge \\text{SourceVariableParameter}_t, \\quad \\forall t \\in \\{1,\\dots, T\\}\n\\end{align*}\n```\n"
  },
  {
    "path": "docs/src/formulation_library/General.md",
    "content": "# [Formulations](@id formulation_library)\n\nModeling formulations are created by dispatching on abstract subtypes of `PowerSimulations.AbstractDeviceFormulation`\n\n## `FixedOutput`\n\n```@docs\nFixedOutput\n```\n\n**Variables:**\n\nNo variables are created for `DeviceModel(<:DeviceType, FixedOutput)`\n\n**Static Parameters:**\n\n  - ThermalGen:\n    \n      + ``P^\\text{th,max}`` = `PowerSystems.get_max_active_power(device)`\n      + ``Q^\\text{th,max}`` = `PowerSystems.get_max_reactive_power(device)`\n\n  - Storage:\n    \n      + ``P^\\text{st,max}`` = `PowerSystems.get_max_active_power(device)`\n      + ``Q^\\text{st,max}`` = `PowerSystems.get_max_reactive_power(device)`\n\n**Time Series Parameters:**\n\n```@eval\nusing PowerSimulations\nusing HydroPowerSimulations\nusing PowerSystems\nusing DataFrames\nusing Latexify\ncombo_tables = []\nfor t in [RenewableGen, ThermalGen, HydroGen, ElectricLoad]\n    combos = PowerSimulations.get_default_time_series_names(t, FixedOutput)\n    combo_table = DataFrame(\n        \"Parameter\" => map(x -> \"[`$x`](@ref)\", collect(keys(combos))),\n        \"Default Time Series Name\" => map(x -> \"`$x`\", collect(values(combos))),\n    )\n    insertcols!(combo_table, 1, \"Device Type\" => fill(string(t), length(combos)))\n    push!(combo_tables, combo_table)\nend\nmdtable(vcat(combo_tables...); latex = false)\n```\n\n**Objective:**\n\nNo objective terms are created for `DeviceModel(<:DeviceType, FixedOutput)`\n\n**Expressions:**\n\nAdds 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).\n\n**Constraints:**\n\nNo constraints are created for `DeviceModel(<:DeviceType, FixedOutput)`\n\n* * *\n\n## `FunctionData` Options\n\nPowerSimulations 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.\n\n### `LinearFunctionData`\n\n`variable_cost = LinearFunctionData(c)`: creates a fixed marginal cost term in the objective function\n\n```math\n\\begin{aligned}\n&  \\text{min} \\sum_{t} c * G_t\n\\end{aligned}\n```\n\n### `QuadraticFunctionData` and `PolynomialFunctionData`\n\n`variable_cost::QuadraticFunctionData` and `variable_cost::PolynomialFunctionData`: create a polynomial cost term in the objective function\n\n```math\n\\begin{aligned}\n&  \\text{min} \\sum_{t} \\sum_{n} C_n * G_t^n\n\\end{aligned}\n```\n\nwhere\n\n  - For `QuadraticFunctionData`:\n    \n      + ``C_0`` = `get_constant_term(variable_cost)`\n      + ``C_1`` = `get_proportional_term(variable_cost)`\n      + ``C_2`` = `get_quadratic_term(variable_cost)`\n\n  - For `PolynomialFunctionData`:\n    \n      + ``C_n`` = `get_coefficients(variable_cost)[n]`\n\n### `` and `PiecewiseLinearSlopeData`\n\n`variable_cost::PiecewiseLinearData` and `variable_cost::PiecewiseLinearSlopeData`: create a piecewise linear cost term in the objective function\n\n```math\n\\begin{aligned}\n&  \\text{min} \\sum_{t} f(G_t)\n\\end{aligned}\n```\n\nwhere\n\n  - For `variable_cost::PiecewiseLinearData`, ``f(x)`` is the piecewise linear function obtained by connecting the `(x, y)` points `get_points(variable_cost)` in order.\n  - 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.\n\n* * *\n\n## `StorageCost`\n\nAdds an objective function cost term according to:\n\n```math\n\\begin{aligned}\n&  \\text{min} \\sum_{t} \\quad [E^{surplus}_t * C^{penalty} - E^{shortage}_t * C^{value}]\n\\end{aligned}\n```\n\n**Impact of different cost configurations:**\n\nThe 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.\n\n| Case      | Energy Target | Energy Shortage Cost | Energy Value / Energy Surplus cost | Effect                                            |\n|:--------- |:------------- |:-------------------- |:---------------------------------- |:------------------------------------------------- |\n| Case 1(a) | $\\hat{E}=0$   | $C^{penalty}=0$      | $C^{value}=0$                      | no change                                         |\n| Case 1(b) | $\\hat{E}=0$   | $C^{penalty}=0$      | $C^{value}<0$                      | penalty for storing energy                        |\n| Case 1(c) | $\\hat{E}=0$   | $C^{penalty}>0$      | $C^{value}=0$                      | no penalties or incentives applied                |\n| Case 1(d) | $\\hat{E}=0$   | $C^{penalty}=0$      | $C^{value}>0$                      | incentive for storing energy                      |\n| Case 1(e) | $\\hat{E}=0$   | $C^{penalty}>0$      | $C^{value}<0$                      | penalty for storing energy                        |\n| Case 1(f) | $\\hat{E}=0$   | $C^{penalty}>0$      | $C^{value}>0$                      | incentive for storing energy                      |\n| Case 2(a) | $\\hat{E}>0$   | $C^{penalty}=0$      | $C^{value}=0$                      | no change                                         |\n| Case 2(b) | $\\hat{E}>0$   | $C^{penalty}=0$      | $C^{value}<0$                      | penalty on energy storage in excess of target     |\n| Case 2(c) | $\\hat{E}>0$   | $C^{penalty}>0$      | $C^{value}=0$                      | penalty on energy storage short of target         |\n| Case 2(d) | $\\hat{E}>0$   | $C^{penalty}=0$      | $C^{value}>0$                      | incentive on excess energy                        |\n| Case 2(e) | $\\hat{E}>0$   | $C^{penalty}>0$      | $C^{value}<0$                      | penalty on both excess/shortage of energy         |\n| Case 2(f) | $\\hat{E}>0$   | $C^{penalty}>0$      | $C^{value}>0$                      | penalty for shortage, incentive for excess energy |\n"
  },
  {
    "path": "docs/src/formulation_library/Introduction.md",
    "content": "# [Formulations Introduction](@id formulation_intro)\n\nPowerSimulations.jl enables modularity in its formulations by assigning a [`DeviceModel`](@ref) to each `PowerSystems.jl` component type existing in a defined system.\n\n`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**.\n\n## Example Formulation\n\nFor example a typical optimization problem in a [`DecisionModel`](@ref) in `PowerSimulations.jl` with three [`DeviceModel`](@ref) has the abstract form of:\n\n```math\n\\begin{align*}\n    &\\min_{\\boldsymbol{x}}~ \\text{Objective\\_DeviceModelA} + \\text{Objective\\_DeviceModelB} + \\text{Objective\\_DeviceModelC} \\\\\n    & ~~\\text{s.t.} \\\\\n    & \\hspace{0.9cm} \\text{Constraints\\_NetworkModel} \\\\\n    & \\hspace{0.9cm} \\text{Constraints\\_DeviceModelA} \\\\\n    & \\hspace{0.9cm} \\text{Constraints\\_DeviceModelB} \\\\\n    & \\hspace{0.9cm} \\text{Constraints\\_DeviceModelC} \n\\end{align*}\n```\n\nSuppose this is a system with the following characteristics:\n\n  - Horizon: 48 hours\n  - Interval: 24 hours\n  - Resolution: 1 hour\n  - Three Buses: 1, 2 and 3\n  - One `ThermalStandard` (device A) unit at bus 1\n  - One `RenewableDispatch` (device B) unit at bus 2\n  - One `PowerLoad` (device C) at bus 3\n  - Three `Line` that connects all the buses\n\nNow, we assign the following [`DeviceModel`](@ref) to each `PowerSystems.jl` with:\n\n| Type                | Formulation             |\n|:------------------- |:----------------------- |\n| Network             | `CopperPlatePowerModel` |\n| `ThermalStandard`   | `ThermalDispatchNoMin`  |\n| `RenewableDispatch` | `RenewableFullDispatch` |\n| `PowerLoad`         | `StaticPowerLoad`       |\n\nNote 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.\n\nEach [`DeviceModel`](@ref) formulation is described in specific in their respective page, but the overall optimization problem will end-up as:\n\n```math\n\\begin{align*}\n    &\\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} \\\\\n    & ~~\\text{s.t.} \\\\\n    & \\hspace{0.9cm} p_t^\\text{th} + p_t^\\text{re} = P_t^\\text{load}, \\quad \\forall t \\in {1,\\dots, 48} \\\\\n    & \\hspace{0.9cm} 0 \\le p_t^\\text{th} \\le P^\\text{th,max} \\\\\n    & \\hspace{0.9cm} 0 \\le p_t^\\text{re} \\le \\text{ActivePowerTimeSeriesParameter}_t \n\\end{align*}\n```\n\nNote 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}``.\n\n# Nomenclature\n\nIn the formulations described in the other pages, the nomenclature is as follows:\n\n  - Lowercase letters are used for variables, e.g., ``p`` for power.\n  - Uppercase letters are used for parameters, e.g., ``C`` for costs.\n  - Subscripts are used for indexing, e.g., ``(\\cdot)_t`` for indexing at time ``t``.\n  - Superscripts are used for descriptions, e.g., ``(\\cdot)^\\text{th}`` to describe a thermal (th) variable/parameter.\n  - Bold letters are used for vectors, e.g., ``\\boldsymbol{p} = \\{p\\}_{1,\\dots,24}``.\n"
  },
  {
    "path": "docs/src/formulation_library/Load.md",
    "content": "# `PowerSystems.ElectricLoad` Formulations\n\nElectric load formulations define the optimization models that describe load units (demand) mathematical model in different operational settings, such as economic dispatch and unit commitment.\n\n!!! note\n    \n    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.\n\n### Table of contents\n\n 1. [`StaticPowerLoad`](#StaticPowerLoad)\n 2. [`PowerLoadInterruption`](#PowerLoadInterruption)\n 3. [`PowerLoadDispatch`](#PowerLoadDispatch)\n 4. [`PowerLoadShift`](#PowerLoadShift)\n 5. [Valid configurations](#Valid-configurations)\n\n* * *\n\n## `StaticPowerLoad`\n\n```@docs\nStaticPowerLoad\n```\n\n**Variables:**\n\nNo variables are created\n\n**Time Series Parameters:**\n\nUses the `max_active_power`  timeseries parameter to determine the demand value at each time-step\n\n```@eval\nusing PowerSimulations\nusing PowerSystems\nusing DataFrames\nusing Latexify\ncombos = PowerSimulations.get_default_time_series_names(ElectricLoad, StaticPowerLoad)\ncombo_table = DataFrame(\n    \"Parameter\" => map(x -> \"[`$x`](@ref)\", collect(keys(combos))),\n    \"Default Time Series Name\" => map(x -> \"`$x`\", collect(values(combos))),\n)\nmdtable(combo_table; latex = false)\n```\n\n**Expressions:**\n\nSubtracts the parameters listed above from the respective active and reactive power balance expressions created by the selected [Network Formulations](@ref network_formulations).\n\n**Constraints:**\n\nNo constraints are created\n\n* * *\n\n## `PowerLoadInterruption`\n\n```@docs\nPowerLoadInterruption\n```\n\n**Variables:**\n\n  - [`ActivePowerVariable`](@ref):\n    \n      + Bounds: [0.0, ]\n      + Default initial value: 0.0\n      + Symbol: ``p^\\text{ld}``\n\n  - [`ReactivePowerVariable`](@ref):\n    \n      + Bounds: [0.0, ]\n      + Default initial value: 0.0\n      + Symbol: ``q^\\text{ld}``\n  - [`OnVariable`](@ref):\n    \n      + Bounds: ``\\{0,1\\}``\n      + Default initial value: 1\n      + Symbol: ``u^\\text{ld}``\n\n**Static Parameters:**\n\n  - ``P^\\text{ld,max}`` = `PowerSystems.get_max_active_power(device)`\n  - ``Q^\\text{ld,max}`` = `PowerSystems.get_max_reactive_power(device)`\n\n**Time Series Parameters:**\n\n```@eval\nusing PowerSimulations\nusing PowerSystems\nusing DataFrames\nusing Latexify\ncombos = PowerSimulations.get_default_time_series_names(ElectricLoad, PowerLoadInterruption)\ncombo_table = DataFrame(\n    \"Parameter\" => map(x -> \"[`$x`](@ref)\", collect(keys(combos))),\n    \"Default Time Series Name\" => map(x -> \"`$x`\", collect(values(combos))),\n)\nmdtable(combo_table; latex = false)\n```\n\n**Objective:**\n\nCreates an objective function term based on the [`FunctionData` Options](@ref) where the quantity term is defined as ``p^\\text{ld}``.\n\n**Expressions:**\n\n  - 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)\n\n**Constraints:**\n\n```math\n\\begin{aligned}\n&  p_t^\\text{ld} \\le u_t^\\text{ld} \\cdot \\text{ActivePowerTimeSeriesParameter}_t, \\quad \\forall t \\in \\{1,\\dots, T\\} \\\\\n&  q_t^\\text{re} = \\text{pf} \\cdot p_t^\\text{re}, \\quad \\forall t \\in \\{1,\\dots, T\\}\n\\end{aligned}\n```\n\non which ``\\text{pf} = \\sin(\\arctan(Q^\\text{ld,max}/P^\\text{ld,max}))``.\n\n* * *\n\n## `PowerLoadDispatch`\n\n```@docs\nPowerLoadDispatch\n```\n\n**Variables:**\n\n  - [`ActivePowerVariable`](@ref):\n    \n      + Bounds: [0.0, ]\n      + Default initial value: `PowerSystems.get_active_power(device)`\n      + Symbol: ``p^\\text{ld}``\n\n  - [`ReactivePowerVariable`](@ref):\n    \n      + Bounds: [0.0, ]\n      + Default initial value: `PowerSystems.get_reactive_power(device)`\n      + Symbol: ``q^\\text{ld}``\n\n**Static Parameters:**\n\n  - ``P^\\text{ld,max}`` = `PowerSystems.get_max_active_power(device)`\n  - ``Q^\\text{ld,max}`` = `PowerSystems.get_max_reactive_power(device)`\n\n**Time Series Parameters:**\n\n```@eval\nusing PowerSimulations\nusing PowerSystems\nusing DataFrames\nusing Latexify\ncombos = PowerSimulations.get_default_time_series_names(ElectricLoad, PowerLoadDispatch)\ncombo_table = DataFrame(\n    \"Parameter\" => map(x -> \"[`$x`](@ref)\", collect(keys(combos))),\n    \"Default Time Series Name\" => map(x -> \"`$x`\", collect(values(combos))),\n)\nmdtable(combo_table; latex = false)\n```\n\n**Objective:**\n\nCreates an objective function term based on the [`FunctionData` Options](@ref) where the quantity term is defined as ``p^\\text{ld}``.\n\n**Expressions:**\n\n  - 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)\n\n**Constraints:**\n\n```math\n\\begin{aligned}\n&  p_t^\\text{ld} \\le \\text{ActivePowerTimeSeriesParameter}_t, \\quad \\forall t \\in \\{1,\\dots, T\\}\\\\\n&  q_t^\\text{ld} = \\text{pf} \\cdot p_t^\\text{ld}, \\quad \\forall t \\in \\{1,\\dots, T\\}\\\\\n\\end{aligned}\n```\n\non which ``\\text{pf} = \\sin(\\arctan(Q^\\text{ld,max}/P^\\text{ld,max}))``.\n\n* * *\n\n## `PowerLoadShift`\n\n```@docs\nPowerLoadShift\n```\n\n**Variables:**\n\n  - [`ShiftUpActivePowerVariable`](@ref):\n    \n      + Bounds: [0.0, `ShiftUpActivePowerTimeSeriesParameter`]\n      + Default initial value: 0.0\n      + Symbol: ``p^\\text{shift,up}``\n\n  - [`ShiftDownActivePowerVariable`](@ref):\n    \n      + Bounds: [0.0, `ShiftDownActivePowerTimeSeriesParameter`]\n      + Default initial value: 0.0\n      + Symbol: ``p^\\text{shift,dn}``\n  - [`ReactivePowerVariable`](@ref) *(AC network models only)*:\n    \n      + Bounds: [0.0, ]\n      + Default initial value: `PowerSystems.get_reactive_power(device)`\n      + Symbol: ``q^\\text{ld}``\n\n**Static Parameters:**\n\n  - ``P^\\text{ld,max}`` = `PowerSystems.get_max_active_power(device)`\n  - ``Q^\\text{ld,max}`` = `PowerSystems.get_max_reactive_power(device)`\n\n**Time Series Parameters:**\n\n```@eval\nusing PowerSimulations\nusing PowerSystems\nusing DataFrames\nusing Latexify\ncombos = PowerSimulations.get_default_time_series_names(ShiftablePowerLoad, PowerLoadShift)\ncombo_table = DataFrame(\n    \"Parameter\" => map(x -> \"[`$x`](@ref)\", collect(keys(combos))),\n    \"Default Time Series Name\" => map(x -> \"`$x`\", collect(values(combos))),\n)\nmdtable(combo_table; latex = false)\n```\n\n**Expressions:**\n\n  - Defines the `RealizedShiftedLoad` expression per device per time step:\n    \n    ```math\n    p_t^\\text{realized} = \\text{ActivePowerTimeSeriesParameter}_t + p_t^\\text{shift,up} - p_t^\\text{shift,dn}, \\quad \\forall t \\in \\{1,\\dots,T\\}\n    ```\n\n  - Subtracts ``p_t^\\text{realized}`` from the active power balance expression of the selected [Network Formulations](@ref network_formulations).\n\n**Objective:**\n\nCreates objective function terms based on the [`FunctionData` Options](@ref) for both shift variables:\n\n  - A cost term on ``p^\\text{shift,up}`` (typically zero or negative, rewarding shifting up)\n  - A cost term on ``p^\\text{shift,dn}`` (typically positive, penalizing shifting down)\n\n**Constraints:**\n\n```math\n\\begin{aligned}\n& \\sum_{t=1}^{T} \\left( p_t^\\text{shift,up} - p_t^\\text{shift,dn} \\right) = 0 \\\\\n& \\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)}\n\\end{aligned}\n```\n\n```math\np_t^\\text{realized} \\ge 0, \\quad \\forall t \\in \\{1,\\dots,T\\}\n```\n\n```math\np_t^\\text{shift,up} \\le \\text{ShiftUpActivePowerTimeSeriesParameter}_t, \\quad \\forall t \\in \\{1,\\dots,T\\}\n```\n\n```math\np_t^\\text{shift,dn} \\le \\text{ShiftDownActivePowerTimeSeriesParameter}_t, \\quad \\forall t \\in \\{1,\\dots,T\\}\n```\n\n```math\n\\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\\}\n```\n\n```math\nq_t^\\text{ld} = \\text{pf} \\cdot p_t^\\text{realized}, \\quad \\forall t \\in \\{1,\\dots,T\\}\n```\n\n## Valid configurations\n\nValid [`DeviceModel`](@ref)s for subtypes of `ElectricLoad` include the following:\n\n```@eval\nusing PowerSimulations\nusing PowerSystems\nusing DataFrames\nusing Latexify\ncombos = PowerSimulations.generate_device_formulation_combinations()\nfilter!(x -> x[\"device_type\"] <: ElectricLoad, combos)\ncombo_table = DataFrame(\n    \"Valid DeviceModel\" =>\n        [\"`DeviceModel($(c[\"device_type\"]), $(c[\"formulation\"]))`\" for c in combos],\n    \"Device Type\" => [\n        \"[$(c[\"device_type\"])](https://nrel-Sienna.github.io/PowerSystems.jl/stable/model_library/generated_$(c[\"device_type\"])/)\"\n        for c in combos\n    ],\n    \"Formulation\" => [\"[$(c[\"formulation\"])](@ref)\" for c in combos],\n)\nmdtable(combo_table; latex = false)\n```\n"
  },
  {
    "path": "docs/src/formulation_library/Network.md",
    "content": "# [Network Formulations](@id network_formulations)\n\nNetwork 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.\n\n```@docs\nNetworkModel\n```\n\nAvailable Network Models are:\n\n| Formulation             | Description                                                                                                                                                   |\n|:----------------------- |:------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `CopperPlatePowerModel` | Copper plate connection between all components, i.e. infinite transmission capacity                                                                           |\n| `AreaBalancePowerModel` | Network model approximation to represent inter-area flow with each area represented as a single node                                                          |\n| `PTDFPowerModel`        | Uses the PTDF factor matrix to compute the fraction of power transferred in the network across the branches                                                   |\n| `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 |\n\n[`PowerModels.jl`](https://github.com/lanl-ansi/PowerModels.jl) available formulations:\n\n  - Exact non-convex models: `ACPPowerModel`, `ACRPowerModel`, `ACTPowerModel`.\n  - Linear approximations: `DCPPowerModel`, `NFAPowerModel`.\n  - Quadratic approximations: `DCPLLPowerModel`, `LPACCPowerModel`\n  - Quadratic relaxations: `SOCWRPowerModel`, `SOCWRConicPowerModel`, `SOCBFPowerModel`, `SOCBFConicPowerModel`, `QCRMPowerModel`, `QCLSPowerModel`.\n  - SDP relaxations: `SDPWRMPowerModel`.\n\nAll 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.\n\n* * *\n\n## `CopperPlatePowerModel`\n\n```@docs\nCopperPlatePowerModel\n```\n\n**Variables:**\n\nIf Slack variables are enabled:\n\n  - [`SystemBalanceSlackUp`](@ref):\n    \n      + Bounds: [0.0, ]\n      + Default initial value: 0.0\n      + Default proportional cost: 1e6\n      + Symbol: ``p^\\text{sl,up}``\n\n  - [`SystemBalanceSlackDown`](@ref):\n    \n      + Bounds: [0.0, ]\n      + Default initial value: 0.0\n      + Default proportional cost: 1e6\n      + Symbol: ``p^\\text{sl,dn}``\n\n**Objective:**\n\nAdd a large proportional cost to the objective function if slack variables are used ``+ (p^\\text{sl,up} + p^\\text{sl,dn}) \\cdot 10^6``\n\n**Expressions:**\n\nAdds ``p^\\text{sl,up}`` and ``p^\\text{sl,dn}`` terms to the respective active power balance expressions `ActivePowerBalance` created by this `CopperPlatePowerModel` network formulation.\n\n**Constraints:**\n\nAdds the `CopperPlateBalanceConstraint` to balance the active power of all components available in the system\n\n```math\n\\begin{align}\n&  \\sum_{c \\in \\text{components}} p_t^c = 0, \\quad \\forall t \\in \\{1, \\dots, T\\}\n\\end{align}\n```\n\n* * *\n\n## `AreaBalancePowerModel`\n\n```@docs\nAreaBalancePowerModel\n```\n\n**Variables:**\nIf Slack variables are enabled:\n\n  - [`SystemBalanceSlackUp`](@ref) by area:\n    \n      + Bounds: [0.0, ]\n      + Default initial value: 0.0\n      + Default proportional cost: 1e6\n      + Symbol: ``p^\\text{sl,up}``\n\n  - [`SystemBalanceSlackDown`](@ref) by area:\n    \n      + Bounds: [0.0, ]\n      + Default initial value: 0.0\n      + Default proportional cost: 1e6\n      + Symbol: ``p^\\text{sl,dn}``\n\n**Objective:**\n\nAdds ``p^\\text{sl,up}`` and ``p^\\text{sl,dn}`` terms to the respective active power balance expressions `ActivePowerBalance` per area.\n\n**Expressions:**\n\nCreates `ActivePowerBalance` expressions for each area that then are used to balance active power for all buses within a single area.\n\n**Constraints:**\n\nAdds the `CopperPlateBalanceConstraint` to balance the active power of all components available in an area.\n\n```math\n\\begin{align}\n&  \\sum_{c \\in \\text{components}_a} p_t^c = 0, \\quad \\forall a\\in \\{1,\\dots, A\\}, t \\in \\{1, \\dots, T\\}\n\\end{align}\n```\n\n* * *\n\n## `PTDFPowerModel`\n\n```@docs\nPTDFPowerModel\n```\n\n**Variables:**\n\nIf Slack variables are enabled:\n\n  - [`SystemBalanceSlackUp`](@ref):\n    \n      + Bounds: [0.0, ]\n      + Default initial value: 0.0\n      + Default proportional cost: 1e6\n      + Symbol: ``p^\\text{sl,up}``\n\n  - [`SystemBalanceSlackDown`](@ref):\n    \n      + Bounds: [0.0, ]\n      + Default initial value: 0.0\n      + Default proportional cost: 1e6\n      + Symbol: ``p^\\text{sl,dn}``\n\n**Objective:**\n\nAdd a large proportional cost to the objective function if slack variables are used ``+ (p^\\text{sl,up} + p^\\text{sl,dn}) \\cdot 10^6``\n\n**Expressions:**\n\nAdds ``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.\n\n**Constraints:**\n\nAdds the `CopperPlateBalanceConstraint` to balance the active power of all components available in the system\n\n```math\n\\begin{align}\n&  \\sum_{c \\in \\text{components}} p_t^c = 0, \\quad \\forall t \\in \\{1, \\dots, T\\}\n\\end{align}\n```\n\nIn addition creates `NodalBalanceActiveConstraint` for HVDC buses balance, if DC components are connected to an HVDC network.\n\n## `AreaPTDFPowerModel`\n\n```@docs\nAreaPTDFPowerModel\n```\n\n**Variables**\n\nSlack variables are not supported.\n\n**Objective Function**\n\nNo changes to the objective function.\n\n**Expressions**\n\nCreates 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.\n\n**Constraints:**\n\nAdds the `ActivePowerBalance` constraint to balance the active power of all components available for each area.\n\n```math\n\\begin{align}\n&  \\sum_{c \\in \\text{components}_a} p_t^c = 0, \\quad \\forall a\\in \\{1,\\dots, A\\}, t \\in \\{1, \\dots, T\\}\n\\end{align}\n```\n\nThis includes the flows of lines based on the PTDF factors.\n"
  },
  {
    "path": "docs/src/formulation_library/Piecewise.md",
    "content": "# [Piecewise linear cost functions](@id pwl_cost)\n\nThe 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.\n\n## Special Ordered Set (SOS) Constraints\n\nA special ordered set (SOS) is an ordered set of variables used as an additional way to specify integrality conditions in an optimization model.\n\n  - 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.\n  - 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.\n\n## Standard representation of PWL costs\n\nPiecewise-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.\n\n!!! note\n    \n    `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.\n\n### Commitment formulation\n\nWith this the standard representation of PWL costs for a thermal unit commitment is given by:\n\n```math\n\\begin{align*}\n \\min_{\\substack{p_{t}, \\delta_{k,t}}}\n & \\sum_{t \\in \\mathcal{T}} \\left(\\sum_{k \\in \\mathcal{K}} C_{k,t} \\delta_{k,t} \\right) \\Delta t\\\\\n & \\sum_{k \\in \\mathcal{K}} P_{k}^{\\text{max}} \\delta_{k,t} = p_{t} & \\forall t \\in \\mathcal{T}\\\\\n & \\sum_{k \\in \\mathcal{K}} \\delta_{k,t} = u_{t} & \\forall t \\in \\mathcal{T}\\\\\n & P^{\\text{min}} u_{t} \\leq p_{t} \\leq P^{\\text{max}} u_{t} & \\forall t \\in \\mathcal{T}\\\\\n &\\left \\{\\delta_{1,t}, \\dots, \\delta_{K,t} \\right \\} \\in \\text{SOS}_{2} & \\forall t \\in \\mathcal{T}\n\\end{align*}\n```\n\non 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.\n\n### Dispatch formulation\n\n```math\n\\begin{align*}\n \\min_{\\substack{p_{t}, \\delta_{k,t}}}\n & \\sum_{t \\in \\mathcal{T}} \\left(\\sum_{k \\in \\mathcal{K}} C_{k,t} \\delta_{k,t} \\right) \\Delta t\\\\\n & \\sum_{k \\in \\mathcal{K}} P_{k}^{\\text{max}} \\delta_{k,t} = p_{t} & \\forall t \\in \\mathcal{T}\\\\\n & \\sum_{k \\in \\mathcal{K}} \\delta_{k,t} = \\text{on}_{t} & \\forall t \\in \\mathcal{T}\\\\\n & P^{\\text{min}} \\text{on}_{t} \\leq p_{t} \\leq P^{\\text{max}} \\text{on}_{t} & \\forall t \\in \\mathcal{T}\\\\\n &\\left \\{\\delta_{i,t}, \\dots, \\delta_{k,t} \\right \\} \\in \\text{SOS}_{2} & \\forall t \\in \\mathcal{T}\n\\end{align*}\n```\n\non 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.\n\n## Compact representation of PWL costs\n\n### Commitment Formulation\n\n```math\n\\begin{align*}\n \\min_{\\substack{p_{t}, \\delta_{k,t}}}\n & \\sum_{t \\in \\mathcal{T}} \\left(\\sum_{k \\in \\mathcal{K}} C_{k,t} \\delta_{k,t} \\right) \\Delta t\\\\\n & \\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}\\\\\n & \\sum_{k \\in \\mathcal{K}} \\delta_{k,t} = u_{t} & \\forall t \\in \\mathcal{T}\\\\\n & 0 \\leq \\Delta p_{t} \\leq \\left( P^{\\text{max}} - P^{\\text{min}} \\right)u_{t} &  \\forall t \\in \\mathcal{T}\\\\\n &\\left \\{\\delta_{i,t} \\dots \\delta_{k,t} \\right \\} \\in \\text{SOS}_{2} & \\forall t \\in \\mathcal{T}\n\\end{align*}\n```\n\non 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.\n\n### Dispatch formulation\n\n```math\n\\begin{align*}\n \\min_{\\substack{p_{t}, \\delta_{k,t}}}\n & \\sum_{t \\in \\mathcal{T}} \\left(\\sum_{k \\in \\mathcal{K}} C_{k,t} \\delta_{k,t} \\right) \\Delta t\\\\\n & \\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}\\\\\n & \\sum_{k \\in \\mathcal{K}} \\delta_{k,t} = \\text{on}_{t} & \\forall t \\in \\mathcal{T}\\\\\n & 0 \\leq \\Delta p_{t} \\leq \\left( P^{\\text{max}} - P^{\\text{min}} \\right)\\text{on}_{t} & \\forall t \\in \\mathcal{T}\\\\\n &\\left \\{\\delta_{i,t} \\dots \\delta_{k,t} \\right \\} \\in \\text{SOS}_{2} & \\forall t \\in \\mathcal{T}\n\\end{align*}\n```\n\non 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.\n"
  },
  {
    "path": "docs/src/formulation_library/README.md",
    "content": "# Formulation documentation guide\n\nFormulation documentation should *roughly* follow the template established by RenewableGen.md\n\n## Auto generated items\n\n- Valid DeviceModel table: just change the device category in the filter function\n- Time Series Parameters: just change the device category and formulation in the `get_default_time_series_names` method call\n\n\n## Linked items\n\n- Formulations in the Valid DeviceModel table must have a docstring in src/core/formulations.jl\n- The Formulation in the @docs block must have a docstring in src/core/formulations.jl\n- The Variables must have docstrings in src/core/variables.jl \n- The Time Series Parameters must have docstrings in src/core/parameters.jl\n"
  },
  {
    "path": "docs/src/formulation_library/RenewableGen.md",
    "content": "# `PowerSystems.RenewableGen` Formulations\n\nRenewable generation formulations define the optimization models that describe renewable units mathematical model in different operational settings, such as economic dispatch and unit commitment.\n\n!!! note\n    \n    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.\n\n!!! note\n    \n    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.\n\n### Table of contents\n\n 1. [`RenewableFullDispatch`](#RenewableFullDispatch)\n 2. [`RenewableConstantPowerFactor`](#RenewableConstantPowerFactor)\n 3. [Valid configurations](#Valid-configurations)\n\n* * *\n\n## `RenewableFullDispatch`\n\n```@docs\nRenewableFullDispatch\n```\n\n**Variables:**\n\n  - [`ActivePowerVariable`](@ref):\n    \n      + Bounds: [0.0, ]\n      + Symbol: ``p^\\text{re}``\n\n  - [`ReactivePowerVariable`](@ref):\n    \n      + Bounds: [0.0, ]\n      + Symbol: ``q^\\text{re}``\n\n**Static Parameters:**\n\n  - ``P^\\text{re,min}`` = `PowerSystems.get_active_power_limits(device).min`\n  - ``Q^\\text{re,min}`` = `PowerSystems.get_reactive_power_limits(device).min`\n  - ``Q^\\text{re,max}`` = `PowerSystems.get_reactive_power_limits(device).max`\n\n**Time Series Parameters:**\n\nUses the `max_active_power` timeseries parameter to limit the available active power at each time-step.\n\n```@eval\nusing PowerSimulations\nusing PowerSystems\nusing DataFrames\nusing Latexify\ncombos = PowerSimulations.get_default_time_series_names(RenewableGen, RenewableFullDispatch)\ncombo_table = DataFrame(\n    \"Parameter\" => map(x -> \"[`$x`](@ref)\", collect(keys(combos))),\n    \"Default Time Series Name\" => map(x -> \"`$x`\", collect(values(combos))),\n)\nmdtable(combo_table; latex = false)\n```\n\n**Objective:**\n\nCreates 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.\n\n**Expressions:**\n\nAdds ``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).\n\n**Constraints:**\n\n```math\n\\begin{aligned}\n&  P^\\text{re,min} \\le p_t^\\text{re} \\le \\text{ActivePowerTimeSeriesParameter}_t, \\quad \\forall t \\in \\{1,\\dots, T\\} \\\\\n&  Q^\\text{re,min} \\le q_t^\\text{re} \\le Q^\\text{re,max}, \\quad \\forall t \\in \\{1,\\dots, T\\}\n\\end{aligned}\n```\n\n* * *\n\n## `RenewableConstantPowerFactor`\n\n```@docs\nRenewableConstantPowerFactor\n```\n\n**Variables:**\n\n  - [`ActivePowerVariable`](@ref):\n    \n      + Bounds: [0.0, ]\n      + Default initial value: `PowerSystems.get_active_power(device)`\n      + Symbol: ``p^\\text{re}``\n\n  - [`ReactivePowerVariable`](@ref):\n    \n      + Bounds: [0.0, ]\n      + Default initial value: `PowerSystems.get_reactive_power(device)`\n      + Symbol: ``q^\\text{re}``\n\n**Static Parameters:**\n\n  - ``P^\\text{re,min}`` = `PowerSystems.get_active_power_limits(device).min`\n  - ``Q^\\text{re,min}`` = `PowerSystems.get_reactive_power_limits(device).min`\n  - ``Q^\\text{re,max}`` = `PowerSystems.get_reactive_power_limits(device).max`\n  - ``\\text{pf}`` = `PowerSystems.get_power_factor(device)`\n\n**Time Series Parameters:**\n\n```@eval\nusing PowerSimulations\nusing PowerSystems\nusing DataFrames\nusing Latexify\ncombos = PowerSimulations.get_default_time_series_names(\n    RenewableGen,\n    RenewableConstantPowerFactor,\n)\ncombo_table = DataFrame(\n    \"Parameter\" => map(x -> \"[`$x`](@ref)\", collect(keys(combos))),\n    \"Default Time Series Name\" => map(x -> \"`$x`\", collect(values(combos))),\n)\nmdtable(combo_table; latex = false)\n```\n\n**Objective:**\n\nCreates 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.\n\n**Expressions:**\n\nAdds ``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)\n\n**Constraints:**\n\n```math\n\\begin{aligned}\n&  P^\\text{re,min} \\le p_t^\\text{re} \\le \\text{ActivePowerTimeSeriesParameter}_t, \\quad \\forall t \\in \\{1,\\dots, T\\} \\\\\n&  q_t^\\text{re} = \\text{pf} \\cdot p_t^\\text{re}, \\quad \\forall t \\in \\{1,\\dots, T\\}\n\\end{aligned}\n```\n\n* * *\n\n## Valid configurations\n\nValid [`DeviceModel`](@ref)s for subtypes of `RenewableGen` include the following:\n\n```@eval\nusing PowerSimulations\nusing PowerSystems\nusing DataFrames\nusing Latexify\ncombos = PowerSimulations.generate_device_formulation_combinations()\nfilter!(x -> x[\"device_type\"] <: RenewableGen, combos)\ncombo_table = DataFrame(\n    \"Valid DeviceModel\" =>\n        [\"`DeviceModel($(c[\"device_type\"]), $(c[\"formulation\"]))`\" for c in combos],\n    \"Device Type\" => [\n        \"[$(c[\"device_type\"])](https://nrel-Sienna.github.io/PowerSystems.jl/stable/model_library/generated_$(c[\"device_type\"])/)\"\n        for c in combos\n    ],\n    \"Formulation\" => [\"[$(c[\"formulation\"])](@ref)\" for c in combos],\n)\nmdtable(combo_table; latex = false)\n```\n"
  },
  {
    "path": "docs/src/formulation_library/Service.md",
    "content": "# [`PowerSystems.Service` Formulations](@id service_formulations)\n\n`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.\n\nThe 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.\n\nA 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.\n\nIn 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.\n\n### Table of contents\n\n 1. [`RangeReserve`](#RangeReserve)\n 2. [`StepwiseCostReserve`](#StepwiseCostReserve)\n 3. [`GroupReserve`](#GroupReserve)\n 4. [`RampReserve`](#RampReserve)\n 5. [`NonSpinningReserve`](#NonSpinningReserve)\n 6. [`ConstantMaxInterfaceFlow`](#ConstantMaxInterfaceFlow)\n 7. [`VariableMaxInterfaceFlow`](#VariableMaxInterfaceFlow)\n 8. [Changes on Expressions](#Changes-on-Expressions-due-to-Service-models)\n\n* * *\n\n## `RangeReserve`\n\n```@docs\nRangeReserve\n```\n\nFor each service ``s`` of the model type `RangeReserve` the following variables are created:\n\n**Variables**:\n\n  - [`ActivePowerReserveVariable`](@ref):\n    \n      + Bounds: [0.0, ]\n      + Default proportional cost: ``1.0 / \\text{SystemBasePower}``\n      + Symbol: ``r_{d}`` for ``d`` in contributing devices to the service ``s``\n        If slacks are enabled:\n\n  - [`ReserveRequirementSlack`](@ref):\n    \n      + Bounds: [0.0, ]\n      + Default proportional cost: 1e5\n      + Symbol: ``r^\\text{sl}``\n\nDepending on the `PowerSystems.jl` type associated to the `RangeReserve` formulation model, the parameters are:\n\n**Static Parameters**\n\n  - ``\\text{PF}`` = `PowerSystems.get_max_participation_factor(service)`\n\nFor a `ConstantReserve` `PowerSystems` type:\n\n  - ``\\text{Req}`` = `PowerSystems.get_requirement(service)`\n\n**Time Series Parameters**\n\nFor a `VariableReserve` `PowerSystems` type:\n\n```@eval\nusing PowerSimulations\nusing PowerSystems\nusing DataFrames\nusing Latexify\ncombos = PowerSimulations.get_default_time_series_names(VariableReserve, RangeReserve)\ncombo_table = DataFrame(\n    \"Parameter\" => map(x -> \"[`$x`](@ref)\", collect(keys(combos))),\n    \"Default Time Series Name\" => map(x -> \"`$x`\", collect(values(combos))),\n)\nmdtable(combo_table; latex = false)\n```\n\n**Relevant Methods:**\n\n  - ``\\mathcal{D}_s`` = `PowerSystems.get_contributing_devices(system, service)`: Set (vector) of all contributing devices to the service ``s`` in the system.\n\n**Objective:**\n\nAdd 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.\n\n**Expressions:**\n\nAdds the `ActivePowerReserveVariable` for upper/lower bound expressions of contributing devices.\n\nFor `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\n\n*Example*: for a thermal unit ``d`` contributing to two different `ReserveUp` ``s_1, s_2`` services (e.g. Reg-Up and Spin):\n\n```math\n\\text{ActivePowerRangeExpressionUB}_{t} = p_t^\\text{th} + r_{s_1,t} + r_{s_2, t} \\le P^\\text{th,max}\n```\n\nsimilarly if ``s_3`` is a `ReserveDown` service (e.g. Reg-Down):\n\n```math\n\\text{ActivePowerRangeExpressionLB}_{t} = p_t^\\text{th} - r_{s_3,t}  \\ge P^\\text{th,min}\n```\n\n**Constraints:**\n\nA 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``:\n\n```math\n\\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)} \\\\\n\\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)}\n```\n\nIn addition, there is a restriction on how much each contributing device ``d`` can contribute to the requirement, based on the max participation factor allowed.\n\n```math\nr_{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)} \\\\\nr_{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)}\n```\n\n* * *\n\n## `StepwiseCostReserve`\n\nService must be used with `ReserveDemandCurve` `PowerSystems.jl` type. This service model is used to model ORDC (Operating Reserve Demand Curve) in ERCOT.\n\n```@docs\nStepwiseCostReserve\n```\n\nFor each service ``s`` of the model type `ReserveDemandCurve` the following variables are created:\n\n**Variables**:\n\n  - [`ActivePowerReserveVariable`](@ref):\n    \n      + Bounds: [0.0, ]\n      + Symbol: ``r_{d}`` for ``d`` in contributing devices to the service ``s``\n\n  - [`ServiceRequirementVariable`](@ref):\n    \n      + Bounds: [0.0, ]\n      + Symbol: ``\\text{req}``\n\n**Time Series Parameters**\n\nFor a `ReserveDemandCurve` `PowerSystems` type:\n\n```@eval\nusing PowerSimulations\nusing PowerSystems\nusing DataFrames\nusing Latexify\ncombos =\n    PowerSimulations.get_default_time_series_names(ReserveDemandCurve, StepwiseCostReserve)\ncombo_table = DataFrame(\n    \"Parameter\" => map(x -> \"[`$x`](@ref)\", collect(keys(combos))),\n    \"Default Time Series Name\" => map(x -> \"`$x`\", collect(values(combos))),\n)\nmdtable(combo_table; latex = false)\n```\n\n**Relevant Methods:**\n\n  - ``\\mathcal{D}_s`` = `PowerSystems.get_contributing_devices(system, service)`: Set (vector) of all contributing devices to the service ``s`` in the system.\n\n**Objective:**\n\nThe `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.\n\n**Expressions:**\n\nAdds the `ActivePowerReserveVariable` for upper/lower bound expressions of contributing devices.\n\nFor `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\n\n*Example*: for a thermal unit ``d`` contributing to two different `ReserveUp` ``s_1, s_2`` services (e.g. Reg-Up and Spin):\n\n```math\n\\text{ActivePowerRangeExpressionUB}_{t} = p_t^\\text{th} + r_{s_1,t} + r_{s_2, t} \\le P^\\text{th,max}\n```\n\nsimilarly if ``s_3`` is a `ReserveDown` service (e.g. Reg-Down):\n\n```math\n\\text{ActivePowerRangeExpressionLB}_{t} = p_t^\\text{th} - r_{s_3,t}  \\ge P^\\text{th,min}\n```\n\n**Constraints:**\n\nA `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``:\n\n```math\n\\sum_{d\\in\\mathcal{D}_s} r_{d,t}  \\ge \\text{req}_t,\\quad \\forall t\\in \\{1,\\dots, T\\}  \n```\n\n## `GroupReserve`\n\nService must be used with `ConstantReserveGroup` `PowerSystems.jl` type. This service model is used to model an aggregation of services.\n\n```@docs\nGroupReserve\n```\n\nFor each service ``s`` of the model type `GroupReserve` the following variables are created:\n\n**Variables**:\n\nNo variables are created, but the services associated with the `GroupReserve` must have created variables.\n\n**Static Parameters**\n\n  - ``\\text{Req}`` = `PowerSystems.get_requirement(service)`\n\n**Relevant Methods:**\n\n  - ``\\mathcal{S}_s`` = `PowerSystems.get_contributing_services(system, service)`: Set (vector) of all contributing services to the group service ``s`` in the system.\n  - ``\\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.\n\n**Objective:**\n\nDoes not modify the objective function, besides the changes to the objective function due to the other services associated to the group service.\n\n**Expressions:**\n\nNo changes, besides the changes to the expressions due to the other services associated to the group service.\n\n**Constraints:**\n\nA 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``:\n\n```math\n\\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\\} \n```\n\n* * *\n\n## `RampReserve`\n\n```@docs\nRampReserve\n```\n\nFor each service ``s`` of the model type `RampReserve` the following variables are created:\n\n**Variables**:\n\n  - [`ActivePowerReserveVariable`](@ref):\n    \n      + Bounds: [0.0, ]\n      + Default proportional cost: ``1.0 / \\text{SystemBasePower}``\n      + Symbol: ``r_{d}`` for ``d`` in contributing devices to the service ``s``\n        If slacks are enabled:\n\n  - [`ReserveRequirementSlack`](@ref):\n    \n      + Bounds: [0.0, ]\n      + Default proportional cost: 1e5\n      + Symbol: ``r^\\text{sl}``\n\n`RampReserve` only accepts `VariableReserve` `PowerSystems.jl` type. With that, the parameters are:\n\n**Static Parameters**\n\n  - ``\\text{TF}`` = `PowerSystems.get_time_frame(service)`\n  - ``R^\\text{th,up}`` = `PowerSystems.get_ramp_limits(device).up` for thermal contributing devices\n  - ``R^\\text{th,dn}`` = `PowerSystems.get_ramp_limits(device).down` for thermal contributing devices\n\n**Time Series Parameters**\n\nFor a `VariableReserve` `PowerSystems` type:\n\n```@eval\nusing PowerSimulations\nusing PowerSystems\nusing DataFrames\nusing Latexify\ncombos = PowerSimulations.get_default_time_series_names(VariableReserve, RampReserve)\ncombo_table = DataFrame(\n    \"Parameter\" => map(x -> \"[`$x`](@ref)\", collect(keys(combos))),\n    \"Default Time Series Name\" => map(x -> \"`$x`\", collect(values(combos))),\n)\nmdtable(combo_table; latex = false)\n```\n\n**Relevant Methods:**\n\n  - ``\\mathcal{D}_s`` = `PowerSystems.get_contributing_devices(system, service)`: Set (vector) of all contributing devices to the service ``s`` in the system.\n\n**Objective:**\n\nAdd 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.\n\n**Expressions:**\n\nAdds the `ActivePowerReserveVariable` for upper/lower bound expressions of contributing devices.\n\nFor `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\n\n*Example*: for a thermal unit ``d`` contributing to two different `ReserveUp` ``s_1, s_2`` services (e.g. Reg-Up and Spin):\n\n```math\n\\text{ActivePowerRangeExpressionUB}_{t} = p_t^\\text{th} + r_{s_1,t} + r_{s_2, t} \\le P^\\text{th,max}\n```\n\nsimilarly if ``s_3`` is a `ReserveDown` service (e.g. Reg-Down):\n\n```math\n\\text{ActivePowerRangeExpressionLB}_{t} = p_t^\\text{th} - r_{s_3,t}  \\ge P^\\text{th,min}\n```\n\n**Constraints:**\n\nA 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``:\n\n```math\n\\sum_{d\\in\\mathcal{D}_s} r_{d,t} + r_t^\\text{sl} \\ge \\text{RequirementTimeSeriesParameter}_{t},\\quad \\forall t\\in \\{1,\\dots, T\\}\n```\n\nFinally, there is a restriction based on the ramp limits of the contributing devices:\n\n```math\nr_{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)} \\\\\nr_{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)}\n```\n\n* * *\n\n## `NonSpinningReserve`\n\n```@docs\nNonSpinningReserve\n```\n\nFor each service ``s`` of the model type `NonSpinningReserve`, the following variables are created:\n\n**Variables**:\n\n  - [`ActivePowerReserveVariable`](@ref):\n    \n      + Bounds: [0.0, ]\n      + Default proportional cost: ``1.0 / \\text{SystemBasePower}``\n      + Symbol: ``r_{d}`` for ``d`` in contributing devices to the service ``s``\n        If slacks are enabled:\n\n  - [`ReserveRequirementSlack`](@ref):\n    \n      + Bounds: [0.0, ]\n      + Default proportional cost: 1e5\n      + Symbol: ``r^\\text{sl}``\n\n`NonSpinningReserve` only accepts `VariableReserve` `PowerSystems.jl` type. With that, the parameters are:\n\n**Static Parameters**\n\n  - ``\\text{PF}`` = `PowerSystems.get_max_participation_factor(service)`\n  - ``\\text{TF}`` = `PowerSystems.get_time_frame(service)`\n  - ``P^\\text{th,min}`` = `PowerSystems.get_active_power_limits(device).min` for thermal contributing devices\n  - ``T^\\text{st,up}`` = `PowerSystems.get_time_limits(d).up` for thermal contributing devices\n  - ``R^\\text{th,up}`` = `PowerSystems.get_ramp_limits(device).down` for thermal contributing devices\n\nOther parameters:\n\n  - ``\\Delta T``: Resolution of the problem in minutes.\n\n**Time Series Parameters**\n\nFor a `VariableReserve` `PowerSystems` type:\n\n```@eval\nusing PowerSimulations\nusing PowerSystems\nusing DataFrames\nusing Latexify\ncombos = PowerSimulations.get_default_time_series_names(VariableReserve, NonSpinningReserve)\ncombo_table = DataFrame(\n    \"Parameter\" => map(x -> \"[`$x`](@ref)\", collect(keys(combos))),\n    \"Default Time Series Name\" => map(x -> \"`$x`\", collect(values(combos))),\n)\nmdtable(combo_table; latex = false)\n```\n\n**Relevant Methods:**\n\n  - ``\\mathcal{D}_s`` = `PowerSystems.get_contributing_devices(system, service)`: Set (vector) of all contributing devices to the service ``s`` in the system.\n\n**Objective:**\n\nAdd 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.\n\n**Expressions:**\n\nAdds the `ActivePowerReserveVariable` for upper/lower bound expressions of contributing devices.\n\nFor `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\n\n*Example*: for a thermal unit ``d`` contributing to two different `ReserveUp` ``s_1, s_2`` services (e.g. Reg-Up and Spin):\n\n```math\n\\text{ActivePowerRangeExpressionUB}_{t} = p_t^\\text{th} + r_{s_1,t} + r_{s_2, t} \\le P^\\text{th,max}\n```\n\nsimilarly if ``s_3`` is a `ReserveDown` service (e.g. Reg-Down):\n\n```math\n\\text{ActivePowerRangeExpressionLB}_{t} = p_t^\\text{th} - r_{s_3,t}  \\ge P^\\text{th,min}\n```\n\n**Constraints:**\n\nA 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``:\n\n```math\n\\sum_{d\\in\\mathcal{D}_s} r_{d,t} + r_t^\\text{sl} \\ge \\text{RequirementTimeSeriesParameter}_{t},\\quad \\forall t\\in \\{1,\\dots, T\\}\n```\n\nIn addition, there is a restriction on how much each contributing device ``d`` can contribute to the requirement, based on the max participation factor allowed.\n\n```math\nr_{d,t} \\le \\text{RequirementTimeSeriesParameter}_{t} \\cdot \\text{PF}\\quad  \\forall d\\in \\mathcal{D}_s, \\forall t\\in \\{1,\\dots, T\\},\n```\n\nFinally, 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:\n\n```math\nR^\\text{limit}_d = \\begin{cases}\n0 & \\text{ if TF } \\le T^\\text{st,up}_d \\\\\nP^\\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\n\\end{cases}, \\quad \\forall d\\in \\mathcal{D}_s\n```\n\nThen, the constraint depends on the commitment variable ``u_t^\\text{th}`` as:\n\n```math\nr_{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\\}\n```\n\n* * *\n\n## `ConstantMaxInterfaceFlow`\n\nThis 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.\n\n```@docs\nConstantMaxInterfaceFlow\n```\n\n**Variables**\n\nIf slacks are used:\n\n  - [`InterfaceFlowSlackUp`](@ref):\n    \n      + Bounds: [0.0, ]\n      + Symbol: ``f^\\text{sl,up}``\n\n  - [`InterfaceFlowSlackDown`](@ref):\n    \n      + Bounds: [0.0, ]\n      + Symbol: ``f^\\text{sl,dn}``\n\n**Static Parameters**\n\n  - ``F^\\text{max}`` = `PowerSystems.get_active_power_flow_limits(service).max`\n  - ``F^\\text{min}`` = `PowerSystems.get_active_power_flow_limits(service).min`\n  - ``C^\\text{flow}`` = `PowerSystems.get_violation_penalty(service)`\n  - ``\\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.\n\n**Relevant Methods**\n\n  - ``\\mathcal{D}_s`` = `PowerSystems.get_contributing_devices(system, service)`: Set (vector) of all contributing branches to the service ``s`` in the system.\n\n**Objective:**\n\nAdd 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}``.\n\n**Expressions:**\n\nCreates the expression `InterfaceTotalFlow` to keep track of all `FlowActivePowerVariable` of contributing branches to the transmission interface.\n\n**Constraints:**\n\nIt adds the constraint to limit the `InterfaceTotalFlow` by the specified bounds of the service ``s``:\n\n```math\nF^\\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\\}\n```\n\n## `VariableMaxInterfaceFlow`\n\nThis 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.\n\n```@docs\nVariableMaxInterfaceFlow\n```\n\n**Variables**\n\nIf slacks are used:\n\n  - [`InterfaceFlowSlackUp`](@ref):\n    \n      + Bounds: [0.0, ]\n      + Symbol: ``f^\\text{sl,up}``\n\n  - [`InterfaceFlowSlackDown`](@ref):\n    \n      + Bounds: [0.0, ]\n      + Symbol: ``f^\\text{sl,dn}``\n\n**Static Parameters**\n\n  - ``F^\\text{max}`` = `PowerSystems.get_active_power_flow_limits(service).max`\n  - ``F^\\text{min}`` = `PowerSystems.get_active_power_flow_limits(service).min`\n  - ``C^\\text{flow}`` = `PowerSystems.get_violation_penalty(service)`\n  - ``\\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.\n\n**Time Series Parameters**\n\nFor a `TransmissionInterface` `PowerSystems` type:\n\n```@eval\nusing PowerSimulations\nusing PowerSystems\nusing DataFrames\nusing Latexify\ncombos = PowerSimulations.get_default_time_series_names(\n    TransmissionInterface,\n    VariableMaxInterfaceFlow,\n)\ncombo_table = DataFrame(\n    \"Parameter\" => map(x -> \"[`$x`](@ref)\", collect(keys(combos))),\n    \"Default Time Series Name\" => map(x -> \"`$x`\", collect(values(combos))),\n)\nmdtable(combo_table; latex = false)\n```\n\n**Relevant Methods**\n\n  - ``\\mathcal{D}_s`` = `PowerSystems.get_contributing_devices(system, service)`: Set (vector) of all contributing branches to the service ``s`` in the system.\n\n**Objective:**\n\nAdd 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}``.\n\n**Expressions:**\n\nCreates the expression `InterfaceTotalFlow` to keep track of all `FlowActivePowerVariable` of contributing branches to the transmission interface.\n\n**Constraints:**\n\nIt adds the constraint to limit the `InterfaceTotalFlow` by the specified bounds of the service ``s``:\n\n```math\nF^\\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\\}\n```\n\n## Changes on Expressions due to Service models\n\nIt 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:\n\n```math\nr_{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\\}\n```\n\n### Changes on UpperBound (UB) and LowerBound (LB) limits\n\nEach 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:\n\n```math\n\\text{ActivePowerRangeExpressionUB}_t \\le P^\\text{max} \\\\\n\\text{ActivePowerRangeExpressionLB}_t \\ge P^\\text{min}\n```\n\n`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`:\n\n```math\n\\begin{align*}\n& 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\\} \\\\\n& 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\\}\n\\end{align*}\n```\n\nwhile for a renewable generator ``d`` using a `RenewableFullDispatch`:\n\n```math\n\\begin{align*}\n& 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\\}\\\\\n& 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\\}\n\\end{align*}\n```\n\n### Changes in Ramp limits\n\nFor 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:\n\n```math\n\\begin{align*}\n& 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\\}\\\\\n& 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\\}\n\\end{align*}\n```\n"
  },
  {
    "path": "docs/src/formulation_library/Source.md",
    "content": "# `Source` Formulations\n\nSource 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.\n\n!!! note\n    \n    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.\n\n!!! note\n    \n    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.\n\n### Table of Contents\n\n 1. [`ImportExportSourceModel`](#ImportExportSourceModel)\n\n* * *\n\n## `ImportExportSourceModel`\n\n```@docs\nImportExportSourceModel\n```\n\nTODO\n"
  },
  {
    "path": "docs/src/formulation_library/ThermalGen.md",
    "content": "# `ThermalGen` Formulations\n\nThermal generation formulations define the optimization models that describe thermal units mathematical model in different operational settings, such as economic dispatch and unit commitment.\n\n!!! note\n    \n    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).\n\n!!! note\n    \n    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.\n\n!!! note\n    \n    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.\n\n### Table of Contents\n\n 1. [`ThermalBasicDispatch`](#ThermalBasicDispatch)\n 2. [`ThermalDispatchNoMin`](#ThermalDispatchNoMin)\n 3. [`ThermalCompactDispatch`](#ThermalCompactDispatch)\n 4. [`ThermalStandardDispatch`](#ThermalStandardDispatch)\n 5. [`ThermalBasicUnitCommitment`](#ThermalBasicUnitCommitment)\n 6. [`ThermalBasicCompactUnitCommitment`](#ThermalBasicCompactUnitCommitment)\n 7. [`ThermalStandardUnitCommitment`](#ThermalStandardUnitCommitment)\n 8. [`ThermalMultiStartUnitCommitment`](#ThermalMultiStartUnitCommitment)\n 9. [Valid configurations](#Valid-configurations)\n\n* * *\n\n## `ThermalBasicDispatch`\n\n```@docs\nThermalBasicDispatch\n```\n\n**Variables:**\n\n  - [`ActivePowerVariable`](@ref):\n    \n      + Bounds: [0.0, ]\n      + Symbol: ``p^\\text{th}``\n\n  - [`ReactivePowerVariable`](@ref):\n    \n      + Bounds: [0.0, ]\n      + Symbol: ``q^\\text{th}``\n\n**Static Parameters:**\n\n  - ``P^\\text{th,min}`` = `PowerSystems.get_active_power_limits(device).min`\n  - ``P^\\text{th,max}`` = `PowerSystems.get_active_power_limits(device).max`\n  - ``Q^\\text{th,min}`` = `PowerSystems.get_reactive_power_limits(device).min`\n  - ``Q^\\text{th,max}`` = `PowerSystems.get_reactive_power_limits(device).max`\n\n**Objective:**\n\nAdd a cost to the objective function depending on the defined cost structure of the thermal unit by adding it to its `ProductionCostExpression`.\n\n**Expressions:**\n\nAdds ``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.\n\n**Constraints:**\n\nFor each thermal unit creates the range constraints for its active and reactive power depending on its static parameters.\n\n```math\n\\begin{align*}\n&  P^\\text{th,min} \\le p^\\text{th}_t \\le P^\\text{th,max}, \\quad \\forall t\\in \\{1, \\dots, T\\} \\\\\n&  Q^\\text{th,min} \\le q^\\text{th}_t \\le Q^\\text{th,max}, \\quad \\forall t\\in \\{1, \\dots, T\\} \n\\end{align*}\n```\n\n* * *\n\n## `ThermalDispatchNoMin`\n\n```@docs\nThermalDispatchNoMin\n```\n\n**Variables:**\n\n  - [`ActivePowerVariable`](@ref):\n    \n      + Bounds: [0.0, ]\n      + Symbol: ``p^\\text{th}``\n\n  - [`ReactivePowerVariable`](@ref):\n    \n      + Bounds: [0.0, ]\n      + Symbol: ``q^\\text{th}``\n\n**Static Parameters:**\n\n  - ``P^\\text{th,max}`` = `PowerSystems.get_active_power_limits(device).max`\n  - ``Q^\\text{th,min}`` = `PowerSystems.get_reactive_power_limits(device).min`\n  - ``Q^\\text{th,max}`` = `PowerSystems.get_reactive_power_limits(device).max`\n\n**Objective:**\n\nAdd a cost to the objective function depending on the defined cost structure of the thermal unit by adding it to its `ProductionCostExpression`.\n\n**Expressions:**\n\nAdds ``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.\n\n**Constraints:**\n\nFor each thermal unit creates the range constraints for its active and reactive power depending on its static parameters.\n\n```math\n\\begin{align}\n&  0 \\le p^\\text{th}_t \\le P^\\text{th,max}, \\quad \\forall t\\in \\{1, \\dots, T\\} \\\\\n&  Q^\\text{th,min} \\le q^\\text{th}_t \\le Q^\\text{th,max}, \\quad \\forall t\\in \\{1, \\dots, T\\} \n\\end{align}\n```\n\n* * *\n\n## `ThermalCompactDispatch`\n\n```@docs\nThermalCompactDispatch\n```\n\n**Variables:**\n\n  - [`PowerAboveMinimumVariable`](@ref):\n    \n      + Bounds: [0.0, ]\n      + Symbol: ``\\Delta p^\\text{th}``\n\n  - [`ReactivePowerVariable`](@ref):\n    \n      + Bounds: [0.0, ]\n      + Symbol: ``q^\\text{th}``\n\n**Auxiliary Variables:**\n\n  - [`PowerOutput`](@ref):\n    \n      + Symbol: ``P^\\text{th}``\n      + Definition: ``P^\\text{th} = \\text{on}^\\text{th}P^\\text{min} + \\Delta p^\\text{th}``\n\n**Static Parameters:**\n\n  - ``P^\\text{th,min}`` = `PowerSystems.get_active_power_limits(device).min`\n  - ``P^\\text{th,max}`` = `PowerSystems.get_active_power_limits(device).max`\n  - ``Q^\\text{th,min}`` = `PowerSystems.get_reactive_power_limits(device).min`\n  - ``Q^\\text{th,max}`` = `PowerSystems.get_reactive_power_limits(device).max`\n  - ``R^\\text{th,up}`` = `PowerSystems.get_ramp_limits(device).up`\n  - ``R^\\text{th,dn}`` = `PowerSystems.get_ramp_limits(device).down`\n\n**Variable Value Parameters:**\n\n  - ``\\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.\n\n**Objective:**\n\nAdd a cost to the objective function depending on the defined cost structure of the thermal unit by adding it to its `ProductionCostExpression`.\n\n**Expressions:**\n\nAdds ``\\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.\n\n**Constraints:**\n\nFor 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.\n\n```math\n\\begin{align*}\n&  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\\} \\\\\n&  \\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\\}  \\\\\n& -R^\\text{th,dn} \\le \\Delta p_1^\\text{th} - \\Delta p^\\text{th, init} \\le R^\\text{th,up} \\\\\n& -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\\}\n\\end{align*}\n```\n\n* * *\n\n## `ThermalStandardDispatch`\n\n```@docs\nThermalStandardDispatch\n```\n\n**Variables:**\n\n  - [`ActivePowerVariable`](@ref):\n    \n      + Bounds: [0.0, ]\n      + Symbol: ``p^\\text{th}``\n\n  - [`ReactivePowerVariable`](@ref):\n    \n      + Bounds: [0.0, ]\n      + Symbol: ``q^\\text{th}``\n\nIf Slack variables are enabled (`use_slacks = true`):\n\n  - [`RateofChangeConstraintSlackUp`](@ref):\n    \n      + Bounds: [0.0, ]\n      + Default initial value: 0.0\n      + Default proportional cost: 2e5\n      + Symbol: ``p^\\text{sl,up}``\n\n  - [`RateofChangeConstraintSlackDown`](@ref):\n    \n      + Bounds: [0.0, ]\n      + Default initial value: 0.0\n      + Default proportional cost: 2e5\n      + Symbol: ``p^\\text{sl,dn}``\n\n**Static Parameters:**\n\n  - ``P^\\text{th,min}`` = `PowerSystems.get_active_power_limits(device).min`\n  - ``P^\\text{th,max}`` = `PowerSystems.get_active_power_limits(device).max`\n  - ``Q^\\text{th,min}`` = `PowerSystems.get_reactive_power_limits(device).min`\n  - ``Q^\\text{th,max}`` = `PowerSystems.get_reactive_power_limits(device).max`\n  - ``R^\\text{th,up}`` = `PowerSystems.get_ramp_limits(device).up`\n  - ``R^\\text{th,dn}`` = `PowerSystems.get_ramp_limits(device).down`\n\n**Objective:**\n\nAdd a cost to the objective function depending on the defined cost structure of the thermal unit by adding it to its `ProductionCostExpression`.\n\n**Expressions:**\n\nAdds ``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.\n\n**Constraints:**\n\nFor each thermal unit creates the range constraints for its active and reactive power depending on its static parameters.\n\n```math\n\\begin{align*}\n&  P^\\text{th,min} \\le p^\\text{th}_t \\le P^\\text{th,max}, \\quad \\forall t\\in \\{1, \\dots, T\\} \\\\\n&  Q^\\text{th,min} \\le q^\\text{th}_t \\le Q^\\text{th,max}, \\quad \\forall t\\in \\{1, \\dots, T\\} \\\\\n& p_1^\\text{th} - p^\\text{th, init} - p_1^\\text{sl,up} \\le R^\\text{th,up} \\\\\n& 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\\} \\\\\n& -R^\\text{th,dn} \\le  p_1^\\text{th} - p^\\text{th, init} + p_1^\\text{sl,dn} \\\\\n&  -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\\} \\\\\n\\end{align*}\n```\n\n* * *\n\n## `ThermalBasicUnitCommitment`\n\n```@docs\nThermalBasicUnitCommitment\n```\n\n**Variables:**\n\n  - [`ActivePowerVariable`](@ref):\n    \n      + Bounds: [0.0, ]\n      + Symbol: ``p^\\text{th}``\n\n  - [`ReactivePowerVariable`](@ref):\n    \n      + Bounds: [0.0, ]\n      + Symbol: ``q^\\text{th}``\n  - [`OnVariable`](@ref):\n    \n      + Bounds: ``\\{0,1\\}``\n      + Symbol: ``u_t^\\text{th}``\n  - [`StartVariable`](@ref):\n    \n      + Bounds: ``\\{0,1\\}``\n      + Symbol: ``v_t^\\text{th}``\n  - [`StopVariable`](@ref):\n    \n      + Bounds: ``\\{0,1\\}``\n      + Symbol: ``w_t^\\text{th}``\n\n**Static Parameters:**\n\n  - ``P^\\text{th,min}`` = `PowerSystems.get_active_power_limits(device).min`\n  - ``P^\\text{th,max}`` = `PowerSystems.get_active_power_limits(device).max`\n  - ``Q^\\text{th,min}`` = `PowerSystems.get_reactive_power_limits(device).min`\n  - ``Q^\\text{th,max}`` = `PowerSystems.get_reactive_power_limits(device).max`\n\n**Objective:**\n\nAdd a cost to the objective function depending on the defined cost structure of the thermal unit by adding it to its `ProductionCostExpression`.\n\n**Expressions:**\n\nAdds ``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.\n\n**Constraints:**\n\nFor 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.\n\n```math\n\\begin{align*}\n&  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\\} \\\\\n&  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\\} \\\\\n& u_1^\\text{th} = u^\\text{th,init} + v_1^\\text{th} - w_1^\\text{th} \\\\\n& u_t^\\text{th} = u_{t-1}^\\text{th} + v_t^\\text{th} - w_t^\\text{th}, \\quad \\forall t \\in \\{2,\\dots,T\\} \\\\\n& v_t^\\text{th} + w_t^\\text{th} \\le 1, \\quad \\forall t \\in \\{1,\\dots,T\\}\n\\end{align*}\n```\n\n* * *\n\n## `ThermalBasicCompactUnitCommitment`\n\n```@docs\nThermalBasicCompactUnitCommitment\n```\n\n**Variables:**\n\n  - [`PowerAboveMinimumVariable`](@ref):\n    \n      + Bounds: [0.0, ]\n      + Symbol: ``\\Delta p^\\text{th}``\n\n  - [`ReactivePowerVariable`](@ref):\n    \n      + Bounds: [0.0, ]\n      + Symbol: ``q^\\text{th}``\n  - [`OnVariable`](@ref):\n    \n      + Bounds: ``\\{0,1\\}``\n      + Symbol: ``u_t^\\text{th}``\n  - [`StartVariable`](@ref):\n    \n      + Bounds: ``\\{0,1\\}``\n      + Symbol: ``v_t^\\text{th}``\n  - [`StopVariable`](@ref):\n    \n      + Bounds: ``\\{0,1\\}``\n      + Symbol: ``w_t^\\text{th}``\n\n**Auxiliary Variables:**\n\n  - [`PowerOutput`](@ref):\n    \n      + Symbol: ``P^\\text{th}``\n      + Definition: ``P^\\text{th} = u^\\text{th}P^\\text{min} + \\Delta p^\\text{th}``\n\n**Static Parameters:**\n\n  - ``P^\\text{th,min}`` = `PowerSystems.get_active_power_limits(device).min`\n  - ``P^\\text{th,max}`` = `PowerSystems.get_active_power_limits(device).max`\n  - ``Q^\\text{th,min}`` = `PowerSystems.get_reactive_power_limits(device).min`\n  - ``Q^\\text{th,max}`` = `PowerSystems.get_reactive_power_limits(device).max`\n\n**Objective:**\n\nAdd a cost to the objective function depending on the defined cost structure of the thermal unit by adding it to its `ProductionCostExpression`.\n\n**Expressions:**\n\nAdds ``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.\n\n**Constraints:**\n\nFor 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.\n\n```math\n\\begin{align*}\n&  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\\} \\\\\n&  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\\} \\\\\n& u_1^\\text{th} = u^\\text{th,init} + v_1^\\text{th} - w_1^\\text{th} \\\\\n& u_t^\\text{th} = u_{t-1}^\\text{th} + v_t^\\text{th} - w_t^\\text{th}, \\quad \\forall t \\in \\{2,\\dots,T\\} \\\\\n& v_t^\\text{th} + w_t^\\text{th} \\le 1, \\quad \\forall t \\in \\{1,\\dots,T\\}\n\\end{align*}\n```\n\n* * *\n\n## `ThermalCompactUnitCommitment`\n\n```@docs\nThermalCompactUnitCommitment\n```\n\n**Variables:**\n\n  - [`PowerAboveMinimumVariable`](@ref):\n    \n      + Bounds: [0.0, ]\n      + Symbol: ``\\Delta p^\\text{th}``\n\n  - [`ReactivePowerVariable`](@ref):\n    \n      + Bounds: [0.0, ]\n      + Symbol: ``q^\\text{th}``\n  - [`OnVariable`](@ref):\n    \n      + Bounds: ``\\{0,1\\}``\n      + Symbol: ``u_t^\\text{th}``\n  - [`StartVariable`](@ref):\n    \n      + Bounds: ``\\{0,1\\}``\n      + Symbol: ``v_t^\\text{th}``\n  - [`StopVariable`](@ref):\n    \n      + Bounds: ``\\{0,1\\}``\n      + Symbol: ``w_t^\\text{th}``\n\n**Auxiliary Variables:**\n\n  - [`PowerOutput`](@ref):\n    \n      + Symbol: ``P^\\text{th}``\n      + Definition: ``P^\\text{th} = u^\\text{th}P^\\text{min} + \\Delta p^\\text{th}``\n\n  - [`TimeDurationOn`](@ref):\n    \n      + Symbol: ``V_t^\\text{th}``\n      + Definition: Computed post optimization by adding consecutive turned on variable ``u_t^\\text{th}``\n  - [`TimeDurationOff`](@ref):\n    \n      + Symbol: ``W_t^\\text{th}``\n      + Definition: Computed post optimization by adding consecutive turned off variable ``1 - u_t^\\text{th}``\n\n**Static Parameters:**\n\n  - ``P^\\text{th,min}`` = `PowerSystems.get_active_power_limits(device).min`\n  - ``P^\\text{th,max}`` = `PowerSystems.get_active_power_limits(device).max`\n  - ``Q^\\text{th,min}`` = `PowerSystems.get_reactive_power_limits(device).min`\n  - ``Q^\\text{th,max}`` = `PowerSystems.get_reactive_power_limits(device).max`\n  - ``R^\\text{th,up}`` = `PowerSystems.get_ramp_limits(device).up`\n  - ``R^\\text{th,dn}`` = `PowerSystems.get_ramp_limits(device).down`\n  - ``D^\\text{min,up}`` = `PowerSystems.get_time_limits(device).up`\n  - ``D^\\text{min,dn}`` = `PowerSystems.get_time_limits(device).down`\n\n**Objective:**\n\nAdd a cost to the objective function depending on the defined cost structure of the thermal unit by adding it to its `ProductionCostExpression`.\n\n**Expressions:**\n\nAdds ``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.\n\n**Constraints:**\n\nFor 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.\n\n```math\n\\begin{align*}\n&  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\\} \\\\\n&  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\\} \\\\\n& -R^\\text{th,dn} \\le \\Delta p_1^\\text{th} - \\Delta p^\\text{th, init} \\le R^\\text{th,up} \\\\\n& -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\\} \\\\\n& u_1^\\text{th} = u^\\text{th,init} + v_1^\\text{th} - w_1^\\text{th} \\\\\n& u_t^\\text{th} = u_{t-1}^\\text{th} + v_t^\\text{th} - w_t^\\text{th}, \\quad \\forall t \\in \\{2,\\dots,T\\} \\\\\n& v_t^\\text{th} + w_t^\\text{th} \\le 1, \\quad \\forall t \\in \\{1,\\dots,T\\} \n\\end{align*}\n```\n\nIn 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.\n\nThe 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.\n\nMinimum up-time constraint for ``t \\in \\{1,\\dots T\\}``:\n\n```math\n\\begin{align*}\n&  \\text{If } t \\leq D^\\text{min,up} - D^\\text{init,up} \\text{ and } D^\\text{init,up} > 0: \\\\\n& 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).} \\\\\n& \\text{Otherwise:} \\\\ \n& \\sum_{i=t-D^\\text{min,up} + 1}^t v_i^\\text{th} \\leq  u_t^\\text{th} \n\\end{align*}\n```\n\nMinimum down-time constraint for ``t \\in \\{1,\\dots T\\}``:\n\n```math\n\\begin{align*}\n&  \\text{If } t \\leq D^\\text{min,dn} - D^\\text{init,dn} \\text{ and } D^\\text{init,up} > 0: \\\\\n& 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).} \\\\\n& \\text{Otherwise:} \\\\ \n& \\sum_{i=t-D^\\text{min,dn} + 1}^t w_i^\\text{th}  \\leq 1 - u_t^\\text{th}\n\\end{align*}\n```\n\n* * *\n\n## `ThermalStandardUnitCommitment`\n\n```@docs\nThermalStandardUnitCommitment\n```\n\n**Variables:**\n\n  - [`ActivePowerVariable`](@ref):\n    \n      + Bounds: [0.0, ]\n      + Symbol: ``p^\\text{th}``\n\n  - [`ReactivePowerVariable`](@ref):\n    \n      + Bounds: [0.0, ]\n      + Symbol: ``q^\\text{th}``\n  - [`OnVariable`](@ref):\n    \n      + Bounds: ``\\{0,1\\}``\n      + Symbol: ``u_t^\\text{th}``\n  - [`StartVariable`](@ref):\n    \n      + Bounds: ``\\{0,1\\}``\n      + Symbol: ``v_t^\\text{th}``\n  - [`StopVariable`](@ref):\n    \n      + Bounds: ``\\{0,1\\}``\n      + Symbol: ``w_t^\\text{th}``\n\nIf Slack variables are enabled (`use_slacks = true`):\n\n  - [`RateofChangeConstraintSlackUp`](@ref):\n    \n      + Bounds: [0.0, ]\n      + Default initial value: 0.0\n      + Default proportional cost: 2e5\n      + Symbol: ``p^\\text{sl,up}``\n\n  - [`RateofChangeConstraintSlackDown`](@ref):\n    \n      + Bounds: [0.0, ]\n      + Default initial value: 0.0\n      + Default proportional cost: 2e5\n      + Symbol: ``p^\\text{sl,dn}``\n\n**Auxiliary Variables:**\n\n  - [`TimeDurationOn`](@ref):\n    \n      + Symbol: ``V_t^\\text{th}``\n      + Definition: Computed post optimization by adding consecutive turned on variable ``u_t^\\text{th}``\n\n  - [`TimeDurationOff`](@ref):\n    \n      + Symbol: ``W_t^\\text{th}``\n      + Definition: Computed post optimization by adding consecutive turned off variable ``1 - u_t^\\text{th}``\n\n**Static Parameters:**\n\n  - ``P^\\text{th,min}`` = `PowerSystems.get_active_power_limits(device).min`\n  - ``P^\\text{th,max}`` = `PowerSystems.get_active_power_limits(device).max`\n  - ``Q^\\text{th,min}`` = `PowerSystems.get_reactive_power_limits(device).min`\n  - ``Q^\\text{th,max}`` = `PowerSystems.get_reactive_power_limits(device).max`\n  - ``R^\\text{th,up}`` = `PowerSystems.get_ramp_limits(device).up`\n  - ``R^\\text{th,dn}`` = `PowerSystems.get_ramp_limits(device).down`\n  - ``D^\\text{min,up}`` = `PowerSystems.get_time_limits(device).up`\n  - ``D^\\text{min,dn}`` = `PowerSystems.get_time_limits(device).down`\n\n**Objective:**\n\nAdd a cost to the objective function depending on the defined cost structure of the thermal unit by adding it to its `ProductionCostExpression`.\n\n**Expressions:**\n\nAdds ``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.\n\n**Constraints:**\n\nFor 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.\n\n```math\n\\begin{align*}\n&  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\\} \\\\\n&  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\\} \\\\\n& p_1^\\text{th} -  p^\\text{th, init} - p_1^\\text{sl,up} \\le R^\\text{th,up} \\\\\n& 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\\} \\\\\n& -R^\\text{th,dn} \\le p_1^\\text{th} -  p^\\text{th, init} + p_1^\\text{sl,dn} \\\\\n& -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\\} \\\\\n& u_1^\\text{th} = u^\\text{th,init} + v_1^\\text{th} - w_1^\\text{th} \\\\\n& u_t^\\text{th} = u_{t-1}^\\text{th} + v_t^\\text{th} - w_t^\\text{th}, \\quad \\forall t \\in \\{2,\\dots,T\\} \\\\\n& v_t^\\text{th} + w_t^\\text{th} \\le 1, \\quad \\forall t \\in \\{1,\\dots,T\\} \n\\end{align*}\n```\n\nIn 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.\n\nThe 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.\n\nMinimum up-time constraint for ``t \\in \\{1,\\dots T\\}``:\n\n```math\n\\begin{align*}\n&  \\text{If } t \\leq D^\\text{min,up} - D^\\text{init,up} \\text{ and } D^\\text{init,up} > 0: \\\\\n& 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).} \\\\\n& \\text{Otherwise:} \\\\ \n& \\sum_{i=t-D^\\text{min,up} + 1}^t v_i^\\text{th} \\leq  u_t^\\text{th} \n\\end{align*}\n```\n\nMinimum down-time constraint for ``t \\in \\{1,\\dots T\\}``:\n\n```math\n\\begin{align*}\n&  \\text{If } t \\leq D^\\text{min,dn} - D^\\text{init,dn} \\text{ and } D^\\text{init,up} > 0: \\\\\n& 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).} \\\\\n& \\text{Otherwise:} \\\\ \n& \\sum_{i=t-D^\\text{min,dn} + 1}^t w_i^\\text{th}  \\leq 1 - u_t^\\text{th}\n\\end{align*}\n```\n\n* * *\n\n## `ThermalMultiStartUnitCommitment`\n\n```@docs\nThermalMultiStartUnitCommitment\n```\n\n**Variables:**\n\n  - [`PowerAboveMinimumVariable`](@ref):\n    \n      + Bounds: [0.0, ]\n      + Symbol: ``\\Delta p^\\text{th}``\n\n  - [`ReactivePowerVariable`](@ref):\n    \n      + Bounds: [0.0, ]\n      + Symbol: ``q^\\text{th}``\n  - [`OnVariable`](@ref):\n    \n      + Bounds: ``\\{0,1\\}``\n      + Symbol: ``u_t^\\text{th}``\n  - [`StartVariable`](@ref):\n    \n      + Bounds: ``\\{0,1\\}``\n      + Symbol: ``v_t^\\text{th}``\n  - [`StopVariable`](@ref):\n    \n      + Bounds: ``\\{0,1\\}``\n      + Symbol: ``w_t^\\text{th}``\n  - [`ColdStartVariable`](@ref):\n    \n      + Bounds: ``\\{0,1\\}``\n      + Symbol: ``x_t^\\text{th}``\n  - [`WarmStartVariable`](@ref):\n    \n      + Bounds: ``\\{0,1\\}``\n      + Symbol: ``y_t^\\text{th}``\n  - [`HotStartVariable`](@ref):\n    \n      + Bounds: ``\\{0,1\\}``\n      + Symbol: ``z_t^\\text{th}``\n\n**Auxiliary Variables:**\n\n  - [`PowerOutput`](@ref):\n    \n      + Symbol: ``P^\\text{th}``\n      + Definition: ``P^\\text{th} = u^\\text{th}P^\\text{min} + \\Delta p^\\text{th}``\n\n  - [`TimeDurationOn`](@ref):\n    \n      + Symbol: ``V_t^\\text{th}``\n      + Definition: Computed post optimization by adding consecutive turned on variable ``u_t^\\text{th}``\n  - [`TimeDurationOff`](@ref):\n    \n      + Symbol: ``W_t^\\text{th}``\n      + Definition: Computed post optimization by adding consecutive turned off variable ``1 - u_t^\\text{th}``\n\n**Static Parameters:**\n\n  - ``P^\\text{th,min}`` = `PowerSystems.get_active_power_limits(device).min`\n  - ``P^\\text{th,max}`` = `PowerSystems.get_active_power_limits(device).max`\n  - ``Q^\\text{th,min}`` = `PowerSystems.get_reactive_power_limits(device).min`\n  - ``Q^\\text{th,max}`` = `PowerSystems.get_reactive_power_limits(device).max`\n  - ``R^\\text{th,up}`` = `PowerSystems.get_ramp_limits(device).up`\n  - ``R^\\text{th,dn}`` = `PowerSystems.get_ramp_limits(device).down`\n  - ``D^\\text{min,up}`` = `PowerSystems.get_time_limits(device).up`\n  - ``D^\\text{min,dn}`` = `PowerSystems.get_time_limits(device).down`\n  - ``D^\\text{cold}`` = `PowerSystems.get_start_time_limits(device).cold`\n  - ``D^\\text{warm}`` = `PowerSystems.get_start_time_limits(device).warm`\n  - ``D^\\text{hot}`` = `PowerSystems.get_start_time_limits(device).hot`\n  - ``P^\\text{th,startup}`` = `PowerSystems.get_power_trajectory(device).startup`\n  - ``P^\\text{th, shdown}`` = `PowerSystems.get_power_trajectory(device).shutdown`\n\n**Objective:**\n\nAdd a cost to the objective function depending on the defined cost structure of the thermal unit by adding it to its `ProductionCostExpression`.\n\n**Expressions:**\n\nAdds ``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.\n\n**Constraints:**\n\nFor 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.\n\n```math\n\\begin{align*}\n&  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\\} \\\\\n&  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\\} \\\\\n& -R^\\text{th,dn} \\le \\Delta p_1^\\text{th} - \\Delta p^\\text{th, init} \\le R^\\text{th,up} \\\\\n& -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\\} \\\\\n& u_1^\\text{th} = u^\\text{th,init} + v_1^\\text{th} - w_1^\\text{th} \\\\\n& u_t^\\text{th} = u_{t-1}^\\text{th} + v_t^\\text{th} - w_t^\\text{th}, \\quad \\forall t \\in \\{2,\\dots,T\\} \\\\\n& v_t^\\text{th} + w_t^\\text{th} \\le 1, \\quad \\forall t \\in \\{1,\\dots,T\\} \\\\\n& \\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}\n\\end{align*}\n```\n\nIn 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.\n\nThe 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.\n\nMinimum up-time constraint for ``t \\in \\{1,\\dots T\\}``:\n\n```math\n\\begin{align*}\n&  \\text{If } t \\leq D^\\text{min,up} - D^\\text{init,up} \\text{ and } D^\\text{init,up} > 0: \\\\\n& 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).} \\\\\n& \\text{Otherwise:} \\\\ \n& \\sum_{i=t-D^\\text{min,up} + 1}^t v_i^\\text{th} \\leq  u_t^\\text{th} \n\\end{align*}\n```\n\nMinimum down-time constraint for ``t \\in \\{1,\\dots T\\}``:\n\n```math\n\\begin{align*}\n&  \\text{If } t \\leq D^\\text{min,dn} - D^\\text{init,dn} \\text{ and } D^\\text{init,up} > 0: \\\\\n& 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).} \\\\\n& \\text{Otherwise:} \\\\ \n& \\sum_{i=t-D^\\text{min,dn} + 1}^t w_i^\\text{th}  \\leq 1 - u_t^\\text{th}\n\\end{align*}\n```\n\nFinally, multi temperature start/stop constraints are implemented using the following constraints:\n\n```math\n\\begin{align*}\n& v_t^\\text{th} = x_t^\\text{th} + y_t^\\text{th} + z_t^\\text{th}, \\quad \\forall t \\in \\{1, \\dots, T\\} \\\\\n& 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\\} \\\\\n& 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\\} \\\\\n& (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\\}  \\\\\n& 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\\} \\\\\n& (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\\}  \\\\\n& 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\\} \\\\\n\\end{align*}\n```\n\n* * *\n\n## Valid configurations\n\nValid `DeviceModel`s for subtypes of `ThermalGen` include the following:\n\n```@eval\nusing PowerSimulations\nusing PowerSystems\nusing DataFrames\nusing Latexify\ncombos = PowerSimulations.generate_device_formulation_combinations()\nfilter!(x -> x[\"device_type\"] <: ThermalGen, combos)\ncombo_table = DataFrame(\n    \"Valid DeviceModel\" =>\n        [\"`DeviceModel($(c[\"device_type\"]), $(c[\"formulation\"]))`\" for c in combos],\n    \"Device Type\" => [\n        \"[$(c[\"device_type\"])](https://nrel-Sienna.github.io/PowerSystems.jl/stable/model_library/generated_$(c[\"device_type\"])/)\"\n        for c in combos\n    ],\n    \"Formulation\" => [\"[$(c[\"formulation\"])](@ref)\" for c in combos],\n)\nmdtable(combo_table; latex = false)\n```\n"
  },
  {
    "path": "docs/src/get_test_data.jl",
    "content": "using Cbc\nusing PowerSimulations\nusing PowerSystems\nusing DataStructures\nusing InfrastructureSystems\nimport InfrastructureSystems as IS\nimport PowerSimulations as PSI\nimport PowerSystems as PSY\n\ninclude(\"../../../test/test_utils/get_test_data.jl\")\n\nabstract type TestOpProblem <: PSI.DefaultDecisionProblem end\n\nsystem = build_c_sys5_re(; add_reserves = true)\nsolver = optimizer_with_attributes(Cbc.Optimizer)\n\ndevices = Dict{Symbol, DeviceModel}(\n    :Generators => DeviceModel(ThermalStandard, ThermalBasicDispatch),\n    :Loads => DeviceModel(PowerLoad, StaticPowerLoad),\n)\nbranches = Dict{Symbol, DeviceModel}(\n    :L => DeviceModel(Line, StaticLine),\n    :T => DeviceModel(Transformer2W, StaticBranch),\n    :TT => DeviceModel(TapTransformer, StaticBranch),\n);\nservices = Dict{Symbol, ServiceModel}();\n\ntemplate = PSI.ProblemTemplate(CopperPlatePowerModel, devices, branches, services);\n\noperation_problem = PSI.DecisionModel(TestOpProblem, template, system; optimizer = solver);\n\nset_services_template!(\n    operation_problem,\n    Dict(\n        :Reserve => ServiceModel(VariableReserve{ReserveUp}, RangeReserve),\n        :Down_Reserve => ServiceModel(VariableReserve{ReserveDown}, RangeReserve),\n    ),\n)\n\nop_results = solve!(operation_problem)\n"
  },
  {
    "path": "docs/src/how_to/adding_new_problem_model.md",
    "content": "# Adding an Operations Problem Model\n\nThis tutorial will show how to create a custom decision problem model. These cases are the ones\nwhere the user want to solve a fully specified problem. Some examples of custom decision models include:\n\n  - Solving a custom Security Constrained Unit Commitment Problem\n  - Solving a market agent utility maximization Problem. See examples of this functionality in HybridSystemsSimulations.jl\n\nThe 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\nthe 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.\n\n!!! warning\n    \n    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.\n\n## General Rules\n\n 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.\n    Finally, developing a custom model decision model that will employ an optimization model under the hood requires understanding JuMP.jl.\n\n 2. Need to employ [anonymous constraints and variables in JuMP](https://jump.dev/JuMP.jl/stable/manual/variables/#anonymous_variables)\n    and register the constraints, variables and other optimization objects into PowerSimulations.jl's optimization container. Otherwise the\n    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.\n 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.\n\n## Decision Problem\n\n### Step 1: Define a Custom Decision Problem\n\nDefine 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\nfor the parameters, references and the optimization problem.\n\nIt 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\n\n```julia\nstruct MyCustomDecisionProblem <: PSI.DecisionProblem end\n```\n\nAlternatively, 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.\n\n```julia\nstruct MyCustomDecisionProblem <: PSI.DefaultDecisionProblem end\n```\n\nOnce 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`\n\n```julia\nmy_model = DecisionModel{MyCustomDecisionProblem}(\n    sys;\n    name = \"MyModel\",\n    optimizer = optimizer_with_attributes(HiGHS.Optimizer),\n    optimizer_solve_log_print = true,\n)\n```\n\n#### Mandatory Method Implementations\n\n 1. `build_model!`: This method build the `JuMP` optimization model.\n\n#### Optional Method Overloads\n\nThese 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.\n\n 1. `validate_template`\n 2. `validate_time_series!`\n 3. `reset!`\n 4. `solve_impl!`\n\n### How to develop your `build_model!` function\n\n#### Registering a variable in the model\n\nTo register a variable in the model, the developer must first allocate the container into the\noptimization container and then populate it. For example, it require start the build function as follows:\n\n!!! info\n    \n    We recommend calling `import PowerSimulations` and defining the constant `CONST PSI = PowerSimulations` to\n    make it easier to read the code and determine which package is responsible for defining the functions.\n\n```julia\nfunction PSI.build_model!(model::PSI.DecisionModel{MyCustomDecisionProblem})\n    container = PSI.get_optimization_container(model)\n    time_steps = 1:24\n    PSI.set_time_steps!(container, time_steps)\n    system = PSI.get_system(model)\n\n    thermal_gens = PSY.get_components(PSY.ThermalStandard, system)\n    thermal_gens_names = PSY.get_name.(thermal_gens)\n\n    # Create the container for the variable\n    variable = PSI.add_variable_container!(\n        container,\n        PSI.ActivePowerVariable(), # <- This variable is defined in PowerSimulations but the user can define their own\n        PSY.ThermalGeneration, # <- Device type for the variable. Can be from PSY or custom defined\n        thermal_gens_names, # <- First container dimension\n        time_steps, # <- Second container dimension\n    )\n\n    # Iterate over the devices and time to store the JuMP variables into the container.\n    for t in time_steps, d in thermal_gens_names\n        name = PSY.get_name(d)\n        variable[name, t] = JuMP.@variable(get_jump_model(container))\n        # It is possible to use PSY getter functions to retrieve data from the generators\n        JuMP.set_upper_bound(variable[name, t], UB_DATA) # <- Optional\n        JuMP.set_lower_bound(variable[name, t], LB_DATA) # <- Optional\n    end\n\n    # Add More Variables.....\n\n    return\nend\n```\n\n#### Registering a constraint in the model\n\nA similar pattern is used to add constraints to the model, in this example the field `meta` is used\nto 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.\n\n```julia\nfunction PSI.build_model!(model::PSI.DecisionModel{MyCustomDecisionProblem})\n    container = PSI.get_optimization_container(model)\n    time_steps = 1:24\n    PSI.set_time_steps!(container, time_steps)\n    system = PSI.get_system(model)\n\n    # VARIABLE ADDITION CODE\n\n    # Constraint additions\n    con_ub = PSI.add_constraints_container!(\n        container,\n        PSI.RangeLimitConstraint(), # <- Constraint Type defined by PSI or your own\n        PSY.ThermalGeneration, # <- Device type for variable. Can be PSY or custom\n        thermal_gens_names, # <- First container dimension\n        time_steps; # <- Second container dimension\n        meta = \"ub\", # <- meta allows to reuse a constraint definition for similar constraints. It only requires to be a string\n    )\n\n    con_lb = PSI.add_constraints_container!(\n        container,\n        PSI.RangeLimitConstraint(),\n        PSY.ThermalGeneration,\n        thermal_gens_names, # <- First container dimension\n        time_steps; # <- Second container dimension\n        meta = \"lb\", # <- meta allows to reuse a constraint definition for similar constraints. It only requires to be a string\n    )\n\n    # Retrieve a relevant variable from the container if not defined in\n    variable = PSI.get_variable(container, PSI.ActivePowerVariable(), PSY.ThermalGeneration)\n    for device in devices, t in time_steps\n        ci_name = PSY.get_name(device)\n        limits = get_min_max_limits(device) # depends on constraint type and formulation type\n        con_ub[ci_name, t] =\n            JuMP.@constraint(get_jump_model(container), variable[ci_name, t] >= limits.min)\n        con_lb[ci_name, t] =\n            JuMP.@constraint(get_jump_model(container), variable[ci_name, t] >= limits.min)\n    end\n\n    return\nend\n```\n"
  },
  {
    "path": "docs/src/how_to/debugging_infeasible_models.md",
    "content": "# Debugging infeasible models\n\nGetting infeasible solutions to models is a common occurrence in operations simulations, there are multiple reasons why this can happen.\n`PowerSimulations.jl` has several tools to help debug this situation.\n\n## Adding slacks to the model\n\nOne 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.\n\nThe recommended solution for any of these cases is adding slack variables to the network model, for example:\n\n```@repl tutorial\ntemplate_uc = ProblemTemplate(\n    NetworkModel(\n        CopperPlatePowerModel;\n        use_slacks = true,\n    ),\n)\n```\n\nwill add slack variables to the `ActivePowerBalance` expression.\n\nIn 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.\n\n### Services cases\n\nIn 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:\n\n```@repl tutorial\nset_service_model!(\n    template_uc,\n    ServiceModel(\n        VariableReserve{ReserveUp},\n        RangeReserve;\n        use_slacks = true,\n    ),\n)\n```\n\nAgain, 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.\n\n## Getting the infeasibility conflict\n\nSome 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.\n\nTo 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.\n\nThe following code creates a decision model with the `Xpress` optimizer, and enabling the `calculate_conflict=true` keyword argument.\n\n```julia\nDecisionModel(\n    template_ed,\n    sys_rts_rt;\n    name = \"ED\",\n    optimizer = optimizer_with_attributes(Xpress.Optimizer, \"MIPRELSTOP\" => 1e-2),\n    optimizer_solve_log_print = true,\n    calculate_conflict = true,\n    store_variable_names = true,\n)\n```\n\nHere is an example on how the IIS will be displayed as:\n\n```raw\nError: Constraints participating in conflict basis (IIS) \n│ \n│ ┌──────────────────────────────────────┐\n│ │ CopperPlateBalanceConstraint__System │\n│ ├──────────────────────────────────────┤\n│ │                            (113, 26) │\n│ └──────────────────────────────────────┘\n│ ┌──────────────────────────────────┐\n│ │ EnergyAssetBalance__HybridSystem │\n│ ├──────────────────────────────────┤\n│ │               (\"317_Hybrid\", 26) │\n│ └──────────────────────────────────┘\n│ ┌─────────────────────────────────────────────┐\n│ │ PiecewiseLinearCostConstraint__HybridSystem │\n│ ├─────────────────────────────────────────────┤\n│ │                          (\"317_Hybrid\", 26) │\n│ └─────────────────────────────────────────────┘\n│ ┌────────────────────────────────────────────────┐\n│ │ PiecewiseLinearCostConstraint__ThermalStandard │\n│ ├────────────────────────────────────────────────┤\n│ │                            (\"202_STEAM_3\", 26) │\n│ │                            (\"101_STEAM_3\", 26) │\n│ │                               (\"118_CC_1\", 26) │\n│ │                            (\"202_STEAM_4\", 26) │\n│ │                               (\"315_CT_6\", 26) │\n│ │                            (\"201_STEAM_3\", 26) │\n│ │                            (\"102_STEAM_4\", 26) │\n│ └────────────────────────────────────────────────┘\n│ ┌──────────────────────────────────────────────────────────────────────┐\n│ │ ActivePowerVariableTimeSeriesLimitsConstraint__RenewableDispatch__ub │\n│ ├──────────────────────────────────────────────────────────────────────┤\n│ │                                                   (\"122_WIND_1\", 26) │\n│ │                                                     (\"324_PV_3\", 26) │\n│ │                                                     (\"312_PV_1\", 26) │\n│ │                                                     (\"102_PV_1\", 26) │\n│ │                                                     (\"101_PV_1\", 26) │\n│ │                                                     (\"324_PV_2\", 26) │\n│ │                                                     (\"313_PV_2\", 26) │\n│ │                                                     (\"104_PV_1\", 26) │\n│ │                                                     (\"101_PV_2\", 26) │\n│ │                                                   (\"309_WIND_1\", 26) │\n│ │                                                     (\"310_PV_2\", 26) │\n│ │                                                     (\"113_PV_1\", 26) │\n│ │                                                     (\"314_PV_1\", 26) │\n│ │                                                     (\"324_PV_1\", 26) │\n│ │                                                     (\"103_PV_1\", 26) │\n│ │                                                   (\"303_WIND_1\", 26) │\n│ │                                                     (\"314_PV_2\", 26) │\n│ │                                                     (\"102_PV_2\", 26) │\n│ │                                                     (\"314_PV_3\", 26) │\n│ │                                                     (\"320_PV_1\", 26) │\n│ │                                                     (\"101_PV_3\", 26) │\n│ │                                                     (\"319_PV_1\", 26) │\n│ │                                                     (\"314_PV_4\", 26) │\n│ │                                                     (\"310_PV_1\", 26) │\n│ │                                                     (\"215_PV_1\", 26) │\n│ │                                                     (\"313_PV_1\", 26) │\n│ │                                                     (\"101_PV_4\", 26) │\n│ │                                                     (\"119_PV_1\", 26) │\n│ └──────────────────────────────────────────────────────────────────────┘\n│ ┌─────────────────────────────────────────────────────────────────────────────┐\n│ │ FeedforwardSemiContinuousConstraint__ThermalStandard__ActivePowerVariable_ub │\n│ ├─────────────────────────────────────────────────────────────────────────────┤\n│ │                                                            (\"322_CT_6\", 26) │\n│ │                                                            (\"321_CC_1\", 26) │\n│ │                                                            (\"223_CT_4\", 26) │\n│ │                                                            (\"213_CT_1\", 26) │\n│ │                                                            (\"223_CT_6\", 26) │\n│ │                                                            (\"123_CT_1\", 26) │\n│ │                                                            (\"113_CT_3\", 26) │\n│ │                                                            (\"302_CT_3\", 26) │\n│ │                                                            (\"215_CT_4\", 26) │\n│ │                                                            (\"301_CT_4\", 26) │\n│ │                                                            (\"113_CT_2\", 26) │\n│ │                                                            (\"221_CC_1\", 26) │\n│ │                                                            (\"223_CT_5\", 26) │\n│ │                                                            (\"315_CT_7\", 26) │\n│ │                                                            (\"215_CT_5\", 26) │\n│ │                                                            (\"113_CT_1\", 26) │\n│ │                                                            (\"307_CT_2\", 26) │\n│ │                                                            (\"213_CT_2\", 26) │\n│ │                                                            (\"113_CT_4\", 26) │\n│ │                                                            (\"218_CC_1\", 26) │\n│ │                                                            (\"213_CC_3\", 26) │\n│ │                                                            (\"323_CC_2\", 26) │\n│ │                                                            (\"322_CT_5\", 26) │\n│ │                                                            (\"207_CT_2\", 26) │\n│ │                                                            (\"123_CT_5\", 26) │\n│ │                                                            (\"123_CT_4\", 26) │\n│ │                                                            (\"207_CT_1\", 26) │\n│ │                                                            (\"301_CT_3\", 26) │\n│ │                                                            (\"302_CT_4\", 26) │\n│ │                                                            (\"307_CT_1\", 26) │\n│ └─────────────────────────────────────────────────────────────────────────────┘\n│ ┌───────────────────────────────────────────────────────┐\n│ │ RenewableActivePowerLimitConstraint__HybridSystem__ub │\n│ ├───────────────────────────────────────────────────────┤\n│ │                                    (\"317_Hybrid\", 26) │\n│ └───────────────────────────────────────────────────────┘\n│ ┌───────────────────────────────────────┐\n│ │ ThermalOnVariableUb__HybridSystem__ub │\n│ ├───────────────────────────────────────┤\n│ │                    (\"317_Hybrid\", 26) │\n│ └───────────────────────────────────────┘\n\n Error: Serializing Infeasible Problem at /var/folders/1v/t69qyl0n5059n6c1nn7sp8zm7g8s6z/T/jl_jNSREb/compact_sim/problems/ED/infeasible_ED_2020-10-06T15:00:00.json\n```\n\nNote 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.\n\nFinally, 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).\n"
  },
  {
    "path": "docs/src/how_to/logging.md",
    "content": "# Logging\n\n`PowerSimulations.jl` will output many log messages when building systems and\nrunning simulations. You may want to customize what gets logged to the console\nand, optionally, a file.\n\nBy default all log messages of level `Logging.Info` or higher will get\ndisplayed to the console.  When you run a simulation a simulation-specific\nlogger will take over and log its messages to a file in the `logs` directory in\nthe simulation output directory. When finished it will relinquish control back\nto the global logger.\n\n## Configuring the global logger\n\nTo configure the global logger in a Jupyter Notebook or REPL you may configure\nyour own logger with the Julia Logging standard library or use the convenience\nfunction provided by PowerSimulations.  This example will log messages of level\n`Logging.Error` to console and `Logging.Info` and higher to the file\n`power-simulations.log` in the current directory.\n\n```julia\nimport Logging\nusing PowerSimulations\nlogger = configure_logging(;\n    console_level = Logging.Error,\n    file_level = Logging.Info,\n    filename = \"power-simulations.log\",\n)\n```\n\n## Configuring the simulation logger\n\nYou can configure the logging level used by the simulation logger when you call\n`build!(simulation)`.  Here is an example that increases logging verbosity:\n\n```julia\nimport Logging\nusing PowerSimulations\nsimulation = Simulation(...)\nbuild!(simulation; console_level = Logging.Info, file_level = Logging.Debug)\n```\n\nThe log file will be located at `<your-output-path>/<simulation-name>/<run-output-dir>/logs/simulation.log`.\n\n## Solver logs\n\nYou can configure logging for the solver you use.  Refer to the solver\ndocumentation.  PowerSimulations does not redirect or intercept prints to\n`stdout` or `stderr` from other libraries.\n\n## Recorder events\n\nPowerSimulations uses the `InfrastructureSystems.Recorder` to store simulation\nevents in a log file.  Refer to this [link](./simulation_recorder.md) for more\ninformation.\n"
  },
  {
    "path": "docs/src/how_to/parallel_simulations.md",
    "content": "# Parallel Simulations\n\nThis section contains instructions to:\n\n  - [Run a Simulation in Parallel on a local computer](@ref)\n  - [Run a Simulation in Parallel on an HPC](@ref)\n\n## Run a Simulation in Parallel on a local computer\n\nThis page describes how to split a simulation into partitions, run each partition in parallel,\nand then join the results.\n\n### Setup\n\nCreate a Julia script to build and run simulations. It must meet the requirements below.\nA full example is in the PowerSimulations repository in `test/run_partitioned_simulation.jl`.\n\n  - Call `using PowerSimulations`.\n\n  - Implement a build function that matches the signature below.\n    It must construct a `Simulation`, call `build!`, and then return the `Simulation` instance.\n    It must throw an exception if the build fails.\n\n```\nfunction build_simulation(\n    output_dir::AbstractString,\n    simulation_name::AbstractString,\n    partitions::SimulationPartitions,\n    index::Union{Nothing, Integer}=nothing,\n)\n```\n\nHere is example code to construct the `Simulation` with these parameters:\n\n```\n    sim = Simulation(\n        name=simulation_name,\n        steps=partitions.num_steps,\n        models=models,\n        sequence=sequence,\n        simulation_folder=output_dir,\n    )\n    status = build!(sim; partitions=partitions, index=index, serialize=isnothing(index))\n    if status != PSI.SimulationBuildStatus.BUILT\n        error(\"Failed to build simulation: status=$status\")\n    end\n```\n\n  - Implement an execute function that matches the signature below. It must throw an exception\n    if the execute fails.\n\n```\nfunction execute_simulation(sim, args...; kwargs...)\n    status = execute!(sim)\n    if status != PSI.RunStatus.SUCCESSFULLY_FINALIZED\n        error(\"Simulation failed to execute: status=$status\")\n    end\nend\n```\n\n### Execution\n\nAfter loading your script, call the function `run_parallel_simulation` as shown below.\n\nThis example splits a year-long simulation into weekly partitions for a total of 53 individual\njobs and then runs them four at a time.\n\n```\njulia> include(\"my_simulation.jl\")\njulia> run_parallel_simulation(\n        build_simulation,\n        execute_simulation,\n        script=\"my_simulation.jl\",\n        output_dir=\"my_simulation_output\",\n        name=\"my_simulation\",\n        num_steps=365,\n        period=7,\n        num_overlap_steps=1,\n        num_parallel_processes=4,\n        exeflags=\"--project=<path-to-your-julia-environment>\",\n    )\n```\n\nThe final results will be in `./my_simulation_otuput/my_simulation`\n\nNote the log files and results for each partition are located in\n`./my_simulation_otuput/my_simulation/simulation_partitions`\n\n## Run a Simulation in Parallel on an HPC\n\nThis page describes how to split a simulation into partitions, run each partition in parallel\non HPC compute nodes, and then join the results.\n\nThese steps can be used on a local computer or any HPC supported by the submission software.\nSome steps may be specific to NREL's HPC `Eagle` cluster.\n\n*Note*: Some instructions are preliminary and will change if functionality is moved\nto a new Julia package.\n\n### Setup\n\n 1. Create a conda environment and install the Python package `NREL-jade`:\n    https://nrel.github.io/jade/installation.html. The rest of this page assumes that\n    the environment is called `jade`.\n 2. Activate the environment with `conda activate jade`.\n 3. Locate the path to that conda environment. It will likely be `~/.conda-envs/jade` or\n    `~/.conda/envs/jade`.\n 4. Load the Julia environment that you use to run simulations. Add the packages `Conda` and\n    `PyCall`.\n 5. Setup Conda to use the existing `jade` environment by running these commands:\n\n```\njulia> run(`conda create -n conda_jl python conda`)\njulia> ENV[\"CONDA_JL_HOME\"] = joinpath(ENV[\"HOME\"], \".conda-envs\", \"jade\")  # change this to your path\npkg> build Conda\n```\n\n 6. Copy the code below into a Julia file called `configure_parallel_simulation.jl`.\n    This is an interface to Jade through PyCall. It will be used to create a Jade configuration.\n    (It may eventually be moved to a separate package.)\n\n```\nfunction configure_parallel_simulation(\n    script::AbstractString,\n    num_steps::Integer,\n    num_period_steps::Integer;\n    num_overlap_steps::Integer=0,\n    project_path=nothing,\n    simulation_name=\"simulation\",\n    config_file=\"config.json\",\n    force=false,\n)\n    partitions = SimulationPartitions(num_steps, num_period_steps, num_overlap_steps)\n    jgc = pyimport(\"jade.extensions.generic_command\")\n    julia_cmd = isnothing(project_path) ? \"julia\" : \"julia --project=$project_path\"\n    setup_command = \"$julia_cmd $script setup --simulation-name=$simulation_name \" *\n    \"--num-steps=$num_steps --num-period-steps=$num_period_steps \" *\n    \"--num-overlap-steps=$num_overlap_steps\"\n    teardown_command = \"$julia_cmd $script join --simulation-name=$simulation_name\"\n    config = jgc.GenericCommandConfiguration(\n        setup_command=setup_command,\n        teardown_command=teardown_command,\n    )\n\n    for i in 1:get_num_partitions(partitions)\n        cmd = \"$julia_cmd $script execute --simulation-name=$simulation_name --index=$i\"\n        job = jgc.GenericCommandParameters(command=cmd, name=\"execute-$i\")\n        config.add_job(job)\n    end\n\n    config.dump(config_file, indent=2)\n    println(\"Created Jade configuration in $config_file. \" *\n            \"Run 'jade submit-jobs [options] $config_file' to execute them.\")\nend\n```\n\n 7. Create a Julia script to build and run simulations. It must meet the requirements below.\n    A full example is in the PowerSimulations repository in `test/run_partitioned_simulation.jl`.\n\n  - Call `using PowerSimulations`.\n\n  - Implement a build function that matches the signature below.\n    It must construct a `Simulation`, call `build!`, and then return the `Simulation` instance.\n    It must throw an exception if the build fails.\n\n```\nfunction build_simulation(\n    output_dir::AbstractString,\n    simulation_name::AbstractString,\n    partitions::SimulationPartitions,\n    index::Union{Nothing, Integer}=nothing,\n)\n```\n\nHere is example code to construct the `Simulation` with these parameters:\n\n```\n    sim = Simulation(\n        name=simulation_name,\n        steps=partitions.num_steps,\n        models=models,\n        sequence=sequence,\n        simulation_folder=output_dir,\n    )\n    status = build!(sim; partitions=partitions, index=index, serialize=isnothing(index))\n    if status != PSI.SimulationBuildStatus.BUILT\n        error(\"Failed to build simulation: status=$status\")\n    end\n```\n\n  - Implement an execute function that matches the signature below. It must throw an exception\n    if the execute fails.\n\n```\nfunction execute_simulation(sim, args...; kwargs...)\n    status = execute!(sim)\n    if status != PSI.RunStatus.SUCCESSFULLY_FINALIZED\n        error(\"Simulation failed to execute: status=$status\")\n    end\nend\n```\n\n  - Make the script runnable as a CLI command by including the following code at the bottom of the\n    file.\n\n```\nfunction main()\n    process_simulation_partition_cli_args(build_simulation, execute_simulation, ARGS...)\nend\n\nif abspath(PROGRAM_FILE) == @__FILE__\n    main()\nend\n```\n\n### Execution\n\n 1. Create a Jade configuration that defines the partitioned simulation jobs. Load your Julia\n    environment.\n    \n    This example splits a year-long simulation into weekly partitions for a total of 53 individual\n    jobs.\n\n```\njulia> include(\"configure_parallel_simulation.jl\")\njulia> num_steps = 365\njulia> period = 7\njulia> num_overlap_steps = 1\njulia> configure_parallel_simulation(\n    \"my_simulation.jl\",  # this is your build/execute script\n    num_steps,\n    period,\n    num_overlap_steps=1,\n    project_path=\".\",  # This optionally specifies the Julia project environment to load.\n)\nCreated Jade configuration in config.json. Run 'jade submit-jobs [options] config.json' to execute them.\n```\n\nExit Julia.\n\n 2. View the configuration for accuracy.\n\n```\n$ jade config show config.json\n```\n\n 3. Start an interactive session on a debug node. *Do not submit the jobs on a login node!* The submission\n    step will run a full build of the simulation and that may consume too many CPU and memory resources\n    for the login node.\n\n```\n$ salloc -t 01:00:00 -N1 --account=<your-account> --partition=debug\n```\n\n 4. Follow the instructions at https://nrel.github.io/jade/tutorial.html to submit the jobs.\n    The example below will configure Jade to run each partition on its own compute node. Depending on\n    the compute and memory constraints of your simulation, you may be able to pack more jobs on each\n    node.\n    \n    Adjust the walltime as necessary.\n\n```\n$ jade config hpc -c hpc_config.toml -t slurm  --walltime=04:00:00 -a <your-account>\n$ jade submit-jobs config.json --per-node-batch-size=1 -o output\n```\n\nIf you are unsure about how much memory and CPU resources your simulation consumes, add these options:\n\n```\n$ jade submit-jobs config.json --per-node-batch-size=1 -o output --resource-monitor-type periodic --resource-monitor-interval 3\n```\n\nJade will create HTML plots of the resource utilization in `output/stats`. You may be able to customize\n`--per-node-batch-size` and `--num-processes` to finish the simulations more quickly.\n\n 5. Jade will run a final command to join the simulation partitions into one unified file. You can load the\n    results as you normally would.\n\n```\njulia> results = SimulationResults(\"<output-dir>/job-outputs/<simulation-name>\")\n```\n\nNote the log files and results for each partition are located in\n`<output-dir>/job-outputs/<simulation-name>/simulation_partitions`\n"
  },
  {
    "path": "docs/src/how_to/problem_templates.md",
    "content": "# [Operations `ProblemTemplate`s](@id op_problem_template)\n\nTemplates are used to specify the modeling properties of the devices and network that are going to he used to specify a problem.\nA `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).\n\n## Building a `ProblemTemplate`\n\nYou can build a `ProblemTemplate` by adding a [`NetworkModel`](@ref), [`DeviceModel`](@ref)s, and [`ServiceModel`](@ref)s.\n\n```julia\ntemplate = ProblemTemplate()\nset_network_model!(template, NetworkModel(CopperPlatePowerModel))\nset_device_model!(template, PowerLoad, StaticPowerLoad)\nset_device_model!(template, ThermalStandard, ThermalBasicUnitCommitment)\nset_service_model!(template, VariableReserve{ReserveUp}, RangeReserve)\n```\n\n## Default Templates\n\n`PowerSimulations.jl` provides default templates for common operation problems. You can retrieve a default template and modify it according\nto your requirements. Currently supported default templates are:\n\n```@docs; canonical=false\ntemplate_economic_dispatch\n```\n\n```@example\nusing PowerSimulations #hide\ntemplate_economic_dispatch()\n```\n\n```@docs; canonical=false\ntemplate_unit_commitment\n```\n\n```@example\nusing PowerSimulations #hide\ntemplate_unit_commitment()\n```\n"
  },
  {
    "path": "docs/src/how_to/read_results.md",
    "content": "# [Read results](@id read_results)\n\nOnce 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.\n\n## Read results of a Decision Problem\n\nOnce a [`DecisionModel`](@ref) is solved, results are accessed using `OptimizationProblemResults(model)` as follows:\n\n```julia\n# The DecisionModel is already constructed\nbuild!(model; output_dir = mktempdir())\nsolve!(model)\n\nresults = OptimizationProblemResults(model)\n```\n\nThe output will showcase the available expressions, parameters and variables to read. For example it will look like:\n\n```raw\nStart: 2020-01-01T00:00:00\nEnd: 2020-01-03T23:00:00\nResolution: 60 minutes\n\nPowerSimulations Problem Auxiliary variables Results\n┌──────────────────────────────────────────┐\n│ CumulativeCyclingCharge__HybridSystem    │\n│ CumulativeCyclingDischarge__HybridSystem │\n└──────────────────────────────────────────┘\n\nPowerSimulations Problem Expressions Results\n┌─────────────────────────────────────────────┐\n│ ProductionCostExpression__RenewableDispatch │\n│ ProductionCostExpression__ThermalStandard   │\n└─────────────────────────────────────────────┘\n\nPowerSimulations Problem Duals Results\n┌──────────────────────────────────────┐\n│ CopperPlateBalanceConstraint__System │\n└──────────────────────────────────────┘\n\nPowerSimulations Problem Parameters Results\n┌────────────────────────────────────────────────────────────────────────┐\n│ ActivePowerTimeSeriesParameter__RenewableNonDispatch                           │\n│ RenewablePowerTimeSeries__HybridSystem                                 │\n│ RequirementTimeSeriesParameter__VariableReserve__ReserveUp__Spin_Up_R3 │\n│ RequirementTimeSeriesParameter__VariableReserve__ReserveUp__Reg_Up     │\n│ ActivePowerTimeSeriesParameter__PowerLoad                              │\n│ ActivePowerTimeSeriesParameter__RenewableDispatch                      │\n│ RequirementTimeSeriesParameter__VariableReserve__ReserveDown__Reg_Down │\n│ ActivePowerTimeSeriesParameter__HydroDispatch                          │\n│ RequirementTimeSeriesParameter__VariableReserve__ReserveUp__Spin_Up_R1 │\n│ RequirementTimeSeriesParameter__VariableReserve__ReserveUp__Spin_Up_R2 │\n└────────────────────────────────────────────────────────────────────────┘\n\nPowerSimulations Problem Variables Results\n┌────────────────────────────────────────────────────────────────────┐\n│ ActivePowerOutVariable__HybridSystem                               │\n│ ReservationVariable__HybridSystem                                  │\n│ RenewablePower__HybridSystem                                       │\n│ ActivePowerReserveVariable__VariableReserve__ReserveUp__Spin_Up_R1 │\n│ SystemBalanceSlackUp__System                                       │\n│ BatteryEnergyShortageVariable__HybridSystem                        │\n│ ActivePowerReserveVariable__VariableReserve__ReserveUp__Reg_Up     │\n│ StopVariable__ThermalStandard                                      │\n│ BatteryStatus__HybridSystem                                        │\n│ BatteryDischarge__HybridSystem                                     │\n│ ActivePowerInVariable__HybridSystem                                │\n│ DischargeRegularizationVariable__HybridSystem                      │\n│ BatteryCharge__HybridSystem                                        │\n│ ActivePowerVariable__RenewableDispatch                             │\n│ ActivePowerReserveVariable__VariableReserve__ReserveDown__Reg_Down │\n│ EnergyVariable__HybridSystem                                       │\n│ OnVariable__HybridSystem                                           │\n│ BatteryEnergySurplusVariable__HybridSystem                         │\n│ SystemBalanceSlackDown__System                                     │\n│ ActivePowerReserveVariable__VariableReserve__ReserveUp__Spin_Up_R2 │\n│ ThermalPower__HybridSystem                                         │\n│ ActivePowerVariable__ThermalStandard                               │\n│ StartVariable__ThermalStandard                                     │\n│ ActivePowerReserveVariable__VariableReserve__ReserveUp__Spin_Up_R3 │\n│ OnVariable__ThermalStandard                                        │\n│ ChargeRegularizationVariable__HybridSystem                         │\n└────────────────────────────────────────────────────────────────────┘\n```\n\nThen the following code can be used to read results:\n\n```julia\n# Read active power of Thermal Standard\nthermal_active_power = read_variable(results, \"ActivePowerVariable__ThermalStandard\")\n\n# Read max active power parameter of RenewableDispatch\nrenewable_param =\n    read_parameter(results, \"ActivePowerTimeSeriesParameter__RenewableDispatch\")\n\n# Read cost expressions of ThermalStandard units\ncost_thermal = read_expression(results, \"ProductionCostExpression__ThermalStandard\")\n\n# Read dual variables\ndual_balance_constraint = read_dual(results, \"CopperPlateBalanceConstraint__System\")\n\n# Read auxiliary variables\naux_var_result = read_aux_variable(results, \"CumulativeCyclingCharge__HybridSystem\")\n```\n\nResults will be in the form of DataFrames that can be easily explored.\n\n## Read results of a Simulation\n\n```julia\n# The Simulation is already constructed\nbuild!(sim)\nexecute!(sim; enable_progress_bar = true)\n\nresults_sim = SimulationResults(sim)\n```\n\nAs an example, the `SimulationResults` printing will look like:\n\n```raw\nDecision Problem Results\n┌──────────────┬─────────────────────┬──────────────┬─────────────────────────┐\n│ Problem Name │ Initial Time        │ Resolution   │ Last Solution Timestamp │\n├──────────────┼─────────────────────┼──────────────┼─────────────────────────┤\n│ ED           │ 2020-10-02T00:00:00 │ 60 minutes   │ 2020-10-09T23:00:00     │\n│ UC           │ 2020-10-02T00:00:00 │ 1440 minutes │ 2020-10-09T00:00:00     │\n└──────────────┴─────────────────────┴──────────────┴─────────────────────────┘\n\nEmulator Results\n┌─────────────────┬───────────┐\n│ Name            │ Emulator  │\n│ Resolution      │ 5 minutes │\n│ Number of steps │ 2304      │\n└─────────────────┴───────────┘\n```\n\nWith this, it is possible to obtain results of each [`DecisionModel`](@ref) and `EmulationModel` as follows:\n\n```julia\n# Use the Problem Name for Decision Problems\nresults_uc = get_decision_problem_results(results_sim, \"UC\")\nresults_ed = get_decision_problem_results(results_sim, \"ED\")\nresults_emulator = get_emulation_problem_results(results_sim)\n```\n\nOnce we have each decision (or emulation) problem results, we can explore directly using the approach for Decision Models, mentioned in the previous section.\n\n### Reading solutions for all simulation steps\n\nIn 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:\n\n```julia\nthermal_active_power = read_variable(results_uc, \"ActivePowerVariable__ThermalStandard\")\n```\n\nwill return:\n\n```\nDataStructures.SortedDict{Any, Any, Base.Order.ForwardOrdering} with 8 entries:\n  DateTime(\"2020-10-02T00:00:00\") => 72×54 DataFrame…\n  DateTime(\"2020-10-03T00:00:00\") => 72×54 DataFrame…\n  DateTime(\"2020-10-04T00:00:00\") => 72×54 DataFrame…\n  DateTime(\"2020-10-05T00:00:00\") => 72×54 DataFrame…\n  DateTime(\"2020-10-06T00:00:00\") => 72×54 DataFrame…\n  DateTime(\"2020-10-07T00:00:00\") => 72×54 DataFrame…\n  DateTime(\"2020-10-08T00:00:00\") => 72×54 DataFrame…\n  DateTime(\"2020-10-09T00:00:00\") => 72×54 DataFrame…\n```\n\nThat is, a sorted dictionary for each simulation step, using as a key the initial timestamp for that specific simulation step.\n\nNote 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.\n\n### Reading the \"realized\" solution (i.e. the interval)\n\nUsing `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.\n\nFor example, the code:\n\n```julia\nth_realized_power =\n    read_realized_variable(results_uc, \"ActivePowerVariable__ThermalStandard\")\n```\n\nwill return:\n\n```raw\n92×54 DataFrame\n 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 ⋯\n     │ DateTime             Float64       Float64   Float64       Float64   Float64        Float64   Float64   Float64   Float64      Float ⋯\n─────┼───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────\n   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   ⋯\n   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\n   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\n   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\n   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   ⋯\n   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\n  ⋮  │          ⋮                ⋮           ⋮           ⋮           ⋮            ⋮           ⋮         ⋮         ⋮           ⋮             ⋱\n 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\n 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\n 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   ⋯\n 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\n 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\n 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\n                                                                                                              44 columns and 180 rows omitted\n```\n\nIn 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.\n"
  },
  {
    "path": "docs/src/how_to/register_variable.md",
    "content": "# Register a variable in a custom operation model\n\nIn most cases, operation problem models are optimization models. Although in `PowerSimulations.jl` it is\npossible to define arbitrary problems that can reflect heuristic decision rules, this is not the common case.\n\nThe 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.\n\n!!! info\n    \n    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.\n\nTo register a variable in the model, the developer must first allocate the container into the\noptimization container and then populate it. For example, it require start the build function as follows:\n\n!!! warning\n    \n    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.\n\n```julia\nusing PowerSystems\nusing PowerSimulations\nimport PowerSystems as PSY\nimport PowerSimulations as PSI\n\nfunction PSI.build_model!(model::PSI.DecisionModel{MyCustomModel})\n    container = PSI.get_optimization_container(model)\n    PSI.set_time_steps!(container, 1:24)\n\n    # Create the container for the variable\n    variable = PSI.add_variable_container!(\n        container,\n        PSI.ActivePowerVariable(), # <- This variable is defined in PowerSimulations but the user can define their own\n        PSY.ThermalGeneration, # <- Device type for the variable. Can be from PSY or custom defined\n        devices_names, # <- First container dimension\n        time_steps, # <- Second container dimension\n    )\n\n    # Iterate over the devices and time to store the JuMP variables into the container.\n    for t in time_steps, d in devices\n        name = PSY.get_name(d)\n        variable[name, t] = JuMP.@variable(get_jump_model(container))\n        # It is possible to use PSY getter functions to retrieve data from the generators\n        # Any other variable property can be specified inside this loop.\n        JuMP.set_upper_bound(variable[name, t], UB_DATA) # <- Optional\n        JuMP.set_lower_bound(variable[name, t], LB_DATA) # <- Optional\n    end\n\n    return\nend\n```\n"
  },
  {
    "path": "docs/src/how_to/simulation_recorder.md",
    "content": "# Simulation Recorder\n\nPowerSimulations.jl provides the ability to record structured data as events\nduring a simulation. These events can be post-processed to help debug problems.\n\nBy default only SimulationStepEvent and ProblemExecutionEvent are recorded.  Here is an example.\n\nSuppose a simulation is run in the directory `./output`.\n\nAssume that setup commands have been run:\n\n```julia\nusing PowerSimulations\nimport PowerSimulations as PSI\n```\n\nNote that for all functions below you can optionally specify a function to filter events.\nThe function must accept the event type and return true or false.\n\n## Show all events of type PSI.SimulationStepEvent\n\n```julia\njulia> show_simulation_events(PSI.SimulationStepEvent, \"./output/aggregation/1\")\n┌─────────────────────┬─────────────────────┬──────┬────────┐\n│                name │     simulation_time │ step │ status │\n├─────────────────────┼─────────────────────┼──────┼────────┤\n│ SimulationStepEvent │ 2024-01-01T00:00:00 │    1 │  start │\n│ SimulationStepEvent │ 2024-01-01T23:00:00 │    1 │   done │\n│ SimulationStepEvent │ 2024-01-01T23:00:00 │    2 │  start │\n│ SimulationStepEvent │ 2024-01-02T23:00:00 │    2 │   done │\n└─────────────────────┴─────────────────────┴──────┴────────┘\n```\n\n## Show events of type PSI.ProblemExecutionEvent for a specific step and stage.\n\n```julia\nshow_simulation_events(\n    PSI.ProblemExecutionEvent,\n    \"./output/aggregation/1\",\n    x -> x.step == 1 && x.stage == 2 && x.status == \"start\"\n)\n┌──────────────────────┬─────────────────────┬──────┬───────┬────────┐\n│                 name │     simulation_time │ step │ stage │ status │\n├──────────────────────┼─────────────────────┼──────┼───────┼────────┤\n│ ProblemExecutionEvent │ 2024-01-01T00:00:00 │    1 │     2 │  start │\n│ ProblemExecutionEvent │ 2024-01-01T00:00:00 │    1 │     2 │  start │\n│ ProblemExecutionEvent │ 2024-01-01T01:00:00 │    1 │     2 │  start │\n│ ProblemExecutionEvent │ 2024-01-01T02:00:00 │    1 │     2 │  start │\n│ ProblemExecutionEvent │ 2024-01-01T03:00:00 │    1 │     2 │  start │\n│ ProblemExecutionEvent │ 2024-01-01T04:00:00 │    1 │     2 │  start │\n│ ProblemExecutionEvent │ 2024-01-01T05:00:00 │    1 │     2 │  start │\n│ ProblemExecutionEvent │ 2024-01-01T06:00:00 │    1 │     2 │  start │\n│ ProblemExecutionEvent │ 2024-01-01T07:00:00 │    1 │     2 │  start │\n│ ProblemExecutionEvent │ 2024-01-01T08:00:00 │    1 │     2 │  start │\n│ ProblemExecutionEvent │ 2024-01-01T09:00:00 │    1 │     2 │  start │\n│ ProblemExecutionEvent │ 2024-01-01T10:00:00 │    1 │     2 │  start │\n│ ProblemExecutionEvent │ 2024-01-01T11:00:00 │    1 │     2 │  start │\n│ ProblemExecutionEvent │ 2024-01-01T12:00:00 │    1 │     2 │  start │\n│ ProblemExecutionEvent │ 2024-01-01T13:00:00 │    1 │     2 │  start │\n│ ProblemExecutionEvent │ 2024-01-01T14:00:00 │    1 │     2 │  start │\n│ ProblemExecutionEvent │ 2024-01-01T15:00:00 │    1 │     2 │  start │\n│ ProblemExecutionEvent │ 2024-01-01T16:00:00 │    1 │     2 │  start │\n│ ProblemExecutionEvent │ 2024-01-01T17:00:00 │    1 │     2 │  start │\n│ ProblemExecutionEvent │ 2024-01-01T18:00:00 │    1 │     2 │  start │\n│ ProblemExecutionEvent │ 2024-01-01T19:00:00 │    1 │     2 │  start │\n│ ProblemExecutionEvent │ 2024-01-01T20:00:00 │    1 │     2 │  start │\n│ ProblemExecutionEvent │ 2024-01-01T21:00:00 │    1 │     2 │  start │\n│ ProblemExecutionEvent │ 2024-01-01T22:00:00 │    1 │     2 │  start │\n└──────────────────────┴─────────────────────┴──────┴───────┴────────┘\n```\n"
  },
  {
    "path": "docs/src/index.md",
    "content": "# PowerSimulations.jl\n\n```@meta\nCurrentModule = PowerSimulations\n```\n\n## Overview\n\n`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.\n\n`PowerSimulations.jl` supports the workflows to develop simulations by separating the development\nof operations models and simulation models.\n\n  - **Operation Models**: Optimization model used to find the solution of an operation problem.\n  - **Simulations Models**: Defined the requirements to find solutions to a sequence of operation problems in a way that resembles the procedures followed by operators.\n\nThe 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.\n\n`PowerSimulations.jl` is an active project under development, and we welcome your feedback, suggestions, and bug reports.\n\n## About Sienna\n\n`PowerSimulations.jl` is part of the National Renewable Energy Laboratory's\n[Sienna ecosystem](https://sienna-platform.github.io/Sienna/), an open source framework for\npower system modeling, simulation, and optimization. The Sienna ecosystem can be\n[found on Github](https://github.com/Sienna-Platform/Sienna). It contains three applications:\n\n  - [Sienna\\Data](https://sienna-platform.github.io/Sienna/pages/applications/sienna_data.html) enables\n    efficient data input, analysis, and transformation\n  - [Sienna\\Ops](https://sienna-platform.github.io/Sienna/pages/applications/sienna_ops.html) enables\n    enables system scheduling simulations by formulating and solving optimization problems\n  - [Sienna\\Dyn](https://sienna-platform.github.io/Sienna/pages/applications/sienna_dyn.html) enables\n    system transient analysis including small signal stability and full system dynamic\n    simulations\n\nEach application uses multiple packages in the [`Julia`](http://www.julialang.org)\nprogramming language.\n\n## Installation and Quick Links\n\n  - [Sienna installation page](https://sienna-platform.github.io/Sienna/SiennaDocs/docs/build/how-to/install/):\n    Instructions to install `PowerSimulations.jl` and other Sienna\\Ops packages\n  - [`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.\n  - [Sienna Documentation Hub](https://sienna-platform.github.io/Sienna/SiennaDocs/docs/build/index.html):\n    Links to other Sienna packages' documentation\n\n## How To Use This Documentation\n\nThere are five main sections containing different information:\n\n  - **Tutorials** - Detailed walk-throughs to help you *learn* how to use\n    `PowerSimulations.jl`\n  - **How to...** - Directions to help *guide* your work for a particular task\n  - **Explanation** - Additional details and background information to help you *understand*\n    `PowerSimulations.jl`, its structure, and how it works behind the scenes\n  - **Reference** - Technical references and API for a quick *look-up* during your work\n  - **Formulation Library** - Technical reference for the variables, parameters, and\n  equations that PowerSimulations.jl uses to define device behavior\n\n`PowerSimulations.jl` strives to follow the [Diataxis](https://diataxis.fr/) documentation\nframework.\n"
  },
  {
    "path": "docs/src/tutorials/decision_problem.jl",
    "content": "#!nb # ```@meta\n#!nb # EditURL = \"decision_problem.jl\"\n#!nb # ```\n#!nb #\n# # Running a Single-Step Problem\n#\n# ## Introduction\n#\n# `PowerSimulations.jl` supports the construction and solution of optimal power system\n# scheduling problems (Operations Problems). Operations problems form the fundamental\n# building blocks for sequential simulations. This example shows how to specify and customize\n# the mathematics that will be applied to the data with a [`ProblemTemplate`](@ref),\n# build and execute a [`DecisionModel`](@ref), and access the results.\n\nusing PowerSystems\nusing PowerSimulations\nusing HydroPowerSimulations\nusing PowerSystemCaseBuilder\nusing HiGHS # solver\nusing Dates\n\n# ## Data\n#\n# !!! note\n#\n#     [PowerSystemCaseBuilder.jl](https://github.com/Sienna-Platform/PowerSystemCaseBuilder.jl)\n#     is a helper library that makes it easier to reproduce examples in the documentation\n#     and tutorials. Normally you would pass your local files to create the system data\n#     instead of calling the function `build_system`.\n#     For more details visit\n#     [PowerSystemCaseBuilder Documentation](https://sienna-platform.github.io/PowerSystems.jl/stable/how_to/powersystembuilder/)\n\nsys = build_system(PSISystems, \"modified_RTS_GMLC_DA_sys\")\n\n# ## Define a problem specification with a `ProblemTemplate`\n#\n# You can create an empty template with:\n\ntemplate_uc = ProblemTemplate()\n\n# Now, you can add a [`DeviceModel`](@ref) for each device type to create an assignment\n# between PowerSystems device types and the subtypes of `AbstractDeviceFormulation`.\n# PowerSimulations has a variety of different `AbstractDeviceFormulation` subtypes\n# that can be applied to different PowerSystems device types, each dispatching to different\n# methods for populating optimization problem objectives, variables, and constraints.\n# Documentation on the formulation options for various devices can be found in the\n# [formulation library docs](https://sienna-platform.github.io/PowerSimulations.jl/latest/formulation_library/General/#formulation_library)\n\n# ### Branch Formulations\n#\n# Here is an example of relatively standard branch formulations. Other formulations allow\n# for selective enforcement of transmission limits and greater control on transformer settings.\n\nset_device_model!(template_uc, Line, StaticBranch)\nset_device_model!(template_uc, Transformer2W, StaticBranch)\nset_device_model!(template_uc, TapTransformer, StaticBranch)\n\n# ### Injection Device Formulations\n#\n# Here we define template entries for all devices that inject or withdraw power on the\n# network. For each device type, we can define a distinct `AbstractDeviceFormulation`. In\n# this case, we're defining a basic unit commitment model for thermal generators,\n# curtailable renewable generators, and fixed dispatch (net-load reduction) formulations\n# for `HydroDispatch` and `RenewableNonDispatch` devices.\n\nset_device_model!(template_uc, ThermalStandard, ThermalStandardUnitCommitment)\nset_device_model!(template_uc, RenewableDispatch, RenewableFullDispatch)\nset_device_model!(template_uc, PowerLoad, StaticPowerLoad)\nset_device_model!(template_uc, HydroDispatch, HydroDispatchRunOfRiver)\nset_device_model!(template_uc, RenewableNonDispatch, FixedOutput)\n\n# ### Service Formulations\n#\n# We have two `VariableReserve` types, parameterized by their direction. So, similar to\n# creating [`DeviceModel`](@ref)s, we can create [`ServiceModel`](@ref)s. The primary difference being\n# that [`DeviceModel`](@ref) objects define how constraints get created, while [`ServiceModel`](@ref) objects\n# define how constraints get modified.\n\nset_service_model!(template_uc, VariableReserve{ReserveUp}, RangeReserve)\nset_service_model!(template_uc, VariableReserve{ReserveDown}, RangeReserve)\n\n# ### Network Formulations\n#\n# Finally, we can define the transmission network specification that we'd like to model.\n# For simplicity, we'll choose a copper plate formulation. But there are dozens of\n# specifications available through an integration with\n# [PowerModels.jl](https://lanl-ansi.github.io/PowerModels.jl/stable/).\n#\n# *Note that many formulations will require appropriate data and may be computationally intractable*\n\nset_network_model!(template_uc, NetworkModel(CopperPlatePowerModel))\n\n# ## `DecisionModel`\n#\n# Now that we have a `System` and a [`ProblemTemplate`](@ref), we can put the two together\n# to create a [`DecisionModel`](@ref) that we solve.\n\n# ### Optimizer\n#\n# It's most convenient to define an optimizer instance upfront and pass it into the\n# [`DecisionModel`](@ref) constructor. For this example, we can use the free HiGHS solver\n# with a relatively relaxed MIP gap (`ratioGap`) setting to improve speed.\n\nsolver = optimizer_with_attributes(HiGHS.Optimizer, \"mip_rel_gap\" => 0.5)\n\n# ### Build a `DecisionModel`\n#\n# The construction of a [`DecisionModel`](@ref) essentially applies a [`ProblemTemplate`](@ref)\n# to `System` data to create a JuMP model.\n\nproblem = DecisionModel(template_uc, sys; optimizer = solver, horizon = Hour(24))\nbuild!(problem; output_dir = mktempdir())\n\n# !!! tip\n#\n#     The principal component of the [`DecisionModel`](@ref) is the JuMP model.\n#     But you can serialize to a file using the following command:\n#\n#     ```julia\n#     serialize_optimization_model(problem, save_path)\n#     ```\n#\n#     Keep in mind that if the setting `\"store_variable_names\"` is set to `False` then\n#     the file won't show the model's names.\n\n# ### Solve a `DecisionModel`\n\nsolve!(problem)\n\n# ## Results Inspection\n#\n# PowerSimulations collects the [`DecisionModel`](@ref) results into a\n# `OptimizationProblemResults` struct:\n\nres = OptimizationProblemResults(problem)\n\n# ### Optimizer Stats\n#\n# The optimizer summary is included\n\nget_optimizer_stats(res)\n\n# ### Objective Function Value\n\nget_objective_value(res)\n\n# ### Variable, Parameter, Auxiliary Variable, Dual, and Expression Values\n#\n# The solution value data frames for variables, parameters, auxiliary variables, duals, and\n# expressions can be accessed using the `read_` methods:\n\nread_variables(res)\n\n# Or, you can read a single parameter value for parameters that exist in the results.\n\nlist_parameter_names(res)\nread_parameter(res, \"ActivePowerTimeSeriesParameter__RenewableDispatch\")\n\n# ## Plotting\n#\n# Take a look at the plotting capabilities in\n# [PowerGraphics.jl](https://sienna-platform.github.io/PowerGraphics.jl/stable/)\n"
  },
  {
    "path": "docs/src/tutorials/pcm_simulation.jl",
    "content": "#!nb # ```@meta\n#!nb # EditURL = \"pcm_simulation.jl\"\n#!nb # ```\n#!nb #\n# # Running a Multi-Stage Production Cost Simulation\n#\n# ## Introduction\n#\n# PowerSimulations.jl supports simulations that consist of sequential optimization problems\n# where results from previous problems inform subsequent problems in a variety of ways. This\n# example demonstrates some of these capabilities to represent electricity market clearing.\n# This example is intended to be an extension of the tutorial on \n# [Running a Single-Step Problem](@ref).\n#\n# ### Load Packages\n\nusing PowerSystems\nusing PowerSimulations\nusing HydroPowerSimulations\nimport PowerSimulations as PSI\nusing PowerSystemCaseBuilder\nusing Dates\nusing HiGHS #solver\n\n# ### Optimizer\n#\n# It's most convenient to define an optimizer instance upfront and pass it into the\n# [`DecisionModel`](@ref) constructor. For this example, we can use the free HiGHS solver with a\n# relatively relaxed MIP gap (`ratioGap`) setting to improve speed.\n\nsolver = optimizer_with_attributes(HiGHS.Optimizer, \"mip_rel_gap\" => 0.5)\n\n# !!! note\n#\n#     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\n#\n#     ```julia\n#     using Gurobi\n#\n#     gurobi_env = Gurobi.Env()\n#\n#     solver = optimizer_with_attributes(() -> Gurobi.Optimizer(gurobi_env),\"MIPGap\" => 0.01)\n#     ```\n#\n#     Conversely, if a unique optimizer constructor is defined within the SimulationModels for each stage, a separate license will be obtained for each stage.\n#\n# ### Hourly day-ahead system\n#\n# First, we'll create a `System` with hourly data to represent day-ahead forecasted wind,\n# solar, and load profiles:\n\nsys_DA = build_system(PSISystems, \"modified_RTS_GMLC_DA_sys\"; skip_serialization = true)\n\n# ### 5-Minute system\n#\n# The RTS data also includes 5-minute resolution time series data. So, we can create another\n# `System` to represent 15 minute ahead forecasted data for a \"real-time\" market:\n\nsys_RT = build_system(PSISystems, \"modified_RTS_GMLC_RT_sys\"; skip_serialization = true)\n\n# ## `ProblemTemplate`s define stages\n#\n# Sequential simulations in PowerSimulations are created by defining `OperationsProblems`\n# that represent stages, and how information flows between executions of a stage and\n# between different stages.\n#\n# Let's start by defining a two stage simulation that might look like a typical day-Ahead\n# and real-time electricity market clearing process.\n#\n# ### Day-ahead unit commitment stage\n#\n# First, we can define a unit commitment template for the day ahead problem. We can use the\n# included UC template, but in this example, we'll replace the `ThermalBasicUnitCommitment`\n# with the slightly more complex `ThermalStandardUnitCommitment` for the thermal generators.\n\ntemplate_uc = template_unit_commitment()\nset_device_model!(template_uc, ThermalStandard, ThermalStandardUnitCommitment)\nset_device_model!(template_uc, HydroDispatch, HydroDispatchRunOfRiver)\n\n# ### Define the reference model for the real-time economic dispatch\n#\n# In addition to the manual specification process demonstrated in the OperationsProblem\n# example, PSI also provides pre-specified templates for some standard problems:\n\ntemplate_ed = template_economic_dispatch(;\n    network = NetworkModel(PTDFPowerModel; use_slacks = true),\n)\n\n# ### Define the `SimulationModels`\n#\n# [`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.\n\nmodels = SimulationModels(;\n    decision_models = [\n        DecisionModel(template_uc, sys_DA; optimizer = solver, name = \"UC\"),\n        DecisionModel(template_ed, sys_RT; optimizer = solver, name = \"ED\"),\n    ],\n)\n\n# ### `SimulationSequence`\n#\n# Similar to a `ProblemTemplate`, the `SimulationSequence` provides a template of\n# how to execute a sequential set of operations problems.\n#\n# Let's review some of the `SimulationSequence` arguments.\n#\n# ### Chronologies\n#\n# In PowerSimulations, chronologies define where information is flowing. There are two types\n# of chronologies.\n#\n#   - inter-stage chronologies: Define how information flows between stages. e.g. day-ahead solutions are used to inform economic dispatch problems\n#   - 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.\n#\n# ### `FeedForward`\n#\n# The definition of exactly what information is passed using the defined chronologies is\n# accomplished with `FeedForward`. Specifically, `FeedForward` is used\n# to define what to do with information being passed with an inter-stage chronology. Let's\n# define a `FeedForward` that affects the semi-continuous range constraints of thermal generators\n# in the economic dispatch problems based on the value of the unit-commitment variables.\n\nfeedforward = Dict(\n    \"ED\" => [\n        SemiContinuousFeedforward(;\n            component_type = ThermalStandard,\n            source = OnVariable,\n            affected_values = [ActivePowerVariable],\n        ),\n    ],\n)\n\n# ### Sequencing\n#\n# The stage problem length, look-ahead, and other details surrounding the temporal Sequencing\n# of stages are controlled using the structure of the time series data in the `System`s.\n# So, to define a typical day-ahead - real-time sequence:\n#\n#   - Day ahead problems should represent 48 hours, advancing 24 hours after each execution (24-hour look-ahead)\n#   - Real time problems should represent 1 hour (12 5-minute periods), advancing 15 min after each execution (15 min look-ahead)\n#\n# We can adjust the time series data to reflect this structure in each `System`:\n#\n#   - `transform_single_time_series!(sys_DA, Hour(48), Hour(24))`\n#   - `transform_single_time_series!(sys_RT, Minute(60), Minute(15))`\n#\n# Now we can put it all together to define a `SimulationSequence`\n\nDA_RT_sequence = SimulationSequence(;\n    models = models,\n    ini_cond_chronology = InterProblemChronology(),\n    feedforwards = feedforward,\n)\n\n# ## `Simulation`\n#\n# Now, we can build and execute a simulation using the `SimulationSequence` and `Stage`s\n# that we've defined.\n\npath = mkdir(joinpath(\".\", \"rts-store\")) #hide\nsim = Simulation(;\n    name = \"rts-test\",\n    steps = 2,\n    models = models,\n    sequence = DA_RT_sequence,\n    simulation_folder = joinpath(\".\", \"rts-store\"),\n)\n\n# ### Build simulation\n\nbuild!(sim)\n\n# ### Execute simulation\n#\n# the following command returns the status of the simulation (0: is proper execution) and\n# stores the results in a set of HDF5 files on disk.\n\nexecute!(sim; enable_progress_bar = false)\n\n# ## Results\n#\n# To access the results, we need to load the simulation result metadata and then make\n# requests to the specific data of interest. This allows you to efficiently access the\n# results of interest without overloading resources.\n\nresults = SimulationResults(sim);\nuc_results = get_decision_problem_results(results, \"UC\"); # UC stage result metadata\ned_results = get_decision_problem_results(results, \"ED\"); # ED stage result metadata\n\n# We can read all the result variables\n\nread_variables(uc_results)\n\n# or all the parameters\n\nread_parameters(uc_results)\n\n# We can just list the variable names contained in `uc_results`:\n\nlist_variable_names(uc_results)\n\n# and a number of parameters (this pattern also works for aux_variables, expressions, and duals)\n\nlist_parameter_names(uc_results)\n\n# Now we can read the specific results of interest for a specific problem, time window (optional),\n# and set of variables, duals, or parameters (optional)\n\nDict([\n    v => read_variable(uc_results, v) for v in [\n        \"ActivePowerVariable__RenewableDispatch\",\n        \"ActivePowerVariable__HydroDispatch\",\n        \"StopVariable__ThermalStandard\",\n    ]\n])\n\n# Or if we want the result of just one variable, parameter, or dual (must be defined in the\n# problem definition), we can use:\n\nread_parameter(\n    ed_results,\n    \"ActivePowerTimeSeriesParameter__RenewableNonDispatch\";\n    initial_time = DateTime(\"2020-01-01T06:00:00\"),\n    count = 5,\n)\n\n# !!! info\n#\n# note that this returns the results of each execution step in a separate dataframe\n# If you want the realized results (without lookahead periods), you can call `read_realized_*`:\n\nread_realized_variables(\n    uc_results,\n    [\"ActivePowerVariable__ThermalStandard\", \"ActivePowerVariable__RenewableDispatch\"],\n)\nrm(path; force = true, recursive = true) #hide\n\n# ## Plotting\n#\n# Take a look at the plotting capabilities in [PowerGraphics.jl](https://sienna-platform.github.io/PowerGraphics.jl/stable/)\n"
  },
  {
    "path": "scripts/formatter/Project.toml",
    "content": "uuid = \"c6367ca8-164d-4469-afe3-c91cf8860505\"\nauthors = [\"Jose Daniel Lara <jdlara@berkeley.edu>\"]\n\n[deps]\nJuliaFormatter = \"98e50ef6-434e-11e9-1051-2b60c6c9e899\"\nPkg = \"44cfe95a-1eb2-52ea-b672-e2afdf69b78f\"\n\n[compat]\nJuliaFormatter = \"1.0\"\njulia = \"^1.7\"\n"
  },
  {
    "path": "scripts/formatter/formatter_code.jl",
    "content": "using Pkg\nPkg.activate(@__DIR__)\nPkg.instantiate()\nPkg.update()\n\nusing JuliaFormatter\n\nmain_paths = [\".\"]\nfor main_path in main_paths\n    for (root, dir, files) in walkdir(main_path)\n        for f in files\n            @show file_path = abspath(root, f)\n            !((occursin(\".jl\", f) || occursin(\".md\", f))) && continue\n            format(file_path;\n                whitespace_ops_in_indices = true,\n                remove_extra_newlines = true,\n                verbose = true,\n                always_for_in = true,\n                whitespace_typedefs = true,\n                conditional_to_if = true,\n                join_lines_based_on_source = true,\n                separate_kwargs_with_semicolon = true,\n                format_markdown = true,\n                # extending_powersimulations causes format failures but is not current public\n                ignore = [\n                    \"README.md\",\n                    \"index.md\",\n                    \"extending_powersimulations.md\",\n                    \"simulation_recorder.md\",\n                    \"how_to/install.md\",\n                ],\n\n                # always_use_return = true. # Disabled since it throws a lot of false positives\n            )\n        end\n    end\nend\n"
  },
  {
    "path": "src/PowerSimulations.jl",
    "content": "isdefined(Base, :__precompile__) && __precompile__()\nmodule PowerSimulations\n\n#################################################################################\n# Exports\n\n# Base Models\nexport Simulation\nexport DecisionModel\nexport EmulationModel\nexport ProblemTemplate\nexport InitialCondition\nexport SimulationModels\nexport SimulationSequence\nexport SimulationResults\nexport SimulationPartitions\nexport SimulationPartitionResults\nexport TableFormat\n\n# Network Relevant Exports\nexport NetworkModel\nexport PTDFPowerModel\nexport CopperPlatePowerModel\nexport AreaBalancePowerModel\nexport AreaPTDFPowerModel\n\n# HVDC Network Relevant exports\nexport TransportHVDCNetworkModel\nexport VoltageDispatchHVDCNetworkModel\n\n######## Device Models ########\nexport DeviceModel\nexport FixedOutput\n\n####### Event Models ########\nexport EventModel\n\n######## Service Models ########\nexport ServiceModel\nexport RangeReserve\nexport RampReserve\nexport StepwiseCostReserve\nexport NonSpinningReserve\nexport PIDSmoothACE\nexport GroupReserve\nexport ConstantMaxInterfaceFlow\nexport VariableMaxInterfaceFlow\n\n######## Branch Models ########\nexport StaticBranch\nexport StaticBranchBounds\nexport StaticBranchUnbounded\nexport HVDCTwoTerminalLossless\nexport HVDCTwoTerminalDispatch\nexport HVDCTwoTerminalUnbounded\nexport HVDCTwoTerminalLCC\nexport PhaseAngleControl\nexport PTDFBranchFlow\n\n######## HVDC models ########\nexport LosslessConverter\nexport QuadraticLossConverter\nexport LosslessLine\nexport DCLossyLine\n######## Load Models ########\nexport StaticPowerLoad\nexport PowerLoadInterruption\nexport PowerLoadDispatch\nexport PowerLoadShift\n######## Renewable Formulations ########\nexport RenewableFullDispatch\nexport RenewableConstantPowerFactor\n\n######## Thermal Formulations ########\nexport ThermalStandardUnitCommitment\nexport ThermalBasicUnitCommitment\nexport ThermalBasicCompactUnitCommitment\nexport ThermalBasicDispatch\nexport ThermalStandardDispatch\nexport ThermalDispatchNoMin\nexport ThermalMultiStartUnitCommitment\nexport ThermalCompactUnitCommitment\nexport ThermalCompactDispatch\n\n###### Regulation Device Formulation #######\nexport DeviceLimitedRegulation\nexport ReserveLimitedRegulation\n\n###### Source Formulations ######\nexport ImportExportSourceModel\n\n###### SynCons Formulations ######\nexport SynchronousCondenserBasicDispatch\n\n# feedforward models\nexport UpperBoundFeedforward\nexport LowerBoundFeedforward\nexport SemiContinuousFeedforward\nexport FixValueFeedforward\n\n# InitialConditions chrons\nexport InterProblemChronology\nexport IntraProblemChronology\n\n# Initial Conditions Quantities\nexport DevicePower\nexport DeviceStatus\nexport InitialTimeDurationOn\nexport InitialTimeDurationOff\nexport InitialEnergyLevel\n\n# operation_models\nexport GenericOpProblem\nexport UnitCommitmentProblem\nexport EconomicDispatchProblem\n# export OptimalPowerFlow\n\n# Functions\nexport build!\n## Op Model Exports\nexport get_initial_conditions\nexport serialize_results\nexport serialize_optimization_model\n## Decision Model Export\nexport solve!\n## Emulation Model Exports\nexport run!\n## Sim Model Exports\nexport execute!\nexport get_simulation_model\nexport run_parallel_simulation\n## Template Exports\nexport template_economic_dispatch\nexport template_unit_commitment\nexport EconomicDispatchProblem\nexport UnitCommitmentProblem\nexport AGCReserveDeployment\nexport set_device_model!\nexport set_service_model!\nexport set_network_model!\nexport get_network_formulation\nexport get_hvdc_network_model\nexport set_hvdc_network_model!\n## Results interfaces\nexport SimulationResultsExport\nexport export_results\nexport export_realized_results\nexport export_optimizer_stats\nexport get_variable_values\nexport get_dual_values\nexport get_parameter_values\nexport get_aux_variable_values\nexport get_expression_values\nexport get_timestamps\nexport get_model_name\nexport get_decision_problem_results\nexport get_emulation_problem_results\nexport get_system\nexport get_system!\nexport set_system!\nexport list_variable_keys\nexport list_dual_keys\nexport list_parameter_keys\nexport list_aux_variable_keys\nexport list_expression_keys\nexport list_variable_names\nexport list_dual_names\nexport list_parameter_names\nexport list_aux_variable_names\nexport list_expression_names\nexport list_decision_problems\nexport list_supported_formats\nexport load_results!\nexport read_variable\nexport read_dual\nexport read_parameter\nexport read_aux_variable\nexport read_expression\nexport read_variables\nexport read_duals\nexport read_parameters\nexport read_aux_variables\nexport read_expressions\nexport read_realized_variable\nexport read_realized_dual\nexport read_realized_parameter\nexport read_realized_aux_variable\nexport read_realized_expression\nexport read_realized_variables\nexport read_realized_duals\nexport read_realized_parameters\nexport read_realized_aux_variables\nexport read_realized_expressions\nexport get_realized_timestamps\nexport get_problem_base_power\nexport get_objective_value\nexport read_optimizer_stats\nexport serialize_optimization_model\n\n## Utils Exports\nexport OptimizationProblemResults\nexport OptimizationProblemResultsExport\nexport OptimizerStats\nexport get_all_constraint_index\nexport get_all_variable_index\nexport get_constraint_index\nexport get_variable_index\nexport list_recorder_events\nexport show_recorder_events\nexport list_simulation_events\nexport show_simulation_events\nexport get_num_partitions\n\n# Variables\nexport ActivePowerVariable\nexport ActivePowerInVariable\nexport ActivePowerOutVariable\nexport HotStartVariable\nexport WarmStartVariable\nexport ColdStartVariable\nexport EnergyVariable\nexport LiftVariable\nexport OnVariable\nexport ReactivePowerVariable\nexport ReservationVariable\nexport ActivePowerReserveVariable\nexport ServiceRequirementVariable\nexport StartVariable\nexport StopVariable\nexport SteadyStateFrequencyDeviation\nexport AreaMismatchVariable\nexport DeltaActivePowerUpVariable\nexport DeltaActivePowerDownVariable\nexport AdditionalDeltaActivePowerUpVariable\nexport AdditionalDeltaActivePowerDownVariable\nexport SmoothACE\nexport SystemBalanceSlackUp\nexport SystemBalanceSlackDown\nexport ReserveRequirementSlack\nexport VoltageMagnitude\nexport VoltageAngle\nexport FlowActivePowerVariable\nexport FlowActivePowerSlackUpperBound\nexport FlowActivePowerSlackLowerBound\nexport FlowActivePowerFromToVariable\nexport FlowActivePowerToFromVariable\nexport FlowReactivePowerFromToVariable\nexport FlowReactivePowerToFromVariable\nexport PowerAboveMinimumVariable\nexport PhaseShifterAngle\nexport UpperBoundFeedForwardSlack\nexport LowerBoundFeedForwardSlack\nexport InterfaceFlowSlackUp\nexport InterfaceFlowSlackDown\nexport PiecewiseLinearCostVariable\nexport RateofChangeConstraintSlackUp\nexport RateofChangeConstraintSlackDown\nexport PostContingencyActivePowerChangeVariable\nexport PostContingencyActivePowerReserveDeploymentVariable\nexport DCVoltage\nexport DCLineCurrent\nexport ConverterPowerDirection\nexport ConverterCurrent\nexport SquaredConverterCurrent\nexport InterpolationSquaredCurrentVariable\nexport InterpolationBinarySquaredCurrentVariable\nexport ConverterPositiveCurrent\nexport ConverterNegativeCurrent\nexport SquaredDCVoltage\nexport InterpolationSquaredVoltageVariable\nexport InterpolationBinarySquaredVoltageVariable\nexport AuxBilinearConverterVariable\nexport AuxBilinearSquaredConverterVariable\nexport InterpolationSquaredBilinearVariable\nexport InterpolationBinarySquaredBilinearVariable\nexport ShiftUpActivePowerVariable\nexport ShiftDownActivePowerVariable\n\n# Auxiliary variables\nexport TimeDurationOn\nexport TimeDurationOff\nexport PowerOutput\nexport PowerFlowVoltageAngle\nexport PowerFlowVoltageMagnitude\nexport PowerFlowBranchReactivePowerFromTo, PowerFlowBranchReactivePowerToFrom\nexport PowerFlowBranchActivePowerFromTo, PowerFlowBranchActivePowerToFrom\nexport PowerFlowBranchActivePowerLoss\nexport PowerFlowLossFactors\nexport PowerFlowVoltageStabilityFactors\n\n# Constraints\nexport AbsoluteValueConstraint\nexport ActivePowerVariableTimeSeriesLimitsConstraint\nexport LineFlowBoundConstraint\nexport ActivePowerVariableLimitsConstraint\nexport ActivePowerInVariableTimeSeriesLimitsConstraint\nexport ActivePowerOutVariableTimeSeriesLimitsConstraint\nexport ActiveRangeICConstraint\nexport AreaParticipationAssignmentConstraint\nexport BalanceAuxConstraint\nexport CommitmentConstraint\nexport CopperPlateBalanceConstraint\nexport DurationConstraint\nexport EnergyBalanceConstraint\nexport EqualityConstraint\nexport FeedforwardSemiContinuousConstraint\nexport FeedforwardUpperBoundConstraint\nexport FeedforwardLowerBoundConstraint\nexport FeedforwardIntegralLimitConstraint\nexport FlowLimitConstraint\nexport FlowLimitFromToConstraint\nexport FlowLimitToFromConstraint\nexport FrequencyResponseConstraint\nexport HVDCPowerBalance\nexport HVDCLosses\nexport HVDCFlowDirectionVariable\nexport InputActivePowerVariableLimitsConstraint\nexport InterfaceFlowLimit\nexport NetworkFlowConstraint\nexport NodalBalanceActiveConstraint\nexport NodalBalanceReactiveConstraint\nexport OutputActivePowerVariableLimitsConstraint\nexport PiecewiseLinearCostConstraint\nexport ParticipationAssignmentConstraint\nexport ParticipationFractionConstraint\nexport PhaseAngleControlLimit\nexport RampConstraint\nexport RampLimitConstraint\nexport RangeLimitConstraint\nexport FlowRateConstraint\nexport FlowRateConstraintFromTo\nexport FlowRateConstraintToFrom\nexport PostContingencyEmergencyRateLimitConstraint\nexport ReactivePowerVariableLimitsConstraint\nexport RegulationLimitsConstraint\nexport RequirementConstraint\nexport ReserveEnergyCoverageConstraint\nexport ReservePowerConstraint\nexport SACEPIDAreaConstraint\nexport StartTypeConstraint\nexport StartupInitialConditionConstraint\nexport StartupTimeLimitTemperatureConstraint\nexport PostContingencyActivePowerVariableLimitsConstraint\nexport PostContingencyActivePowerReserveDeploymentVariableLimitsConstraint\nexport PostContingencyGenerationBalanceConstraint\nexport PostContingencyRampConstraint\nexport ImportExportBudgetConstraint\nexport PiecewiseLinearBlockIncrementalOfferConstraint\nexport PiecewiseLinearBlockDecrementalOfferConstraint\nexport NodalBalanceCurrentConstraint\nexport DCLineCurrentConstraint\nexport ConverterPowerCalculationConstraint\nexport ConverterMcCormickEnvelopes\nexport InterpolationVoltageConstraints\nexport InterpolationCurrentConstraints\nexport InterpolationBilinearConstraints\nexport ConverterLossConstraint\nexport CurrentAbsoluteValueConstraint\nexport ShiftedActivePowerBalanceConstraint\nexport ShiftUpActivePowerVariableLimitsConstraint\nexport ShiftDownActivePowerVariableLimitsConstraint\nexport RealizedShiftedLoadMinimumBoundConstraint\nexport NonAnticipativityConstraint\n\n# Parameters\n# Time Series Parameters\nexport ActivePowerTimeSeriesParameter\nexport ActivePowerOutTimeSeriesParameter\nexport ActivePowerInTimeSeriesParameter\nexport ReactivePowerTimeSeriesParameter\nexport DynamicBranchRatingTimeSeriesParameter\nexport FuelCostParameter\nexport PostContingencyDynamicBranchRatingTimeSeriesParameter\nexport RequirementTimeSeriesParameter\nexport FromToFlowLimitParameter\nexport ToFromFlowLimitParameter\n\n# Cost Parameters\nexport CostFunctionParameter\n\n# Feedforward Parameters\nexport OnStatusParameter\nexport UpperBoundValueParameter\nexport LowerBoundValueParameter\nexport FixValueParameter\n\n# Event Parameters\nexport AvailableStatusParameter\nexport AvailableStatusChangeCountdownParameter\nexport ActivePowerOffsetParameter\nexport ReactivePowerOffsetParameter\n\n# Expressions\nexport SystemBalanceExpressions\nexport RangeConstraintLBExpressions\nexport RangeConstraintUBExpressions\nexport CostExpressions\nexport ConstituentCostExpression\nexport ActivePowerBalance\nexport ReactivePowerBalance\nexport EmergencyUp\nexport EmergencyDown\nexport RawACE\nexport ProductionCostExpression\nexport FuelCostExpression\nexport StartUpCostExpression\nexport ShutDownCostExpression\nexport FixedCostExpression\nexport VOMCostExpression\nexport CurtailmentCostExpression\nexport FuelConsumptionExpression\nexport ActivePowerRangeExpressionLB\nexport ActivePowerRangeExpressionUB\nexport PostContingencyBranchFlow\nexport PostContingencyActivePowerGeneration\nexport PostContingencyActivePowerBalance\nexport NetActivePower\nexport DCCurrentBalance\n\n#################################################################################\n# Imports\nimport DataStructures: OrderedDict, Deque, SortedDict\nimport Logging\nimport Serialization\n# Modeling Imports\nimport JuMP\n# so that users do not need to import JuMP to use a solver with PowerModels\nimport JuMP: optimizer_with_attributes\nimport JuMP.Containers: DenseAxisArray, SparseAxisArray\nexport optimizer_with_attributes\nimport MathOptInterface as MOI\nimport LinearAlgebra\nimport SparseArrays\nimport JSON3\nimport PowerSystems as PSY\nimport InfrastructureSystems as IS\nimport PowerFlows\nimport PowerNetworkMatrices as PNM\nimport PowerNetworkMatrices: PTDF, VirtualPTDF, LODF, VirtualLODF\nexport PTDF\nexport VirtualPTDF\nexport LODF\nexport VirtualLODF\nimport InfrastructureSystems: @assert_op, TableFormat, list_recorder_events, get_name\n\n# IS.Optimization imports: functions that have PSY methods that IS needs to access (therefore necessary)\nimport InfrastructureSystems.Optimization: get_data_field\n\n# IS.Optimization imports that get reexported: no additional methods in PowerSimulations (therefore necessary)\nimport InfrastructureSystems.Optimization:\n    OptimizationProblemResults, OptimizationProblemResultsExport, OptimizerStats\nimport InfrastructureSystems.Optimization:\n    read_variables, read_duals, read_parameters, read_aux_variables, read_expressions\nimport InfrastructureSystems.Optimization: get_variable_values, get_dual_values,\n    get_parameter_values, get_aux_variable_values, get_expression_values, get_value\nimport InfrastructureSystems.Optimization:\n    get_objective_value, export_realized_results, export_optimizer_stats\n\n# IS.Optimization imports that get reexported: yes additional methods in PowerSimulations (therefore may or may not be desired)\nimport InfrastructureSystems.Optimization:\n    read_variable, read_dual, read_parameter, read_aux_variable, read_expression\nimport InfrastructureSystems.Optimization: list_variable_keys, list_dual_keys,\n    list_parameter_keys, list_aux_variable_keys, list_expression_keys\nimport InfrastructureSystems.Optimization: list_variable_names, list_dual_names,\n    list_parameter_names, list_aux_variable_names, list_expression_names\nimport InfrastructureSystems.Optimization: read_optimizer_stats, get_optimizer_stats,\n    export_results, serialize_results, get_timestamps, get_model_base_power\nimport InfrastructureSystems.Optimization: get_resolution, get_forecast_horizon\n\n# IS.Optimization imports that stay private, may or may not be additional methods in PowerSimulations\nimport InfrastructureSystems.Optimization: ArgumentConstructStage, ModelConstructStage\nimport InfrastructureSystems.Optimization: STORE_CONTAINERS, STORE_CONTAINER_DUALS,\n    STORE_CONTAINER_EXPRESSIONS, STORE_CONTAINER_PARAMETERS, STORE_CONTAINER_VARIABLES,\n    STORE_CONTAINER_AUX_VARIABLES\nimport InfrastructureSystems.Optimization: OptimizationContainerKey, VariableKey,\n    ConstraintKey, ExpressionKey, AuxVarKey, InitialConditionKey, ParameterKey\nimport InfrastructureSystems.Optimization:\n    RightHandSideParameter, ObjectiveFunctionParameter, TimeSeriesParameter\nimport InfrastructureSystems.Optimization: VariableType, ConstraintType, AuxVariableType,\n    ParameterType, InitialConditionType, ExpressionType\nimport InfrastructureSystems.Optimization: should_export_variable, should_export_dual,\n    should_export_parameter, should_export_aux_variable, should_export_expression\nimport InfrastructureSystems.Optimization:\n    get_entry_type, get_component_type, get_output_dir\nimport InfrastructureSystems.Optimization: read_results_with_keys, deserialize_key,\n    encode_key_as_string, encode_keys_as_strings, should_write_resulting_value,\n    convert_result_to_natural_units, to_matrix, get_store_container_type\nimport InfrastructureSystems.Optimization: get_source_data\n\n# IS.Optimization imports that stay private, may or may not be additional methods in PowerSimulations\n\n# PowerSystems imports\nimport PowerSystems:\n    get_components, get_component, get_available_components, get_available_component,\n    get_groups, get_available_groups\nimport PowerSystems: StartUpStages\n\nexport get_name\nexport get_model_base_power\nexport get_optimizer_stats\nexport get_timestamps\nexport get_resolution\n\nimport PowerModels as PM\nimport TimerOutputs\nimport ProgressMeter\nimport Distributed\nimport Distributions: Bernoulli, Geometric\nimport Random\nimport Random: AbstractRNG, rand\n\n# Base Imports\nimport Base.getindex\nimport Base.isempty\nimport Base.length\nimport Base.first\nimport InteractiveUtils: methodswith\n\n# TimeStamp Management Imports\nimport Dates\nimport TimeSeries\n\n# I/O Imports\nimport DataFrames\nimport DataFrames: DataFrame, DataFrameRow, Not, innerjoin\nimport DataFramesMeta: @chain, @orderby, @rename, @select, @subset, @transform\nimport CSV\nimport HDF5\nimport PrettyTables\n\n# PowerModels exports\nexport ACPPowerModel\nexport ACRPowerModel\nexport ACTPowerModel\nexport DCPPowerModel\nexport NFAPowerModel\nexport DCPLLPowerModel\nexport LPACCPowerModel\nexport SOCWRPowerModel\nexport SOCWRConicPowerModel\nexport QCRMPowerModel\nexport QCLSPowerModel\n\nexport process_simulation_partition_cli_args\n\n################################################################################\n\n# Type Alias From other Packages\nconst PSI = PowerSimulations\nconst ISOPT = IS.Optimization\nconst MOIU = MOI.Utilities\nconst MOPFM = MOI.FileFormats.Model\nconst PFS = PowerFlows\nconst TS = TimeSeries\n\n################################################################################\n\nfunction progress_meter_enabled()\n    return isa(stderr, Base.TTY) &&\n           (get(ENV, \"CI\", nothing) != \"true\") &&\n           (get(ENV, \"RUNNING_PSI_TESTS\", nothing) != \"true\")\nend\n\nusing DocStringExtensions\n\n@template DEFAULT = \"\"\"\n                    $(TYPEDSIGNATURES)\n                    $(DOCSTRING)\n                    \"\"\"\n# Includes\ninclude(\"core/definitions.jl\")\n\n# Core components\ninclude(\"core/formulations.jl\")\ninclude(\"core/network_formulations.jl\")\ninclude(\"core/abstract_simulation_store.jl\")\ninclude(\"core/operation_model_abstract_types.jl\")\ninclude(\"core/abstract_feedforward.jl\")\ninclude(\"core/variables.jl\")\ninclude(\"core/network_reductions.jl\")\ninclude(\"core/parameters.jl\")\ninclude(\"core/service_model.jl\")\ninclude(\"core/event_keys.jl\")\ninclude(\"core/event_model.jl\")\ninclude(\"core/device_model.jl\")\ninclude(\"core/network_model.jl\")\ninclude(\"core/auxiliary_variables.jl\")\ninclude(\"core/constraints.jl\")\ninclude(\"core/expressions.jl\")\ninclude(\"core/initial_conditions.jl\")\ninclude(\"core/settings.jl\")\ninclude(\"core/cache_utils.jl\")\ninclude(\"core/dataset.jl\")\ninclude(\"core/dataset_container.jl\")\ninclude(\"core/results_by_time.jl\")\n\n# Order Required\ninclude(\"operation/problem_template.jl\")\ninclude(\"core/power_flow_data_wrapper.jl\")\ninclude(\"core/optimization_container.jl\")\ninclude(\"core/dual_processing.jl\")\ninclude(\"core/store_common.jl\")\ninclude(\"initial_conditions/initial_condition_chronologies.jl\")\ninclude(\"operation/operation_model_interface.jl\")\ninclude(\"core/model_store_params.jl\")\ninclude(\"simulation/simulation_store_requirements.jl\")\ninclude(\"operation/decision_model_store.jl\")\ninclude(\"operation/emulation_model_store.jl\")\ninclude(\"operation/initial_conditions_update_in_memory_store.jl\")\ninclude(\"simulation/simulation_info.jl\")\ninclude(\"operation/operation_model_types.jl\")\ninclude(\"operation/template_validation.jl\")\ninclude(\"operation/decision_model.jl\")\ninclude(\"operation/emulation_model.jl\")\ninclude(\"operation/problem_results.jl\")\ninclude(\"operation/time_series_interface.jl\")\ninclude(\"operation/optimization_debugging.jl\")\ninclude(\"operation/model_numerical_analysis_utils.jl\")\n\ninclude(\"initial_conditions/add_initial_condition.jl\")\ninclude(\"initial_conditions/update_initial_conditions.jl\")\ninclude(\"initial_conditions/calculate_initial_condition.jl\")\n\ninclude(\"feedforward/feedforwards.jl\")\ninclude(\"feedforward/feedforward_arguments.jl\")\ninclude(\"feedforward/feedforward_constraints.jl\")\n\ninclude(\"contingency_model/contingency.jl\")\ninclude(\"contingency_model/contingency_arguments.jl\")\ninclude(\"contingency_model/contingency_constraints.jl\")\n\ninclude(\"parameters/add_parameters.jl\")\n\ninclude(\"simulation/optimization_output_cache.jl\")\ninclude(\"simulation/optimization_output_caches.jl\")\ninclude(\"simulation/simulation_models.jl\")\ninclude(\"simulation/simulation_state.jl\")\ninclude(\"simulation/initial_condition_update_simulation.jl\")\ninclude(\"simulation/simulation_store_params.jl\")\ninclude(\"simulation/hdf_simulation_store.jl\")\ninclude(\"simulation/in_memory_simulation_store.jl\")\ninclude(\"simulation/simulation_problem_results.jl\")\ninclude(\"simulation/get_components_interface.jl\")\ninclude(\"simulation/decision_model_simulation_results.jl\")\ninclude(\"simulation/emulation_model_simulation_results.jl\")\ninclude(\"simulation/realized_meta.jl\")\ninclude(\"simulation/simulation_partitions.jl\")\ninclude(\"simulation/simulation_partition_results.jl\")\ninclude(\"simulation/simulation_sequence.jl\")\ninclude(\"simulation/simulation_internal.jl\")\ninclude(\"simulation/simulation.jl\")\ninclude(\"simulation/simulation_events.jl\")\ninclude(\"simulation/simulation_results_export.jl\")\ninclude(\"simulation/simulation_results.jl\")\ninclude(\"operation/operation_model_simulation_interface.jl\")\ninclude(\"parameters/update_container_parameter_values.jl\")\ninclude(\"parameters/update_cost_parameters.jl\")\ninclude(\"parameters/update_parameters.jl\")\n\ninclude(\"devices_models/devices/common/objective_function/common.jl\")\ninclude(\"devices_models/devices/common/objective_function/linear_curve.jl\")\ninclude(\"devices_models/devices/common/objective_function/quadratic_curve.jl\")\ninclude(\"devices_models/devices/common/objective_function/market_bid.jl\")\ninclude(\"devices_models/devices/common/objective_function/piecewise_linear.jl\")\ninclude(\"devices_models/devices/common/objective_function/import_export.jl\")\ninclude(\"devices_models/devices/common/range_constraint.jl\")\ninclude(\"devices_models/devices/common/add_variable.jl\")\ninclude(\"devices_models/devices/common/add_auxiliary_variable.jl\")\ninclude(\"devices_models/devices/common/add_constraint_dual.jl\")\ninclude(\"devices_models/devices/common/rateofchange_constraints.jl\")\ninclude(\"devices_models/devices/common/duration_constraints.jl\")\ninclude(\"devices_models/devices/common/get_time_series.jl\")\ninclude(\"devices_models/devices/common/add_pwl_methods.jl\")\n\n# Device Modeling components\ninclude(\"devices_models/devices/default_interface_methods.jl\")\ninclude(\"devices_models/devices/common/add_to_expression.jl\")\ninclude(\"devices_models/devices/common/set_expression.jl\")\ninclude(\"devices_models/devices/renewable_generation.jl\")\ninclude(\"devices_models/devices/thermal_generation.jl\")\ninclude(\"devices_models/devices/electric_loads.jl\")\ninclude(\"devices_models/devices/AC_branches.jl\")\ninclude(\"devices_models/devices/area_interchange.jl\")\ninclude(\"devices_models/devices/TwoTerminalDC_branches.jl\")\ninclude(\"devices_models/devices/HVDCsystems.jl\")\ninclude(\"devices_models/devices/source.jl\")\ninclude(\"devices_models/devices/reactivepower_device.jl\")\n#include(\"devices_models/devices/regulation_device.jl\")\n\n# Services Models\n#include(\"services_models/agc.jl\")\ninclude(\"services_models/reserves.jl\")\ninclude(\"services_models/reserve_group.jl\")\ninclude(\"services_models/transmission_interface.jl\")\ninclude(\"services_models/service_slacks.jl\")\ninclude(\"services_models/services_constructor.jl\")\n\n# Network models\ninclude(\"network_models/copperplate_model.jl\")\ninclude(\"network_models/powermodels_interface.jl\")\ninclude(\"network_models/pm_translator.jl\")\ninclude(\"network_models/network_slack_variables.jl\")\ninclude(\"network_models/area_balance_model.jl\")\ninclude(\"network_models/hvdc_networks.jl\")\ninclude(\"network_models/power_flow_evaluation.jl\")\n\ninclude(\"initial_conditions/initialization.jl\")\n\n# Device constructors\ninclude(\"devices_models/device_constructors/thermalgeneration_constructor.jl\")\ninclude(\"devices_models/device_constructors/hvdcsystems_constructor.jl\")\ninclude(\"devices_models/device_constructors/branch_constructor.jl\")\ninclude(\"devices_models/device_constructors/renewablegeneration_constructor.jl\")\ninclude(\"devices_models/device_constructors/load_constructor.jl\")\ninclude(\"devices_models/device_constructors/source_constructor.jl\")\ninclude(\"devices_models/device_constructors/reactivepowerdevice_constructor.jl\")\n#include(\"devices_models/device_constructors/regulationdevice_constructor.jl\")\n\n# Network constructors\ninclude(\"network_models/network_constructor.jl\")\ninclude(\"network_models/hvdc_network_constructor.jl\")\n\n# Templates for Operation Problems\ninclude(\"operation/operation_problem_templates.jl\")\n\n# Utils\ninclude(\"utils/indexing.jl\")\n@static if pkgversion(PrettyTables).major == 2\n    # When PrettyTables v3 is more widely adopted in the ecosystem, we can remove this file.\n    # In this case, we should also update the compat bounds in Project.toml to list only\n    # PrettyTables v3.\n    include(\"utils/print_pt_v2.jl\")\nelse\n    include(\"utils/print_pt_v3.jl\")\nend\ninclude(\"utils/file_utils.jl\")\ninclude(\"utils/logging.jl\")\ninclude(\"utils/dataframes_utils.jl\")\ninclude(\"utils/jump_utils.jl\")\ninclude(\"utils/powersystems_utils.jl\")\ninclude(\"utils/time_series_utils.jl\")\ninclude(\"utils/recorder_events.jl\")\ninclude(\"utils/datetime_utils.jl\")\ninclude(\"utils/generate_valid_formulations.jl\")\n\nend\n"
  },
  {
    "path": "src/contingency_model/contingency.jl",
    "content": "#! format: off\n# This value could change depending on the event modeling choices\nget_parameter_multiplier(::EventParameter, ::PSY.Device, ::EventModel) = 1.0\nget_initial_parameter_value(::ActivePowerOffsetParameter, ::PSY.Device, ::EventModel) = 0.0\nget_initial_parameter_value(::ReactivePowerOffsetParameter, ::PSY.Device, ::EventModel) = 0.0\nget_initial_parameter_value(::AvailableStatusChangeCountdownParameter, ::PSY.Device, ::EventModel) = 0.0\nget_initial_parameter_value(::AvailableStatusParameter, ::PSY.Device, ::EventModel) = 1.0\n\nsupports_outages(::Type{T}) where {T <: PSY.StaticInjection} = false\nsupports_outages(::Type{T}) where {T <: PSY.ThermalStandard} = true\nsupports_outages(::Type{T}) where {T <: PSY.RenewableGen} = true\nsupports_outages(::Type{T}) where {T <: PSY.ElectricLoad} = true\nsupports_outages(::Type{T}) where {T <: PSY.Storage} = true\nsupports_outages(::Type{T}) where {T <: PSY.HydroGen} = true\n#! format: on\n"
  },
  {
    "path": "src/contingency_model/contingency_arguments.jl",
    "content": "function add_event_arguments!(\n    container::OptimizationContainer,\n    devices::T,\n    device_model::DeviceModel{U, V},\n    network_model::NetworkModel,\n) where {\n    T <: Union{Vector{U}, IS.FlattenIteratorWrapper{U}},\n    V <: AbstractDeviceFormulation,\n} where {U <: PSY.StaticInjection}\n    for (key, event_model) in get_events(device_model)\n        event_type = get_entry_type(key)\n        devices_with_attributes =\n            [d for d in devices if PSY.has_supplemental_attributes(d, event_type)]\n        isempty(devices_with_attributes) &&\n            error(\"no devices found with a supplemental attribute for event $event_type\")\n        for p_type in [AvailableStatusChangeCountdownParameter, AvailableStatusParameter]\n            add_parameters!(\n                container,\n                p_type,\n                devices_with_attributes,\n                device_model,\n                event_model,\n            )\n        end\n    end\n\n    return\nend\n\nfunction add_event_arguments!(\n    container::OptimizationContainer,\n    devices::T,\n    device_model::DeviceModel{U, V},\n    network_model::NetworkModel{<:PM.AbstractActivePowerModel},\n) where {\n    T <: Union{Vector{U}, IS.FlattenIteratorWrapper{U}},\n    V <: Union{StaticPowerLoad, PowerLoadDispatch, PowerLoadInterruption},\n} where {U <: PSY.PowerLoad}\n    for (key, event_model) in get_events(device_model)\n        event_type = get_entry_type(key)\n        devices_with_attributes =\n            [d for d in devices if PSY.has_supplemental_attributes(d, event_type)]\n        isempty(devices_with_attributes) &&\n            error(\"no devices found with a supplemental attribute for event $event_type\")\n        for p_type in [AvailableStatusChangeCountdownParameter, AvailableStatusParameter]\n            add_parameters!(\n                container,\n                p_type,\n                devices_with_attributes,\n                device_model,\n                event_model,\n            )\n        end\n        add_parameters!(\n            container,\n            ActivePowerOffsetParameter,\n            devices_with_attributes,\n            device_model,\n            event_model,\n        )\n        add_to_expression!(\n            container,\n            ActivePowerBalance,\n            ActivePowerOffsetParameter,\n            devices_with_attributes,\n            device_model,\n            network_model,\n        )\n    end\n\n    return\nend\n\nfunction add_event_arguments!(\n    container::OptimizationContainer,\n    devices::T,\n    device_model::DeviceModel{U, V},\n    network_model::NetworkModel{<:PM.AbstractPowerModel},\n) where {\n    T <: Union{Vector{U}, IS.FlattenIteratorWrapper{U}},\n    V <: Union{StaticPowerLoad, PowerLoadDispatch, PowerLoadInterruption},\n} where {U <: PSY.PowerLoad}\n    for (key, event_model) in get_events(device_model)\n        event_type = get_entry_type(key)\n        devices_with_attributes =\n            [d for d in devices if PSY.has_supplemental_attributes(d, event_type)]\n        isempty(devices_with_attributes) &&\n            error(\"no devices found with a supplemental attribute for event $event_type\")\n        for p_type in [AvailableStatusChangeCountdownParameter, AvailableStatusParameter]\n            add_parameters!(\n                container,\n                p_type,\n                devices_with_attributes,\n                device_model,\n                event_model,\n            )\n        end\n        add_parameters!(\n            container,\n            ActivePowerOffsetParameter,\n            devices_with_attributes,\n            device_model,\n            event_model,\n        )\n        add_to_expression!(\n            container,\n            ActivePowerBalance,\n            ActivePowerOffsetParameter,\n            devices_with_attributes,\n            device_model,\n            network_model,\n        )\n        add_parameters!(\n            container,\n            ReactivePowerOffsetParameter,\n            devices_with_attributes,\n            device_model,\n            event_model,\n        )\n        add_to_expression!(\n            container,\n            ReactivePowerBalance,\n            ReactivePowerOffsetParameter,\n            devices_with_attributes,\n            device_model,\n            network_model,\n        )\n    end\n\n    return\nend\n\nfunction add_event_arguments!(\n    container::OptimizationContainer,\n    devices::T,\n    device_model::DeviceModel{U, FixedOutput},\n    network_model::NetworkModel{<:PM.AbstractActivePowerModel},\n) where {\n    T <: Union{Vector{U}, IS.FlattenIteratorWrapper{U}},\n} where {U <: PSY.StaticInjection}\n    for (key, event_model) in get_events(device_model)\n        event_type = get_entry_type(key)\n        devices_with_attributes =\n            [d for d in devices if PSY.has_supplemental_attributes(d, event_type)]\n        isempty(devices_with_attributes) &&\n            error(\"no devices found with a supplemental attribute for event $event_type\")\n        for p_type in [AvailableStatusChangeCountdownParameter, AvailableStatusParameter]\n            add_parameters!(\n                container,\n                p_type,\n                devices_with_attributes,\n                device_model,\n                event_model,\n            )\n        end\n        add_parameters!(\n            container,\n            ActivePowerOffsetParameter,\n            devices_with_attributes,\n            device_model,\n            event_model,\n        )\n        add_to_expression!(\n            container,\n            ActivePowerBalance,\n            ActivePowerOffsetParameter,\n            devices_with_attributes,\n            device_model,\n            network_model,\n        )\n    end\n    return\nend\n\nfunction add_event_arguments!(\n    container::OptimizationContainer,\n    devices::T,\n    device_model::DeviceModel{U, FixedOutput},\n    network_model::NetworkModel{<:PM.AbstractPowerModel},\n) where {\n    T <: Union{Vector{U}, IS.FlattenIteratorWrapper{U}},\n} where {U <: PSY.StaticInjection}\n    for (key, event_model) in get_events(device_model)\n        event_type = get_entry_type(key)\n        devices_with_attributes =\n            [d for d in devices if PSY.has_supplemental_attributes(d, event_type)]\n        isempty(devices_with_attributes) &&\n            error(\"no devices found with a supplemental attribute for event $event_type\")\n        for p_type in [AvailableStatusChangeCountdownParameter, AvailableStatusParameter]\n            add_parameters!(\n                container,\n                p_type,\n                devices_with_attributes,\n                device_model,\n                event_model,\n            )\n        end\n        add_parameters!(\n            container,\n            ActivePowerOffsetParameter,\n            devices_with_attributes,\n            device_model,\n            event_model,\n        )\n        add_to_expression!(\n            container,\n            ActivePowerBalance,\n            ActivePowerOffsetParameter,\n            devices_with_attributes,\n            device_model,\n            network_model,\n        )\n        add_parameters!(\n            container,\n            ReactivePowerOffsetParameter,\n            devices_with_attributes,\n            device_model,\n            event_model,\n        )\n        add_to_expression!(\n            container,\n            ReactivePowerBalance,\n            ReactivePowerOffsetParameter,\n            devices_with_attributes,\n            device_model,\n            network_model,\n        )\n    end\n    return\nend\n\nfunction _add_parameters!(\n    container,\n    ::T,\n    devices::Vector{U},\n    device_model::DeviceModel{U, W},\n    event_model::EventModel{V, X},\n) where {\n    T <: EventParameter,\n    U <: PSY.Component,\n    V <: PSY.Contingency,\n    W <: AbstractDeviceFormulation,\n    X <: AbstractEventCondition,\n}\n    @debug \"adding\" T U V _group = LOG_GROUP_OPTIMIZATION_CONTAINER\n    time_steps = get_time_steps(container)\n    parameter_container = add_param_container!(\n        container,\n        T(),\n        U,\n        V,\n        PSY.get_name.(devices),\n        time_steps,\n    )\n\n    jump_model = get_jump_model(container)\n    parent_mult = get_multiplier_array_data(parameter_container)\n    parent_param = get_parameter_array_data(parameter_container)\n\n    for (i, d) in enumerate(devices)\n        ini_val = get_initial_parameter_value(T(), d, event_model)\n        name = PSY.get_name(d)\n        _set_multiplier_at!(\n            parent_mult,\n            get_parameter_multiplier(T(), d, event_model),\n            i,\n        )\n        for t in time_steps\n            _set_parameter_at!(parent_param, jump_model, ini_val, i, t)\n        end\n    end\n    return\nend\n\n\"\"\"\nDefault implementation to add parameters to SystemBalanceExpressions\n\"\"\"\nfunction add_to_expression!(\n    container::OptimizationContainer,\n    ::Type{T},\n    ::Type{U},\n    devices::Union{Vector{V}, IS.FlattenIteratorWrapper{V}},\n    device_model::DeviceModel{V, W},\n    network_model::NetworkModel{X},\n) where {\n    T <: SystemBalanceExpressions,\n    U <: EventParameter,\n    V <: PSY.StaticInjection,\n    W <: AbstractDeviceFormulation,\n    X <: CopperPlatePowerModel,\n}\n    param_container = get_parameter(container, U(), V)\n    multiplier = get_multiplier_array(param_container)\n    expression = get_expression(container, T(), PSY.System)\n    for d in devices\n        device_bus = PSY.get_bus(d)\n        ref_bus = get_reference_bus(network_model, device_bus)\n        name = PSY.get_name(d)\n        for t in get_time_steps(container)\n            _add_to_jump_expression!(\n                expression[ref_bus, t],\n                get_parameter_array(param_container)[name, t],\n                multiplier[name, t],\n            )\n        end\n    end\n    return\nend\n\n\"\"\"\nDefault implementation to add parameters to SystemBalanceExpressions\n\"\"\"\nfunction add_to_expression!(\n    container::OptimizationContainer,\n    ::Type{T},\n    ::Type{U},\n    devices::Union{Vector{V}, IS.FlattenIteratorWrapper{V}},\n    device_model::DeviceModel{V, W},\n    network_model::NetworkModel{X},\n) where {\n    T <: SystemBalanceExpressions,\n    U <: EventParameter,\n    V <: PSY.StaticInjection,\n    W <: AbstractDeviceFormulation,\n    X <: AbstractPTDFModel,\n}\n    param_container = get_parameter(container, U(), V)\n    multiplier = get_multiplier_array(param_container)\n    sys_expr = get_expression(container, T(), _system_expression_type(X))\n    nodal_expr = get_expression(container, T(), PSY.ACBus)\n    network_reduction = get_network_reduction(network_model)\n    for d in devices\n        name = PSY.get_name(d)\n        device_bus = PSY.get_bus(d)\n        bus_no_ = PSY.get_number(device_bus)\n        bus_no = PNM.get_mapped_bus_number(network_reduction, bus_no_)\n        ref_index = _ref_index(network_model, device_bus)\n        for t in get_time_steps(container)\n            param = get_parameter_array(param_container)[name, t]\n            _add_to_jump_expression!(sys_expr[ref_index, t], param, multiplier[name, t])\n            _add_to_jump_expression!(nodal_expr[bus_no, t], param, multiplier[name, t])\n        end\n    end\n    return\nend\n\n\"\"\"\nDefault implementation to add parameters to SystemBalanceExpressions\n\"\"\"\nfunction add_to_expression!(\n    container::OptimizationContainer,\n    ::Type{T},\n    ::Type{U},\n    devices::Union{Vector{V}, IS.FlattenIteratorWrapper{V}},\n    model::DeviceModel{V, W},\n    network_model::NetworkModel{X},\n) where {\n    T <: SystemBalanceExpressions,\n    U <: EventParameter,\n    V <: PSY.Device,\n    W <: AbstractDeviceFormulation,\n    X <: PM.AbstractPowerModel,\n}\n    param_container = get_parameter(container, U(), V)\n    multiplier = get_multiplier_array(param_container)\n    network_reduction = get_network_reduction(network_model)\n    for d in devices, t in get_time_steps(container)\n        bus_no = PNM.get_mapped_bus_number(network_reduction, PSY.get_bus(d))\n        name = PSY.get_name(d)\n        _add_to_jump_expression!(\n            get_expression(container, T(), PSY.ACBus)[bus_no, t],\n            get_parameter_array(param_container)[name, t],\n            multiplier[name, t],\n        )\n    end\n    return\nend\n\nfunction add_to_expression!(\n    container::OptimizationContainer,\n    ::Type{T},\n    ::Type{U},\n    devices::Union{Vector{V}, IS.FlattenIteratorWrapper{V}},\n    model::DeviceModel{V, W},\n    network_model::NetworkModel{AreaBalancePowerModel},\n) where {\n    T <: SystemBalanceExpressions,\n    U <: EventParameter,\n    V <: PSY.Device,\n    W <: AbstractDeviceFormulation,\n}\n    param_container = get_parameter(container, U(), V)\n    multiplier = get_multiplier_array(param_container)\n    for d in devices, t in get_time_steps(container)\n        bus = PSY.get_bus(d)\n        area_name = PSY.get_name(PSY.get_area(bus))\n        name = PSY.get_name(d)\n        _add_to_jump_expression!(\n            get_expression(container, T(), PSY.Area)[area_name, t],\n            get_parameter_array(param_container)[name, t],\n            multiplier[name, t],\n        )\n    end\n    return\nend\n"
  },
  {
    "path": "src/contingency_model/contingency_constraints.jl",
    "content": "function add_event_constraints!(\n    container::OptimizationContainer,\n    devices::T,\n    device_model::DeviceModel{U, V},\n    network_model::NetworkModel{W},\n) where {\n    T <: Union{Vector{U}, IS.FlattenIteratorWrapper{U}},\n    V <: AbstractDeviceFormulation,\n    W <: PM.AbstractActivePowerModel,\n} where {U <: PSY.ThermalGen}\n    for (key, event_model) in get_events(device_model)\n        event_type = get_entry_type(key)\n        devices_with_attributes =\n            [d for d in devices if PSY.has_supplemental_attributes(d, event_type)]\n        isempty(devices_with_attributes) &&\n            error(\"no devices found with a supplemental attribute for event $event_type\")\n        add_parameterized_upper_bound_range_constraints(\n            container,\n            ActivePowerOutageConstraint,\n            ActivePowerRangeExpressionUB,\n            AvailableStatusParameter,\n            devices_with_attributes,\n            device_model,\n            W,\n        )\n    end\n    return\nend\n\nfunction add_event_constraints!(\n    container::OptimizationContainer,\n    devices::T,\n    device_model::DeviceModel{U, V},\n    network_model::NetworkModel{W},\n) where {\n    T <: Union{Vector{U}, IS.FlattenIteratorWrapper{U}},\n    V <: AbstractDeviceFormulation,\n    W <: PM.AbstractPowerModel,\n} where {U <: PSY.ThermalGen}\n    for (key, event_model) in get_events(device_model)\n        event_type = get_entry_type(key)\n        devices_with_attributes =\n            [d for d in devices if PSY.has_supplemental_attributes(d, event_type)]\n        isempty(devices_with_attributes) &&\n            error(\"no devices found with a supplemental attribute for event $event_type\")\n        add_parameterized_upper_bound_range_constraints(\n            container,\n            ActivePowerOutageConstraint,\n            ActivePowerRangeExpressionUB,\n            AvailableStatusParameter,\n            devices_with_attributes,\n            device_model,\n            W,\n        )\n        add_reactive_power_contingency_constraint(\n            container,\n            ReactivePowerOutageConstraint,\n            ReactivePowerVariable,\n            AvailableStatusParameter,\n            devices_with_attributes,\n            device_model,\n            W,\n        )\n    end\n    return\nend\n\nfunction add_event_constraints!(\n    container::OptimizationContainer,\n    devices::T,\n    device_model::DeviceModel{U, V},\n    network_model::NetworkModel{W},\n) where {\n    T <: Union{Vector{U}, IS.FlattenIteratorWrapper{U}},\n    V <: AbstractDeviceFormulation,\n    W <: PM.AbstractActivePowerModel,\n} where {U <: PSY.RenewableGen}\n    for (key, event_model) in get_events(device_model)\n        event_type = get_entry_type(key)\n        devices_with_attributes =\n            [d for d in devices if PSY.has_supplemental_attributes(d, event_type)]\n        isempty(devices_with_attributes) &&\n            error(\"no devices found with a supplemental attribute for event $event_type\")\n        if has_service_model(device_model)\n            lhs_type = ActivePowerRangeExpressionUB\n        else\n            lhs_type = ActivePowerVariable\n        end\n        add_parameterized_upper_bound_range_constraints(\n            container,\n            ActivePowerOutageConstraint,\n            lhs_type,\n            AvailableStatusParameter,\n            devices_with_attributes,\n            device_model,\n            W,\n        )\n    end\n    return\nend\n\nfunction add_event_constraints!(\n    container::OptimizationContainer,\n    devices::T,\n    device_model::DeviceModel{U, V},\n    network_model::NetworkModel{W},\n) where {\n    T <: Union{Vector{U}, IS.FlattenIteratorWrapper{U}},\n    V <: AbstractDeviceFormulation,\n    W <: PM.AbstractPowerModel,\n} where {U <: PSY.RenewableGen}\n    for (key, event_model) in get_events(device_model)\n        event_type = get_entry_type(key)\n        devices_with_attributes =\n            [d for d in devices if PSY.has_supplemental_attributes(d, event_type)]\n        isempty(devices_with_attributes) &&\n            error(\"no devices found with a supplemental attribute for event $event_type\")\n        if has_service_model(device_model)\n            lhs_type = ActivePowerRangeExpressionUB\n        else\n            lhs_type = ActivePowerVariable\n        end\n        add_parameterized_upper_bound_range_constraints(\n            container,\n            ActivePowerOutageConstraint,\n            lhs_type,\n            AvailableStatusParameter,\n            devices_with_attributes,\n            device_model,\n            W,\n        )\n        add_reactive_power_contingency_constraint(\n            container,\n            ReactivePowerOutageConstraint,\n            ReactivePowerVariable,\n            AvailableStatusParameter,\n            devices_with_attributes,\n            device_model,\n            W,\n        )\n    end\n    return\nend\n\nfunction add_event_constraints!(\n    container::OptimizationContainer,\n    devices::T,\n    device_model::DeviceModel{U, V},\n    network_model::NetworkModel{W},\n) where {\n    T <: Union{Vector{U}, IS.FlattenIteratorWrapper{U}},\n    V <: AbstractDeviceFormulation,\n    W <: PM.AbstractActivePowerModel,\n} where {U <: PSY.ElectricLoad}\n    for (key, event_model) in get_events(device_model)\n        event_type = get_entry_type(key)\n        devices_with_attributes =\n            [d for d in devices if PSY.has_supplemental_attributes(d, event_type)]\n        isempty(devices_with_attributes) &&\n            error(\"no devices found with a supplemental attribute for event $event_type\")\n        add_parameterized_upper_bound_range_constraints(\n            container,\n            ActivePowerOutageConstraint,\n            ActivePowerVariable,\n            AvailableStatusParameter,\n            devices_with_attributes,\n            device_model,\n            W,\n        )\n    end\n    return\nend\n\nfunction add_event_constraints!(\n    container::OptimizationContainer,\n    devices::T,\n    device_model::DeviceModel{U, V},\n    network_model::NetworkModel{W},\n) where {\n    T <: Union{Vector{U}, IS.FlattenIteratorWrapper{U}},\n    V <: AbstractDeviceFormulation,\n    W <: PM.AbstractPowerModel,\n} where {U <: PSY.ElectricLoad}\n    for (key, event_model) in get_events(device_model)\n        event_type = get_entry_type(key)\n        devices_with_attributes =\n            [d for d in devices if PSY.has_supplemental_attributes(d, event_type)]\n        isempty(devices_with_attributes) &&\n            error(\"no devices found with a supplemental attribute for event $event_type\")\n        add_parameterized_upper_bound_range_constraints(\n            container,\n            ActivePowerOutageConstraint,\n            ActivePowerVariable,\n            AvailableStatusParameter,\n            devices_with_attributes,\n            device_model,\n            W,\n        )\n        add_reactive_power_contingency_constraint(\n            container,\n            ReactivePowerOutageConstraint,\n            ReactivePowerVariable,\n            AvailableStatusParameter,\n            devices_with_attributes,\n            device_model,\n            W,\n        )\n    end\n    return\nend\n\nfunction add_reactive_power_contingency_constraint(\n    container::OptimizationContainer,\n    ::Type{ReactivePowerOutageConstraint},\n    ::Type{ReactivePowerVariable},\n    ::Type{AvailableStatusParameter},\n    devices::Union{Vector{V}, IS.FlattenIteratorWrapper{V}},\n    model::DeviceModel{V, W},\n    ::Type{X},\n) where {\n    V <: PSY.Component,\n    W <: AbstractDeviceFormulation,\n    X <: PM.AbstractPowerModel,\n}\n    array_reactive = get_variable(container, ReactivePowerVariable(), V)\n    _add_reactive_power_contingency_constraint_impl!(\n        container,\n        ReactivePowerOutageConstraint,\n        array_reactive,\n        AvailableStatusParameter(),\n        devices,\n        model,\n    )\n    return\nend\n\nfunction _add_reactive_power_contingency_constraint_impl!(\n    container::OptimizationContainer,\n    ::Type{ReactivePowerOutageConstraint},\n    array_reactive,\n    param::AvailableStatusParameter,\n    devices::Union{Vector{V}, IS.FlattenIteratorWrapper{V}},\n    model::DeviceModel{V, W},\n) where {\n    V <: PSY.Component,\n    W <: AbstractDeviceFormulation,\n}\n    time_steps = get_time_steps(container)\n    names = PSY.get_name.(devices)\n    constraint_container =\n        add_constraints_container!(\n            container,\n            ReactivePowerOutageConstraint(),\n            V,\n            names,\n            time_steps;\n            meta = \"ub\",\n        )\n\n    param_array = get_parameter_array(container, param, V)\n    param_multiplier =\n        get_parameter_multiplier_array(container, AvailableStatusParameter(), V)\n    jump_model = get_jump_model(container)\n    time_steps = axes(constraint_container)[2]\n    for device in devices, t in time_steps\n        name = PSY.get_name(device)\n        ub = _get_reactive_power_upper_bound(device)\n        constraint_container[name, t] = JuMP.@constraint(\n            jump_model,\n            (array_reactive[name, t])^2 <= (ub * param_array[name, t])\n        )\n    end\n    return\nend\n\nfunction _get_reactive_power_upper_bound(device::PSY.StaticInjection)\n    return maximum([\n        PSY.get_reactive_power_limits(device).max^2,\n        PSY.get_reactive_power_limits(device).min^2,\n    ])\nend\n\nfunction _get_reactive_power_upper_bound(device::PSY.ElectricLoad)\n    return PSY.get_max_reactive_power(device)^2\nend\n"
  },
  {
    "path": "src/core/abstract_feedforward.jl",
    "content": "abstract type AbstractAffectFeedforward end\n\nget_device_type(x::AbstractAffectFeedforward) = x.device_type\n"
  },
  {
    "path": "src/core/abstract_simulation_store.jl",
    "content": "\"\"\"\nProvides storage of simulation data\n\"\"\"\nabstract type SimulationStore end\n\n# Required methods:\n# - open_store\n# - Base.isopen(store::SimulationStore)\n# - Base.close(store::SimulationStore)\n# - Base.flush(store::SimulationStore)\n# - get_params(store::SimulationStore)\n# - initialize_problem_storage!\n# - list_decision_model_keys(store::SimulationStore, problem::Symbol, container_type::Symbol)\n# - list_emulation_model_keys(store::SimulationStore, container_type::Symbol)\n# - list_decision_models(store::SimulationStore)\n# - log_cache_hit_percentages(store::SimulationStore)\n# - write_result!\n# - read_result!\n# - read_results\n# - write_optimizer_stats!\n# - read_optimizer_stats\n# - get_dm_data\n# - get_em_data\n# - get_container_key_lookup\n\nget_dm_data(store::SimulationStore) = store.dm_data\nget_em_data(store::SimulationStore) = store.em_data\n"
  },
  {
    "path": "src/core/auxiliary_variables.jl",
    "content": "\"\"\"\nAuxiliary Variable for Thermal Generation Models to keep track of time elapsed on\n\"\"\"\nstruct TimeDurationOn <: AuxVariableType end\n\n\"\"\"\nAuxiliary Variable for Thermal Generation Models to keep track of time elapsed off\n\"\"\"\nstruct TimeDurationOff <: AuxVariableType end\n\n\"\"\"\nAuxiliary Variable for Thermal Generation Models that solve for power above min\n\"\"\"\nstruct PowerOutput <: AuxVariableType end\n\n\"\"\"\nAuxiliary Variable of DC Current Variables for DC Lines formulations\nDocs abbreviation: ``p_l^{loss}``\n\"\"\"\nstruct DCLineLosses <: AuxVariableType end\n\n\"\"\"\nAuxiliary Variables that are calculated using a `PowerFlowEvaluationModel`\n\"\"\"\nabstract type PowerFlowAuxVariableType <: AuxVariableType end\n\n\"\"\"\nAuxiliary Variable for the bus angle results from power flow evaluation\n\"\"\"\nstruct PowerFlowVoltageAngle <: PowerFlowAuxVariableType end\n\n\"\"\"\nAuxiliary Variable for the bus voltage magnitued results from power flow evaluation\n\"\"\"\nstruct PowerFlowVoltageMagnitude <: PowerFlowAuxVariableType end\n\n\"\"\"\nAuxiliary Variable for line power flow results from power flow evaluation\n\"\"\"\nabstract type BranchFlowAuxVariableType <: PowerFlowAuxVariableType end\n\n\"\"\"\nAuxiliary Variable for the line reactive flow in the from -> to direction from power flow evaluation\n\"\"\"\nstruct PowerFlowBranchReactivePowerFromTo <: BranchFlowAuxVariableType end\n\n\"\"\"\nAuxiliary Variable for the line reactive flow in the to -> from direction from power flow evaluation\n\"\"\"\nstruct PowerFlowBranchReactivePowerToFrom <: BranchFlowAuxVariableType end\n\n\"\"\"\nAuxiliary Variable for the line active flow in the from -> to direction from power flow evaluation\n\"\"\"\nstruct PowerFlowBranchActivePowerFromTo <: BranchFlowAuxVariableType end\n\n\"\"\"\nAuxiliary Variable for the line active flow in the to -> from direction from power flow evaluation\n\"\"\"\nstruct PowerFlowBranchActivePowerToFrom <: BranchFlowAuxVariableType end\n\n\"\"\"\nAuxiliary Variable for the loss factors from AC power flow evaluation that are calculated using the Jacobian matrix\n\"\"\"\nstruct PowerFlowLossFactors <: PowerFlowAuxVariableType end\n\n\"\"\"\nAuxiliary Variable for the voltage stability factors from AC power flow evaluation that are calculated using the Jacobian matrix\n\"\"\"\nstruct PowerFlowVoltageStabilityFactors <: PowerFlowAuxVariableType end\n\n# should this be a subtype of BranchFlowAuxVariableType? It's line-related but has no flow direction.\n\"\"\"\nAuxiliary Variable for the active power loss on a line from AC power flow evaluation.\n\"\"\"\nstruct PowerFlowBranchActivePowerLoss <: PowerFlowAuxVariableType end\n\n# TODO reactive loss?\n\nconvert_result_to_natural_units(::Type{PowerOutput}) = true\nconvert_result_to_natural_units(\n    ::Type{\n        <:Union{\n            PowerFlowBranchReactivePowerFromTo, PowerFlowBranchReactivePowerToFrom,\n            PowerFlowBranchActivePowerFromTo, PowerFlowBranchActivePowerToFrom,\n            PowerFlowBranchActivePowerLoss,\n        },\n    },\n) = true\n\n\"Whether the auxiliary variable is calculated using a `PowerFlowEvaluationModel`\"\nis_from_power_flow(::Type{<:AuxVariableType}) = false\nis_from_power_flow(::Type{<:PowerFlowAuxVariableType}) = true\n"
  },
  {
    "path": "src/core/cache_utils.jl",
    "content": "\nstruct OptimizationResultCacheKey\n    model::Symbol\n    key::OptimizationContainerKey\nend\n\nstruct CacheFlushRule\n    keep_in_cache::Bool\nend\n\nCacheFlushRule() = CacheFlushRule(false)\n\nconst DEFAULT_SIMULATION_STORE_CACHE_SIZE_MiB = 1024\nconst DEFAULT_SIMULATION_STORE_CACHE_SIZE = DEFAULT_SIMULATION_STORE_CACHE_SIZE_MiB * MiB\nconst MIN_CACHE_FLUSH_SIZE_MiB = 1\nconst MIN_CACHE_FLUSH_SIZE = MIN_CACHE_FLUSH_SIZE_MiB * MiB\n\n\"\"\"\nInforms the flusher on what data to keep in cache.\n\"\"\"\nstruct CacheFlushRules\n    data::Dict{OptimizationResultCacheKey, CacheFlushRule}\n    min_flush_size::Int\n    max_size::Int\nend\n\nfunction CacheFlushRules(;\n    max_size = DEFAULT_SIMULATION_STORE_CACHE_SIZE,\n    min_flush_size = MIN_CACHE_FLUSH_SIZE,\n)\n    return CacheFlushRules(\n        Dict{OptimizationResultCacheKey, CacheFlushRule}(),\n        min_flush_size,\n        max_size,\n    )\nend\n\nfunction add_rule!(\n    rules::CacheFlushRules,\n    model_name,\n    op_container_key,\n    keep_in_cache::Bool,\n)\n    key = OptimizationResultCacheKey(model_name, op_container_key)\n    rules.data[key] = CacheFlushRule(keep_in_cache)\n    return\nend\n\nfunction get_rule(x::CacheFlushRules, model, op_container_key)\n    return get_rule(x, OptimizationResultCacheKey(model, op_container_key))\nend\n\nget_rule(x::CacheFlushRules, key::OptimizationResultCacheKey) = x.data[key]\n\nmutable struct CacheStats\n    hits::Int\n    misses::Int\nend\n\nCacheStats() = CacheStats(0, 0)\n\nfunction get_cache_hit_percentage(x::CacheStats)\n    total = x.hits + x.misses\n    total == 0 && return 0.0\n    return x.hits / (total) * 100\nend\n"
  },
  {
    "path": "src/core/constraints.jl",
    "content": "abstract type PostContingencyConstraintType <: ConstraintType end\n\nstruct AbsoluteValueConstraint <: ConstraintType end\n\"\"\"\n\nStruct to create the constraint for starting up ThermalMultiStart units.\nFor more information check [ThermalGen Formulations](@ref ThermalGen-Formulations) for ThermalMultiStartUnitCommitment.\n\nThe specified constraint is formulated as:\n\n```math\n\\\\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}\n```\n\"\"\"\nstruct ActiveRangeICConstraint <: ConstraintType end\n\"\"\"\nStruct to create the constraint to balance power across specified areas.\nFor more information check [Network Formulations](@ref network_formulations).\n\nThe specified constraint is generally formulated as:\n\n```math\n\\\\sum_{c \\\\in \\\\text{components}_a} p_t^c = 0, \\\\quad \\\\forall a\\\\in \\\\{1,\\\\dots, A\\\\}, t \\\\in \\\\{1, \\\\dots, T\\\\}\n```\n\"\"\"\nstruct AreaParticipationAssignmentConstraint <: ConstraintType end\nstruct BalanceAuxConstraint <: ConstraintType end\n\"\"\"\nStruct to create the commitment constraint between the on, start, and stop variables.\nFor more information check [ThermalGen Formulations](@ref ThermalGen-Formulations).\n\nThe specified constraints are formulated as:\n\n```math\nu_1^\\\\text{th} = u^\\\\text{th,init} + v_1^\\\\text{th} - w_1^\\\\text{th} \\\\\\\\\nu_t^\\\\text{th} = u_{t-1}^\\\\text{th} + v_t^\\\\text{th} - w_t^\\\\text{th}, \\\\quad \\\\forall t \\\\in \\\\{2,\\\\dots,T\\\\} \\\\\\\\\nv_t^\\\\text{th} + w_t^\\\\text{th} \\\\le 1, \\\\quad \\\\forall t \\\\in \\\\{1,\\\\dots,T\\\\}\n```\n\"\"\"\nstruct CommitmentConstraint <: ConstraintType end\n\"\"\"\nStruct to create the constraint to balance power in the copperplate model.\nFor more information check [Network Formulations](@ref network_formulations).\n\nThe specified constraint is generally formulated as:\n\n```math\n\\\\sum_{c \\\\in \\\\text{components}} p_t^c = 0, \\\\quad \\\\forall t \\\\in \\\\{1, \\\\dots, T\\\\}\n```\n\"\"\"\nstruct CopperPlateBalanceConstraint <: ConstraintType end\n\n\"\"\"\nStruct to create the constraint to balance active power.\nFor more information check [ThermalGen Formulations](@ref ThermalGen-Formulations).\n\nThe specified constraint is generally formulated as:\n\n```math\n\\\\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\\\\}\n```\n\"\"\"\nstruct PostContingencyGenerationBalanceConstraint <: PostContingencyConstraintType end\n\n\"\"\"\nStruct to create the duration constraint for commitment formulations, i.e. min-up and min-down.\n\nFor more information check [ThermalGen Formulations](@ref ThermalGen-Formulations).\n\"\"\"\nstruct DurationConstraint <: ConstraintType end\nstruct EnergyBalanceConstraint <: ConstraintType end\n\n\"\"\"\nStruct to create the constraint that sets the reactive power to the power factor\nin the RenewableConstantPowerFactor formulation for renewable units.\n\nFor more information check [RenewableGen Formulations](@ref PowerSystems.RenewableGen-Formulations).\n\nThe specified constraint is formulated as:\n\n```math\nq_t^\\\\text{re} = \\\\text{pf} \\\\cdot p_t^\\\\text{re}, \\\\quad \\\\forall t \\\\in \\\\{1,\\\\dots, T\\\\}\n```\n\"\"\"\nstruct EqualityConstraint <: ConstraintType end\n\"\"\"\nStruct to create the constraint for semicontinuous feedforward limits.\n\nFor more information check [Feedforward Formulations](@ref ff_formulations).\n\nThe specified constraint is formulated as:\n\n```math\n\\\\begin{align*}\n&  \\\\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\\\\}  \\\\\\\\\n&  \\\\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\\\\}\n\\\\end{align*}\n```\n\"\"\"\nstruct FeedforwardSemiContinuousConstraint <: ConstraintType end\nstruct FeedforwardIntegralLimitConstraint <: ConstraintType end\n\"\"\"\nStruct to create the constraint for upper bound feedforward limits.\n\nFor more information check [Feedforward Formulations](@ref ff_formulations).\n\nThe specified constraint is formulated as:\n\n```math\n\\\\begin{align*}\n&  \\\\text{AffectedVariable}_t - p_t^\\\\text{ff,ubsl} \\\\le \\\\text{SourceVariableParameter}_t, \\\\quad \\\\forall t \\\\in \\\\{1,\\\\dots, T\\\\}\n\\\\end{align*}\n```\n\"\"\"\nstruct FeedforwardUpperBoundConstraint <: ConstraintType end\n\"\"\"\nStruct to create the constraint for lower bound feedforward limits.\n\nFor more information check [Feedforward Formulations](@ref ff_formulations).\n\nThe specified constraint is formulated as:\n\n```math\n\\\\begin{align*}\n&  \\\\text{AffectedVariable}_t + p_t^\\\\text{ff,lbsl} \\\\ge \\\\text{SourceVariableParameter}_t, \\\\quad \\\\forall t \\\\in \\\\{1,\\\\dots, T\\\\}\n\\\\end{align*}\n```\n\"\"\"\nstruct FeedforwardLowerBoundConstraint <: ConstraintType end\nstruct FeedforwardEnergyTargetConstraint <: ConstraintType end\n\"\"\"\nStruct to create the constraint that set the flow limits through a PhaseShiftingTransformer.\n\nFor more information check [Branch Formulations](@ref PowerSystems.Branch-Formulations).\n\nThe specified constraint is formulated as:\n\n```math\n-R^\\\\text{max} \\\\le f_t \\\\le R^\\\\text{max}, \\\\quad \\\\forall t \\\\in \\\\{1,\\\\dots,T\\\\}\n```\n\"\"\"\nstruct FlowLimitConstraint <: ConstraintType end\nstruct FlowLimitFromToConstraint <: ConstraintType end\nstruct FlowLimitToFromConstraint <: ConstraintType end\n\"\"\"\nStruct to create the constraints that set the power balance across a lossy HVDC two-terminal line.\n\nFor more information check [Branch Formulations](@ref PowerSystems.Branch-Formulations).\n\nThe specified constraints are formulated as:\n\n```math\n\\\\begin{align*}\n& 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\\\\} \\\\\\\\\n& 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\\\\} \\\\\\\\\n& 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\\\\} \\\\\\\\\n& 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\\\\} \\\\\\\\\n\\\\end{align*}\n```\n\"\"\"\nstruct HVDCPowerBalance <: ConstraintType end\nstruct FrequencyResponseConstraint <: ConstraintType end\n\"\"\"\nStruct to create the constraint the AC branch flows depending on the network model.\nFor more information check [Branch Formulations](@ref PowerSystems.Branch-Formulations).\n\nThe specified constraint depends on the network model chosen. The most common application is the StaticBranch in a PTDF Network Model:\n\n```math\nf_t = \\\\sum_{i=1}^N \\\\text{PTDF}_{i,b} \\\\cdot \\\\text{Bal}_{i,t}, \\\\quad \\\\forall t \\\\in \\\\{1,\\\\dots, T\\\\}\n```\n\"\"\"\nstruct NetworkFlowConstraint <: ConstraintType end\n\"\"\"\nStruct to create the constraint to balance active power in nodal formulation.\nFor more information check [Network Formulations](@ref network_formulations).\n\nThe specified constraint depends on the network model chosen.\n\"\"\"\nstruct NodalBalanceActiveConstraint <: ConstraintType end\n\"\"\"\nStruct to create the constraint to balance reactive power in nodal formulation.\nFor more information check [Network Formulations](@ref network_formulations).\n\nThe specified constraint depends on the network model chosen.\n\"\"\"\nstruct NodalBalanceReactiveConstraint <: ConstraintType end\nstruct ParticipationAssignmentConstraint <: ConstraintType end\n\"\"\"\nStruct to create the constraint to participation assignments limits in the active power reserves.\nFor more information check [Service Formulations](@ref service_formulations).\n\nThe constraint is as follows:\n\n```math\nr_{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)} \\\\\\\\\nr_{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)}\n```\n\"\"\"\nstruct ParticipationFractionConstraint <: ConstraintType end\n\"\"\"\nStruct to create the PiecewiseLinearCostConstraint associated with a specified variable.\n\nSee [Piecewise linear cost functions](@ref pwl_cost) for more information.\n\"\"\"\nstruct PiecewiseLinearCostConstraint <: ConstraintType end\n\nabstract type AbstractPiecewiseLinearBlockOfferConstraint <: ConstraintType end\n\n\"\"\"\nStruct to create the PiecewiseLinearBlockIncrementalOfferConstraint associated with a specified variable.\n\nSee [Piecewise linear cost functions](@ref pwl_cost) for more information.\n\"\"\"\nstruct PiecewiseLinearBlockIncrementalOfferConstraint <:\n       AbstractPiecewiseLinearBlockOfferConstraint end\n\n\"\"\"\nStruct to create the PiecewiseLinearBlockDecrementalOfferConstraint associated with a specified variable.\n\nSee [Piecewise linear cost functions](@ref pwl_cost) for more information.\n\"\"\"\nstruct PiecewiseLinearBlockDecrementalOfferConstraint <:\n       AbstractPiecewiseLinearBlockOfferConstraint end\n\n\"\"\"\nStruct to create the PiecewiseLinearUpperBoundConstraint associated with a specified variable.\n\nSee [Piecewise linear cost functions](@ref pwl_cost) for more information.\n\"\"\"\nstruct PiecewiseLinearUpperBoundConstraint <: ConstraintType end\n\n\"\"\"\nStruct to create the RampConstraint associated with a specified thermal device or reserve service.\n\nFor thermal units, see more information in [Thermal Formulations](@ref ThermalGen-Formulations). The constraint is as follows:\n```math\n-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\\\\}\n```\n\nFor Ramp Reserve, see more information in [Service Formulations](@ref service_formulations). The constraint is as follows:\n\n```math\nr_{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)} \\\\\\\\\nr_{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)}\n```\n\"\"\"\nstruct RampConstraint <: ConstraintType end\nstruct PostContingencyRampConstraint <: PostContingencyConstraintType end\nstruct RampLimitConstraint <: ConstraintType end\nstruct RangeLimitConstraint <: ConstraintType end\n\"\"\"\nStruct to create the constraint that set the AC flow limits through AC branches and HVDC two-terminal branches.\n\nFor more information check [Branch Formulations](@ref PowerSystems.Branch-Formulations).\n\nThe specified constraint is formulated as:\n\n```math\n\\\\begin{align*}\n&  f_t - f_t^\\\\text{sl,up} \\\\le R^\\\\text{max},\\\\quad \\\\forall t \\\\in \\\\{1,\\\\dots, T\\\\} \\\\\\\\\n&  f_t + f_t^\\\\text{sl,lo} \\\\ge -R^\\\\text{max},\\\\quad \\\\forall t \\\\in \\\\{1,\\\\dots, T\\\\}\n\\\\end{align*}\n```\n\"\"\"\nstruct FlowRateConstraint <: ConstraintType end\nstruct PostContingencyEmergencyRateLimitConstraint <: PostContingencyConstraintType end\n\n\"\"\"\nStruct to create the constraint for branch flow rate limits from the 'from' bus to the 'to' bus.\nFor more information check [Branch Formulations](@ref PowerSystems.Branch-Formulations).\n\"\"\"\nstruct FlowRateConstraintFromTo <: ConstraintType end\n\n\"\"\"\nStruct to create the constraint for branch flow rate limits from the 'to' bus to the 'from' bus.\nFor more information check [Branch Formulations](@ref PowerSystems.Branch-Formulations).\n\"\"\"\nstruct FlowRateConstraintToFrom <: ConstraintType end\nstruct RegulationLimitsConstraint <: ConstraintType end\n\n\"\"\"\nStruct to create the constraint for satisfying active power reserve requirements.\nFor more information check [Service Formulations](@ref service_formulations).\n\nThe constraint is as follows:\n\n```math\n\\\\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)} \\\\\\\\\n\\\\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)}\n```\n\"\"\"\nstruct RequirementConstraint <: ConstraintType end\nstruct ReserveEnergyCoverageConstraint <: ConstraintType end\n\"\"\"\nStruct to create the constraint for ensuring that NonSpinning Reserve can be delivered from turn-off thermal units.\n\nFor more information check [Service Formulations](@ref service_formulations) for NonSpinningReserve.\n\nThe constraint is as follows:\n\n```math\nr_{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\\\\}\n```\n\"\"\"\nstruct ReservePowerConstraint <: ConstraintType end\nstruct SACEPIDAreaConstraint <: ConstraintType end\nstruct StartTypeConstraint <: ConstraintType end\n\"\"\"\nStruct to create the start-up initial condition constraints for ThermalMultiStart.\n\nFor more information check [ThermalGen Formulations](@ref ThermalGen-Formulations) for ThermalMultiStartUnitCommitment.\n\"\"\"\nstruct StartupInitialConditionConstraint <: ConstraintType end\n\"\"\"\nStruct to create the start-up time limit constraints for ThermalMultiStart.\n\nFor more information check [ThermalGen Formulations](@ref ThermalGen-Formulations) for ThermalMultiStartUnitCommitment.\n\"\"\"\nstruct StartupTimeLimitTemperatureConstraint <: ConstraintType end\n\"\"\"\nStruct to create the constraint that set the angle limits through a PhaseShiftingTransformer.\n\nFor more information check [Branch Formulations](@ref PowerSystems.Branch-Formulations).\n\nThe specified constraint is formulated as:\n\n```math\n\\\\Theta^\\\\text{min} \\\\le \\\\theta^\\\\text{shift}_t \\\\le \\\\Theta^\\\\text{max}, \\\\quad \\\\forall t \\\\in \\\\{1,\\\\dots,T\\\\}\n```\n\"\"\"\nstruct PhaseAngleControlLimit <: ConstraintType end\nstruct InterfaceFlowLimit <: ConstraintType end\nstruct HVDCFlowCalculationConstraint <: ConstraintType end\n\n\"\"\"\nStruct to create the constraint that calculates the Rectifier DC line voltage.\n\n```math\nv_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)\n```\n\"\"\"\nstruct HVDCRectifierDCLineVoltageConstraint <: ConstraintType end\n\n\"\"\"\nStruct to create the constraint that calculates the Inverter DC line voltage.\n\n```math\nv_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)\n```\n\"\"\"\nstruct HVDCInverterDCLineVoltageConstraint <: ConstraintType end\n\n\"\"\"\nStruct to create the constraint that calculates the Rectifier Overlap Angle.\n\n```math\n\\\\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\n```\n\"\"\"\nstruct HVDCRectifierOverlapAngleConstraint <: ConstraintType end\n\n\"\"\"\nStruct to create the constraint that calculates the Inverter Overlap Angle.\n\n```math\n\\\\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\n```\n\"\"\"\nstruct HVDCInverterOverlapAngleConstraint <: ConstraintType end\n\n\"\"\"\nStruct to create the constraint that calculates the Rectifier Power Factor Angle.\n\n```math\n\\\\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)\n```\n\"\"\"\nstruct HVDCRectifierPowerFactorAngleConstraint <: ConstraintType end\n\n\"\"\"\nStruct to create the constraint that calculates the Inverter Power Factor Angle.\n\n```math\n\\\\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)\n```\n\"\"\"\nstruct HVDCInverterPowerFactorAngleConstraint <: ConstraintType end\n\n\"\"\"\nStruct to create the constraint that calculates the AC Current flowing into the AC side of the rectifier.\n\n```math\ni_\\text{ac}^r = \\\\sqrt{6} \\\\frac{N^r}{\\\\pi}I_d\n```\n\"\"\"\nstruct HVDCRectifierACCurrentFlowConstraint <: ConstraintType end\n\n\"\"\"\nStruct to create the constraint that calculates the AC Current flowing into the AC side of the inverter.\n\n```math\ni_\\text{ac}^i = \\\\sqrt{6} \\\\frac{N^i}{\\\\pi}I_d\n```\n\"\"\"\nstruct HVDCInverterACCurrentFlowConstraint <: ConstraintType end\n\n\"\"\"\nStruct to create the constraint that calculates the AC Power injection at the AC side of the rectifier.\n\n```math\n\\\\begin{align*}\np_\\\\text{ac}^r = \\\\sqrt{3} i_\\\\text{ac}^r \\\\frac{a^r v_\\\\text{ac}^r}{t^r}\\\\cos{\\\\phi^r} \\\\\\\\\nq_\\\\text{ac}^r = \\\\sqrt{3} i_\\\\text{ac}^r \\\\frac{a^r v_\\\\text{ac}^r}{t^r}\\\\sin{\\\\phi^r} \\\\\\\\\n\\\\end{align*}\n```\n\"\"\"\nstruct HVDCRectifierPowerCalculationConstraint <: ConstraintType end\n\n\"\"\"\nStruct to create the constraint that calculates the AC Power injection at the AC side of the inverter.\n\n```math\n\\\\begin{align*}\np_\\\\text{ac}^i = \\\\sqrt{3} i_\\\\text{ac}^i \\\\frac{a^i v_\\\\text{ac}^i}{t^i}\\\\cos{\\\\phi^i} \\\\\\\\\nq_\\\\text{ac}^i = \\\\sqrt{3} i_\\\\text{ac}^i \\\\frac{a^i v_\\\\text{ac}^i}{t^i}\\\\sin{\\\\phi^i} \\\\\\\\\n\\\\end{align*}\n```\n\"\"\"\nstruct HVDCInverterPowerCalculationConstraint <: ConstraintType end\n\n\"\"\"\nStruct to create the constraint that links the AC and DC side of the network.\n\n```math\nv_d^i = v_d^r - R_d I_d\n```\n\"\"\"\nstruct HVDCTransmissionDCLineConstraint <: ConstraintType end\n\nabstract type PowerVariableLimitsConstraint <: ConstraintType end\n\"\"\"\nStruct to create the constraint to limit active power input expressions.\nFor more information check [Device Formulations](@ref formulation_intro).\n\nThe specified constraint depends on the UpperBound and LowerBound expressions, but\nin its most basic formulation is of the form:\n\n```math\nP^\\\\text{min} \\\\le p_t^\\\\text{in} \\\\le P^\\\\text{max}, \\\\quad \\\\forall t \\\\in \\\\{1,\\\\dots,T\\\\}\n```\n\"\"\"\n\nabstract type PostContingencyVariableLimitsConstraint <: PowerVariableLimitsConstraint end\n\n\"\"\"\nStruct to create the constraint to limit active power input expressions.\nFor more information check [Device Formulations](@ref formulation_intro).\n\nThe specified constraint depends on the UpperBound and LowerBound expressions, but\nin its most basic formulation is of the form:\n\n```math\nP^\\\\text{min} \\\\le p_t^\\\\text{in} \\\\le P^\\\\text{max}, \\\\quad \\\\forall t \\\\in \\\\{1,\\\\dots,T\\\\}\n```\n\"\"\"\nstruct InputActivePowerVariableLimitsConstraint <: PowerVariableLimitsConstraint end\n\"\"\"\nStruct to create the constraint to limit active power output expressions.\nFor more information check [Device Formulations](@ref formulation_intro).\n\nThe specified constraint depends on the UpperBound and LowerBound expressions, but\nin its most basic formulation is of the form:\n\n```math\nP^\\\\text{min} \\\\le p_t^\\\\text{out} \\\\le P^\\\\text{max}, \\\\quad \\\\forall t \\\\in \\\\{1,\\\\dots,T\\\\}\n```\n\"\"\"\nstruct OutputActivePowerVariableLimitsConstraint <: PowerVariableLimitsConstraint end\n\"\"\"\nStruct to create the constraint to limit active power expressions.\nFor more information check [Device Formulations](@ref formulation_intro).\n\nThe specified constraint depends on the UpperBound and LowerBound expressions, but\nin its most basic formulation is of the form:\n\n```math\nP^\\\\text{min} \\\\le p_t \\\\le P^\\\\text{max}, \\\\quad \\\\forall t \\\\in \\\\{1,\\\\dots,T\\\\}\n```\n\"\"\"\nstruct ActivePowerVariableLimitsConstraint <: PowerVariableLimitsConstraint end\n\n\"\"\"\nStruct to create the constraint to limit post-contingency active power expressions.\nFor more information check [Device Formulations](@ref formulation_intro).\n\nThe specified constraint depends on the UpperBound and LowerBound expressions, but\nin its most basic formulation is of the form:\n\n```math\nP^\\\\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\\\\}\n```\n\"\"\"\nstruct PostContingencyActivePowerVariableLimitsConstraint <:\n       PostContingencyVariableLimitsConstraint end\n\n\"\"\"\nStruct to create the constraint to limit post-contingency active power reserve deploymentexpressions.\nFor more information check [Device Formulations](@ref formulation_intro).\n\nThe specified constraint depends on the UpperBound and LowerBound expressions, but\nin its most basic formulation is of the form:\n\n```math\n\\\\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\\\\}\n```\n\"\"\"\nstruct PostContingencyActivePowerReserveDeploymentVariableLimitsConstraint <:\n       PostContingencyVariableLimitsConstraint end\n\n\"\"\"\nStruct to create the constraint to limit reactive power expressions.\nFor more information check [Device Formulations](@ref formulation_intro).\n\nThe specified constraint depends on the UpperBound and LowerBound expressions, but\nin its most basic formulation is of the form:\n\n```math\nQ^\\\\text{min} \\\\le q_t \\\\le Q^\\\\text{max}, \\\\quad \\\\forall t \\\\in \\\\{1,\\\\dots,T\\\\}\n```\n\"\"\"\nstruct ReactivePowerVariableLimitsConstraint <: PowerVariableLimitsConstraint end\n\"\"\"\nStruct to create the constraint to limit active power expressions by a time series parameter.\nFor more information check [Device Formulations](@ref formulation_intro).\n\nThe specified constraint depends on the UpperBound expressions, but\nin its most basic formulation is of the form:\n\n```math\np_t \\\\le \\\\text{ActivePowerTimeSeriesParameter}_t, \\\\quad \\\\forall t \\\\in \\\\{1,\\\\dots,T\\\\}\n```\n\"\"\"\nstruct ActivePowerVariableTimeSeriesLimitsConstraint <: PowerVariableLimitsConstraint end\n\n\"\"\"\nStruct to create the constraint to limit active power expressions by a time series parameter.\nFor more information check [Device Formulations](@ref formulation_intro).\n\nThe specified constraint depends on the UpperBound expressions, but\nin its most basic formulation is of the form:\n\n```math\np_t^{out} \\\\le \\\\text{ActivePowerTimeSeriesParameter}_t, \\\\quad \\\\forall t \\\\in \\\\{1,\\\\dots,T\\\\}\n```\n\"\"\"\nstruct ActivePowerOutVariableTimeSeriesLimitsConstraint <: PowerVariableLimitsConstraint end\n\n\"\"\"\nStruct to create the constraint to limit active power expressions by a time series parameter.\nFor more information check [Device Formulations](@ref formulation_intro).\n\nThe specified constraint depends on the UpperBound expressions, but\nin its most basic formulation is of the form:\n\n```math\np_t^{in} \\\\le \\\\text{ActivePowerTimeSeriesParameter}_t, \\\\quad \\\\forall t \\\\in \\\\{1,\\\\dots,T\\\\}\n```\n\"\"\"\nstruct ActivePowerInVariableTimeSeriesLimitsConstraint <: PowerVariableLimitsConstraint end\n\n\"\"\"\nStruct to create the constraint to limit the import and exports in a determined period.\nFor more information check [Device Formulations](@ref formulation_intro).\n\"\"\"\nstruct ImportExportBudgetConstraint <: ConstraintType end\n\nstruct LineFlowBoundConstraint <: ConstraintType end\n\nabstract type EventConstraint <: ConstraintType end\nstruct ActivePowerOutageConstraint <: EventConstraint end\nstruct ReactivePowerOutageConstraint <: EventConstraint end\n\n############################################################\n########## Multi-Terminal Converter Constraints ############\n############################################################\n\"\"\"\nStruct to create the constraints that set the current flowing through a DC line.\n```math\n\\\\begin{align*}\n& i_l^{dc} = \\\\frac{1}{r_l} (v_{from,l} - v_{to,l}), \\\\quad \\\\forall t \\\\in \\\\{1,\\\\dots, T\\\\} \n\\\\end{align*}\n```\n\"\"\"\nstruct DCLineCurrentConstraint <: ConstraintType end\n\nstruct NodalBalanceCurrentConstraint <: ConstraintType end\n\n\"\"\"\nStruct to create the constraints that compute the converter DC power based on current and voltage.\n\nThe specified constraints are formulated as:\n```math\n\\\\begin{align*}\n& p_c = 0.5 * (γ^sq - v^sq - i^sq), \\\\quad \\\\forall t \\\\in \\\\{1,\\\\dots, T\\\\} \\\\\\\\\n& γ_c = v_c + i_c, \\\\quad \\\\forall t \\\\in \\\\{1,\\\\dots, T\\\\} \\\\\\\\\n\\\\end{align*}\n```\n\"\"\"\nstruct ConverterPowerCalculationConstraint <: ConstraintType end\n\n\"\"\"\nStruct to create the constraints that decide the balance of AC and DC power of the converter.\n\nThe specified constraints are formulated as:\n```math\n\\\\begin{align*}\n& p_ac = p_dc - loss_t  \\\\quad \\\\forall t \\\\in \\\\{1,\\\\dots, T\\\\} \\\\\\\\\n& loss_t = a i_c^2 + b i_c + c \\\\\\\\\n\\\\end{align*}\n```\n\"\"\"\nstruct ConverterLossConstraint <: ConstraintType end\n\n\"\"\"\nStruct to create the McCormick envelopes constraints that decide the bounds on the DC active power.\n\nThe specified constraints are formulated as:\n```math\n\\\\begin{align*}\n& p_c >= V^{min} i_c + v_c I^{min} - I^{min}V^{min},  \\\\quad \\\\forall t \\\\in \\\\{1,\\\\dots, T\\\\} \\\\\\\\\n& p_c >= V^{max} i_c + v_c I^{max} - I^{max}V^{max},  \\\\quad \\\\forall t \\\\in \\\\{1,\\\\dots, T\\\\} \\\\\\\\\n& p_c <= V^{max} i_c + v_c I^{min} - I^{min}V^{max},  \\\\quad \\\\forall t \\\\in \\\\{1,\\\\dots, T\\\\} \\\\\\\\\n& p_c <= V^{min} i_c + v_c I^{max} - I^{max}V^{min},  \\\\quad \\\\forall t \\\\in \\\\{1,\\\\dots, T\\\\} \\\\\\\\\n\\\\end{align*}\n```\n\"\"\"\nstruct ConverterMcCormickEnvelopes <: ConstraintType end\n\n\"\"\"\nStruct to create the Quadratic PWL interpolation constraints that decide square value of the voltage.\nIn this case x = voltage and y = squared_voltage.\nThe specified constraints are formulated as:\n```math\n\\\\begin{align*}\n& x = x_0 + \\\\sum_{k=1}^K (x_{k} - x_{k-1}) \\\\delta_k,  \\\\quad \\\\forall t \\\\in \\\\{1,\\\\dots, T\\\\} \\\\\\\\\n& y = y_0 + \\\\sum_{k=1}^K (x_{k} - x_{k-1}) \\\\delta_k,  \\\\quad \\\\forall t \\\\in \\\\{1,\\\\dots, T\\\\} \\\\\\\\\n& z_k \\\\le \\\\delta_k,  \\\\quad \\\\forall t \\\\in \\\\{1,\\\\dots, T\\\\}, \\\\forall k \\\\in \\\\{1,\\\\dots, K-1\\\\} \\\\\\\\\n& z_k \\\\ge \\\\delta_{k+1},  \\\\quad \\\\forall t \\\\in \\\\{1,\\\\dots, T\\\\}, \\\\forall k \\\\in \\\\{1,\\\\dots, K-1\\\\} \\\\\\\\\n\\\\end{align*}\n```\n\"\"\"\nstruct InterpolationVoltageConstraints <: ConstraintType end\n\n\"\"\"\nStruct to create the Quadratic PWL interpolation constraints that decide square value of the current.\nIn this case x = current and y = squared_current.\nThe specified constraints are formulated as:\n```math\n\\\\begin{align*}\n& x = x_0 + \\\\sum_{k=1}^K (x_{k} - x_{k-1}) \\\\delta_k,  \\\\quad \\\\forall t \\\\in \\\\{1,\\\\dots, T\\\\} \\\\\\\\\n& y = y_0 + \\\\sum_{k=1}^K (x_{k} - x_{k-1}) \\\\delta_k,  \\\\quad \\\\forall t \\\\in \\\\{1,\\\\dots, T\\\\} \\\\\\\\\n& z_k \\\\le \\\\delta_k,  \\\\quad \\\\forall t \\\\in \\\\{1,\\\\dots, T\\\\}, \\\\forall k \\\\in \\\\{1,\\\\dots, K-1\\\\} \\\\\\\\\n& z_k \\\\ge \\\\delta_{k+1},  \\\\quad \\\\forall t \\\\in \\\\{1,\\\\dots, T\\\\}, \\\\forall k \\\\in \\\\{1,\\\\dots, K-1\\\\} \\\\\\\\\n\\\\end{align*}\n```\n\"\"\"\nstruct InterpolationCurrentConstraints <: ConstraintType end\n\n\"\"\"\nStruct to create the Quadratic PWL interpolation constraints that decide square value of the bilinear variable γ.\nIn this case x = γ and y = squared_γ.\nThe specified constraints are formulated as:\n```math\n\\\\begin{align*}\n& x = x_0 + \\\\sum_{k=1}^K (x_{k} - x_{k-1}) \\\\delta_k,  \\\\quad \\\\forall t \\\\in \\\\{1,\\\\dots, T\\\\} \\\\\\\\\n& y = y_0 + \\\\sum_{k=1}^K (x_{k} - x_{k-1}) \\\\delta_k,  \\\\quad \\\\forall t \\\\in \\\\{1,\\\\dots, T\\\\} \\\\\\\\\n& z_k \\\\le \\\\delta_k,  \\\\quad \\\\forall t \\\\in \\\\{1,\\\\dots, T\\\\}, \\\\forall k \\\\in \\\\{1,\\\\dots, K-1\\\\} \\\\\\\\\n& z_k \\\\ge \\\\delta_{k+1},  \\\\quad \\\\forall t \\\\in \\\\{1,\\\\dots, T\\\\}, \\\\forall k \\\\in \\\\{1,\\\\dots, K-1\\\\} \\\\\\\\\n\\\\end{align*}\n```\n\"\"\"\nstruct InterpolationBilinearConstraints <: ConstraintType end\n\n\"\"\"\nStruct to create the constraints that set the absolute value for the current to use in losses through a lossy Interconnecting Power Converter.\nThe specified constraint is formulated as:\n```math\n\\\\begin{align*}\n& i_c^{dc} = i_c^+ - i_c^-, \\\\quad \\\\forall t \\\\in \\\\{1,\\\\dots, T\\\\}  \\\\\\\\\n& i_c^+ \\\\le I_{max} \\\\cdot \\\\nu_c,  \\\\quad \\\\forall t \\\\in \\\\{1,\\\\dots, T\\\\}  \\\\\\\\\n& i_c^+ \\\\le I_{max} \\\\cdot (1 - \\\\nu_c),  \\\\quad \\\\forall t \\\\in \\\\{1,\\\\dots, T\\\\}  \n\\\\end{align*}\n```\n\"\"\"\nstruct CurrentAbsoluteValueConstraint <: ConstraintType end\n\n\"\"\"\nStruct to create the constraint to balance shifted power over the user-defined time horizons.\nFor more information check the [`PowerLoadShift`](@ref) formulation.\nThe specified constraints are formulated as:\n```math\n\\\\sum_{t \\\\in \\\\text{time horizon}_k } p_t^\\\\text{shift,up} - p_t^\\\\text{shift,dn} = 0 , \\\\quad \\\\forall k \\\\text{ time horizons}\n```\n\"\"\"\nstruct ShiftedActivePowerBalanceConstraint <: ConstraintType end\n\n\"\"\"\nStruct to create the constraint to balance shifted power over the user-defined time horizons.\nFor more information check the [`PowerLoadShift`](@ref) formulation.\nThe specified constraints are formulated as:\n```math\np_t^\\\\text{realized} \\\\ge 0.0 , \\\\quad \\\\forall k \\\\text{ time horizons}\n```\n\"\"\"\nstruct RealizedShiftedLoadMinimumBoundConstraint <: ConstraintType end\n\n\"\"\"\nStruct to create the non-anticipativity constraint for the [`PowerLoadShift`](@ref) formulation.\nThis enforces that shift up can only occur after an equal or greater amount of shift down has\nalready been committed, preventing the optimizer from shifting load up before it has been\nshifted down. The constraint is formulated as:\n\n```math\n\\\\sum_{\\\\tau=1}^{t} \\\\left( p_\\\\tau^\\\\text{shift,dn} - p_\\\\tau^\\\\text{shift,up} \\\\right) \\\\ge 0,\n\\\\quad \\\\forall t \\\\in \\\\{1,\\\\dots,T\\\\}\n```\n\"\"\"\nstruct NonAnticipativityConstraint <: ConstraintType end\n\n\"\"\"\nStruct to create the constraint to limit shifted power active power between upper and lower bounds.\nFor more information check the [`PowerLoadShift`](@ref) formulation.\nThe specified constraints are formulated as:\n```math\n0 \\\\le p_t^\\\\text{shift, up} \\\\le P_t^\\\\text{upper}, \\\\quad \\\\forall t \\\\in \\\\{1,\\\\dots,T\\\\}\n```\n\"\"\"\nstruct ShiftUpActivePowerVariableLimitsConstraint <: PowerVariableLimitsConstraint end\n\n\"\"\"\nStruct to create the constraint to limit shifted power active power between upper and lower bounds.\nFor more information check the [`PowerLoadShift`](@ref) formulation.\nThe specified constraints are formulated as:\n```math\n0 \\\\le p_t^\\\\text{shift, dn} \\\\le P_t^\\\\text{lower}, \\\\quad \\\\forall t \\\\in \\\\{1,\\\\dots,T\\\\}\n```\n\"\"\"\nstruct ShiftDownActivePowerVariableLimitsConstraint <: PowerVariableLimitsConstraint end\n"
  },
  {
    "path": "src/core/dataset.jl",
    "content": "abstract type AbstractDataset end\n\nget_data_resolution(s::AbstractDataset)::Dates.Millisecond = s.resolution\nget_last_recorded_row(s::AbstractDataset) = s.last_recorded_row\n\n\"\"\"\nReturn the timestamp from the data used in the last update\n\"\"\"\nget_update_timestamp(s::AbstractDataset) = s.update_timestamp\n\nfunction set_last_recorded_row!(s::AbstractDataset, val::Int)\n    s.last_recorded_row = val\n    return\nend\n\nfunction set_update_timestamp!(s::AbstractDataset, val::Dates.DateTime)\n    s.update_timestamp = val\n    return\nend\n\n# Values field is accessed with dot syntax to avoid type instability\n\nmutable struct InMemoryDataset{N} <: AbstractDataset\n    \"Data with dimensions (N column names, row indexes)\"\n    values::DenseAxisArray{Float64, N}\n    # We use Array here to allow for overwrites when updating the state\n    timestamps::Vector{Dates.DateTime}\n    # Resolution is needed because AbstractDataset might have just one row\n    resolution::Dates.Millisecond\n    end_of_step_index::Int\n    last_recorded_row::Int\n    update_timestamp::Dates.DateTime\nend\n\nfunction InMemoryDataset(\n    values::DenseAxisArray{Float64, N},\n    timestamps::Vector{Dates.DateTime},\n    resolution::Dates.Millisecond,\n    end_of_step_index::Int,\n) where {N}\n    return InMemoryDataset{N}(\n        values,\n        timestamps,\n        resolution,\n        end_of_step_index,\n        0,\n        UNSET_INI_TIME,\n    )\nend\n\nfunction InMemoryDataset(values::DenseAxisArray{Float64, N}) where {N}\n    return InMemoryDataset{N}(\n        values,\n        Vector{Dates.DateTime}(),\n        Dates.Second(0.0),\n        1,\n        0,\n        UNSET_INI_TIME,\n    )\nend\n\n# Helper method for one dimensional cases\nfunction InMemoryDataset(\n    fill_val::Float64,\n    initial_time::Dates.DateTime,\n    resolution::Dates.Millisecond,\n    end_of_step_index::Int,\n    row_count::Int,\n    column_names::Vector{String})\n    return InMemoryDataset(\n        fill_val,\n        initial_time,\n        resolution,\n        end_of_step_index,\n        row_count,\n        (column_names,),\n    )\nend\n\nfunction InMemoryDataset(\n    fill_val::Float64,\n    initial_time::Dates.DateTime,\n    resolution::Dates.Millisecond,\n    end_of_step_index::Int,\n    row_count::Int,\n    column_names::NTuple{N, <:Any}) where {N}\n    return InMemoryDataset(\n        fill!(\n            DenseAxisArray{Float64}(undef, column_names..., 1:row_count),\n            fill_val,\n        ),\n        collect(\n            range(\n                initial_time;\n                step = resolution,\n                length = row_count,\n            ),\n        ),\n        resolution,\n        end_of_step_index,\n    )\nend\n\nget_num_rows(s::InMemoryDataset{N}) where {N} = size(s.values)[N]\n\nfunction make_system_state(\n    timestamp::Dates.DateTime,\n    resolution::Dates.Millisecond,\n    columns::NTuple{N, <:Any},\n) where {N}\n    return InMemoryDataset(NaN, timestamp, resolution, 0, 1, columns)\nend\n\nfunction get_dataset_value(\n    s::T,\n    date::Dates.DateTime,\n) where {T <: Union{InMemoryDataset{1}, InMemoryDataset{2}}}\n    s_index = find_timestamp_index(s.timestamps, date)\n    if isnothing(s_index)\n        error(\"Request time stamp $date not in the state\")\n    end\n    return s.values[:, s_index]\nend\n\nfunction get_dataset_value(s::InMemoryDataset{3}, date::Dates.DateTime)\n    s_index = find_timestamp_index(s.timestamps, date)\n    if isnothing(s_index)\n        error(\"Request time stamp $date not in the state\")\n    end\n    return s.values[:, :, s_index]\nend\n\nfunction get_column_names(k::OptimizationContainerKey, s::InMemoryDataset)\n    return get_column_names_from_axis_array(k, s.values)\nend\n\nfunction get_last_recorded_value(s::InMemoryDataset{2})\n    if get_last_recorded_row(s) == 0\n        error(\"The Dataset hasn't been written yet\")\n    end\n    return s.values[:, get_last_recorded_row(s)]\nend\n\nfunction get_last_recorded_value(s::InMemoryDataset{3})\n    if get_last_recorded_row(s) == 0\n        error(\"The Dataset hasn't been written yet\")\n    end\n    return s.values[:, :, get_last_recorded_row(s)]\nend\n\nfunction get_end_of_step_timestamp(s::InMemoryDataset)\n    return s.timestamps[s.end_of_step_index]\nend\n\n\"\"\"\nReturn 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`\n\"\"\"\nfunction get_last_updated_timestamp(s::InMemoryDataset)\n    last_recorded_row = get_last_recorded_row(s)\n    if last_recorded_row == 0\n        return UNSET_INI_TIME\n    end\n    return s.timestamps[last_recorded_row]\nend\n\nfunction get_value_timestamp(s::InMemoryDataset, date::Dates.DateTime)\n    s_index = find_timestamp_index(s.timestamps, date)\n    if isnothing(s_index)\n        error(\"Request time stamp $date not in the state\")\n    end\n    return s.timestamps[s_index]\nend\n\n# These set_value! methods expect a single time_step value because they are used to update\n#the state so the incoming vals will have one dimension less than the DataSet. The exception\n# is for vals of Dimension 1 which are still stored in DataSets of dimension 2.\nfunction set_value!(s::InMemoryDataset{2}, vals::DenseAxisArray{Float64, 2}, index::Int)\n    s.values[:, index] = vals[:, index]\n    return\nend\n\nfunction set_value!(s::InMemoryDataset{2}, vals::DenseAxisArray{Float64, 1}, index::Int)\n    s.values[:, index] = vals\n    return\nend\n\nfunction set_value!(s::InMemoryDataset{3}, vals::DenseAxisArray{Float64, 2}, index::Int)\n    s.values[:, :, index] = vals\n    return\nend\n\nfunction set_value!(s::InMemoryDataset{2}, vals::Array{Float64, 1}, index::Int)\n    s.values[:, index] = vals\n    return\nend\n\n# HDF5Dataset does not account of overwrites in the data. Values are written sequentially.\nmutable struct HDF5Dataset{N} <: AbstractDataset\n    values::HDF5.Dataset\n    column_dataset::HDF5.Dataset\n    write_index::Int\n    last_recorded_row::Int\n    resolution::Dates.Millisecond\n    initial_timestamp::Dates.DateTime\n    update_timestamp::Dates.DateTime\n    column_names::NTuple{N, Vector{String}}\n\n    function HDF5Dataset{N}(values,\n        column_dataset,\n        write_index,\n        last_recorded_row,\n        resolution,\n        initial_timestamp,\n        update_timestamp,\n        column_names::NTuple{N, Vector{String}},\n    ) where {N}\n        new{N}(values, column_dataset, write_index, last_recorded_row, resolution,\n            initial_timestamp,\n            update_timestamp, column_names)\n    end\nend\n\nfunction HDF5Dataset{1}(\n    values::HDF5.Dataset,\n    column_dataset::HDF5.Dataset,\n    ::NTuple{1, Int},\n    resolution::Dates.Millisecond,\n    initial_time::Dates.DateTime,\n)\n    HDF5Dataset{1}(\n        values,\n        column_dataset,\n        1,\n        0,\n        resolution,\n        initial_time,\n        UNSET_INI_TIME,\n        (column_dataset[:],),\n    )\nend\n\nfunction HDF5Dataset{2}(\n    values::HDF5.Dataset,\n    column_dataset::HDF5.Dataset,\n    column_lengths::NTuple{2, Int},\n    resolution::Dates.Period,\n    initial_time::Dates.DateTime,\n)\n    # The indexing is done in this way because we save all the names in an\n    # adjacent column entry in the HDF5 Datatset. The indexes for each column\n    # are known because we know how many elements are in each dimension.\n    # the names for the first column are store in the 1:first_column_number_of_elements.\n    col1 = column_dataset[1:column_lengths[1]]\n    # the names for the second column are store in the first_column_number_of elements + 1:end of the column with the names.\n    col2 = column_dataset[(column_lengths[1] + 1):end]\n    HDF5Dataset{2}(\n        values,\n        column_dataset,\n        1,\n        0,\n        resolution,\n        initial_time,\n        UNSET_INI_TIME,\n        (col1, col2),\n    )\nend\n\nfunction get_column_names(::OptimizationContainerKey, s::HDF5Dataset)\n    return s.column_names\nend\n\n\"\"\"\nReturn 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`\n\"\"\"\nfunction get_last_updated_timestamp(s::HDF5Dataset)\n    last_recorded_row = get_last_recorded_row(s)\n    if last_recorded_row == 0\n        return UNSET_INI_TIME\n    end\n    return s.initial_timestamp + s.resolution * (last_recorded_row - 1)\nend\n\nfunction get_value_timestamp(s::HDF5Dataset, date::Dates.DateTime)\n    error(\"Not implemented for HDF5Dataset. Required if it is used for simulation state.\")\n    # TODO: This code is broken because timestamps is not a field.\n    #s_index = find_timestamp_index(s.timestamps, date)\n    #if isnothing(s_index)\n    #    error(\"Request time stamp $date not in the state\")\n    #end\n    #return s.initial_timestamp + s.resolution * (s_index - 1)\nend\n\nfunction set_value!(s::HDF5Dataset, vals, index::Int)\n    # Temporary while there is no implementation of caching of em_data\n    _write_dataset!(s.values, vals, index)\n    return\nend\n"
  },
  {
    "path": "src/core/dataset_container.jl",
    "content": "struct DatasetContainer{T}\n    duals::Dict{ConstraintKey, T}\n    aux_variables::Dict{AuxVarKey, T}\n    variables::Dict{VariableKey, T}\n    parameters::Dict{ParameterKey, T}\n    expressions::Dict{ExpressionKey, T}\nend\n\nfunction DatasetContainer{T}() where {T <: AbstractDataset}\n    return DatasetContainer(\n        Dict{ConstraintKey, T}(),\n        Dict{AuxVarKey, T}(),\n        Dict{VariableKey, T}(),\n        Dict{ParameterKey, T}(),\n        Dict{ExpressionKey, T}(),\n    )\nend\n\nfunction Base.empty!(container::DatasetContainer)\n    for field in fieldnames(DatasetContainer)\n        field_dict = getfield(container, field)\n        for val in values(field_dict)\n            empty!(val)\n        end\n    end\n    @debug \"Emptied the store\" _group = LOG_GROUP_SIMULATION_STORE\n    return\nend\n\nfunction get_duals_values(container::DatasetContainer{InMemoryDataset})\n    return container.duals\nend\n\nfunction get_aux_variables_values(container::DatasetContainer{InMemoryDataset})\n    return container.aux_variables\nend\n\nfunction get_variables_values(container::DatasetContainer{InMemoryDataset})\n    return container.variables\nend\n\nfunction get_parameters_values(container::DatasetContainer{InMemoryDataset})\n    return container.parameters\nend\n\nfunction get_expression_values(container::DatasetContainer{InMemoryDataset})\n    return container.expressions\nend\n\nfunction get_duals_values(::DatasetContainer{HDF5Dataset})\n    error(\"Operation not allowed on a HDF5Dataset.\")\nend\n\nfunction get_aux_variables_values(::DatasetContainer{HDF5Dataset})\n    error(\"Operation not allowed on a HDF5Dataset.\")\nend\n\nfunction get_variables_values(::DatasetContainer{HDF5Dataset})\n    error(\"Operation not allowed on a HDF5Dataset.\")\nend\n\nfunction get_parameters_values(::DatasetContainer{HDF5Dataset})\n    error(\"Operation not allowed on a HDF5Dataset.\")\nend\n\nfunction get_expression_values(::DatasetContainer{HDF5Dataset})\n    error(\"Operation not allowed on a HDF5Dataset.\")\nend\n\nfunction get_dataset_keys(container::DatasetContainer)\n    return Iterators.flatten(\n        keys(getfield(container, f)) for f in fieldnames(DatasetContainer)\n    )\nend\n\nfunction get_dataset(container::DatasetContainer, key::OptimizationContainerKey)\n    datasets = getfield(container, get_store_container_type(key))\n    return datasets[key]\nend\n\nfunction set_dataset!(\n    container::DatasetContainer{T},\n    key::OptimizationContainerKey,\n    val::T,\n) where {T <: AbstractDataset}\n    datasets = getfield(container, get_store_container_type(key))\n    datasets[key] = val\n    return\nend\n\nfunction has_dataset(container::DatasetContainer, key::OptimizationContainerKey)\n    datasets = getfield(container, get_store_container_type(key))\n    return haskey(datasets, key)\nend\n\nfunction get_dataset(\n    container::DatasetContainer,\n    ::T,\n    ::Type{U},\n) where {T <: ConstraintType, U <: Union{PSY.Component, PSY.System}}\n    return get_dataset(container, ConstraintKey(T, U))\nend\n\nfunction get_dataset(\n    container::DatasetContainer,\n    ::T,\n    ::Type{U},\n) where {T <: VariableType, U <: Union{PSY.Component, PSY.System}}\n    return get_dataset(container, VariableKey(T, U))\nend\n\nfunction get_dataset(\n    container::DatasetContainer,\n    ::T,\n    ::Type{U},\n) where {T <: AuxVariableType, U <: Union{PSY.Component, PSY.System}}\n    return get_dataset(container, AuxVarKey(T, U))\nend\n\nfunction get_dataset(\n    container::DatasetContainer,\n    ::T,\n    ::Type{U},\n) where {T <: ParameterType, U <: Union{PSY.Component, PSY.System}}\n    return get_dataset(container, ParameterKey(T, U))\nend\n\nfunction get_dataset(\n    container::DatasetContainer,\n    ::T,\n    ::Type{U},\n) where {T <: ExpressionType, U <: Union{PSY.Component, PSY.System}}\n    return get_dataset(container, ExpressionKey(T, U))\nend\n\nfunction get_dataset_values(container::DatasetContainer, key::OptimizationContainerKey)\n    return get_dataset(container, key).values\nend\n\nfunction get_dataset_values(\n    container::DatasetContainer,\n    ::T,\n    ::Type{U},\n) where {T <: ConstraintType, U <: Union{PSY.Component, PSY.System}}\n    return get_dataset_values(container, ConstraintKey(T, U))\nend\n\nfunction get_dataset_values(\n    container::DatasetContainer,\n    ::T,\n    ::Type{U},\n) where {T <: VariableType, U <: Union{PSY.Component, PSY.System}}\n    return get_dataset_values(container, VariableKey(T, U))\nend\n\nfunction get_dataset_values(\n    container::DatasetContainer,\n    ::T,\n    ::Type{U},\n) where {T <: AuxVariableType, U <: Union{PSY.Component, PSY.System}}\n    return get_dataset_values(container, AuxVarKey(T, U))\nend\n\nfunction get_dataset_values(\n    container::DatasetContainer,\n    ::T,\n    ::Type{U},\n) where {T <: ParameterType, U <: Union{PSY.Component, PSY.System}}\n    return get_dataset_values(container, ParameterKey(T, U))\nend\n\nfunction get_dataset_values(\n    container::DatasetContainer,\n    ::T,\n    ::Type{U},\n) where {T <: ExpressionType, U <: Union{PSY.Component, PSY.System}}\n    return get_dataset_values(container, ExpressionKey(T, U))\nend\n\nfunction get_dataset_values(\n    container::DatasetContainer,\n    key::OptimizationContainerKey,\n    date::Dates.DateTime,\n)\n    return get_dataset_value(get_dataset(container, key), date)\nend\n\nfunction get_last_recorded_row(\n    container::DatasetContainer,\n    key::OptimizationContainerKey,\n)\n    return get_last_recorded_row(get_dataset(container, key))\nend\n\n\"\"\"\nReturn the timestamp from the data used in the last update\n\"\"\"\nfunction get_update_timestamp(container::DatasetContainer, key::OptimizationContainerKey)\n    return get_update_timestamp(get_dataset(container, key))\nend\n\n\"\"\"\nReturn 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`\n\"\"\"\nfunction get_last_updated_timestamp(\n    container::DatasetContainer,\n    key::OptimizationContainerKey,\n)\n    return get_last_updated_timestamp(get_dataset(container, key))\nend\n\nfunction get_last_update_value(\n    container::DatasetContainer,\n    key::OptimizationContainerKey,\n)\n    return get_last_recorded_value(get_dataset(container, key))\nend\n\nfunction set_dataset_values!(\n    container::DatasetContainer,\n    key::OptimizationContainerKey,\n    index::Int,\n    vals,\n)\n    set_value!(get_dataset(container, key), vals, index)\n    return\nend\n"
  },
  {
    "path": "src/core/definitions.jl",
    "content": "#################################################################################\n# Type Alias for long type signatures\nconst MinMax = NamedTuple{(:min, :max), NTuple{2, Float64}}\nconst NamedMinMax = Tuple{String, MinMax}\nconst UpDown = NamedTuple{(:up, :down), NTuple{2, Float64}}\nconst InOut = NamedTuple{(:in, :out), NTuple{2, Float64}}\n\nconst BUILD_PROBLEMS_TIMER = TimerOutputs.TimerOutput()\nconst RUN_OPERATION_MODEL_TIMER = TimerOutputs.TimerOutput()\nconst RUN_SIMULATION_TIMER = TimerOutputs.TimerOutput()\n\n# Type Alias for JuMP containers\nconst JuMPOrFloat = Union{JuMP.AbstractJuMPScalar, Float64}\nconst GAE = JuMP.GenericAffExpr{Float64, JuMP.VariableRef}\nconst JuMPAffineExpressionArray = Matrix{GAE}\nconst JuMPAffineExpressionVector = Vector{GAE}\nconst JuMPConstraintArray = DenseAxisArray{JuMP.ConstraintRef}\nconst JuMPAffineExpressionDArrayStringInt = JuMP.Containers.DenseAxisArray{\n    JuMP.AffExpr,\n    2,\n    Tuple{Vector{String}, UnitRange{Int64}},\n    Tuple{\n        JuMP.Containers._AxisLookup{Dict{String, Int64}},\n        JuMP.Containers._AxisLookup{Tuple{Int64, Int64}},\n    },\n}\nconst JuMPAffineExpressionDArrayIntInt = JuMP.Containers.DenseAxisArray{\n    JuMP.AffExpr,\n    2,\n    Tuple{Vector{Int64}, UnitRange{Int64}},\n    Tuple{\n        JuMP.Containers._AxisLookup{Dict{Int64, Int64}},\n        JuMP.Containers._AxisLookup{Tuple{Int64, Int64}},\n    },\n}\n\nconst JuMPVariableTensor{N} = DenseAxisArray{\n    JuMP.VariableRef,\n    N,\n    <:Tuple{Vector{String}, Vararg{Any}},  # last element is always UnitRange{Int64} but we cannot specify anything after the Vararg\n    <:Tuple{\n        JuMP.Containers._AxisLookup{Dict{String, Int64}},\n        Vararg{JuMP.Containers._AxisLookup{<:Any}},\n        # last element is always JuMP.Containers._AxisLookup{Tuple{Int64, Int64}} but we cannot specify anything after the Vararg\n    },\n}\n\nconst JuMPFloatMatrix = DenseAxisArray{Float64, 2}\nconst JuMPFloatArray = DenseAxisArray{Float64}\nconst JuMPVariableArray = DenseAxisArray{JuMP.VariableRef}\nconst JumpSupportedLiterals =\n    Union{Number, Vector{<:Tuple{Number, Number}}, Tuple{Vararg{Number}}}\n\n# Settings constants\nconst UNSET_HORIZON = Dates.Millisecond(0)\nconst UNSET_RESOLUTION = Dates.Millisecond(0)\nconst UNSET_INTERVAL = Dates.Millisecond(0)\nconst UNSET_INI_TIME = Dates.DateTime(0)\n\n# Tolerance of comparisons\n# MIP gap tolerances in most solvers are set to 1e-4\nconst ABSOLUTE_TOLERANCE = 1.0e-3\nconst BALANCE_SLACK_COST = 1e6\nconst CONSTRAINT_VIOLATION_SLACK_COST = 2e5\nconst SERVICES_SLACK_COST = 1e5\nconst COST_EPSILON = 1e-3\nconst PTDF_ZERO_TOL = 1e-9\nconst MISSING_INITIAL_CONDITIONS_TIME_COUNT = 999.0\nconst SECONDS_IN_MINUTE = 60.0\nconst MINUTES_IN_HOUR = 60.0\nconst SECONDS_IN_HOUR = 3600.0\nconst MILLISECONDS_IN_HOUR = 3600000.0\nconst HOURS_IN_WEEK = 168.0\nconst MAX_START_STAGES = 3\nconst OBJECTIVE_FUNCTION_POSITIVE = 1.0\nconst OBJECTIVE_FUNCTION_NEGATIVE = -1.0\nconst INITIALIZATION_PROBLEM_HORIZON_COUNT = 3\n# The DEFAULT_RESERVE_COST value is used to avoid degeneracy of the solutions, reserve cost isn't provided.\nconst DEFAULT_RESERVE_COST = 1.0\nconst DEFAULT_INTERPOLATION_LENGTH = 4\nconst KiB = 1024\nconst MiB = KiB * KiB\nconst GiB = MiB * KiB\n\nconst PSI_NAME_DELIMITER = \"__\"\n\nconst M_VALUE = 1e6\n\nconst NO_SERVICE_NAME_PROVIDED = \"\"\nconst UPPER_BOUND = \"ub\"\nconst LOWER_BOUND = \"lb\"\nconst MAX_OPTIMIZE_TRIES = 2\n\n# File Names definitions\nconst PROBLEM_LOG_FILENAME = \"operation_problem.log\"\nconst SIMULATION_LOG_FILENAME = \"simulation.log\"\nconst REQUIRED_RECORDERS = (:simulation_status, :execution)\nconst KNOWN_SIMULATION_PATHS = [\n    \"data_store\",\n    \"logs\",\n    \"models_json\",\n    \"problems\",\n    \"recorder\",\n    \"results\",\n    \"simulation_files\",\n    \"simulation_partitions\",\n]\n\"If the name of an extraneous file that appears in simulation results matches one of these regexes, it is safe to ignore\"\nconst IGNORABLE_FILES = [\n    r\"^\\.DS_Store$\",\n    r\"^\\.Trashes$\",\n    r\"^\\.Trash-.*$\",\n    r\"^\\.nfs.*$\",\n    r\"^[Dd]esktop.ini$\",\n]\nconst RESULTS_DIR = \"results\"\n\n# Enums\nModelBuildStatus = ISOPT.ModelBuildStatus\nSimulationBuildStatus = IS.Simulation.SimulationBuildStatus\n\nRunStatus = IS.Simulation.RunStatus\n\nIS.@scoped_enum(SOSStatusVariable, NO_VARIABLE = 1, PARAMETER = 2, VARIABLE = 3,)\n\nIS.@scoped_enum(COMPACT_PWL_STATUS, VALID = 1, INVALID = 2, UNDETERMINED = 3)\n\nconst ENUMS = (ModelBuildStatus, SimulationBuildStatus, RunStatus, SOSStatusVariable)\n\nconst ENUM_MAPPINGS = Dict()\n\nfor enum in ENUMS\n    ENUM_MAPPINGS[enum] = Dict()\n    for value in instances(enum)\n        ENUM_MAPPINGS[enum][lowercase(string(value))] = value\n    end\nend\n\n# Special cases for backwards compatibility\nENUM_MAPPINGS[RunStatus][\"ready\"] = RunStatus.INITIALIZED\nENUM_MAPPINGS[RunStatus][\"successful\"] = RunStatus.SUCCESSFULLY_FINALIZED\n\n\"\"\"\nGet the enum value for the string. Case insensitive.\n\"\"\"\nfunction get_enum_value(enum, value::String)\n    if !haskey(ENUM_MAPPINGS, enum)\n        throw(ArgumentError(\"enum=$enum is not valid\"))\n    end\n\n    val = lowercase(value)\n    if !haskey(ENUM_MAPPINGS[enum], val)\n        throw(ArgumentError(\"enum=$enum does not have value=$val\"))\n    end\n\n    return ENUM_MAPPINGS[enum][val]\nend\n\nBase.convert(::Type{SimulationBuildStatus}, val::String) =\n    get_enum_value(SimulationBuildStatus, val)\nBase.convert(::Type{ModelBuildStatus}, val::String) = get_enum_value(ModelBuildStatus, val)\nBase.convert(::Type{RunStatus}, val::String) = get_enum_value(RunStatus, val)\nBase.convert(::Type{SOSStatusVariable}, x::String) = get_enum_value(SOSStatusVariable, x)\n"
  },
  {
    "path": "src/core/device_model.jl",
    "content": "\"\"\"\nFormulation type to augment the power balance constraint expression with a time series parameter\n\"\"\"\nstruct FixedOutput <: AbstractDeviceFormulation end\n\nfunction _check_device_formulation(\n    ::Type{D},\n) where {D <: Union{AbstractDeviceFormulation, PSY.Device}}\n    if !isconcretetype(D)\n        throw(\n            ArgumentError(\n                \"The device model must contain only concrete types, $(D) is an Abstract Type\",\n            ),\n        )\n    end\nend\n\n\"\"\"\n    DeviceModel(\n        ::Type{D},\n        ::Type{B},\n        feedforwards::Vector{<:AbstractAffectFeedforward}\n        use_slacks::Bool,\n        duals::Vector{DataType},\n        services::Vector{ServiceModel}\n        attributes::Dict{String, Any}\n    )\n\nEstablishes the model for a particular device specified by type. Uses the keyword argument\nfeedforward to enable passing values between operation model at simulation time\n\n# Arguments\n\n  - `::Type{D} where D<:PSY.Device`: Power System Device Type\n  - `::Type{B} where B<:AbstractDeviceFormulation`: Abstract Device Formulation\n  - `feedforward::Array{<:AbstractAffectFeedforward} = Vector{AbstractAffectFeedforward}()` : use to pass parameters between models\n  - `use_slacks::Bool = false` : Add slacks to the device model. Implementation is model dependent and not all models feature slacks\n  - `duals::Vector{DataType} = Vector{DataType}()`: use to pass constraint type to calculate the duals. The DataType needs to be a valid ConstraintType\n  - `time_series_names::Dict{Type{<:TimeSeriesParameter}, String} = get_default_time_series_names(D, B)` : use to specify time series names associated to the device`\n  - `attributes::Dict{String, Any} = get_default_attributes(D, B)` : use to specify attributes to the device\n\n# Example\n```julia\nthermal_gens = DeviceModel(ThermalStandard, ThermalBasicUnitCommitment)\n```\n\"\"\"\nmutable struct DeviceModel{D <: PSY.Device, B <: AbstractDeviceFormulation}\n    feedforwards::Vector{<:AbstractAffectFeedforward}\n    use_slacks::Bool\n    duals::Vector{DataType}\n    services::Vector{ServiceModel}\n    time_series_names::Dict{Type{<:ParameterType}, String}\n    attributes::Dict{String, Any}\n    subsystem::Union{Nothing, String}\n    events::Dict{EventKey, EventModel}\n    device_cache::Vector{D}\n    function DeviceModel(\n        ::Type{D},\n        ::Type{B};\n        feedforwards = Vector{AbstractAffectFeedforward}(),\n        use_slacks = false,\n        duals = Vector{DataType}(),\n        time_series_names = get_default_time_series_names(D, B),\n        attributes = Dict{String, Any}(),\n    ) where {D <: PSY.Device, B <: AbstractDeviceFormulation}\n        attributes_ = get_default_attributes(D, B)\n        for (k, v) in attributes\n            attributes_[k] = v\n        end\n\n        _check_device_formulation(D)\n        _check_device_formulation(B)\n        new{D, B}(\n            feedforwards,\n            use_slacks,\n            duals,\n            Vector{ServiceModel}(),\n            time_series_names,\n            attributes_,\n            nothing,\n            Dict{EventKey, EventModel}(),\n            Vector{D}(),\n        )\n    end\nend\n\nget_component_type(\n    ::DeviceModel{D, B},\n) where {D <: PSY.Device, B <: AbstractDeviceFormulation} = D\nget_events(m::DeviceModel) = m.events\nget_formulation(\n    ::DeviceModel{D, B},\n) where {D <: PSY.Device, B <: AbstractDeviceFormulation} = B\nget_feedforwards(m::DeviceModel) = m.feedforwards\nget_services(m::DeviceModel) = m.services\nget_services(::Nothing) = nothing\nget_use_slacks(m::DeviceModel) = m.use_slacks\nget_duals(m::DeviceModel) = m.duals\nget_time_series_names(m::DeviceModel) = m.time_series_names\nget_attributes(m::DeviceModel) = m.attributes\nget_attribute(::Nothing, ::String) = nothing\nget_attribute(m::DeviceModel, key::String) = get(m.attributes, key, nothing)\nget_subsystem(m::DeviceModel) = m.subsystem\nget_device_cache(m::DeviceModel) = m.device_cache\n\nset_subsystem!(m::DeviceModel, id::String) = m.subsystem = id\n\nfunction _set_model!(\n    dict::Dict,\n    model::DeviceModel{D, B},\n) where {D <: PSY.Device, B <: AbstractDeviceFormulation}\n    key = Symbol(D)\n    if haskey(dict, key)\n        @warn \"Overwriting $(D) existing model\"\n    end\n    dict[key] = model\n    return\nend\n\nhas_service_model(model::DeviceModel) = !isempty(get_services(model))\n\nfunction set_event_model!(\n    model::DeviceModel{D, B},\n    key::EventKey,\n    event_model::EventModel,\n) where {D <: PSY.Device, B <: AbstractDeviceFormulation}\n    if haskey(model.events, key)\n        error(\"EventModel $key already exists in model for device $D\")\n    end\n    model.events[key] = event_model\n    return\nend\n"
  },
  {
    "path": "src/core/dual_processing.jl",
    "content": "struct VarRestoreInfo{A <: AbstractArray}\n    lb::Union{Nothing, A}\n    ub::Union{Nothing, A}\n    fixed_int_value::Union{Nothing, A}\n    is_integer::Bool\nend\n\n_first_element(v::DenseAxisArray) = first(v)\n_first_element(v::SparseAxisArray) = first(values(v.data))\n\nfunction _round_cache_values!(cache::DenseAxisArray)\n    cache.data .= round.(cache.data)\n    return\nend\n\nfunction _round_cache_values!(cache::SparseAxisArray)\n    for k in keys(cache.data)\n        cache.data[k] = round(cache.data[k])\n    end\n    return\nend\n\nfunction process_duals(container::OptimizationContainer, lp_optimizer)\n    var_container = get_variables(container)\n    for (k, v) in var_container\n        container.primal_values_cache.variables_cache[k] = jump_value.(v)\n    end\n\n    for (k, v) in get_expressions(container)\n        container.primal_values_cache.expressions_cache[k] = jump_value.(v)\n    end\n    var_cache = container.primal_values_cache.variables_cache\n    cache = sizehint!(Dict{VariableKey, VarRestoreInfo}(), length(var_container))\n    for (key, variable) in var_container\n        is_integer_flag = false\n        elem = _first_element(variable)\n        if JuMP.is_binary(elem)\n            JuMP.unset_binary.(variable)\n        elseif JuMP.is_integer(elem)\n            JuMP.unset_integer.(variable)\n            is_integer_flag = true\n        else\n            continue\n        end\n        lb = if JuMP.has_lower_bound(elem)\n            JuMP.lower_bound.(variable)\n        else\n            nothing\n        end\n        ub = if JuMP.has_upper_bound(elem)\n            JuMP.upper_bound.(variable)\n        else\n            nothing\n        end\n        fixed_int_value = if JuMP.is_fixed(elem) && is_integer_flag\n            jump_value.(variable)\n        else\n            nothing\n        end\n        cache[key] =\n            VarRestoreInfo{typeof(var_cache[key])}(lb, ub, fixed_int_value, is_integer_flag)\n        # Round cached values in-place to nearest integer to avoid infeasibilities\n        # from MIP solver numerical tolerances (e.g. 0.9999997 instead of 1.0)\n        _round_cache_values!(var_cache[key])\n        JuMP.fix.(variable, var_cache[key]; force = true)\n    end\n    @assert !isempty(cache)\n    jump_model = get_jump_model(container)\n\n    if JuMP.mode(jump_model) != JuMP.DIRECT\n        JuMP.set_optimizer(jump_model, lp_optimizer)\n    else\n        @debug(\"JuMP model set in direct mode during dual calculation\")\n    end\n\n    JuMP.optimize!(jump_model)\n\n    model_status = JuMP.primal_status(jump_model)\n    if model_status ∉ (\n        MOI.FEASIBLE_POINT::MOI.ResultStatusCode,\n        MOI.NEARLY_FEASIBLE_POINT::MOI.ResultStatusCode,\n    )\n        @error \"Optimizer returned $model_status during dual calculation\"\n        return RunStatus.FAILED\n    end\n\n    if JuMP.has_duals(jump_model)\n        for (key, dual) in get_duals(container)\n            constraint = get_constraint(container, key)\n            dual.data .= jump_value.(constraint).data\n        end\n    end\n\n    for (key, variable) in var_container\n        if !haskey(cache, key)\n            continue\n        end\n        info = cache[key]\n        JuMP.unfix.(variable)\n        if info.lb !== nothing\n            JuMP.set_lower_bound.(variable, info.lb)\n        end\n        if info.ub !== nothing\n            JuMP.set_upper_bound.(variable, info.ub)\n        end\n        if info.is_integer\n            JuMP.set_integer.(variable)\n        else\n            JuMP.set_binary.(variable)\n        end\n        if info.fixed_int_value !== nothing\n            JuMP.fix.(variable, info.fixed_int_value)\n        end\n    end\n    return RunStatus.SUCCESSFULLY_FINALIZED\nend\n"
  },
  {
    "path": "src/core/event_keys.jl",
    "content": "struct EventKey{T <: PSY.Contingency, U <: Union{PSY.Component, PSY.System}}\n    meta::String\nend\n\nfunction EventKey(\n    ::Type{T},\n    ::Type{U},\n) where {T <: PSY.Contingency, U <: Union{PSY.Component, PSY.System}}\n    if isabstracttype(U)\n        error(\"Type $U can't be abstract\")\n    end\n    return EventKey{T, U}(\"\")\nend\n\nget_entry_type(\n    ::EventKey{T, U},\n) where {T <: PSY.Contingency, U <: Union{PSY.Component, PSY.System}} = T\nget_component_type(\n    ::EventKey{T, U},\n) where {T <: PSY.Contingency, U <: Union{PSY.Component, PSY.System}} = U\n"
  },
  {
    "path": "src/core/event_model.jl",
    "content": "abstract type AbstractEventCondition end\n\n\"\"\"\n    ContinuousCondition()\n\nEstablishes an event condition that is triggered at all timesteps.  \n\"\"\"\nstruct ContinuousCondition <: AbstractEventCondition end\n\n\"\"\"\n    PresetTimeCondition(time_stamps::Vector{Dates.DateTime})\n\nEstablishes an event condition that is triggered at pre-determined times.  \n\n# Arguments\n  - `time_stamps::Vector{Dates.DateTime}`: times when event is triggered\n\"\"\"\nstruct PresetTimeCondition <: AbstractEventCondition\n    time_stamps::Vector{Dates.DateTime}\nend\n\nget_time_stamps(c::PresetTimeCondition) = c.time_stamps\n\n\"\"\"\n    StateVariableValueCondition(\n        variable_type::Type{<:VariableType}\n        device_type::Type{<:PSY.Device}\n        device_name::String\n        value::Float64\n    )\n\nEstablishes an event condition that is triggered if a variable of type `variable_type` for a device of type\n`device_type` and name `device_name` is equal to `value`.\nand name \n\n# Arguments\n  - `variable_type::Type{<:VariableType}`: variable to be monitored\n  - `device_type::Type{<:PSY.Device}`: device type to be monitored\n  - `device_name::String`: name of monitored device\n  - `value::Float64`: value to compare to in p.u.\n\"\"\"\nstruct StateVariableValueCondition <: AbstractEventCondition\n    variable_type::VariableType\n    device_type::Type{<:PSY.Device}\n    device_name::String\n    value::Float64\nend\n\nget_variable_type(c::StateVariableValueCondition) = c.variable_type\nget_device_type(c::StateVariableValueCondition) = c.device_type\nget_device_name(c::StateVariableValueCondition) = c.device_name\nget_value(c::StateVariableValueCondition) = c.value\n\n\"\"\"\n    DiscreteEventCondition(condition_function::Function)\n\nEstablishes an event condition that is triggered if when a user defined function evaluates to true.\nThe function should take SimulationState as its only arguement and return true when the event should be triggered and false otherwise.\n\n# Arguments\n  - `condition_function::Function`: user defined function `f(::SimulationState)`to determine if event is triggered.\n\"\"\"\nstruct DiscreteEventCondition <: AbstractEventCondition\n    condition_function::Function\nend\n\nget_condition_function(c::DiscreteEventCondition) = c.condition_function\n\nmutable struct EventModel{D <: PSY.Contingency, B <: AbstractEventCondition}\n    condition::B\n    timeseries_mapping::Dict{Symbol, Union{String, Nothing}}\n    attribute_device_map::Dict{Symbol, Dict{Base.UUID, Dict{DataType, Set{String}}}}\n    attributes::Dict{String, Any}\n\n    function EventModel(\n        contingency_type::Type{D},\n        condition::B;\n        timeseries_mapping = get_empty_timeseries_mapping(contingency_type),\n        attributes = Dict{String, Any}(),\n    ) where {D <: PSY.Contingency, B <: AbstractEventCondition}\n        new{D, B}(\n            condition,\n            timeseries_mapping,\n            Dict{Symbol, Dict{Base.UUID, Dict{DataType, Set{String}}}}(),\n            attributes,\n        )\n    end\nend\n\nfunction get_empty_timeseries_mapping(\n    ::Type{PSY.FixedForcedOutage},\n)\n    return Dict{Symbol, Union{String, Nothing}}(\n        :outage_status => nothing,\n    )\nend\n\nfunction get_empty_timeseries_mapping(\n    ::Type{PSY.GeometricDistributionForcedOutage},\n)\n    return Dict{Symbol, Union{String, Nothing}}(\n        :mean_time_to_recovery => nothing,\n        :outage_transition_probability => nothing,\n    )\nend\n\nget_event_type(\n    ::EventModel{D, B},\n) where {D <: PSY.Contingency, B <: AbstractEventCondition} = D\n\nget_event_condition(\n    e::EventModel{D, B},\n) where {D <: PSY.Contingency, B <: AbstractEventCondition} = e.condition\n\nget_attribute_device_map(e::EventModel) = e.attribute_device_map\n"
  },
  {
    "path": "src/core/expressions.jl",
    "content": "abstract type SystemBalanceExpressions <: ExpressionType end\nabstract type RangeConstraintLBExpressions <: ExpressionType end\nabstract type RangeConstraintUBExpressions <: ExpressionType end\nabstract type CostExpressions <: ExpressionType end\nabstract type PostContingencyExpressions <: ExpressionType end\n\nabstract type PostContingencySystemBalanceExpressions <: SystemBalanceExpressions end\n\nstruct ActivePowerBalance <: SystemBalanceExpressions end\nstruct PostContingencyActivePowerBalance <: PostContingencySystemBalanceExpressions end\nstruct ReactivePowerBalance <: SystemBalanceExpressions end\nstruct EmergencyUp <: ExpressionType end\nstruct EmergencyDown <: ExpressionType end\nstruct RawACE <: ExpressionType end\nstruct ProductionCostExpression <: CostExpressions end\nabstract type ConstituentCostExpression <: CostExpressions end\nstruct FuelCostExpression <: ConstituentCostExpression end\nstruct StartUpCostExpression <: ConstituentCostExpression end\nstruct ShutDownCostExpression <: ConstituentCostExpression end\nstruct FixedCostExpression <: ConstituentCostExpression end\nstruct VOMCostExpression <: ConstituentCostExpression end\nstruct CurtailmentCostExpression <: CostExpressions end\nstruct FuelConsumptionExpression <: ExpressionType end\nstruct ActivePowerRangeExpressionLB <: RangeConstraintLBExpressions end\nstruct ActivePowerRangeExpressionUB <: RangeConstraintUBExpressions end\nstruct ComponentReserveUpBalanceExpression <: ExpressionType end\nstruct ComponentReserveDownBalanceExpression <: ExpressionType end\nstruct InterfaceTotalFlow <: ExpressionType end\nstruct PTDFBranchFlow <: ExpressionType end\nstruct PostContingencyBranchFlow <: PostContingencyExpressions end\nstruct PostContingencyActivePowerGeneration <: PostContingencyExpressions end\nstruct PostContingencyNodalActivePowerDeployment <: PostContingencyExpressions end\nstruct NetActivePower <: ExpressionType end\nstruct RealizedShiftedLoad <: ExpressionType end\n\"\"\"\nStruct for DC current balance in multi-terminal DC networks\n\"\"\"\nstruct DCCurrentBalance <: ExpressionType end\n\nshould_write_resulting_value(::Type{<:CostExpressions}) = true\nshould_write_resulting_value(::Type{FuelConsumptionExpression}) = true\nshould_write_resulting_value(::Type{InterfaceTotalFlow}) = true\nshould_write_resulting_value(::Type{RawACE}) = true\nshould_write_resulting_value(::Type{ActivePowerBalance}) = true\nshould_write_resulting_value(::Type{ReactivePowerBalance}) = true\nshould_write_resulting_value(::Type{DCCurrentBalance}) = true\nshould_write_resulting_value(::Type{PTDFBranchFlow}) = true\nshould_write_resulting_value(::Type{RealizedShiftedLoad}) = true\n#should_write_resulting_value(::Type{PostContingencyBranchFlow}) = true\n#should_write_resulting_value(::Type{PostContingencyActivePowerGeneration}) = true\n\nconvert_result_to_natural_units(::Type{InterfaceTotalFlow}) = true\nconvert_result_to_natural_units(::Type{PostContingencyBranchFlow}) = true\nconvert_result_to_natural_units(::Type{PostContingencyActivePowerGeneration}) = true\nconvert_result_to_natural_units(::Type{PTDFBranchFlow}) = true\nconvert_result_to_natural_units(::Type{RealizedShiftedLoad}) = true\n"
  },
  {
    "path": "src/core/formulations.jl",
    "content": "\"\"\"\nAbstract type for Device Formulations (a.k.a Models)\n\n# Example\n\nimport PowerSimulations as PSI\nstruct MyCustomDeviceFormulation <: PSI.AbstractDeviceFormulation\n\"\"\"\nabstract type AbstractDeviceFormulation end\n\n########################### Thermal Generation Formulations ################################\nabstract type AbstractThermalFormulation <: AbstractDeviceFormulation end\n\nabstract type AbstractThermalDispatchFormulation <: AbstractThermalFormulation end\nabstract type AbstractThermalUnitCommitment <: AbstractThermalFormulation end\n\nabstract type AbstractStandardUnitCommitment <: AbstractThermalUnitCommitment end\nabstract type AbstractCompactUnitCommitment <: AbstractThermalUnitCommitment end\n\n\"\"\"\nFormulation type to enable basic unit commitment representation without any intertemporal (ramp, min on/off time) constraints\n\"\"\"\nstruct ThermalBasicUnitCommitment <: AbstractStandardUnitCommitment end\n\"\"\"\nFormulation type to enable standard unit commitment with intertemporal constraints and simplified startup profiles\n\"\"\"\nstruct ThermalStandardUnitCommitment <: AbstractStandardUnitCommitment end\n\"\"\"\nFormulation type to enable basic dispatch without any intertemporal (ramp) constraints\n\"\"\"\nstruct ThermalBasicDispatch <: AbstractThermalDispatchFormulation end\n\"\"\"\nFormulation type to enable standard dispatch with a range and enforce intertemporal ramp constraints\n\"\"\"\nstruct ThermalStandardDispatch <: AbstractThermalDispatchFormulation end\n\"\"\"\nFormulation type to enable basic dispatch without any intertemporal constraints and relaxed minimum generation. *May not work with non-convex PWL cost definitions*\n\"\"\"\nstruct ThermalDispatchNoMin <: AbstractThermalDispatchFormulation end\n\"\"\"\nFormulation type to enable pg-lib commitment formulation with startup/shutdown profiles\n\"\"\"\nstruct ThermalMultiStartUnitCommitment <: AbstractCompactUnitCommitment end\n\"\"\"\nFormulation type to enable thermal compact commitment\n\"\"\"\nstruct ThermalCompactUnitCommitment <: AbstractCompactUnitCommitment end\n\"\"\"\nFormulation type to enable thermal compact commitment without intertemporal (ramp, min on/off time) constraints\n\"\"\"\nstruct ThermalBasicCompactUnitCommitment <: AbstractCompactUnitCommitment end\n\"\"\"\nFormulation type to enable thermal compact dispatch\n\"\"\"\nstruct ThermalCompactDispatch <: AbstractThermalDispatchFormulation end\n\n############################# Electric Load Formulations ###################################\nabstract type AbstractLoadFormulation <: AbstractDeviceFormulation end\nabstract type AbstractControllablePowerLoadFormulation <: AbstractLoadFormulation end\n\n\"\"\"\nFormulation type to add a time series parameter for non-dispatchable `ElectricLoad` withdrawals to power balance constraints\n\"\"\"\nstruct StaticPowerLoad <: AbstractLoadFormulation end\n\n\"\"\"\nFormulation type to enable (binary) load interruptions\n\"\"\"\nstruct PowerLoadInterruption <: AbstractControllablePowerLoadFormulation end\n\n\"\"\"\nFormulation type to enable (continuous) load interruption dispatch\n\"\"\"\nstruct PowerLoadDispatch <: AbstractControllablePowerLoadFormulation end\n\n\"\"\"\nFormulation type to enable load shifting\n\"\"\"\nstruct PowerLoadShift <: AbstractControllablePowerLoadFormulation end\n\n############################ Regulation Device Formulations ################################\nabstract type AbstractRegulationFormulation <: AbstractDeviceFormulation end\nstruct ReserveLimitedRegulation <: AbstractRegulationFormulation end\nstruct DeviceLimitedRegulation <: AbstractRegulationFormulation end\n\n########################### Renewable Generation Formulations ##############################\nabstract type AbstractRenewableFormulation <: AbstractDeviceFormulation end\nabstract type AbstractRenewableDispatchFormulation <: AbstractRenewableFormulation end\n\n\"\"\"\nFormulation type to add injection variables constrained by a maximum injection time series for `RenewableGen`\n\"\"\"\nstruct RenewableFullDispatch <: AbstractRenewableDispatchFormulation end\n\n\"\"\"\nFormulation type to add real and reactive injection variables with constant power factor with maximum real power injections constrained by a time series for `RenewableGen`\n\"\"\"\nstruct RenewableConstantPowerFactor <: AbstractRenewableDispatchFormulation end\n\n########################### Source Formulations ##############################\nabstract type AbstractSourceFormulation <: AbstractDeviceFormulation end\n\n\"\"\"\nFormulation type to add import and export model for `Source`\n\"\"\"\nstruct ImportExportSourceModel <: AbstractSourceFormulation end\n\n########################### Reactive Power Device Formulations ##############################\nabstract type AbstractReactivePowerDeviceFormulation <: AbstractDeviceFormulation end\n\n\"\"\"\nFormulation type to add reactive power dispatch variables for `SynchronousCondenser`\n\"\"\"\nstruct SynchronousCondenserBasicDispatch <: AbstractReactivePowerDeviceFormulation end\n\n\"\"\"\nAbstract type for Branch Formulations (a.k.a Models)\n\n# Example\nimport PowerSimulations as PSI\nstruct MyCustomBranchFormulation <: PSI.AbstractDeviceFormulation\n\"\"\"\n# Generic Branch Models\nabstract type AbstractBranchFormulation <: AbstractDeviceFormulation end\n\n############################### AC/DC Branch Formulations #####################################\n\"\"\"\nBranch type to add unbounded flow variables and use flow constraints\n\"\"\"\nstruct StaticBranch <: AbstractBranchFormulation end\n\"\"\"\nBranch type to add bounded flow variables and use flow constraints\n\"\"\"\nstruct StaticBranchBounds <: AbstractBranchFormulation end\n\"\"\"\nBranch type to avoid flow constraints\n\"\"\"\nstruct StaticBranchUnbounded <: AbstractBranchFormulation end\n\"\"\"\nBranch formulation for PhaseShiftingTransformer flow control\n\"\"\"\nstruct PhaseAngleControl <: AbstractBranchFormulation end\n\n############################### DC Branch Formulations #####################################\nabstract type AbstractTwoTerminalDCLineFormulation <: AbstractBranchFormulation end\n\"\"\"\nBranch type to avoid flow constraints\n\"\"\"\nstruct HVDCTwoTerminalUnbounded <: AbstractTwoTerminalDCLineFormulation end\n\"\"\"\nBranch type to represent lossless power flow on DC lines\n\"\"\"\nstruct HVDCTwoTerminalLossless <: AbstractTwoTerminalDCLineFormulation end\n\"\"\"\nBranch type to represent lossy power flow on DC lines\n\"\"\"\nstruct HVDCTwoTerminalDispatch <: AbstractTwoTerminalDCLineFormulation end\n\"\"\"\nBranch type to represent piecewise lossy power flow on two terminal DC lines\n\"\"\"\nstruct HVDCTwoTerminalPiecewiseLoss <: AbstractTwoTerminalDCLineFormulation end\n\n\"\"\"\nBranch type to represent non-linear LCC (line commutated converter) model on two-terminal DC lines\n\"\"\"\nstruct HVDCTwoTerminalLCC <: AbstractTwoTerminalDCLineFormulation end\n\n# Not Implemented\n# struct VoltageSourceDC <: AbstractTwoTerminalDCLineFormulation end\n\n############################### AC/DC Converter Formulations #####################################\nabstract type AbstractConverterFormulation <: AbstractDeviceFormulation end\n\n\"\"\"\nLossless InterconnectingConverter Model\n\"\"\"\nstruct LosslessConverter <: AbstractConverterFormulation end\n\n\"\"\"\nLinear Loss InterconnectingConverter Model\n\"\"\"\nstruct LinearLossConverter <: AbstractConverterFormulation end\n\n\"\"\"\nQuadratic Loss InterconnectingConverter Model\n\"\"\"\nstruct QuadraticLossConverter <: AbstractConverterFormulation end\n\n############################## HVDC Lines Formulations ##################################\nabstract type AbstractDCLineFormulation <: AbstractBranchFormulation end\n\n\"\"\"\nLossless Line Abstract Model\n\"\"\"\nstruct DCLosslessLine <: AbstractDCLineFormulation end\n\n\"\"\"\nLossy Line Abstract Model\n\"\"\"\nstruct DCLossyLine <: AbstractDCLineFormulation end\n\n\"\"\"\nLossless Line struct formulation\n\"\"\"\nstruct LosslessLine <: AbstractDCLineFormulation end\n\n############################## HVDC Network Model Formulations ##################################\nabstract type AbstractHVDCNetworkModel end\n\n\"\"\"\nTransport Lossless HVDC network model. No DC voltage variables are added and DC lines are modeled as lossless power transport elements\n\"\"\"\nstruct TransportHVDCNetworkModel <: AbstractHVDCNetworkModel end\n\"\"\"\nDC Voltage HVDC network model, where currents are solved based on DC voltage difference between DC buses\n\"\"\"\nstruct VoltageDispatchHVDCNetworkModel <: AbstractHVDCNetworkModel end\n\n\"\"\"\nAbstract type for Service Formulations (a.k.a Models)\n\n# Example\n\nimport PowerSimulations as PSI\nstruct MyServiceFormulation <: PSI.AbstractServiceFormulation\n\"\"\"\nabstract type AbstractServiceFormulation end\n\nabstract type AbstractReservesFormulation <: AbstractServiceFormulation end\n\nabstract type AbstractSecurityConstrainedReservesFormulation <: AbstractReservesFormulation end\n\nabstract type AbstractAGCFormulation <: AbstractServiceFormulation end\n\nstruct PIDSmoothACE <: AbstractAGCFormulation end\n\n\"\"\"\nStruct to add reserves to be larger than a specified requirement for an aggregated collection of services\n\"\"\"\nstruct GroupReserve <: AbstractReservesFormulation end\n\n\"\"\"\nStruct for to add reserves to be larger than a specified requirement\n\"\"\"\nstruct RangeReserve <: AbstractReservesFormulation end\n\n\"\"\"\nStruct for to add reserves to be larger than a variable requirement depending of costs\n\"\"\"\nstruct StepwiseCostReserve <: AbstractReservesFormulation end\n\"\"\"\nStruct to add reserves to be larger than a specified requirement, with ramp constraints\n\"\"\"\nstruct RampReserve <: AbstractReservesFormulation end\n\"\"\"\nStruct to add non spinning reserve requirements larger than specified requirement\n\"\"\"\nstruct NonSpinningReserve <: AbstractReservesFormulation end\n\"\"\"\nStruct to add a constant maximum transmission flow for specified interface\n\"\"\"\nstruct ConstantMaxInterfaceFlow <: AbstractServiceFormulation end\n\"\"\"\nStruct to add a variable maximum transmission flow for specified interface\n\"\"\"\nstruct VariableMaxInterfaceFlow <: AbstractServiceFormulation end\n"
  },
  {
    "path": "src/core/initial_conditions.jl",
    "content": "\"\"\"\nContainer for the initial condition data\n\"\"\"\nmutable struct InitialCondition{\n    T <: InitialConditionType,\n    U <: Union{JuMP.VariableRef, Float64, Nothing},\n}\n    component::PSY.Component\n    value::U\nend\n\nfunction InitialCondition(\n    ::Type{T},\n    component::PSY.Component,\n    value::U,\n) where {T <: InitialConditionType, U <: Union{JuMP.VariableRef, Float64}}\n    return InitialCondition{T, U}(component, value)\nend\n\nfunction InitialCondition(\n    ::InitialConditionKey{T, U},\n    component::U,\n    value::V,\n) where {\n    T <: InitialConditionType,\n    U <: PSY.Component,\n    V <: Union{JuMP.VariableRef, Float64},\n}\n    return InitialCondition{T, U}(component, value)\nend\n\nfunction get_condition(p::InitialCondition{T, Float64}) where {T <: InitialConditionType}\n    return p.value\nend\n\nfunction get_condition(\n    p::InitialCondition{T, JuMP.VariableRef},\n) where {T <: InitialConditionType}\n    return jump_value(p.value)\nend\n\nfunction get_condition(\n    ::InitialCondition{T, Nothing},\n) where {T <: InitialConditionType}\n    return nothing\nend\n\nget_component(ic::InitialCondition) = ic.component\nget_value(ic::InitialCondition) = ic.value\nget_component_name(ic::InitialCondition) = PSY.get_name(ic.component)\nget_component_type(ic::InitialCondition) = typeof(ic.component)\nget_ic_type(\n    ::Type{InitialCondition{T, U}},\n) where {T <: InitialConditionType, U <: Union{JuMP.VariableRef, Float64, Nothing}} = T\nget_ic_type(\n    ::InitialCondition{T, U},\n) where {T <: InitialConditionType, U <: Union{JuMP.VariableRef, Float64, Nothing}} = T\n\n\"\"\"\nStores data to populate initial conditions before the build call\n\"\"\"\nmutable struct InitialConditionsData\n    duals::Dict{ConstraintKey, AbstractArray}\n    parameters::Dict{ParameterKey, AbstractArray}\n    variables::Dict{VariableKey, AbstractArray}\n    aux_variables::Dict{AuxVarKey, AbstractArray}\nend\n\nfunction InitialConditionsData()\n    return InitialConditionsData(\n        Dict{ConstraintKey, AbstractArray}(),\n        Dict{ParameterKey, AbstractArray}(),\n        Dict{VariableKey, AbstractArray}(),\n        Dict{AuxVarKey, AbstractArray}(),\n    )\nend\n\nfunction get_initial_condition_value(\n    ic_data::InitialConditionsData,\n    ::T,\n    ::Type{U},\n) where {T <: VariableType, U <: Union{PSY.Component, PSY.System}}\n    return ic_data.variables[VariableKey(T, U)]\nend\n\nfunction get_initial_condition_value(\n    ic_data::InitialConditionsData,\n    ::T,\n    ::Type{U},\n) where {T <: AuxVariableType, U <: Union{PSY.Component, PSY.System}}\n    return ic_data.aux_variables[AuxVarKey(T, U)]\nend\n\nfunction get_initial_condition_value(\n    ic_data::InitialConditionsData,\n    ::T,\n    ::Type{U},\n) where {T <: ConstraintType, U <: Union{PSY.Component, PSY.System}}\n    return ic_data.duals[ConstraintKey(T, U)]\nend\n\nfunction get_initial_condition_value(\n    ic_data::InitialConditionsData,\n    ::T,\n    ::Type{U},\n) where {T <: ParameterType, U <: Union{PSY.Component, PSY.System}}\n    return ic_data.parameters[ParameterKey(T, U)]\nend\n\nfunction has_initial_condition_value(\n    ic_data::InitialConditionsData,\n    ::T,\n    ::Type{U},\n) where {T <: VariableType, U <: Union{PSY.Component, PSY.System}}\n    return haskey(ic_data.variables, VariableKey(T, U))\nend\n\nfunction has_initial_condition_value(\n    ic_data::InitialConditionsData,\n    ::T,\n    ::Type{U},\n) where {T <: AuxVariableType, U <: Union{PSY.Component, PSY.System}}\n    return haskey(ic_data.aux_variables, AuxVarKey(T, U))\nend\n\nfunction has_initial_condition_value(\n    ic_data::InitialConditionsData,\n    ::T,\n    ::Type{U},\n) where {T <: ConstraintType, U <: Union{PSY.Component, PSY.System}}\n    return haskey(ic_data.duals, ConstraintKey(T, U))\nend\n\nfunction has_initial_condition_value(\n    ic_data::InitialConditionsData,\n    ::T,\n    ::Type{U},\n) where {T <: ParameterType, U <: Union{PSY.Component, PSY.System}}\n    return haskey(ic_data.parameters, ParameterKey(T, U))\nend\n\n######################### Initial Conditions Definitions#####################################\nstruct DevicePower <: InitialConditionType end\nstruct DeviceAboveMinPower <: InitialConditionType end\nstruct DeviceStatus <: InitialConditionType end\nstruct InitialTimeDurationOn <: InitialConditionType end\nstruct InitialTimeDurationOff <: InitialConditionType end\nstruct InitialEnergyLevel <: InitialConditionType end\nstruct AreaControlError <: InitialConditionType end\n\n# Decide whether to run the initial conditions reconciliation algorithm based on the presence of any of these\nrequires_reconciliation(::Type{<:InitialConditionType}) = false\n\nrequires_reconciliation(::Type{InitialTimeDurationOn}) = true\nrequires_reconciliation(::Type{InitialTimeDurationOff}) = true\nrequires_reconciliation(::Type{DeviceStatus}) = true\nrequires_reconciliation(::Type{DevicePower}) = true # to capture a case when device is off in HA but producing power in ED\nrequires_reconciliation(::Type{DeviceAboveMinPower}) = true # ramping limits may make power differences in thermal compact devices between models infeasible\nrequires_reconciliation(::Type{InitialEnergyLevel}) = true # large differences in initial storage levels could lead to infeasibilities\n# Not requiring reconciliation for AreaControlError\n"
  },
  {
    "path": "src/core/model_store_params.jl",
    "content": "struct ModelStoreParams <: ISOPT.AbstractModelStoreParams\n    num_executions::Int\n    horizon_count::Int\n    interval::Dates.Millisecond\n    resolution::Dates.Millisecond\n    base_power::Float64\n    system_uuid::Base.UUID\n    container_metadata::ISOPT.OptimizationContainerMetadata\n\n    function ModelStoreParams(\n        num_executions::Int,\n        horizon_count::Int,\n        interval::Dates.Millisecond,\n        resolution::Dates.Millisecond,\n        base_power::Float64,\n        system_uuid::Base.UUID,\n        container_metadata = ISOPT.OptimizationContainerMetadata(),\n    )\n        new(\n            num_executions,\n            horizon_count,\n            Dates.Millisecond(interval),\n            Dates.Millisecond(resolution),\n            base_power,\n            system_uuid,\n            container_metadata,\n        )\n    end\nend\n\nfunction ModelStoreParams(\n    num_executions::Int,\n    horizon::Dates.Millisecond,\n    interval::Dates.Millisecond,\n    resolution::Dates.Millisecond,\n    base_power::Float64,\n    system_uuid::Base.UUID,\n    container_metadata = ISOPT.OptimizationContainerMetadata(),\n)\n    return ModelStoreParams(\n        num_executions,\n        horizon ÷ resolution,\n        Dates.Millisecond(interval),\n        Dates.Millisecond(resolution),\n        base_power,\n        system_uuid,\n        container_metadata,\n    )\nend\n\nget_num_executions(params::ModelStoreParams) = params.num_executions\nget_horizon_count(params::ModelStoreParams) = params.horizon_count\nget_interval(params::ModelStoreParams) = params.interval\nget_resolution(params::ModelStoreParams) = params.resolution\nget_base_power(params::ModelStoreParams) = params.base_power\nget_system_uuid(params::ModelStoreParams) = params.system_uuid\ndeserialize_key(params::ModelStoreParams, name) =\n    deserialize_key(params.container_metadata, name)\n"
  },
  {
    "path": "src/core/network_formulations.jl",
    "content": "############################## Network Model Formulations ##################################\n# These formulations are taken directly from PowerModels\n\nabstract type AbstractPTDFModel <: PM.AbstractDCPModel end\n\"\"\"\nLinear active power approximation using the power transfer distribution factor [PTDF](https://sienna-platform.github.io/PowerNetworkMatrices.jl/stable/tutorials/tutorial_PTDF_matrix/) matrix.\n\"\"\"\nstruct PTDFPowerModel <: AbstractPTDFModel end\n\"\"\"\nInfinite capacity approximation of network flow to represent entire system with a single node.\n\"\"\"\nstruct CopperPlatePowerModel <: PM.AbstractActivePowerModel end\n\"\"\"\nApproximation to represent inter-area flow with each area represented as a single node.\n\"\"\"\nstruct AreaBalancePowerModel <: PM.AbstractActivePowerModel end\n\"\"\"\nLinear 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.\n\"\"\"\nstruct AreaPTDFPowerModel <: AbstractPTDFModel end\n\n#================================================\n    # exact non-convex models\n    ACPPowerModel, ACRPowerModel, ACTPowerModel\n\n    # linear approximations\n    DCPPowerModel, NFAPowerModel\n\n    # quadratic approximations\n    DCPLLPowerModel, LPACCPowerModel\n\n    # quadratic relaxations\n    SOCWRPowerModel, SOCWRConicPowerModel,\n    SOCBFPowerModel, SOCBFConicPowerModel,\n    QCRMPowerModel, QCLSPowerModel,\n\n    # sdp relaxations\n    SDPWRMPowerModel\n================================================#\n\n##### Exact Non-Convex Models #####\nimport PowerModels: ACPPowerModel\n\nimport PowerModels: ACRPowerModel\n\nimport PowerModels: ACTPowerModel\n\n##### Linear Approximations #####\nimport PowerModels: DCPPowerModel\n\nimport PowerModels: NFAPowerModel\n\n##### Quadratic Approximations #####\nimport PowerModels: DCPLLPowerModel\n\nimport PowerModels: LPACCPowerModel\n\n##### Quadratic Relaxations #####\nimport PowerModels: SOCWRPowerModel\n\nimport PowerModels: SOCWRConicPowerModel\n\nimport PowerModels: QCRMPowerModel\n\nimport PowerModels: QCLSPowerModel\n\nsupports_branch_filtering(::Type{<:PM.AbstractPowerModel}) = false\nsupports_branch_filtering(::Type{<:AbstractPTDFModel}) = true\n\nignores_branch_filtering(::Type{<:PM.AbstractPowerModel}) = false\nignores_branch_filtering(::Type{CopperPlatePowerModel}) = true\nignores_branch_filtering(::Type{AreaBalancePowerModel}) = true\n\nrequires_all_branch_models(::Type{<:PM.AbstractPowerModel}) = true\nrequires_all_branch_models(::Type{<:AbstractPTDFModel}) = false\nrequires_all_branch_models(::Type{CopperPlatePowerModel}) = false\nrequires_all_branch_models(::Type{AreaBalancePowerModel}) = false\n"
  },
  {
    "path": "src/core/network_model.jl",
    "content": "const DeviceModelForBranches = DeviceModel{<:PSY.Branch, <:AbstractDeviceFormulation}\nconst BranchModelContainer = Dict{Symbol, DeviceModelForBranches}\n\nfunction _check_pm_formulation(::Type{T}) where {T <: PM.AbstractPowerModel}\n    if !isconcretetype(T)\n        throw(\n            ArgumentError(\n                \"The network model must contain only concrete types, $(T) is an Abstract Type\",\n            ),\n        )\n    end\nend\n\n_maybe_flatten_pfem(pfem::Vector{PFS.PowerFlowEvaluationModel}) = pfem\n_maybe_flatten_pfem(pfem::PFS.PowerFlowEvaluationModel) =\n    PFS.flatten_power_flow_evaluation_model(pfem)\n\n\"\"\"\nEstablishes the NetworkModel for a given PowerModels formulation type.\n\n# Arguments\n- `::Type{T}` where `T <: PM.AbstractPowerModel`: the power-system formulation type.\n\n# Accepted keyword arguments\n- `use_slacks::Bool` = false\n    Adds slack buses to the network modeling.\n- `PTDF_matrix::Union{PNM.PowerNetworkMatrix, Nothing}` = nothing\n    PTDF/VirtualPTDF matrix produced by PowerNetworkMatrices (optional).\n- `LODF_matrix::Union{PNM.PowerNetworkMatrix, Nothing}` = nothing\n    LODF/VirtualLODF matrix produced by PowerNetworkMatrices (optional).\n- `reduce_radial_branches::Bool` = false\n    Enable radial branch reduction when building network matrices.\n- `reduce_degree_two_branches::Bool` = false\n    Enable degree-two branch reduction when building network matrices.\n- `subnetworks::Dict{Int, Set{Int}}` = Dict()\n    Optional mapping of reference bus → set of mapped buses. If not provided,\n    subnetworks are inferred from PTDF/VirtualPTDF or discovered from the system.\n- `duals::Vector{DataType}` = Vector{DataType}()\n    Constraint types for which duals should be recorded.\n- `power_flow_evaluation::Union{PFS.PowerFlowEvaluationModel, Vector{PFS.PowerFlowEvaluationModel}}`\n    Power-flow evaluation model(s). A single model is flattened to a vector internally.\n\n# Notes\n- `modeled_ac_branch_types` and `reduced_branch_tracker` are internal fields managed by the model.\n- `subsystem` can be set after construction via `set_subsystem!(model, id)`.\n- PTDF/LODF inputs are validated against the requested reduction flags and may raise\n  a ConflictingInputsError if they are inconsistent with `reduce_radial_branches`\n  or `reduce_degree_two_branches`.\n\n# Examples\nptdf = PNM.VirtualPTDF(system)\nnw = NetworkModel(PTDFPowerModel; PTDF_matrix = ptdf, reduce_radial_branches = true,\n                  power_flow_evaluation = PFS.PowerFlowEvaluationModel())\n\nnw2 = NetworkModel(CopperPlatePowerModel; subnetworks = Dict(1 => Set([1,2,3])))\n\"\"\"\nmutable struct NetworkModel{T <: PM.AbstractPowerModel}\n    use_slacks::Bool\n    PTDF_matrix::Union{Nothing, PNM.PowerNetworkMatrix}\n    LODF_matrix::Union{Nothing, PNM.PowerNetworkMatrix}\n    subnetworks::Dict{Int, Set{Int}}\n    bus_area_map::Dict{PSY.ACBus, Int}\n    duals::Vector{DataType}\n    network_reduction::PNM.NetworkReductionData\n    reduce_radial_branches::Bool\n    reduce_degree_two_branches::Bool\n    power_flow_evaluation::Vector{PFS.PowerFlowEvaluationModel}\n    subsystem::Union{Nothing, String}\n    hvdc_network_model::Union{Nothing, AbstractHVDCNetworkModel}\n    modeled_ac_branch_types::Vector{DataType}\n    reduced_branch_tracker::BranchReductionOptimizationTracker\n\n    function NetworkModel(\n        ::Type{T};\n        use_slacks = false,\n        PTDF_matrix = nothing,\n        LODF_matrix = nothing,\n        reduce_radial_branches = false,\n        reduce_degree_two_branches = false,\n        subnetworks = Dict{Int, Set{Int}}(),\n        duals = Vector{DataType}(),\n        power_flow_evaluation::Union{\n            PFS.PowerFlowEvaluationModel,\n            Vector{PFS.PowerFlowEvaluationModel},\n        } = PFS.PowerFlowEvaluationModel[],\n        hvdc_network_model = nothing,\n    ) where {T <: PM.AbstractPowerModel}\n        _check_pm_formulation(T)\n        new{T}(\n            use_slacks,\n            PTDF_matrix,\n            LODF_matrix,\n            subnetworks,\n            Dict{PSY.ACBus, Int}(),\n            duals,\n            PNM.NetworkReductionData(),\n            reduce_radial_branches,\n            reduce_degree_two_branches,\n            _maybe_flatten_pfem(power_flow_evaluation),\n            nothing,\n            hvdc_network_model,\n            Vector{DataType}(),\n            BranchReductionOptimizationTracker(),\n        )\n    end\nend\n\nget_use_slacks(m::NetworkModel) = m.use_slacks\nget_PTDF_matrix(m::NetworkModel) = m.PTDF_matrix\nget_LODF_matrix(m::NetworkModel) = m.LODF_matrix\nget_reduce_radial_branches(m::NetworkModel) = m.reduce_radial_branches\nget_network_reduction(m::NetworkModel) = m.network_reduction\nget_duals(m::NetworkModel) = m.duals\nget_network_formulation(::NetworkModel{T}) where {T} = T\nget_reduced_branch_tracker(m::NetworkModel) = m.reduced_branch_tracker\nget_reference_buses(m::NetworkModel{T}) where {T <: PM.AbstractPowerModel} =\n    collect(keys(m.subnetworks))\nget_subnetworks(m::NetworkModel) = m.subnetworks\nget_bus_area_map(m::NetworkModel) = m.bus_area_map\nget_power_flow_evaluation(m::NetworkModel) = m.power_flow_evaluation\nhas_subnetworks(m::NetworkModel) = !isempty(m.bus_area_map)\nget_subsystem(m::NetworkModel) = m.subsystem\nget_hvdc_network_model(m::NetworkModel) = m.hvdc_network_model\n\nset_subsystem!(m::NetworkModel, id::String) = m.subsystem = id\nset_hvdc_network_model!(m::NetworkModel, val::Union{Nothing, AbstractHVDCNetworkModel}) =\n    m.hvdc_network_model = val\n\nfunction add_dual!(model::NetworkModel, dual)\n    dual in model.duals && error(\"dual = $dual is already stored\")\n    push!(model.duals, dual)\n    @debug \"Added dual\" dual _group = LOG_GROUP_NETWORK_CONSTRUCTION\n    return\nend\n\nfunction _get_filters(branch_models::BranchModelContainer)\n    filters = Dict{DataType, Function}()\n    for v in values(branch_models)\n        filter_func = get_attribute(v, \"filter_function\")\n        if filter_func !== nothing\n            filters[get_component_type(v)] = filter_func\n        end\n    end\n    return filters\nend\n\nfunction _get_irreducible_buses_due_to_dlrs(\n    sys::PSY.System,\n    network_model::NetworkModel,\n    branch_models::BranchModelContainer,\n)\n    @debug \"Identifying buses that are irreducible due to dynamic line ratings\"\n    irreducible_buses = Set{Int64}()\n    for branch_type in network_model.modeled_ac_branch_types\n        device_model = branch_models[Symbol(branch_type)]\n        if !haskey(\n            get_time_series_names(device_model),\n            DynamicBranchRatingTimeSeriesParameter,\n        )\n            continue\n        end\n\n        if branch_type == PSY.ThreeWindingTransformer\n            @warn \"Dynamic branch ratings for ThreeWindingTransformers are not implemented yet. Skipping it.\"\n            continue\n        end\n\n        ts_name =\n            get_time_series_names(device_model)[DynamicBranchRatingTimeSeriesParameter]\n        ts_type = PSY.Deterministic #TODO workaround since we dont have the container\n\n        branches = PSY.get_available_components(branch_type, sys)\n        for branch in branches\n            if !PSY.has_time_series(branch, ts_type, ts_name)\n                continue\n            end\n            bus_to = PSY.get_number(PSY.get_to(PSY.get_arc(branch)))\n            bus_from = PSY.get_number(PSY.get_from(PSY.get_arc(branch)))\n            push!(irreducible_buses, bus_to)\n            push!(irreducible_buses, bus_from)\n        end\n    end\n    return collect(irreducible_buses)\nend\n\nfunction instantiate_network_model!(\n    model::NetworkModel{T},\n    branch_models::BranchModelContainer,\n    number_of_steps::Int,\n    sys::PSY.System,\n) where {T <: PM.AbstractPowerModel}\n    if isempty(model.subnetworks)\n        model.subnetworks = PNM.find_subnetworks(sys)\n    end\n    irreducible_buses = _get_irreducible_buses_due_to_dlrs(\n        sys,\n        model,\n        branch_models,\n    )\n    if model.reduce_radial_branches && model.reduce_degree_two_branches\n        @info \"Applying both radial and degree two reductions\"\n        ybus = PNM.Ybus(\n            sys;\n            network_reductions = PNM.NetworkReduction[\n                PNM.RadialReduction(; irreducible_buses = irreducible_buses),\n                PNM.DegreeTwoReduction(; irreducible_buses = irreducible_buses),\n            ],\n        )\n    elseif model.reduce_radial_branches\n        @info \"Applying radial reduction\"\n        if !isempty(irreducible_buses)\n            @warn \"Irreducible buses identified due to DLRs. The reduction of any radial branch between 2 irreducible buses wil be ignored\"\n        end\n        ybus =\n            PNM.Ybus(\n                sys;\n                network_reductions = PNM.NetworkReduction[PNM.RadialReduction(;\n                    irreducible_buses = irreducible_buses,\n                )],\n            )\n    elseif model.reduce_degree_two_branches\n        @info \"Applying degree two reduction\"\n        ybus = PNM.Ybus(\n            sys;\n            network_reductions = PNM.NetworkReduction[PNM.DegreeTwoReduction(;\n                irreducible_buses = irreducible_buses,\n            )],\n        )\n    else\n        ybus = PNM.Ybus(sys)\n    end\n    model.network_reduction = deepcopy(PNM.get_network_reduction_data(ybus))\n    #if !isempty(model.network_reductionget_net_reduction_data)\n    # TODO: Network reimplement this when it becomes necessary. We don't have any\n    # reductions that are incompatible right now.\n    # check_network_reduction_compatibility(T)\n    #end\n    PNM.populate_branch_maps_by_type!(model.network_reduction, _get_filters(branch_models))\n    empty!(model.reduced_branch_tracker)\n    set_number_of_steps!(model.reduced_branch_tracker, number_of_steps)\n    return\nend\n\nfunction instantiate_network_model!(\n    model::NetworkModel{AreaBalancePowerModel},\n    branch_models::BranchModelContainer,\n    number_of_steps::Int,\n    sys::PSY.System,\n)\n    PNM.populate_branch_maps_by_type!(model.network_reduction)\n    empty!(model.reduced_branch_tracker)\n    set_number_of_steps!(model.reduced_branch_tracker, number_of_steps)\n    return\nend\n\nfunction instantiate_network_model!(\n    model::NetworkModel{CopperPlatePowerModel},\n    branch_models::BranchModelContainer,\n    number_of_steps::Int,\n    sys::PSY.System,\n)\n    if isempty(model.subnetworks)\n        model.subnetworks = PNM.find_subnetworks(sys)\n    end\n    if length(model.subnetworks) > 1\n        @debug \"System Contains Multiple Subnetworks. Assigning buses to subnetworks.\"\n        model.network_reduction = deepcopy(PNM.get_network_reduction_data(PNM.Ybus(sys)))\n        _assign_subnetworks_to_buses(model, sys)\n    end\n    empty!(model.reduced_branch_tracker)\n    set_number_of_steps!(model.reduced_branch_tracker, number_of_steps)\n    return\nend\n\nfunction instantiate_network_model!(\n    model::NetworkModel{<:AbstractPTDFModel},\n    branch_models::BranchModelContainer,\n    number_of_steps::Int,\n    sys::PSY.System,\n)\n    irreducible_buses = _get_irreducible_buses_due_to_dlrs(\n        sys,\n        model,\n        branch_models,\n    )\n    if get_PTDF_matrix(model) === nothing || !isempty(irreducible_buses)\n        if get_PTDF_matrix(model) !== nothing\n            @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.\"\n        else\n            @info \"No PTDF Matrix provided. Calculating using PowerNetworkMatrices.VirtualPTDF\"\n        end\n\n        if model.reduce_radial_branches && model.reduce_degree_two_branches\n            @info \"Applying both radial and degree two reductions\"\n            ptdf = PNM.VirtualPTDF(\n                sys;\n                tol = PTDF_ZERO_TOL,\n                network_reductions = PNM.NetworkReduction[\n                    PNM.RadialReduction(; irreducible_buses = irreducible_buses),\n                    PNM.DegreeTwoReduction(;\n                        irreducible_buses = irreducible_buses,\n                    ),\n                ],\n            )\n        elseif model.reduce_radial_branches\n            @info \"Applying radial reduction\"\n            if !isempty(irreducible_buses)\n                @warn \"Irreducible buses identified due to DLRs. The reduction of any radial branch between 2 irreducible buses wil be ignored\"\n            end\n            ptdf = PNM.VirtualPTDF(\n                sys;\n                tol = PTDF_ZERO_TOL,\n                network_reductions = PNM.NetworkReduction[PNM.RadialReduction(;\n                    irreducible_buses = irreducible_buses,\n                )],\n            )\n        elseif model.reduce_degree_two_branches\n            @info \"Applying degree two reduction\"\n            ptdf = PNM.VirtualPTDF(\n                sys;\n                tol = PTDF_ZERO_TOL,\n                network_reductions = PNM.NetworkReduction[PNM.DegreeTwoReduction(;\n                    irreducible_buses = irreducible_buses,\n                )],\n            )\n        else\n            ptdf = PNM.VirtualPTDF(sys; tol = PTDF_ZERO_TOL)\n        end\n        model.PTDF_matrix = ptdf\n        model.network_reduction = deepcopy(ptdf.network_reduction_data)\n    else\n        model.network_reduction = deepcopy(model.PTDF_matrix.network_reduction_data)\n    end\n\n    if !model.reduce_radial_branches && PNM.has_radial_reduction(\n        PNM.get_reductions(model.PTDF_matrix.network_reduction_data),\n    )\n        throw(\n            IS.ConflictingInputsError(\n                \"The provided PTDF Matrix has reduced radial branches and mismatches the network \\\n                model specification reduce_radial_branches = false. Set the keyword argument \\\n                reduce_radial_branches = true in your network model\"),\n        )\n    end\n    if !model.reduce_degree_two_branches && PNM.has_degree_two_reduction(\n        PNM.get_reductions(model.PTDF_matrix.network_reduction_data),\n    )\n        throw(\n            IS.ConflictingInputsError(\n                \"The provided PTDF Matrix has reduced degree two branches and mismatches the network \\\n                model specification reduce_degree_two_branches = false. Set the keyword argument \\\n                reduce_degree_two_branches = true in your network model\"),\n        )\n    end\n    if model.reduce_radial_branches &&\n       PNM.has_ward_reduction(PNM.get_reductions(model.PTDF_matrix.network_reduction_data))\n        throw(\n            IS.ConflictingInputsError(\n                \"The provided PTDF Matrix has  a ward reduction specified and the keyword argument \\\\\n                reduce_radial_branches = true. Set the keyword argument reduce_radial_branches = false \\\\\n                or provide a modified PTDF Matrix without the Ward reduction.\"),\n        )\n    end\n\n    if model.reduce_radial_branches\n        @assert !isempty(model.PTDF_matrix.network_reduction_data)\n    end\n    model.subnetworks = _make_subnetworks_from_subnetwork_axes(model.PTDF_matrix)\n    if length(model.subnetworks) > 1\n        @debug \"System Contains Multiple Subnetworks. Assigning buses to subnetworks.\"\n        _assign_subnetworks_to_buses(model, sys)\n    end\n    PNM.populate_branch_maps_by_type!(model.network_reduction, _get_filters(branch_models))\n    empty!(model.reduced_branch_tracker)\n    set_number_of_steps!(model.reduced_branch_tracker, number_of_steps)\n    return\nend\n\nfunction _make_subnetworks_from_subnetwork_axes(ptdf::PNM.PTDF)\n    subnetworks = Dict{Int, Set{Int}}()\n    for (ref_bus, ptdf_axes) in ptdf.subnetwork_axes\n        subnetworks[ref_bus] = Set(ptdf_axes[1])\n    end\n    return subnetworks\nend\n\nfunction _make_subnetworks_from_subnetwork_axes(ptdf::PNM.VirtualPTDF)\n    subnetworks = Dict{Int, Set{Int}}()\n    for (ref_bus, ptdf_axes) in ptdf.subnetwork_axes\n        subnetworks[ref_bus] = Set(ptdf_axes[2])\n    end\n    return subnetworks\nend\n\nfunction _assign_subnetworks_to_buses(\n    model::NetworkModel{T},\n    sys::PSY.System,\n) where {T <: Union{CopperPlatePowerModel, AbstractPTDFModel}}\n    subnetworks = model.subnetworks\n    temp_bus_map = Dict{Int, Int}()\n    network_reduction = PSI.get_network_reduction(model)\n    for bus in PSI.get_available_components(model, PSY.ACBus, sys)\n        bus_no = PSY.get_number(bus)\n        mapped_bus_no = PNM.get_mapped_bus_number(network_reduction, bus)\n        mapped_bus_no ∈ network_reduction.removed_buses && continue\n        if haskey(temp_bus_map, bus_no)\n            model.bus_area_map[bus] = temp_bus_map[bus_no]\n            continue\n        else\n            bus_mapped = false\n            for (subnet, bus_set) in subnetworks\n                if mapped_bus_no ∈ bus_set\n                    temp_bus_map[bus_no] = subnet\n                    model.bus_area_map[bus] = subnet\n                    bus_mapped = true\n                    break\n                end\n            end\n        end\n        if !bus_mapped\n            error(\n                \"Bus $(PSY.summary(bus)) not mapped to any reference bus: Mapped bus number: $(mapped_bus_no)\",\n            )\n        end\n    end\n    return\nend\n\n_assign_subnetworks_to_buses(\n    ::NetworkModel{T},\n    ::PSY.System,\n) where {T <: PM.AbstractPowerModel} = nothing\n\nfunction get_reference_bus(\n    model::NetworkModel{T},\n    b::PSY.ACBus,\n)::Int where {T <: PM.AbstractPowerModel}\n    if isempty(model.bus_area_map)\n        return first(keys(model.subnetworks))\n    else\n        return model.bus_area_map[b]\n    end\nend\n"
  },
  {
    "path": "src/core/network_reductions.jl",
    "content": "mutable struct BranchReductionOptimizationTracker\n    variable_dict::Dict{\n        Type{<:ISOPT.VariableType},\n        Dict{Tuple{Int, Int}, Vector{JuMP.VariableRef}},\n    }\n    parameter_dict::Dict{\n        Type{<:ISOPT.ParameterType},\n        Dict{Tuple{Int, Int}, Vector{Union{Float64, JuMP.VariableRef}}},\n    }\n    constraint_dict::Dict{Type{<:ISOPT.ConstraintType}, Set{Tuple{Int, Int}}}\n    constraint_map_by_type::Dict{\n        Type{<:ISOPT.ConstraintType},\n        Dict{\n            Type{<:PSY.ACTransmission},\n            SortedDict{String, Tuple{Tuple{Int, Int}, String}},\n        },\n    }\n    number_of_steps::Int\nend\n\nget_variable_dict(reduction_tracker::BranchReductionOptimizationTracker) =\n    reduction_tracker.variable_dict\nget_parameter_dict(reduction_tracker::BranchReductionOptimizationTracker) =\n    reduction_tracker.parameter_dict\nget_constraint_dict(reduction_tracker::BranchReductionOptimizationTracker) =\n    reduction_tracker.constraint_dict\nget_constraint_map_by_type(reduction_tracker::BranchReductionOptimizationTracker) =\n    reduction_tracker.constraint_map_by_type\n\nget_number_of_steps(reduction_tracker::BranchReductionOptimizationTracker) =\n    reduction_tracker.number_of_steps\nset_number_of_steps!(reduction_tracker, number_of_steps) =\n    reduction_tracker.number_of_steps = number_of_steps\n\nBase.isempty(\n    reduction_tracker::BranchReductionOptimizationTracker,\n) =\n    isempty(reduction_tracker.variable_dict) &&\n    isempty(reduction_tracker.parameter_dict) &&\n    isempty(reduction_tracker.constraint_dict)\n\nBase.empty!(\n    reduction_tracker::BranchReductionOptimizationTracker,\n) = begin\n    empty!(reduction_tracker.variable_dict)\n    empty!(reduction_tracker.parameter_dict)\n    empty!(reduction_tracker.constraint_dict)\nend\n\nfunction BranchReductionOptimizationTracker()\n    return BranchReductionOptimizationTracker(Dict(), Dict(), Dict(), Dict(), 0)\nend\n\nfunction _make_empty_variable_tracker_dict(\n    arc_tuple::Tuple{Int, Int},\n    num_steps::Int,\n)\n    return Dict{Tuple{Int, Int}, Vector{JuMP.VariableRef}}(\n        arc_tuple => Vector{JuMP.VariableRef}(undef, num_steps),\n    )\nend\n\nfunction _make_empty_parameter_tracker_dict(\n    arc_tuple::Tuple{Int, Int},\n    num_steps::Int,\n)\n    return Dict{Tuple{Int, Int}, Vector{Union{Float64, JuMP.VariableRef}}}(\n        arc_tuple => Vector{Union{Float64, JuMP.VariableRef}}(undef, num_steps),\n    )\nend\n\n\"\"\"Look up (or register) the tracker entry for `arc_tuple` and `VariableType` T.\nReturns `(has_entry, tracker_vector)` where `has_entry` is `true` when the arc\nwas already registered by a previous call (i.e. a parallel/reduced branch of a\ndifferent device type already created the variable).\"\"\"\nfunction search_for_reduced_branch_variable!(\n    tracker::BranchReductionOptimizationTracker,\n    arc_tuple::Tuple{Int, Int},\n    ::Type{T},\n) where {T <: ISOPT.VariableType}\n    variable_dict = tracker.variable_dict\n    time_steps = get_number_of_steps(tracker)\n    if !haskey(variable_dict, T)\n        variable_dict[T] = _make_empty_variable_tracker_dict(arc_tuple, time_steps)\n        return (false, variable_dict[T][arc_tuple])\n    else\n        if haskey(variable_dict[T], arc_tuple)\n            return (true, variable_dict[T][arc_tuple])\n        else\n            variable_dict[T][arc_tuple] = Vector{JuMP.VariableRef}(undef, time_steps)\n            return (false, variable_dict[T][arc_tuple])\n        end\n    end\nend\n\n\"\"\"Look up (or register) the tracker entry for `arc_tuple` and `ParameterType` T.\nStores `Float64` values when `built_for_recurrent_solves` is `false`, or\n`JuMP.VariableRef` objects (JuMP parameters) when `true`, so that shared arcs\nacross different branch types reuse the same underlying parameter object.\nReturns `(has_entry, tracker_vector)`.\"\"\"\nfunction search_for_reduced_branch_parameter!(\n    tracker::BranchReductionOptimizationTracker,\n    arc_tuple::Tuple{Int, Int},\n    ::Type{T},\n) where {T <: ISOPT.ParameterType}\n    parameter_dict = tracker.parameter_dict\n    time_steps = get_number_of_steps(tracker)\n    if !haskey(parameter_dict, T)\n        parameter_dict[T] = _make_empty_parameter_tracker_dict(arc_tuple, time_steps)\n        return (false, parameter_dict[T][arc_tuple])\n    else\n        if haskey(parameter_dict[T], arc_tuple)\n            return (true, parameter_dict[T][arc_tuple])\n        else\n            parameter_dict[T][arc_tuple] =\n                Vector{Union{Float64, JuMP.VariableRef}}(undef, time_steps)\n            return (false, parameter_dict[T][arc_tuple])\n        end\n    end\nend\n\n# Backwards-compatible dispatcher: routes to the correctly typed dict based on T.\nfunction search_for_reduced_branch_argument!(\n    tracker::BranchReductionOptimizationTracker,\n    arc_tuple::Tuple{Int, Int},\n    ::Type{T},\n) where {T <: ISOPT.VariableType}\n    return search_for_reduced_branch_variable!(tracker, arc_tuple, T)\nend\n\nfunction search_for_reduced_branch_argument!(\n    tracker::BranchReductionOptimizationTracker,\n    arc_tuple::Tuple{Int, Int},\n    ::Type{T},\n) where {T <: ISOPT.ParameterType}\n    return search_for_reduced_branch_parameter!(tracker, arc_tuple, T)\nend\n\nfunction get_branch_argument_parameter_axes(\n    net_reduction_data::PNM.NetworkReductionData,\n    ::IS.FlattenIteratorWrapper{T},\n    ::Type{V},\n    ts_name::String;\n    interval::Dates.Millisecond = UNSET_INTERVAL,\n) where {T <: PSY.ACTransmission, V <: PSY.TimeSeriesData}\n    return get_branch_argument_parameter_axes(\n        net_reduction_data,\n        T,\n        V,\n        ts_name;\n        interval = interval,\n    )\nend\n\nfunction get_branch_argument_parameter_axes(\n    net_reduction_data::PNM.NetworkReductionData,\n    ::Type{T},\n    ::Type{V},\n    ts_name::String;\n    interval::Dates.Millisecond = UNSET_INTERVAL,\n) where {T <: PSY.ACTransmission, V <: PSY.TimeSeriesData}\n    is_interval = _to_is_interval(interval)\n    name_axis = Vector{String}()\n    ts_uuid_axis = Vector{String}()\n    for (name, (arc, reduction)) in net_reduction_data.name_to_arc_map[T]\n        reduction_entry = net_reduction_data.all_branch_maps_by_type[reduction][T][arc]\n        device_with_time_series =\n            PNM.get_device_with_time_series(reduction_entry, V, ts_name)\n        if device_with_time_series !== nothing\n            push!(name_axis, name)\n            push!(\n                ts_uuid_axis,\n                string(\n                    IS.get_time_series_uuid(\n                        V,\n                        device_with_time_series,\n                        ts_name;\n                        interval = is_interval,\n                    ),\n                ),\n            )\n        end\n    end\n    return name_axis, ts_uuid_axis\nend\n\nfunction get_branch_argument_variable_axis(\n    net_reduction_data::PNM.NetworkReductionData,\n    ::IS.FlattenIteratorWrapper{T},\n) where {T <: PSY.ACTransmission}\n    return get_branch_argument_variable_axis(net_reduction_data, T)\nend\n\nfunction get_branch_argument_variable_axis(\n    net_reduction_data::PNM.NetworkReductionData,\n    ::Type{T},\n) where {T <: PSY.ACTransmission}\n    name_axis = net_reduction_data.name_to_arc_map[T]\n    return collect(keys(name_axis))\nend\n\n#= function get_branch_argument_variable_axis(\n    net_reduction_data::PNM.NetworkReductionData,\n    ::Type{PNM.ThreeWindingTransformerWinding{T}},\n) where {T <: PSY.ThreeWindingTransformer}\n    name_axis = net_reduction_data.name_to_arc_map[T]\n    return collect(keys(name_axis))\nend =#\n\nfunction get_branch_argument_constraint_axis(\n    net_reduction_data::PNM.NetworkReductionData,\n    reduced_branch_tracker::BranchReductionOptimizationTracker,\n    ::IS.FlattenIteratorWrapper{T},\n    ::Type{U},\n) where {T <: PSY.ACTransmission, U <: ISOPT.ConstraintType}\n    return get_branch_argument_constraint_axis(\n        net_reduction_data,\n        reduced_branch_tracker,\n        T,\n        U,\n    )\nend\n\nfunction get_branch_argument_constraint_axis(\n    net_reduction_data::PNM.NetworkReductionData,\n    reduced_branch_tracker::BranchReductionOptimizationTracker,\n    ::Type{T},\n    ::Type{U},\n) where {T <: PSY.ACTransmission, U <: ISOPT.ConstraintType}\n    constraint_tracker = get_constraint_dict(reduced_branch_tracker)\n    constraint_map_by_type = get_constraint_map_by_type(reduced_branch_tracker)\n    name_axis = net_reduction_data.name_to_arc_map[T]\n    arc_tuples_with_constraints =\n        get!(constraint_tracker, U, Set{Tuple{Int, Int}}())\n    constraint_map = get!(\n        constraint_map_by_type,\n        U,\n        Dict{\n            Type{<:PSY.ACTransmission},\n            SortedDict{String, Tuple{Tuple{Int, Int}, String}},\n        }(),\n    )\n    constraint_submap =\n        get!(constraint_map, T, SortedDict{String, Tuple{Tuple{Int, Int}, String}}())\n    for (branch_name, name_axis_tuple) in name_axis\n        arc_tuple = name_axis_tuple[1]\n        if !(arc_tuple in arc_tuples_with_constraints)\n            constraint_submap[branch_name] = name_axis_tuple\n            push!(arc_tuples_with_constraints, arc_tuple)\n        end\n    end\n    return collect(keys(constraint_submap))\nend\n"
  },
  {
    "path": "src/core/operation_model_abstract_types.jl",
    "content": "\"\"\"\nAbstract type for Decision Model and Emulation Model. OperationModel structs are parameterized with DecisionProblem or Emulation Problem structs\n\"\"\"\nabstract type OperationModel end\n\n#TODO: Document the required interfaces for custom types\n\"\"\"\nAbstract type for Decision Problems\n\n# Example\n\nimport PowerSimulations as PSI\nstruct MyCustomProblem <: PSI.DecisionProblem\n\"\"\"\nabstract type DecisionProblem end\n\n\"\"\"\nAbstract type for Emulation Problems\n\n# Example\n\nimport PowerSimulations as PSI\nstruct MyCustomEmulator <: PSI.EmulationProblem\n\"\"\"\nabstract type EmulationProblem end\n"
  },
  {
    "path": "src/core/optimization_container.jl",
    "content": "struct PrimalValuesCache\n    variables_cache::Dict{VariableKey, AbstractArray}\n    expressions_cache::Dict{ExpressionKey, AbstractArray}\nend\n\nfunction PrimalValuesCache()\n    return PrimalValuesCache(\n        Dict{VariableKey, AbstractArray}(),\n        Dict{ExpressionKey, AbstractArray}(),\n    )\nend\n\nfunction Base.isempty(pvc::PrimalValuesCache)\n    return isempty(pvc.variables_cache) && isempty(pvc.expressions_cache)\nend\n\nmutable struct ObjectiveFunction\n    invariant_terms::JuMP.AbstractJuMPScalar\n    variant_terms::GAE\n    synchronized::Bool\n    sense::MOI.OptimizationSense\n    function ObjectiveFunction(invariant_terms::JuMP.AbstractJuMPScalar,\n        variant_terms::GAE,\n        synchronized::Bool,\n        sense::MOI.OptimizationSense = MOI.MIN_SENSE)\n        new(invariant_terms, variant_terms, synchronized, sense)\n    end\nend\n\nget_invariant_terms(v::ObjectiveFunction) = v.invariant_terms\nget_variant_terms(v::ObjectiveFunction) = v.variant_terms\nfunction get_objective_expression(v::ObjectiveFunction)\n    if iszero(v.variant_terms)\n        return v.invariant_terms\n    else\n        # JuMP doesn't support expression conversion from Affn to QuadExpressions\n        if isa(v.invariant_terms, JuMP.GenericQuadExpr)\n            # Avoid mutation of invariant term\n            temp_expr = JuMP.QuadExpr()\n            JuMP.add_to_expression!(temp_expr, v.invariant_terms)\n            return JuMP.add_to_expression!(temp_expr, v.variant_terms)\n        else\n            # This will mutate the variant terms, but these are reseted at each step.\n            return JuMP.add_to_expression!(v.variant_terms, v.invariant_terms)\n        end\n    end\nend\nget_sense(v::ObjectiveFunction) = v.sense\nis_synchronized(v::ObjectiveFunction) = v.synchronized\nset_synchronized_status!(v::ObjectiveFunction, value) = v.synchronized = value\nreset_variant_terms(v::ObjectiveFunction) = v.variant_terms = zero(JuMP.AffExpr)\nhas_variant_terms(v::ObjectiveFunction) = !iszero(v.variant_terms)\nset_sense!(v::ObjectiveFunction, sense::MOI.OptimizationSense) = v.sense = sense\n\nfunction ObjectiveFunction()\n    return ObjectiveFunction(\n        zero(JuMP.GenericAffExpr{Float64, JuMP.VariableRef}),\n        zero(JuMP.AffExpr),\n        true,\n    )\nend\n\nmutable struct OptimizationContainer <: ISOPT.AbstractOptimizationContainer\n    JuMPmodel::JuMP.Model\n    time_steps::UnitRange{Int}\n    settings::Settings\n    variables::OrderedDict{VariableKey, AbstractArray}\n    aux_variables::OrderedDict{AuxVarKey, AbstractArray}\n    duals::OrderedDict{ConstraintKey, AbstractArray}\n    constraints::OrderedDict{ConstraintKey, AbstractArray}\n    objective_function::ObjectiveFunction\n    expressions::OrderedDict{ExpressionKey, AbstractArray}\n    parameters::OrderedDict{ParameterKey, ParameterContainer}\n    primal_values_cache::PrimalValuesCache\n    initial_conditions::OrderedDict{InitialConditionKey, Vector{<:InitialCondition}}\n    initial_conditions_data::InitialConditionsData\n    infeasibility_conflict::Dict{Symbol, Array}\n    pm::Union{Nothing, PM.AbstractPowerModel}\n    base_power::Float64\n    optimizer_stats::OptimizerStats\n    built_for_recurrent_solves::Bool\n    metadata::ISOPT.OptimizationContainerMetadata\n    default_time_series_type::Type{<:PSY.TimeSeriesData}\n    power_flow_evaluation_data::Vector{PowerFlowEvaluationData}\nend\n\nfunction OptimizationContainer(\n    sys::PSY.System,\n    settings::Settings,\n    jump_model::Union{Nothing, JuMP.Model},\n    ::Type{T},\n) where {T <: PSY.TimeSeriesData}\n    if isabstracttype(T)\n        error(\"Default Time Series Type $V can't be abstract\")\n    end\n\n    if jump_model !== nothing && get_direct_mode_optimizer(settings)\n        throw(\n            IS.ConflictingInputsError(\n                \"Externally provided JuMP models are not compatible with the direct model keyword argument. Use JuMP.direct_model before passing the custom model\",\n            ),\n        )\n    end\n\n    return OptimizationContainer(\n        jump_model === nothing ? JuMP.Model() : jump_model,\n        1:1,\n        settings,\n        OrderedDict{VariableKey, AbstractArray}(),\n        OrderedDict{AuxVarKey, AbstractArray}(),\n        OrderedDict{ConstraintKey, AbstractArray}(),\n        OrderedDict{ConstraintKey, AbstractArray}(),\n        ObjectiveFunction(),\n        OrderedDict{ExpressionKey, AbstractArray}(),\n        OrderedDict{ParameterKey, ParameterContainer}(),\n        PrimalValuesCache(),\n        OrderedDict{InitialConditionKey, Vector{InitialCondition}}(),\n        InitialConditionsData(),\n        Dict{Symbol, Array}(),\n        nothing,\n        PSY.get_base_power(sys),\n        OptimizerStats(),\n        false,\n        ISOPT.OptimizationContainerMetadata(),\n        T,\n        Vector{PowerFlowEvaluationData}[],\n    )\nend\n\nbuilt_for_recurrent_solves(container::OptimizationContainer) =\n    container.built_for_recurrent_solves\n\nget_aux_variables(container::OptimizationContainer) = container.aux_variables\nget_base_power(container::OptimizationContainer) = container.base_power\nget_constraints(container::OptimizationContainer) = container.constraints\n\nfunction cost_function_unsynch(container::OptimizationContainer)\n    obj_func = get_objective_expression(container)\n    if has_variant_terms(obj_func) && is_synchronized(container)\n        set_synchronized_status!(obj_func, false)\n        reset_variant_terms(obj_func)\n    end\n    return\nend\n\nfunction get_container_keys(container::OptimizationContainer)\n    return Iterators.flatten(keys(getfield(container, f)) for f in STORE_CONTAINERS)\nend\n\nget_default_time_series_type(container::OptimizationContainer) =\n    container.default_time_series_type\nget_duals(container::OptimizationContainer) = container.duals\nget_expressions(container::OptimizationContainer) = container.expressions\nget_infeasibility_conflict(container::OptimizationContainer) =\n    container.infeasibility_conflict\nget_initial_conditions(container::OptimizationContainer) = container.initial_conditions\nget_initial_conditions_data(container::OptimizationContainer) =\n    container.initial_conditions_data\nget_initial_time(container::OptimizationContainer) = get_initial_time(container.settings)\nget_jump_model(container::OptimizationContainer) = container.JuMPmodel\nget_metadata(container::OptimizationContainer) = container.metadata\nget_optimizer_stats(container::OptimizationContainer) = container.optimizer_stats\nget_parameters(container::OptimizationContainer) = container.parameters\nget_power_flow_evaluation_data(container::OptimizationContainer) =\n    container.power_flow_evaluation_data\nget_resolution(container::OptimizationContainer) = get_resolution(container.settings)\nget_settings(container::OptimizationContainer) = container.settings\nget_time_steps(container::OptimizationContainer) = container.time_steps\nget_variables(container::OptimizationContainer) = container.variables\n\nset_initial_conditions_data!(container::OptimizationContainer, data) =\n    container.initial_conditions_data = data\nget_objective_expression(container::OptimizationContainer) = container.objective_function\nis_synchronized(container::OptimizationContainer) =\n    container.objective_function.synchronized\nset_time_steps!(container::OptimizationContainer, time_steps::UnitRange{Int64}) =\n    container.time_steps = time_steps\n\nfunction reset_power_flow_is_solved!(container::OptimizationContainer)\n    for pf_e_data in get_power_flow_evaluation_data(container)\n        pf_e_data.is_solved = false\n    end\nend\n\nfunction has_container_key(\n    container::OptimizationContainer,\n    ::Type{T},\n    ::Type{U},\n    meta = ISOPT.CONTAINER_KEY_EMPTY_META,\n) where {T <: ExpressionType, U <: Union{PSY.Component, PSY.System}}\n    key = ExpressionKey(T, U, meta)\n    return haskey(container.expressions, key)\nend\n\nfunction has_container_key(\n    container::OptimizationContainer,\n    ::Type{T},\n    ::Type{U},\n    meta = ISOPT.CONTAINER_KEY_EMPTY_META,\n) where {T <: VariableType, U <: Union{PSY.Component, PSY.System}}\n    key = VariableKey(T, U, meta)\n    return haskey(container.variables, key)\nend\n\nfunction has_container_key(\n    container::OptimizationContainer,\n    ::Type{T},\n    ::Type{U},\n    meta = ISOPT.CONTAINER_KEY_EMPTY_META,\n) where {T <: AuxVariableType, U <: Union{PSY.Component, PSY.System}}\n    key = AuxVarKey(T, U, meta)\n    return haskey(container.aux_variables, key)\nend\n\nfunction has_container_key(\n    container::OptimizationContainer,\n    ::Type{T},\n    ::Type{U},\n    meta = ISOPT.CONTAINER_KEY_EMPTY_META,\n) where {T <: ConstraintType, U <: Union{PSY.Component, PSY.System}}\n    key = ConstraintKey(T, U, meta)\n    return haskey(container.constraints, key)\nend\n\nfunction has_container_key(\n    container::OptimizationContainer,\n    ::Type{T},\n    ::Type{U},\n    meta = ISOPT.CONTAINER_KEY_EMPTY_META,\n) where {T <: ParameterType, U <: Union{PSY.Component, PSY.System}}\n    key = ParameterKey(T, U, meta)\n    return haskey(container.parameters, key)\nend\n\nfunction has_container_key(\n    container::OptimizationContainer,\n    ::Type{T},\n    ::Type{U},\n    meta = ISOPT.CONTAINER_KEY_EMPTY_META,\n) where {T <: InitialConditionType, U <: Union{PSY.Component, PSY.System}}\n    key = InitialConditionKey(T, U, meta)\n    return haskey(container.initial_conditions, key)\nend\n\nfunction is_milp(container::OptimizationContainer)::Bool\n    !supports_milp(container) && return false\n    if !isempty(\n        JuMP.all_constraints(\n            PSI.get_jump_model(container),\n            JuMP.VariableRef,\n            JuMP.MOI.ZeroOne,\n        ),\n    )\n        return true\n    end\n    return false\nend\n\nfunction supports_milp(container::OptimizationContainer)\n    jump_model = get_jump_model(container)\n    return supports_milp(jump_model)\nend\n\nfunction _validate_warm_start_support(JuMPmodel::JuMP.Model, warm_start_enabled::Bool)\n    !warm_start_enabled && return warm_start_enabled\n    solver_supports_warm_start =\n        MOI.supports(JuMP.backend(JuMPmodel), MOI.VariablePrimalStart(), MOI.VariableIndex)\n    if !solver_supports_warm_start\n        solver_name = JuMP.solver_name(JuMPmodel)\n        @warn(\"$(solver_name) does not support warm start\")\n    end\n    return solver_supports_warm_start\nend\n\nfunction _finalize_jump_model!(container::OptimizationContainer, settings::Settings)\n    @debug \"Instantiating the JuMP model\" _group = LOG_GROUP_OPTIMIZATION_CONTAINER\n    if built_for_recurrent_solves(container) && get_optimizer(settings) === nothing\n        throw(\n            IS.ConflictingInputsError(\n                \"Optimizer can not be nothing when building for recurrent solves\",\n            ),\n        )\n    end\n\n    if get_direct_mode_optimizer(settings)\n        optimizer = () -> MOI.instantiate(get_optimizer(settings))\n        container.JuMPmodel = JuMP.direct_model(optimizer())\n    elseif get_optimizer(settings) === nothing\n        @debug \"The optimization model has no optimizer attached\" _group =\n            LOG_GROUP_OPTIMIZATION_CONTAINER\n    else\n        JuMP.set_optimizer(PSI.get_jump_model(container), get_optimizer(settings))\n    end\n\n    JuMPmodel = PSI.get_jump_model(container)\n    warm_start_enabled = get_warm_start(settings)\n    solver_supports_warm_start = _validate_warm_start_support(JuMPmodel, warm_start_enabled)\n    set_warm_start!(settings, solver_supports_warm_start)\n\n    JuMP.set_string_names_on_creation(JuMPmodel, get_store_variable_names(settings))\n\n    @debug begin\n        JuMP.set_string_names_on_creation(JuMPmodel, true)\n    end\n    if get_optimizer_solve_log_print(settings)\n        JuMP.unset_silent(JuMPmodel)\n        @debug \"optimizer unset to silent\" _group = LOG_GROUP_OPTIMIZATION_CONTAINER\n    else\n        JuMP.set_silent(JuMPmodel)\n        @debug \"optimizer set to silent\" _group = LOG_GROUP_OPTIMIZATION_CONTAINER\n    end\n    return\nend\n\nfunction init_optimization_container!(\n    container::OptimizationContainer,\n    network_model::NetworkModel{T},\n    sys::PSY.System,\n) where {T <: PM.AbstractPowerModel}\n    PSY.set_units_base_system!(sys, \"SYSTEM_BASE\")\n    # The order of operations matter\n    settings = get_settings(container)\n\n    if get_initial_time(settings) == UNSET_INI_TIME\n        if get_default_time_series_type(container) <: PSY.AbstractDeterministic\n            set_initial_time!(settings, PSY.get_forecast_initial_timestamp(sys))\n        elseif get_default_time_series_type(container) <: PSY.SingleTimeSeries\n            ini_time, _ = PSY.check_time_series_consistency(sys, PSY.SingleTimeSeries)\n            set_initial_time!(settings, ini_time)\n        end\n    end\n\n    if get_resolution(settings) == UNSET_RESOLUTION\n        error(\"Resolution not set in the model. Can't continue with the build.\")\n    end\n\n    horizon_count = (get_horizon(settings) ÷ get_resolution(settings))\n    @assert horizon_count > 0\n    container.time_steps = 1:horizon_count\n\n    if T <: CopperPlatePowerModel || T <: AreaBalancePowerModel\n        total_number_of_devices =\n            length(get_available_components(network_model, PSY.Device, sys))\n    else\n        total_number_of_devices =\n            length(get_available_components(network_model, PSY.Device, sys))\n        total_number_of_devices +=\n            length(get_available_components(network_model, PSY.ACBranch, sys))\n    end\n\n    # The 10e6 limit is based on the sizes of the lp benchmark problems http://plato.asu.edu/ftp/lpcom.html\n    # The maximum numbers of constraints and variables in the benchmark problems is 1,918,399 and 1,259,121,\n    # respectively. See also https://prod-ng.sandia.gov/techlib-noauth/access-control.cgi/2013/138847.pdf\n    variable_count_estimate = length(container.time_steps) * total_number_of_devices\n\n    if variable_count_estimate > 10e6\n        @warn(\n            \"The lower estimate of total number of variables that will be created in the model is $(variable_count_estimate). \\\\\n            The total number of variables might be larger than 10e6 and could lead to large build or solve times.\"\n        )\n    end\n\n    stats = get_optimizer_stats(container)\n    stats.detailed_stats = get_detailed_optimizer_stats(settings)\n\n    _finalize_jump_model!(container, settings)\n    return\nend\n\nfunction reset_optimization_model!(container::OptimizationContainer)\n    for field in [:variables, :aux_variables, :constraints, :expressions, :duals]\n        empty!(getfield(container, field))\n    end\n    container.initial_conditions_data = InitialConditionsData()\n    container.objective_function = ObjectiveFunction()\n    container.primal_values_cache = PrimalValuesCache()\n    JuMP.empty!(PSI.get_jump_model(container))\n    return\nend\n\nfunction check_parameter_multiplier_values(multiplier_array::DenseAxisArray)\n    return !all(isnan.(multiplier_array.data))\nend\n\nfunction check_parameter_multiplier_values(multiplier_array::SparseAxisArray)\n    return !all(isnan.(values(multiplier_array.data)))\nend\n\nfunction check_optimization_container(container::OptimizationContainer)\n    for (k, param_container) in container.parameters\n        valid = check_parameter_multiplier_values(param_container.multiplier_array)\n        if !valid\n            error(\"The model container has invalid values in $(encode_key_as_string(k))\")\n        end\n    end\n    return\nend\n\nfunction get_problem_size(container::OptimizationContainer)\n    model = get_jump_model(container)\n    vars = JuMP.num_variables(model)\n    cons = 0\n    for (exp, c_type) in JuMP.list_of_constraint_types(model)\n        cons += JuMP.num_constraints(model, exp, c_type)\n    end\n    return \"The current total number of variables is $(vars) and total number of constraints is $(cons)\"\nend\n\nfunction _make_container_array(ax...)\n    return remove_undef!(DenseAxisArray{GAE}(undef, ax...))\nend\n\nfunction _make_system_expressions!(\n    container::OptimizationContainer,\n    subnetworks::Dict{Int, Set{Int}},\n    ::Vector{Int},\n    ::Type{<:PM.AbstractPowerModel},\n    bus_reduction_map::Dict{Int64, Set{Int64}},\n)\n    time_steps = get_time_steps(container)\n    if isempty(bus_reduction_map)\n        ac_bus_numbers = collect(Iterators.flatten(values(subnetworks)))\n    else\n        ac_bus_numbers = collect(keys(bus_reduction_map))\n    end\n    container.expressions = Dict(\n        ExpressionKey(ActivePowerBalance, PSY.ACBus) =>\n            _make_container_array(ac_bus_numbers, time_steps),\n        ExpressionKey(ReactivePowerBalance, PSY.ACBus) =>\n            _make_container_array(ac_bus_numbers, time_steps),\n    )\n    return\nend\n\nfunction _make_system_expressions!(\n    container::OptimizationContainer,\n    subnetworks::Dict{Int, Set{Int}},\n    ::Vector{Int},\n    ::Type{<:PM.AbstractActivePowerModel},\n    bus_reduction_map::Dict{Int64, Set{Int64}},\n)\n    time_steps = get_time_steps(container)\n    if isempty(bus_reduction_map)\n        ac_bus_numbers = collect(Iterators.flatten(values(subnetworks)))\n    else\n        ac_bus_numbers = collect(keys(bus_reduction_map))\n    end\n    container.expressions = Dict(\n        ExpressionKey(ActivePowerBalance, PSY.ACBus) =>\n            _make_container_array(ac_bus_numbers, time_steps),\n    )\n    return\nend\n\nfunction _make_system_expressions!(\n    container::OptimizationContainer,\n    subnetworks::Dict{Int, Set{Int}},\n    ::Vector{Int},\n    ::Type{CopperPlatePowerModel},\n    bus_reduction_map::Dict{Int64, Set{Int64}},\n)\n    time_steps = get_time_steps(container)\n    subnetworks_ref_buses = collect(keys(subnetworks))\n    container.expressions = Dict(\n        ExpressionKey(ActivePowerBalance, PSY.System) =>\n            _make_container_array(subnetworks_ref_buses, time_steps),\n    )\n    return\nend\n\nfunction _make_system_expressions!(\n    container::OptimizationContainer,\n    subnetworks::Dict{Int, Set{Int}},\n    ::Vector{Int},\n    ::Type{T},\n    bus_reduction_map::Dict{Int64, Set{Int64}},\n) where {T <: Union{PTDFPowerModel}}\n    time_steps = get_time_steps(container)\n    if isempty(bus_reduction_map)\n        ac_bus_numbers = collect(Iterators.flatten(values(subnetworks)))\n    else\n        ac_bus_numbers = collect(keys(bus_reduction_map))\n    end\n    subnetworks = collect(keys(subnetworks))\n    container.expressions = Dict(\n        ExpressionKey(ActivePowerBalance, PSY.System) =>\n            _make_container_array(subnetworks, time_steps),\n        ExpressionKey(ActivePowerBalance, PSY.ACBus) =>\n        # Bus numbers are sorted to guarantee consistency in the order between the\n        # containers\n            _make_container_array(sort!(ac_bus_numbers), time_steps),\n    )\n    return\nend\n\nfunction _make_system_expressions!(\n    container::OptimizationContainer,\n    subnetworks::Dict{Int, Set{Int}},\n    ::Type{AreaBalancePowerModel},\n    areas::IS.FlattenIteratorWrapper{PSY.Area},\n)\n    time_steps = get_time_steps(container)\n    container.expressions = Dict(\n        ExpressionKey(ActivePowerBalance, PSY.Area) =>\n            _make_container_array(PSY.get_name.(areas), time_steps),\n    )\n    return\nend\n\nfunction _make_system_expressions!(\n    container::OptimizationContainer,\n    subnetworks::Dict{Int, Set{Int}},\n    ::Vector{Int},\n    ::Type{AreaPTDFPowerModel},\n    areas::IS.FlattenIteratorWrapper{PSY.Area},\n    bus_reduction_map::Dict{Int64, Set{Int64}},\n)\n    time_steps = get_time_steps(container)\n    if isempty(bus_reduction_map)\n        ac_bus_numbers = collect(Iterators.flatten(values(subnetworks)))\n    else\n        ac_bus_numbers = collect(keys(bus_reduction_map))\n    end\n    container.expressions = Dict(\n        # Enforces the balance by Area\n        ExpressionKey(ActivePowerBalance, PSY.Area) =>\n            _make_container_array(PSY.get_name.(areas), time_steps),\n        # Keeps track of the Injections by bus.\n        ExpressionKey(ActivePowerBalance, PSY.ACBus) =>\n        # Bus numbers are sorted to guarantee consistency in the order between the\n        # containers\n            _make_container_array(sort!(ac_bus_numbers), time_steps),\n    )\n\n    if length(subnetworks) > 1\n        @warn \"The system contains $(length(subnetworks)) synchronous regions. \\\n               When combined with AreaPTDFPowerModel, the model can be infeasible if the data doesn't \\\n               have a well defined topology\"\n        subnetworks_ref_buses = collect(keys(subnetworks))\n        container.expressions[ExpressionKey(ActivePowerBalance, PSY.System)] =\n            _make_container_array(subnetworks_ref_buses, time_steps)\n    end\n\n    return\nend\n\nfunction initialize_system_expressions!(\n    container::OptimizationContainer,\n    network_model::NetworkModel{T},\n    subnetworks::Dict{Int, Set{Int}},\n    ::BranchModelContainer,\n    system::PSY.System,\n    bus_reduction_map::Dict{Int64, Set{Int64}},\n) where {T <: PM.AbstractPowerModel}\n    dc_bus_numbers = [\n        PSY.get_number(b) for\n        b in get_available_components(network_model, PSY.DCBus, system)\n    ]\n    _make_system_expressions!(container, subnetworks, dc_bus_numbers, T, bus_reduction_map)\n    return\nend\n\nfunction _verify_area_subnetwork_topology(sys::PSY.System, subnetworks::Dict{Int, Set{Int}})\n    if length(subnetworks) < 1\n        @debug \"Only one subnetwork detected in the system. Area - Subnetwork topology check is valid.\"\n        return\n    end\n\n    @warn \"More than one subnetwork detected in AreaBalancePowerModel. Topology consistency checks must be conducted.\"\n\n    area_map = PSY.get_aggregation_topology_mapping(PSY.Area, sys)\n    for (area, buses) in area_map\n        bus_numbers =\n            [\n                PSY.get_number(b) for\n                b in buses if PSY.get_bustype(b) != PSY.ACBusTypes.ISOLATED\n            ]\n        subnets = Int[]\n        for (subnet, subnet_bus_numbers) in subnetworks\n            if !isdisjoint(bus_numbers, subnet_bus_numbers)\n                push!(subnets, subnet)\n            end\n        end\n        if length(subnets) > 1\n            @error \"Area $(PSY.get_name(area)) is connected to multiple subnetworks $(subnets).\"\n            throw(\n                IS.ConflictingInputsError(\n                    \"AreaBalancePowerModel doesn't support systems with Areas distributed across multiple asynchronous areas\",\n                ))\n        end\n    end\n    return\nend\n\nfunction initialize_system_expressions!(\n    container::OptimizationContainer,\n    network_model::NetworkModel{AreaBalancePowerModel},\n    subnetworks::Dict{Int, Set{Int}},\n    branch_models::BranchModelContainer,\n    system::PSY.System,\n    ::Dict{Int64, Set{Int64}},\n)\n    areas = get_available_components(network_model, PSY.Area, system)\n    if isempty(areas)\n        throw(\n            IS.ConflictingInputsError(\n                \"AreaBalancePowerModel doesn't support systems with no defined Areas\",\n            ),\n        )\n    end\n    area_interchanges = PSY.get_available_components(PSY.AreaInterchange, system)\n    if isempty(area_interchanges) || !haskey(branch_models, :AreaInterchange)\n        @warn \"The system does not contain any AreaInterchanges. The model won't have any power flowing between the areas.\"\n    end\n    if !isempty(area_interchanges) && !haskey(branch_models, :AreaInterchange)\n        @warn \"AreaInterchanges are not included in the model template. The model won't have any power flowing between the areas.\"\n    end\n    _verify_area_subnetwork_topology(system, subnetworks)\n    _make_system_expressions!(container, subnetworks, AreaBalancePowerModel, areas)\n    return\nend\n\nfunction initialize_system_expressions!(\n    container::OptimizationContainer,\n    network_model::NetworkModel{T},\n    subnetworks::Dict{Int, Set{Int}},\n    ::BranchModelContainer,\n    system::PSY.System,\n    bus_reduction_map::Dict{Int64, Set{Int64}},\n) where {T <: AreaPTDFPowerModel}\n    areas = get_available_components(network_model, PSY.Area, system)\n    if isempty(areas)\n        throw(\n            IS.ConflictingInputsError(\n                \"AreaPTDFPowerModel doesn't support systems with no Areas\",\n            ),\n        )\n    end\n    dc_bus_numbers = [\n        PSY.get_number(b) for\n        b in get_available_components(network_model, PSY.DCBus, system)\n    ]\n    _make_system_expressions!(\n        container,\n        subnetworks,\n        dc_bus_numbers,\n        AreaPTDFPowerModel,\n        areas,\n        bus_reduction_map,\n    )\n    return\nend\n\nfunction initialize_hvdc_system!(\n    container::OptimizationContainer,\n    network_model::NetworkModel{T},\n    dc_model::Nothing,\n    system::PSY.System,\n) where {T <: PM.AbstractPowerModel}\n    dc_buses = get_available_components(network_model, PSY.DCBus, system)\n    if !isempty(dc_buses)\n        @warn \"HVDC Network Model is set to 'Nothing' but DC Buses are present in the system. \\\n               Consider adding an HVDC Network Model or removing DC Buses from the system.\"\n    end\n    return\nend\n\nfunction initialize_hvdc_system!(\n    container::OptimizationContainer,\n    network_model::NetworkModel{T},\n    dc_model::U,\n    system::PSY.System,\n) where {T <: PM.AbstractPowerModel, U <: TransportHVDCNetworkModel}\n    dc_buses = get_available_components(network_model, PSY.DCBus, system)\n    @assert !isempty(dc_buses) \"No DC buses found in the system. Consider adding DC Buses or removing HVDC network model.\"\n    dc_bus_numbers = sort(PSY.get_number.(dc_buses))\n    container.expressions[ExpressionKey(ActivePowerBalance, PSY.DCBus)] =\n        _make_container_array(dc_bus_numbers, get_time_steps(container))\n    return\nend\n\nfunction initialize_hvdc_system!(\n    container::OptimizationContainer,\n    network_model::NetworkModel{T},\n    dc_model::U,\n    system::PSY.System,\n) where {T <: PM.AbstractPowerModel, U <: VoltageDispatchHVDCNetworkModel}\n    dc_buses = get_available_components(network_model, PSY.DCBus, system)\n    @assert !isempty(dc_buses) \"No DC buses found in the system. Consider adding DC Buses or removing HVDC network model.\"\n    dc_bus_numbers = sort(PSY.get_number.(dc_buses))\n    container.expressions[ExpressionKey(DCCurrentBalance, PSY.DCBus)] =\n        _make_container_array(dc_bus_numbers, get_time_steps(container))\n    add_variable!(container, DCVoltage(), dc_buses, dc_model)\n    return\nend\n\nfunction build_impl!(\n    container::OptimizationContainer,\n    template::ProblemTemplate,\n    sys::PSY.System,\n)\n    transmission = get_network_formulation(template)\n    transmission_model = get_network_model(template)\n    hvdc_model = get_hvdc_network_model(template)\n\n    initialize_system_expressions!(\n        container,\n        get_network_model(template),\n        transmission_model.subnetworks,\n        get_branch_models(template),\n        sys,\n        transmission_model.network_reduction.bus_reduction_map)\n\n    initialize_hvdc_system!(\n        container,\n        transmission_model,\n        hvdc_model,\n        sys,\n    )\n\n    # Order is required\n    for device_model in values(template.devices)\n        @debug \"Building Arguments for $(get_component_type(device_model)) with $(get_formulation(device_model)) formulation\" _group =\n            LOG_GROUP_OPTIMIZATION_CONTAINER\n        TimerOutputs.@timeit BUILD_PROBLEMS_TIMER \"$(get_component_type(device_model))\" begin\n            construct_device!(\n                container,\n                sys,\n                ArgumentConstructStage(),\n                device_model,\n                transmission_model,\n            )\n            @debug \"Problem size:\" get_problem_size(container) _group =\n                LOG_GROUP_OPTIMIZATION_CONTAINER\n        end\n    end\n\n    TimerOutputs.@timeit BUILD_PROBLEMS_TIMER \"Services\" begin\n        construct_services!(\n            container,\n            sys,\n            ArgumentConstructStage(),\n            get_service_models(template),\n            get_device_models(template),\n            transmission_model,\n        )\n    end\n\n    # # Sort branch models so that those with time series parameters (e.g. DLR) come first.\n    # # This ensures that DLR-aware constraint builders claim shared arcs before static builders,\n    # # preventing static constraints from overriding DLR constraints for parallel branches of\n    # # different types sharing the same arc.\n    # sorted_branch_models = sort(\n    #     collect(values(template.branches));\n    #     by = b -> isempty(get_time_series_names(b)) ? 1 : 0,\n    # )\n    for branch_model in values(template.branches)\n        @debug \"Building Arguments for $(get_component_type(branch_model)) with $(get_formulation(branch_model)) formulation\" _group =\n            LOG_GROUP_OPTIMIZATION_CONTAINER\n        TimerOutputs.@timeit BUILD_PROBLEMS_TIMER \"$(get_component_type(branch_model))\" begin\n            construct_device!(\n                container,\n                sys,\n                ArgumentConstructStage(),\n                branch_model,\n                transmission_model,\n            )\n            @debug \"Problem size:\" get_problem_size(container) _group =\n                LOG_GROUP_OPTIMIZATION_CONTAINER\n        end\n    end\n    for device_model in values(template.devices)\n        @debug \"Building Model for $(get_component_type(device_model)) with $(get_formulation(device_model)) formulation\" _group =\n            LOG_GROUP_OPTIMIZATION_CONTAINER\n        TimerOutputs.@timeit BUILD_PROBLEMS_TIMER \"$(get_component_type(device_model))\" begin\n            construct_device!(\n                container,\n                sys,\n                ModelConstructStage(),\n                device_model,\n                transmission_model,\n            )\n            @debug \"Problem size:\" get_problem_size(container) _group =\n                LOG_GROUP_OPTIMIZATION_CONTAINER\n        end\n    end\n\n    # This function should be called after construct_device ModelConstructStage\n    TimerOutputs.@timeit BUILD_PROBLEMS_TIMER \"$(transmission)\" begin\n        @debug \"Building $(transmission) network formulation\" _group =\n            LOG_GROUP_OPTIMIZATION_CONTAINER\n        construct_network!(container, sys, transmission_model, template)\n        construct_hvdc_network!(container, sys, transmission_model, hvdc_model, template)\n        @debug \"Problem size:\" get_problem_size(container) _group =\n            LOG_GROUP_OPTIMIZATION_CONTAINER\n    end\n    for branch_model in values(template.branches)\n        @debug \"Building Model for $(get_component_type(branch_model)) with $(get_formulation(branch_model)) formulation\" _group =\n            LOG_GROUP_OPTIMIZATION_CONTAINER\n        TimerOutputs.@timeit BUILD_PROBLEMS_TIMER \"$(get_component_type(branch_model))\" begin\n            construct_device!(\n                container,\n                sys,\n                ModelConstructStage(),\n                branch_model,\n                transmission_model,\n            )\n            @debug \"Problem size:\" get_problem_size(container) _group =\n                LOG_GROUP_OPTIMIZATION_CONTAINER\n        end\n    end\n\n    TimerOutputs.@timeit BUILD_PROBLEMS_TIMER \"Services\" begin\n        construct_services!(\n            container,\n            sys,\n            ModelConstructStage(),\n            get_service_models(template),\n            get_device_models(template),\n            transmission_model,\n        )\n    end\n\n    TimerOutputs.@timeit BUILD_PROBLEMS_TIMER \"Objective\" begin\n        @debug \"Building Objective\" _group = LOG_GROUP_OPTIMIZATION_CONTAINER\n        update_objective_function!(container)\n    end\n    @debug \"Total operation count $(PSI.get_jump_model(container).operator_counter)\" _group =\n        LOG_GROUP_OPTIMIZATION_CONTAINER\n\n    TimerOutputs.@timeit BUILD_PROBLEMS_TIMER \"Power Flow Initialization\" begin\n        add_power_flow_data!(container, get_power_flow_evaluation(transmission_model), sys)\n    end\n    check_optimization_container(container)\n    return\nend\n\nfunction update_objective_function!(container::OptimizationContainer)\n    JuMP.@objective(\n        get_jump_model(container),\n        get_sense(container.objective_function),\n        get_objective_expression(container.objective_function)\n    )\n    return\nend\n\n\"\"\"\nDefault solve method for OptimizationContainer\n\"\"\"\nfunction solve_impl!(container::OptimizationContainer, system::PSY.System)\n    optimizer_stats = get_optimizer_stats(container)\n\n    jump_model = get_jump_model(container)\n\n    model_status = MOI.NO_SOLUTION::MOI.ResultStatusCode\n    conflict_status = MOI.COMPUTE_CONFLICT_NOT_CALLED\n\n    try_count = 0\n    while model_status != MOI.FEASIBLE_POINT::MOI.ResultStatusCode\n        _,\n        optimizer_stats.timed_solve_time,\n        optimizer_stats.solve_bytes_alloc,\n        optimizer_stats.sec_in_gc = @timed JuMP.optimize!(jump_model)\n        model_status = JuMP.primal_status(jump_model)\n\n        if model_status != MOI.FEASIBLE_POINT::MOI.ResultStatusCode\n            if get_calculate_conflict(get_settings(container))\n                @warn \"Optimizer returned $model_status computing conflict\"\n                conflict_status = compute_conflict!(container)\n                if conflict_status == MOI.CONFLICT_FOUND\n                    return RunStatus.FAILED\n                end\n            else\n                @warn \"Optimizer returned $model_status trying optimize! again\"\n            end\n\n            try_count += 1\n            if try_count > MAX_OPTIMIZE_TRIES\n                @error \"Optimizer returned $model_status after $MAX_OPTIMIZE_TRIES optimize! attempts\"\n                return RunStatus.FAILED\n            end\n        end\n    end\n\n    # Order is important because if a dual is needed then it could move the results to the\n    # temporary primal container\n    _, optimizer_stats.timed_calculate_aux_variables =\n        @timed calculate_aux_variables!(container, system)\n\n    # Needs to be called here to avoid issues when getting duals from MILPs\n    write_optimizer_stats!(container)\n\n    _, optimizer_stats.timed_calculate_dual_variables =\n        @timed calculate_dual_variables!(container, system, is_milp(container))\n\n    return RunStatus.SUCCESSFULLY_FINALIZED\nend\n\nfunction compute_conflict!(container::OptimizationContainer)\n    jump_model = get_jump_model(container)\n    settings = get_settings(container)\n    JuMP.unset_silent(jump_model)\n    jump_model.is_model_dirty = false\n    conflict = container.infeasibility_conflict\n    try\n        JuMP.compute_conflict!(jump_model)\n        conflict_status = MOI.get(jump_model, MOI.ConflictStatus())\n        if conflict_status != MOI.CONFLICT_FOUND\n            @error \"No conflict could be found for the model. Status: $conflict_status\"\n            if !get_optimizer_solve_log_print(settings)\n                JuMP.set_silent(jump_model)\n            end\n            return conflict_status\n        end\n\n        for (key, field_container) in get_constraints(container)\n            conflict_indices = check_conflict_status(jump_model, field_container)\n            if isempty(conflict_indices)\n                @info \"Conflict Index returned empty for $key\"\n                continue\n            else\n                conflict[ISOPT.encode_key(key)] = conflict_indices\n            end\n        end\n\n        msg = IOBuffer()\n        for (k, v) in conflict\n            PrettyTables.pretty_table(msg, v; header = [k])\n        end\n\n        @error \"Constraints participating in conflict basis (IIS) \\n\\n$(String(take!(msg)))\"\n\n        return conflict_status\n    catch e\n        jump_model.is_model_dirty = true\n        if isa(e, MethodError)\n            @info \"Can't compute conflict, check that your optimizer supports conflict refining/IIS\"\n        else\n            @error \"Can't compute conflict\" exception = (e, catch_backtrace())\n        end\n    end\n\n    return MOI.NO_CONFLICT_EXISTS\nend\n\nfunction write_optimizer_stats!(container::OptimizationContainer)\n    write_optimizer_stats!(get_optimizer_stats(container), get_jump_model(container))\n    return\nend\n\n\"\"\"\nExports the OpModel JuMP object in MathOptFormat\n\"\"\"\nfunction serialize_optimization_model(container::OptimizationContainer, save_path::String)\n    serialize_jump_optimization_model(get_jump_model(container), save_path)\n    return\nend\n\nfunction serialize_metadata!(container::OptimizationContainer, output_dir::String)\n    for key in Iterators.flatten((\n        keys(container.constraints),\n        keys(container.duals),\n        keys(container.parameters),\n        keys(container.variables),\n        keys(container.aux_variables),\n        keys(container.expressions),\n    ))\n        encoded_key = encode_key_as_string(key)\n        if ISOPT.has_container_key(container.metadata, encoded_key)\n            # Constraints and Duals can store the same key.\n            IS.@assert_op key ==\n                          ISOPT.get_container_key(container.metadata, encoded_key)\n        end\n        ISOPT.add_container_key!(container.metadata, encoded_key, key)\n    end\n\n    filename = ISOPT._make_metadata_filename(output_dir)\n    Serialization.serialize(filename, container.metadata)\n    @debug \"Serialized container keys to $filename\" _group = IS.LOG_GROUP_SERIALIZATION\nend\n\nfunction deserialize_metadata!(\n    container::OptimizationContainer,\n    output_dir::String,\n    model_name,\n)\n    merge!(\n        container.metadata.container_key_lookup,\n        deserialize_metadata(\n            ISOPT.OptimizationContainerMetadata,\n            output_dir,\n            model_name,\n        ),\n    )\n    return\nend\n\n# PERF: compilation hotspot. from string conversion at the container[key] = value line?\nfunction _assign_container!(container::OrderedDict, key::OptimizationContainerKey, value)\n    if haskey(container, key)\n        @error \"$(ISOPT.encode_key(key)) is already stored\" sort!(\n            ISOPT.encode_key.(keys(container)),\n        )\n        throw(IS.InvalidValue(\"$key is already stored\"))\n    end\n    container[key] = value\n    @debug \"Added container entry $(typeof(key)) $(ISOPT.encode_key(key))\" _group =\n        LOG_GROUP_OPTIMZATION_CONTAINER\n    return\nend\n\n####################################### Variable Container #################################\nfunction _add_variable_container!(\n    container::OptimizationContainer,\n    var_key::VariableKey{T, U},\n    sparse::Bool,\n    axs...,\n) where {T <: VariableType, U <: Union{PSY.Component, PSY.System}}\n    if sparse\n        var_container = sparse_container_spec(JuMP.VariableRef, axs...)\n    else\n        var_container = container_spec(JuMP.VariableRef, axs...)\n    end\n    _assign_container!(container.variables, var_key, var_container)\n    return var_container\nend\n\nfunction add_variable_container!(\n    container::OptimizationContainer,\n    ::T,\n    ::Type{U},\n    axs...;\n    sparse = false,\n    meta = ISOPT.CONTAINER_KEY_EMPTY_META,\n) where {T <: VariableType, U <: Union{PSY.Component, PSY.System}}\n    var_key = VariableKey(T, U, meta)\n    return _add_variable_container!(container, var_key, sparse, axs...)\nend\n\nfunction add_variable_container!(\n    container::OptimizationContainer,\n    ::T,\n    ::Type{U},\n    meta::String,\n    axs...;\n    sparse = false,\n) where {T <: VariableType, U <: Union{PSY.Component, PSY.System}}\n    var_key = VariableKey(T, U, meta)\n    return _add_variable_container!(container, var_key, sparse, axs...)\nend\n\nfunction _get_pwl_variables_container()\n    contents = Dict{Tuple{String, Int, Int}, Any}()\n    return SparseAxisArray(contents)\nend\n\nfunction add_variable_container!(\n    container::OptimizationContainer,\n    ::T,\n    ::Type{U};\n    meta = ISOPT.CONTAINER_KEY_EMPTY_META,\n) where {T <: SparseVariableType, U <: Union{PSY.Component, PSY.System}}\n    var_key = VariableKey(T, U, meta)\n    _assign_container!(container.variables, var_key, _get_pwl_variables_container())\n    return container.variables[var_key]\nend\n\nfunction get_variable_keys(container::OptimizationContainer)\n    return collect(keys(container.variables))\nend\n\nfunction get_variable(container::OptimizationContainer, key::VariableKey)\n    var = get(container.variables, key, nothing)\n    if var === nothing\n        name = ISOPT.encode_key(key)\n        keys = ISOPT.encode_key.(get_variable_keys(container))\n        throw(IS.InvalidValue(\"variable $name is not stored. $keys\"))\n    end\n    return var\nend\n\nfunction get_variable(\n    container::OptimizationContainer,\n    ::T,\n    ::Type{U},\n    meta::String = ISOPT.CONTAINER_KEY_EMPTY_META,\n) where {T <: VariableType, U <: Union{PSY.Component, PSY.System}}\n    return get_variable(container, VariableKey(T, U, meta))\nend\n\n##################################### AuxVariable Container ################################\nfunction add_aux_variable_container!(\n    container::OptimizationContainer,\n    ::T,\n    ::Type{U},\n    axs...;\n    sparse = false,\n    meta = ISOPT.CONTAINER_KEY_EMPTY_META,\n) where {T <: AuxVariableType, U <: PSY.Component}\n    var_key = AuxVarKey(T, U, meta)\n    if sparse\n        aux_variable_container = sparse_container_spec(Float64, axs...)\n    else\n        aux_variable_container = container_spec(Float64, axs...)\n    end\n    _assign_container!(container.aux_variables, var_key, aux_variable_container)\n    return aux_variable_container\nend\n\nfunction get_aux_variable_keys(container::OptimizationContainer)\n    return collect(keys(container.aux_variables))\nend\n\nfunction get_aux_variable(container::OptimizationContainer, key::AuxVarKey)\n    aux = get(container.aux_variables, key, nothing)\n    if aux === nothing\n        name = ISOPT.encode_key(key)\n        keys = ISOPT.encode_key.(get_aux_variable_keys(container))\n        throw(IS.InvalidValue(\"Auxiliary variable $name is not stored. $keys\"))\n    end\n    return aux\nend\n\nfunction get_aux_variable(\n    container::OptimizationContainer,\n    ::T,\n    ::Type{U},\n    meta::String = ISOPT.CONTAINER_KEY_EMPTY_META,\n) where {T <: AuxVariableType, U <: PSY.Component}\n    return get_aux_variable(container, AuxVarKey(T, U, meta))\nend\n\n##################################### DualVariable Container ################################\nfunction add_dual_container!(\n    container::OptimizationContainer,\n    ::Type{T},\n    ::Type{U},\n    axs...;\n    sparse = false,\n    meta = ISOPT.CONTAINER_KEY_EMPTY_META,\n) where {T <: ConstraintType, U <: Union{PSY.Component, PSY.System}}\n    if is_milp(container)\n        @warn(\"The model has resulted in a MILP, \\\\\n              dual value retrieval requires solving an additional Linear Program \\\\\n              which increases simulation time and the results could be innacurate.\")\n    end\n    const_key = ConstraintKey(T, U, meta)\n    if sparse\n        dual_container = sparse_container_spec(Float64, axs...)\n    else\n        dual_container = container_spec(Float64, axs...)\n    end\n    _assign_container!(container.duals, const_key, dual_container)\n    return dual_container\nend\n\nfunction get_dual_keys(container::OptimizationContainer)\n    return collect(keys(container.duals))\nend\n\n##################################### Constraint Container #################################\nfunction _add_constraints_container!(\n    container::OptimizationContainer,\n    cons_key::ConstraintKey,\n    axs...;\n    sparse = false,\n)\n    if sparse\n        cons_container = sparse_container_spec(JuMP.ConstraintRef, axs...)\n    else\n        cons_container = container_spec(JuMP.ConstraintRef, axs...)\n    end\n    _assign_container!(container.constraints, cons_key, cons_container)\n    return cons_container\nend\n\nfunction add_constraints_container!(\n    container::OptimizationContainer,\n    ::T,\n    ::Type{U},\n    axs...;\n    sparse = false,\n    meta = ISOPT.CONTAINER_KEY_EMPTY_META,\n) where {T <: ConstraintType, U <: Union{PSY.Component, PSY.System}}\n    cons_key = ConstraintKey(T, U, meta)\n    return _add_constraints_container!(container, cons_key, axs...; sparse = sparse)\nend\n\nfunction get_constraint_keys(container::OptimizationContainer)\n    return collect(keys(container.constraints))\nend\n\nfunction get_constraint(container::OptimizationContainer, key::ConstraintKey)\n    var = get(container.constraints, key, nothing)\n    if var === nothing\n        name = ISOPT.encode_key(key)\n        keys = ISOPT.encode_key.(get_constraint_keys(container))\n        throw(IS.InvalidValue(\"constraint $name is not stored. $keys\"))\n    end\n\n    return var\nend\n\nfunction get_constraint(\n    container::OptimizationContainer,\n    ::T,\n    ::Type{U},\n    meta::String = ISOPT.CONTAINER_KEY_EMPTY_META,\n) where {T <: ConstraintType, U <: Union{PSY.Component, PSY.System}}\n    return get_constraint(container, ConstraintKey(T, U, meta))\nend\n\nfunction read_duals(container::OptimizationContainer)\n    return Dict(k => to_dataframe(jump_value.(v), k) for (k, v) in get_duals(container))\nend\n\n##################################### Parameter Container ##################################\nfunction _add_param_container!(\n    container::OptimizationContainer,\n    key::ParameterKey{T, U},\n    attribute::VariableValueAttributes{<:OptimizationContainerKey},\n    param_type::DataType,\n    axs...;\n    sparse = false,\n) where {T <: VariableValueParameter, U <: PSY.Component}\n    if sparse\n        param_array = sparse_container_spec(param_type, axs...)\n        multiplier_array = sparse_container_spec(Float64, axs...)\n    else\n        param_array = DenseAxisArray{param_type}(undef, axs...)\n        multiplier_array = fill!(DenseAxisArray{Float64}(undef, axs...), NaN)\n    end\n    param_container = ParameterContainer(attribute, param_array, multiplier_array)\n    _assign_container!(container.parameters, key, param_container)\n    return param_container\nend\n\nfunction _add_param_container!(\n    container::OptimizationContainer,\n    key::ParameterKey{T, U},\n    attribute::VariableValueAttributes{<:OptimizationContainerKey},\n    axs...;\n    sparse = false,\n) where {T <: VariableValueParameter, U <: PSY.Component}\n    if built_for_recurrent_solves(container) && !get_rebuild_model(get_settings(container))\n        param_type = JuMP.VariableRef\n    else\n        param_type = Float64\n    end\n    return _add_param_container!(\n        container,\n        key,\n        attribute,\n        param_type,\n        axs...;\n        sparse = sparse,\n    )\nend\n\nfunction _add_param_container!(\n    container::OptimizationContainer,\n    key::ParameterKey{T, U},\n    attribute::TimeSeriesAttributes{V},\n    param_axs,\n    multiplier_axs,\n    additional_axs,\n    time_steps::UnitRange{Int};\n    sparse = false,\n) where {T <: TimeSeriesParameter, U <: PSY.Component, V <: PSY.TimeSeriesData}\n    if built_for_recurrent_solves(container) && !get_rebuild_model(get_settings(container))\n        param_type = JuMP.VariableRef\n    else\n        param_type = Float64\n    end\n    if sparse\n        param_array =\n            sparse_container_spec(param_type, param_axs, additional_axs..., time_steps)\n        multiplier_array =\n            sparse_container_spec(Float64, multiplier_axs, additional_axs..., time_steps)\n    else\n        param_array =\n            DenseAxisArray{param_type}(undef, param_axs, additional_axs..., time_steps)\n        multiplier_array =\n            fill!(\n                DenseAxisArray{Float64}(\n                    undef,\n                    multiplier_axs,\n                    additional_axs...,\n                    time_steps,\n                ),\n                NaN,\n            )\n    end\n    param_container = ParameterContainer(attribute, param_array, multiplier_array)\n    _assign_container!(container.parameters, key, param_container)\n    return param_container\nend\n\nfunction _add_param_container!(\n    container::OptimizationContainer,\n    key::ParameterKey{T, U},\n    attribute::EventParametersAttributes{V, T},\n    param_axs,\n    time_steps;\n    sparse = false,\n) where {T <: EventParameter, U <: PSY.Component, V <: PSY.Contingency}\n    if built_for_recurrent_solves(container) && !get_rebuild_model(get_settings(container))\n        param_type = JuMP.VariableRef\n    else\n        param_type = Float64\n    end\n\n    if sparse\n        error(\"Sparse parameter container is not supported for $V\")\n    else\n        param_array = DenseAxisArray{param_type}(undef, param_axs, time_steps)\n        multiplier_array =\n            fill!(DenseAxisArray{Float64}(undef, param_axs, time_steps), NaN)\n    end\n    param_container = ParameterContainer(attribute, param_array, multiplier_array)\n    _assign_container!(container.parameters, key, param_container)\n    return param_container\nend\n\nfunction _add_param_container!(\n    container::OptimizationContainer,\n    key::ParameterKey{T, U},\n    attributes::CostFunctionAttributes{R},\n    axs...;\n    sparse = false,\n) where {R, T <: ObjectiveFunctionParameter, U <: PSY.Component}\n    if sparse\n        param_array = sparse_container_spec(R, axs...)\n        multiplier_array = sparse_container_spec(Float64, axs...)\n    else\n        param_array = DenseAxisArray{R}(undef, axs...)\n        multiplier_array = fill!(DenseAxisArray{Float64}(undef, axs...), NaN)\n    end\n    param_container = ParameterContainer(attributes, param_array, multiplier_array)\n    _assign_container!(container.parameters, key, param_container)\n    return param_container\nend\n\nfunction add_param_container!(\n    container::OptimizationContainer,\n    ::T,\n    ::Type{U},\n    ::Type{V},\n    name::String,\n    param_axs,\n    multiplier_axs,\n    additional_axs,\n    time_steps::UnitRange{Int};\n    sparse = false,\n    meta = ISOPT.CONTAINER_KEY_EMPTY_META,\n) where {T <: TimeSeriesParameter, U <: PSY.Component, V <: PSY.TimeSeriesData}\n    param_key = ParameterKey(T, U, meta)\n    if isabstracttype(V)\n        error(\"$V can't be abstract: $param_key\")\n    end\n    attributes = TimeSeriesAttributes(V, name)\n    return _add_param_container!(\n        container,\n        param_key,\n        attributes,\n        param_axs,\n        multiplier_axs,\n        additional_axs,\n        time_steps;\n        sparse = sparse,\n    )\nend\n\nfunction add_param_container!(\n    container::OptimizationContainer,\n    ::T,\n    ::Type{U},\n    variable_types::Tuple{Vararg{Type}},\n    sos_variable::SOSStatusVariable = NO_VARIABLE,\n    uses_compact_power::Bool = false,\n    data_type::DataType = Float64,\n    axs...;\n    sparse = false,\n    meta = ISOPT.CONTAINER_KEY_EMPTY_META,\n) where {T <: ObjectiveFunctionParameter, U <: PSY.Component}\n    param_key = ParameterKey(T, U, meta)\n    attributes =\n        CostFunctionAttributes{data_type}(variable_types, sos_variable, uses_compact_power)\n    return _add_param_container!(container, param_key, attributes, axs...; sparse = sparse)\nend\n\nfunction add_param_container!(\n    container::OptimizationContainer,\n    ::T,\n    ::Type{U},\n    ::Type{V},\n    axs...;\n    sparse = false,\n    meta = ISOPT.CONTAINER_KEY_EMPTY_META,\n) where {T <: EventParameter, U <: PSY.Component, V <: PSY.Contingency}\n    param_key = ParameterKey(T, U, meta)\n    attributes = EventParametersAttributes{V, T}(U[])\n    return _add_param_container!(container, param_key, attributes, axs...; sparse = sparse)\nend\n\nfunction add_param_container!(\n    container::OptimizationContainer,\n    ::T,\n    ::Type{U},\n    source_key::V,\n    axs...;\n    sparse = false,\n    meta = ISOPT.CONTAINER_KEY_EMPTY_META,\n) where {T <: VariableValueParameter, U <: PSY.Component, V <: OptimizationContainerKey}\n    param_key = ParameterKey(T, U, meta)\n    attributes = VariableValueAttributes(source_key)\n    return _add_param_container!(container, param_key, attributes, axs...; sparse = sparse)\nend\n\n# FixValue parameters are created using Float64 since we employ JuMP.fix to fix the downstream\n# variables.\nfunction add_param_container!(\n    container::OptimizationContainer,\n    ::T,\n    ::Type{U},\n    source_key::V,\n    axs...;\n    sparse = false,\n    meta = ISOPT.CONTAINER_KEY_EMPTY_META,\n) where {T <: FixValueParameter, U <: PSY.Component, V <: OptimizationContainerKey}\n    if meta == ISOPT.CONTAINER_KEY_EMPTY_META\n        error(\"$T parameters require passing the VariableType to the meta field\")\n    end\n    param_key = ParameterKey(T, U, meta)\n    attributes = VariableValueAttributes(source_key)\n    return _add_param_container!(\n        container,\n        param_key,\n        attributes,\n        Float64,\n        axs...;\n        sparse = sparse,\n    )\nend\n\nfunction get_parameter_keys(container::OptimizationContainer)\n    return collect(keys(container.parameters))\nend\n\nfunction get_parameter(container::OptimizationContainer, key::ParameterKey)\n    param_container = get(container.parameters, key, nothing)\n    if param_container === nothing\n        name = ISOPT.encode_key(key)\n        throw(\n            IS.InvalidValue(\n                \"parameter $name is not stored. $(collect(keys(container.parameters)))\",\n            ),\n        )\n    end\n    return param_container\nend\n\nfunction get_parameter(\n    container::OptimizationContainer,\n    ::T,\n    ::Type{U},\n    meta = ISOPT.CONTAINER_KEY_EMPTY_META,\n) where {T <: ParameterType, U <: Union{PSY.Component, PSY.System}}\n    return get_parameter(container, ParameterKey(T, U, meta))\nend\n\nfunction get_parameter_array(container::OptimizationContainer, key)\n    return get_parameter_array(get_parameter(container, key))\nend\n\nfunction get_parameter_array(\n    container::OptimizationContainer,\n    key::ParameterKey{T, U},\n) where {T <: ParameterType, U <: Union{PSY.Component, PSY.System}}\n    return get_parameter_array(get_parameter(container, key))\nend\n\nfunction get_parameter_multiplier_array(\n    container::OptimizationContainer,\n    key::ParameterKey{T, U},\n) where {T <: ParameterType, U <: Union{PSY.Component, PSY.System}}\n    return get_multiplier_array(get_parameter(container, key))\nend\n\nfunction get_parameter_attributes(\n    container::OptimizationContainer,\n    key::ParameterKey{T, U},\n) where {T <: ParameterType, U <: Union{PSY.Component, PSY.System}}\n    return get_attributes(get_parameter(container, key))\nend\n\nfunction get_parameter_array(\n    container::OptimizationContainer,\n    ::T,\n    ::Type{U},\n    meta = ISOPT.CONTAINER_KEY_EMPTY_META,\n) where {T <: ParameterType, U <: Union{PSY.Component, PSY.System}}\n    return get_parameter_array(container, ParameterKey(T, U, meta))\nend\nfunction get_parameter_multiplier_array(\n    container::OptimizationContainer,\n    ::T,\n    ::Type{U},\n    meta = ISOPT.CONTAINER_KEY_EMPTY_META,\n) where {T <: ParameterType, U <: Union{PSY.Component, PSY.System}}\n    return get_multiplier_array(get_parameter(container, ParameterKey(T, U, meta)))\nend\n\nfunction get_parameter_attributes(\n    container::OptimizationContainer,\n    ::T,\n    ::Type{U},\n    meta = ISOPT.CONTAINER_KEY_EMPTY_META,\n) where {T <: ParameterType, U <: Union{PSY.Component, PSY.System}}\n    return get_attributes(get_parameter(container, ParameterKey(T, U, meta)))\nend\n\n# Slow implementation not to be used in hot loops\nfunction read_parameters(container::OptimizationContainer)\n    params_dict = Dict{ParameterKey, DenseAxisArray}()\n    parameters = get_parameters(container)\n    (parameters === nothing || isempty(parameters)) && return params_dict\n    for (k, v) in parameters\n        # TODO: all functions similar to calculate_parameter_values should be in one\n        # place and be consistent in behavior.\n        #params_dict[k] = to_dataframe(calculate_parameter_values(v))\n        param_array = get_parameter_values(v)\n        multiplier_array = get_multiplier_array(v)\n        params_dict[k] = _calculate_parameter_values(k, param_array, multiplier_array)\n    end\n    return params_dict\nend\n\nfunction _calculate_parameter_values(\n    ::ParameterKey{<:ParameterType, <:PSY.Component},\n    param_array,\n    multiplier_array,\n)\n    return param_array .* multiplier_array\nend\n\nfunction _calculate_parameter_values(\n    ::ParameterKey{<:ObjectiveFunctionParameter, <:PSY.Component},\n    param_array,\n    multiplier_array,\n)\n    return param_array\nend\n##################################### Expression Container #################################\nfunction _add_expression_container!(\n    container::OptimizationContainer,\n    expr_key::ExpressionKey,\n    ::Type{T},\n    axs...;\n    sparse = false,\n) where {T <: JuMP.AbstractJuMPScalar}\n    if sparse\n        expr_container = sparse_container_spec(T, axs...)\n    else\n        expr_container = container_spec(T, axs...)\n    end\n    remove_undef!(expr_container)\n    _assign_container!(container.expressions, expr_key, expr_container)\n    return expr_container\nend\n\nfunction add_expression_container!(\n    container::OptimizationContainer,\n    ::T,\n    ::Type{U},\n    axs...;\n    expr_type = GAE,\n    sparse = false,\n    meta = ISOPT.CONTAINER_KEY_EMPTY_META,\n) where {T <: ExpressionType, U <: Union{PSY.Component, PSY.System}}\n    expr_key = ExpressionKey(T, U, meta)\n    return _add_expression_container!(\n        container,\n        expr_key,\n        expr_type,\n        axs...;\n        sparse = sparse,\n    )\nend\n\nfunction add_expression_container!(\n    container::OptimizationContainer,\n    ::T,\n    ::Type{U},\n    axs...;\n    sparse = false,\n    meta = ISOPT.CONTAINER_KEY_EMPTY_META,\n) where {T <: CostExpressions, U <: Union{PSY.Component, PSY.System}}\n    expr_key = ExpressionKey(T, U, meta)\n    expr_type = JuMP.QuadExpr\n    return _add_expression_container!(\n        container,\n        expr_key,\n        expr_type,\n        axs...;\n        sparse = sparse,\n    )\nend\n\nfunction get_expression_keys(container::OptimizationContainer)\n    return collect(keys(container.expressions))\nend\n\nfunction get_expression(container::OptimizationContainer, key::ExpressionKey)\n    var = get(container.expressions, key, nothing)\n    if var === nothing\n        throw(\n            IS.InvalidValue(\n                \"expression $key is not stored. $(collect(keys(container.expressions)))\",\n            ),\n        )\n    end\n\n    return var\nend\n\nfunction get_expression(\n    container::OptimizationContainer,\n    ::T,\n    ::Type{U},\n    meta = ISOPT.CONTAINER_KEY_EMPTY_META,\n) where {T <: ExpressionType, U <: Union{PSY.Component, PSY.System}}\n    return get_expression(container, ExpressionKey(T, U, meta))\nend\n\nfunction read_expressions(container::OptimizationContainer)\n    return Dict(\n        k => to_dataframe(jump_value.(v), k) for (k, v) in get_expressions(container) if\n        !(get_entry_type(k) <: SystemBalanceExpressions)\n    )\nend\n\n###################################Initial Conditions Containers############################\nfunction _add_initial_condition_container!(\n    container::OptimizationContainer,\n    ic_key::InitialConditionKey{T, U},\n    length_devices::Int,\n) where {T <: InitialConditionType, U <: Union{PSY.Component, PSY.System}}\n    if built_for_recurrent_solves(container) && !get_rebuild_model(get_settings(container))\n        ini_conds = Vector{\n            Union{InitialCondition{T, JuMP.VariableRef}, InitialCondition{T, Nothing}},\n        }(\n            undef,\n            length_devices,\n        )\n    else\n        ini_conds =\n            Vector{Union{InitialCondition{T, Float64}, InitialCondition{T, Nothing}}}(\n                undef,\n                length_devices,\n            )\n    end\n    _assign_container!(container.initial_conditions, ic_key, ini_conds)\n    return ini_conds\nend\n\nfunction add_initial_condition_container!(\n    container::OptimizationContainer,\n    ::T,\n    ::Type{U},\n    axs;\n    meta = ISOPT.CONTAINER_KEY_EMPTY_META,\n) where {T <: InitialConditionType, U <: Union{PSY.Component, PSY.System}}\n    ic_key = InitialConditionKey(T, U, meta)\n    @debug \"add_initial_condition_container\" ic_key _group = LOG_GROUP_SERVICE_CONSTUCTORS\n    return _add_initial_condition_container!(container, ic_key, length(axs))\nend\n\nfunction get_initial_condition(\n    container::OptimizationContainer,\n    ::T,\n    ::Type{D},\n) where {T <: InitialConditionType, D <: PSY.Component}\n    return get_initial_condition(container, InitialConditionKey(T, D))\nend\n\nfunction get_initial_condition(container::OptimizationContainer, key::InitialConditionKey)\n    initial_conditions = get(container.initial_conditions, key, nothing)\n    if initial_conditions === nothing\n        throw(IS.InvalidValue(\"initial conditions are not stored for $(key)\"))\n    end\n    return initial_conditions\nend\n\nfunction get_initial_conditions_keys(container::OptimizationContainer)\n    return collect(keys(container.initial_conditions))\nend\n\nfunction write_initial_conditions_data!(\n    container::OptimizationContainer,\n    ic_container::OptimizationContainer,\n)\n    for field in STORE_CONTAINERS\n        ic_container_dict = getfield(ic_container, field)\n        if field == STORE_CONTAINER_PARAMETERS\n            ic_container_dict = read_parameters(ic_container)\n        end\n        if field == STORE_CONTAINER_EXPRESSIONS\n            continue\n        end\n        isempty(ic_container_dict) && continue\n        ic_data_dict = getfield(get_initial_conditions_data(container), field)\n        for (key, field_container) in ic_container_dict\n            @debug \"Adding $(encode_key_as_string(key)) to InitialConditionsData\" _group =\n                LOG_GROUP_SERVICE_CONSTUCTORS\n            if field == STORE_CONTAINER_PARAMETERS\n                ic_data_dict[key] = ic_container_dict[key]\n            else\n                ic_data_dict[key] = jump_value.(field_container)\n            end\n        end\n    end\n    return\nend\n\n# Note: These methods aren't passing the potential meta fields in the keys\nfunction get_initial_conditions_variable(\n    container::OptimizationContainer,\n    type::VariableType,\n    ::Type{T},\n) where {T <: Union{PSY.Component, PSY.System}}\n    return get_initial_conditions_variable(get_initial_conditions_data(container), type, T)\nend\n\nfunction get_initial_conditions_aux_variable(\n    container::OptimizationContainer,\n    type::AuxVariableType,\n    ::Type{T},\n) where {T <: Union{PSY.Component, PSY.System}}\n    return get_initial_conditions_aux_variable(\n        get_initial_conditions_data(container),\n        type,\n        T,\n    )\nend\n\nfunction get_initial_conditions_dual(\n    container::OptimizationContainer,\n    type::ConstraintType,\n    ::Type{T},\n) where {T <: Union{PSY.Component, PSY.System}}\n    return get_initial_conditions_dual(get_initial_conditions_data(container), type, T)\nend\n\nfunction get_initial_conditions_parameter(\n    container::OptimizationContainer,\n    type::ParameterType,\n    ::Type{T},\n) where {T <: Union{PSY.Component, PSY.System}}\n    return get_initial_conditions_parameter(get_initial_conditions_data(container), type, T)\nend\n\nfunction add_to_objective_invariant_expression!(\n    container::OptimizationContainer,\n    cost_expr::T,\n) where {T <: JuMP.AbstractJuMPScalar}\n    T_cf = typeof(container.objective_function.invariant_terms)\n    if T_cf <: JuMP.GenericAffExpr && T <: JuMP.GenericQuadExpr\n        container.objective_function.invariant_terms += cost_expr\n    else\n        JuMP.add_to_expression!(container.objective_function.invariant_terms, cost_expr)\n    end\n    return\nend\n\nfunction add_to_objective_variant_expression!(\n    container::OptimizationContainer,\n    cost_expr::JuMP.AffExpr,\n)\n    JuMP.add_to_expression!(container.objective_function.variant_terms, cost_expr)\n    return\nend\n\nfunction deserialize_key(container::OptimizationContainer, name::AbstractString)\n    return deserialize_key(container.metadata, name)\nend\n\nfunction calculate_aux_variables!(container::OptimizationContainer, system::PSY.System)\n    aux_var_keys = keys(get_aux_variables(container))\n    pf_aux_var_keys = filter(is_from_power_flow ∘ get_entry_type, aux_var_keys)\n    non_pf_aux_var_keys = setdiff(aux_var_keys, pf_aux_var_keys)\n    # We should only have power flow aux vars if we have power flow evaluators\n    @assert isempty(pf_aux_var_keys) || !isempty(get_power_flow_evaluation_data(container))\n\n    TimerOutputs.@timeit RUN_SIMULATION_TIMER \"Power Flow Evaluation\" begin\n        reset_power_flow_is_solved!(container)\n        # Power flow-related aux vars get calculated once per power flow\n        for (i, pf_e_data) in enumerate(get_power_flow_evaluation_data(container))\n            @debug \"Processing power flow $i\"\n            solve_power_flow!(pf_e_data, container, system)\n            for key in pf_aux_var_keys\n                calculate_aux_variable_value!(container, key, system)\n            end\n        end\n    end\n\n    # Other aux vars get calculated once at the end\n    for key in non_pf_aux_var_keys\n        calculate_aux_variable_value!(container, key, system)\n    end\n    return RunStatus.SUCCESSFULLY_FINALIZED\nend\n\nfunction _calculate_dual_variable_value!(\n    container::OptimizationContainer,\n    key::ConstraintKey{CopperPlateBalanceConstraint, PSY.System},\n    ::PSY.System,\n)\n    constraint_container = get_constraint(container, key)\n    dual_variable_container = get_duals(container)[key]\n\n    for subnet in axes(constraint_container)[1], t in axes(constraint_container)[2]\n        # See https://jump.dev/JuMP.jl/stable/manual/solutions/#Dual-solution-values\n        dual_variable_container[subnet, t] = jump_value(constraint_container[subnet, t])\n    end\n    return\nend\n\nfunction _calculate_dual_variable_value!(\n    container::OptimizationContainer,\n    key::ConstraintKey{T, D},\n    ::PSY.System,\n) where {T <: ConstraintType, D <: Union{PSY.Component, PSY.System}}\n    constraint_duals = jump_value.(get_constraint(container, key))\n    dual_variable_container = get_duals(container)[key]\n\n    # Needs to loop since the container ordering might not match in the DenseAxisArray\n    for index in Iterators.product(axes(constraint_duals)...)\n        dual_variable_container[index...] = constraint_duals[index...]\n    end\n\n    return\nend\n\nfunction _calculate_dual_variables_continous_model!(\n    container::OptimizationContainer,\n    system::PSY.System,\n)\n    duals_vars = get_duals(container)\n    for key in keys(duals_vars)\n        _calculate_dual_variable_value!(container, key, system)\n    end\n    return RunStatus.SUCCESSFULLY_FINALIZED\nend\n\nfunction _calculate_dual_variables_discrete_model!(\n    container::OptimizationContainer,\n    ::PSY.System,\n)\n    return process_duals(container, container.settings.optimizer)\nend\n\nfunction calculate_dual_variables!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    is_milp::Bool,\n)\n    isempty(get_duals(container)) && return RunStatus.SUCCESSFULLY_FINALIZED\n    if is_milp\n        status = _calculate_dual_variables_discrete_model!(container, sys)\n    else\n        status = _calculate_dual_variables_continous_model!(container, sys)\n    end\n    return\nend\n\n########################### Helper Functions to get keys ###################################\nfunction get_optimization_container_key(\n    ::T,\n    ::Type{U},\n    meta::String,\n) where {T <: AuxVariableType, U <: PSY.Component}\n    return AuxVarKey(T, U, meta)\nend\n\nfunction get_optimization_container_key(\n    ::T,\n    ::Type{U},\n    meta::String,\n) where {T <: VariableType, U <: PSY.Component}\n    return VariableKey(T, U, meta)\nend\n\nfunction get_optimization_container_key(\n    ::T,\n    ::Type{U},\n    meta::String,\n) where {T <: ParameterType, U <: PSY.Component}\n    return ParameterKey(T, U, meta)\nend\n\nfunction get_optimization_container_key(\n    ::T,\n    ::Type{U},\n    meta::String,\n) where {T <: ConstraintType, U <: PSY.Component}\n    return ConstraintKey(T, U, meta)\nend\n\nfunction lazy_container_addition!(\n    container::OptimizationContainer,\n    var::T,\n    ::Type{U},\n    axs...;\n    kwargs...,\n) where {T <: VariableType, U <: Union{PSY.Component, PSY.System}}\n    if !has_container_key(container, T, U)\n        var_container = add_variable_container!(container, var, U, axs...; kwargs...)\n    else\n        var_container = get_variable(container, var, U)\n    end\n    return var_container\nend\n\nfunction lazy_container_addition!(\n    container::OptimizationContainer,\n    constraint::T,\n    ::Type{U},\n    axs...;\n    kwargs...,\n) where {T <: ConstraintType, U <: Union{PSY.Component, PSY.System}}\n    meta = get(kwargs, :meta, ISOPT.CONTAINER_KEY_EMPTY_META)\n    if !has_container_key(container, T, U, meta)\n        cons_container =\n            add_constraints_container!(container, constraint, U, axs...; kwargs...)\n    else\n        cons_container = get_constraint(container, constraint, U, meta)\n    end\n    return cons_container\nend\n\nfunction lazy_container_addition!(\n    container::OptimizationContainer,\n    expression::T,\n    ::Type{U},\n    axs...;\n    kwargs...,\n) where {T <: ExpressionType, U <: Union{PSY.Component, PSY.System}}\n    meta = get(kwargs, :meta, IS.Optimization.CONTAINER_KEY_EMPTY_META)\n    if !has_container_key(container, T, U, meta)\n        expr_container =\n            add_expression_container!(container, expression, U, axs...; kwargs...)\n    else\n        expr_container = get_expression(container, expression, U, meta)\n    end\n    return expr_container\nend\n\nfunction get_time_series_initial_values!(\n    container::OptimizationContainer,\n    ::Type{T},\n    component::PSY.Component,\n    time_series_name::AbstractString;\n    interval::Dates.Millisecond = UNSET_INTERVAL,\n    resolution::Dates.Millisecond = UNSET_RESOLUTION,\n) where {T <: PSY.TimeSeriesData}\n    initial_time = get_initial_time(container)\n    time_steps = get_time_steps(container)\n    forecast = PSY.get_time_series(\n        T,\n        component,\n        time_series_name;\n        start_time = initial_time,\n        count = 1,\n        interval = _to_is_interval(interval),\n        resolution = _to_is_resolution(resolution),\n    )\n    ts_values = IS.get_time_series_values(\n        component,\n        forecast;\n        start_time = initial_time,\n        len = length(time_steps),\n        ignore_scaling_factors = true,\n    )\n    return ts_values\nend\n\n\"\"\"\nGet the column names for the specified container in the OptimizationContainer.\n\n# Arguments\n- `container::OptimizationContainer`: The optimization container.\n- `field::Symbol`: The field for which to retrieve the column names.\n- `key::OptimizationContainerKey`: The key for which to retrieve the column names.\n\n# Returns\n- `Tuple`: Tuple of Vector{String}.\n\"\"\"\nfunction get_column_names(\n    ::OptimizationContainer,\n    field::Symbol,\n    subcontainer,\n    key::OptimizationContainerKey,\n)\n    return if field == :parameters\n        # Parameters are stored in ParameterContainer.\n        get_column_names(key, subcontainer)\n    else\n        # The others are in DenseAxisArrays.\n        get_column_names_from_axis_array(key, subcontainer)\n    end\nend\n\nlookup_value(container::OptimizationContainer, key::VariableKey) =\n    get_variable(container, key)\nlookup_value(container::OptimizationContainer, key::ParameterKey) =\n    calculate_parameter_values(get_parameter(container, key))\nlookup_value(container::OptimizationContainer, key::AuxVarKey) =\n    get_aux_variable(container, key)\nlookup_value(container::OptimizationContainer, key::ExpressionKey) =\n    get_expression(container, key)\nlookup_value(container::OptimizationContainer, key::ConstraintKey) =\n    get_constraint(container, key)\n"
  },
  {
    "path": "src/core/parameters.jl",
    "content": "abstract type ParameterAttributes end\n\nstruct NoAttributes end\n\nstruct TimeSeriesAttributes{T <: PSY.TimeSeriesData} <: ParameterAttributes\n    name::String\n    multiplier_id::Base.RefValue{Int}\n    component_name_to_ts_uuid::Dict{String, String}\n    subsystem::Base.RefValue{String}\nend\n\nfunction TimeSeriesAttributes(\n    ::Type{T},\n    name::String,\n    multiplier_id::Int = 1,\n    component_name_to_ts_uuid = Dict{String, String}(),\n) where {T <: PSY.TimeSeriesData}\n    return TimeSeriesAttributes{T}(\n        name,\n        Base.RefValue{Int}(multiplier_id),\n        component_name_to_ts_uuid,\n        Base.RefValue{String}(\"\"),\n    )\nend\n\nget_time_series_type(::TimeSeriesAttributes{T}) where {T <: PSY.TimeSeriesData} = T\nget_time_series_name(attr::TimeSeriesAttributes) = attr.name\nget_time_series_multiplier_id(attr::TimeSeriesAttributes) = attr.multiplier_id[]\n\nget_subsystem(attr::TimeSeriesAttributes) = attr.subsystem[]\nfunction set_subsystem!(attr::TimeSeriesAttributes, val::String)\n    attr.subsystem[] = val\n    return\nend\nset_subsystem!(::TimeSeriesAttributes, ::Nothing) = nothing\n\nfunction add_component_name!(attr::TimeSeriesAttributes, name::String, uuid::String)\n    if haskey(attr.component_name_to_ts_uuid, name)\n        throw(ArgumentError(\"$name is already stored\"))\n    end\n\n    attr.component_name_to_ts_uuid[name] = uuid\n    return\nend\n\nget_component_names(attr::TimeSeriesAttributes) = keys(attr.component_name_to_ts_uuid)\nfunction _get_ts_uuid(attr::TimeSeriesAttributes, name::String)\n    if !haskey(attr.component_name_to_ts_uuid, name)\n        throw(\n            ArgumentError(\n                \"No time series UUID found for in attributes for component $name: available names are $(keys(attr.component_name_to_ts_uuid))\",\n            ),\n        )\n    end\n    return attr.component_name_to_ts_uuid[name]\nend\n\nstruct VariableValueAttributes{T <: OptimizationContainerKey} <: ParameterAttributes\n    attribute_key::T\n    affected_keys::Set\nend\n\nfunction VariableValueAttributes(key::T) where {T <: OptimizationContainerKey}\n    return VariableValueAttributes{T}(key, Set())\nend\n\nget_attribute_key(attr::VariableValueAttributes) = attr.attribute_key\n\nstruct CostFunctionAttributes{T} <: ParameterAttributes\n    variable_types::Tuple{Vararg{Type}}\n    sos_status::SOSStatusVariable\n    uses_compact_power::Bool\nend\n\nget_sos_status(attr::CostFunctionAttributes) = attr.sos_status\nget_variable_types(attr::CostFunctionAttributes) = attr.variable_types\nget_uses_compact_power(attr::CostFunctionAttributes) = attr.uses_compact_power\n\nstruct EventParametersAttributes{T <: PSY.Outage, U <: ParameterType} <: ParameterAttributes\n    affected_devices::Vector{<:PSY.Component}\nend\n\nfunction get_param_type(\n    ::EventParametersAttributes{T, U},\n) where {T <: PSY.Outage, U <: ParameterType}\n    return U\nend\n\nstruct ParameterContainer{T <: AbstractArray, U <: AbstractArray}\n    attributes::ParameterAttributes\n    parameter_array::T\n    multiplier_array::U\nend\n\nfunction ParameterContainer(parameter_array, multiplier_array)\n    return ParameterContainer(NoAttributes(), parameter_array, multiplier_array)\nend\n\nfunction calculate_parameter_values(container::ParameterContainer)\n    return calculate_parameter_values(\n        container.attributes,\n        container.parameter_array,\n        container.multiplier_array,\n    )\nend\n\nfunction calculate_parameter_values(\n    attributes::ParameterAttributes,\n    param_array::DenseAxisArray,\n    multiplier_array::DenseAxisArray,\n)\n    return get_parameter_values(attributes, param_array, multiplier_array) .*\n           multiplier_array\nend\n\nfunction calculate_parameter_values(\n    ::ParameterAttributes,\n    param_array::SparseAxisArray,\n    multiplier_array::SparseAxisArray,\n)\n    p_array = jump_value.(to_matrix(param_array))\n    m_array = to_matrix(multiplier_array)\n    return p_array .* m_array\nend\n\nfunction get_parameter_column_refs(container::ParameterContainer, column::AbstractString)\n    return get_parameter_column_refs(\n        container.attributes,\n        container.parameter_array,\n        column,\n    )\nend\n\nfunction get_parameter_column_refs(::ParameterAttributes, param_array, column)\n    return param_array\nend\n\nfunction get_parameter_column_refs(\n    attributes::TimeSeriesAttributes{T},\n    param_array::DenseAxisArray,\n    column,\n) where {T <: PSY.TimeSeriesData}\n    expand_ixs((_get_ts_uuid(attributes, column),), param_array)\n    return param_array[expand_ixs((_get_ts_uuid(attributes, column),), param_array)...]\nend\n\nfunction get_parameter_column_values(container::ParameterContainer, column::AbstractString)\n    return jump_value.(get_parameter_column_refs(container, column))\nend\n\nfunction get_parameter_values(container::ParameterContainer)\n    return get_parameter_values(\n        container.attributes,\n        container.parameter_array,\n        container.multiplier_array,\n    )\nend\n\n# TODO: SparseAxisArray versions of these functions\n\nfunction get_parameter_values(\n    ::ParameterAttributes,\n    param_array::DenseAxisArray,\n    multiplier_array::DenseAxisArray,\n)\n    return (.*).(jump_value.(param_array), multiplier_array)\nend\n\nfunction get_parameter_values(\n    attr::EventParametersAttributes,\n    param_array::DenseAxisArray,\n    multiplier_array::DenseAxisArray,\n)\n    return jump_value.(param_array)\nend\n\nfunction get_parameter_values(\n    attributes::TimeSeriesAttributes{T},\n    param_array::DenseAxisArray,\n    multiplier_array::DenseAxisArray,\n) where {T <: PSY.TimeSeriesData}\n    exploded_param_array = DenseAxisArray{Float64}(undef, axes(multiplier_array)...)\n    for name in axes(multiplier_array)[1]\n        param_col = param_array[_get_ts_uuid(attributes, name), axes(param_array)[2:end]...]\n        device_axes = axes(multiplier_array)[2:end]\n        exploded_param_array[name, device_axes...] = jump_value.(param_col)\n    end\n\n    return exploded_param_array\nend\n\nget_parameter_array(c::ParameterContainer) = c.parameter_array\n# Underlying dense storage of the parameter array. `parent` on a JuMP `DenseAxisArray`\n# returns the array itself, so reach for `.data` directly to bypass the axis-keyed lookup.\nget_parameter_array_data(c::ParameterContainer) = get_parameter_array(c).data\nget_multiplier_array(c::ParameterContainer) = c.multiplier_array\n# Same shortcut for the multiplier array — used by the integer-indexed fast path.\nget_multiplier_array_data(c::ParameterContainer) = get_multiplier_array(c).data\nget_attributes(c::ParameterContainer) = c.attributes\nBase.length(c::ParameterContainer) = length(c.parameter_array)\nBase.size(c::ParameterContainer) = size(c.parameter_array)\n\nfunction get_column_names(key::ParameterKey, c::ParameterContainer)\n    return get_column_names_from_axis_array(key, get_multiplier_array(c))\nend\n\nconst ValidDataParamEltypes = Union{Float64, Tuple{Vararg{Float64}}}\nfunction _set_parameter!(\n    array::AbstractArray{T},\n    ::JuMP.Model,\n    value::Union{T, AbstractVector{T}},\n    ixs::Tuple,\n) where {T <: ValidDataParamEltypes}\n    assign_maybe_broadcast!(array, value, ixs)\n    return\nend\n\nfunction _set_parameter!(\n    array::AbstractArray{JuMP.VariableRef},\n    model::JuMP.Model,\n    value::Union{T, AbstractVector{T}},\n    ixs::Tuple,\n) where {T <: ValidDataParamEltypes}\n    assign_maybe_broadcast!(array, add_jump_parameter.(Ref(model), value), ixs)\n    return\nend\n\nfunction _set_parameter!(\n    array::SparseAxisArray{Union{Nothing, JuMP.VariableRef}},\n    model::JuMP.Model,\n    value::Union{T, AbstractVector{T}},\n    ixs::Tuple,\n) where {T <: ValidDataParamEltypes}\n    assign_maybe_broadcast!(array, add_jump_parameter.(Ref(model), value), ixs)\n    return\nend\n\nfunction set_multiplier!(container::ParameterContainer, multiplier::Float64, ixs...)\n    assign_maybe_broadcast!(get_multiplier_array(container), multiplier, ixs)\n    return\nend\n\nfunction set_parameter!(\n    container::ParameterContainer,\n    jump_model::JuMP.Model,\n    parameter::Union{ValidDataParamEltypes, AbstractVector{<:ValidDataParamEltypes}},\n    ixs...,\n)\n    param_array = get_parameter_array(container)\n    _set_parameter!(param_array, jump_model, parameter, ixs)\n    return\nend\n\n# Overload for when a JuMP parameter VariableRef is passed directly (recurrent-solve\n# path where a parallel branch type reuses the VariableRef created by the first type).\nfunction set_parameter!(\n    container::ParameterContainer,\n    ::JuMP.Model,\n    parameter::JuMP.VariableRef,\n    ixs...,\n)\n    assign_maybe_broadcast!(get_parameter_array(container), parameter, ixs)\n    return\nend\n\n# Fast-path setters that skip DenseAxisArray's string-keyed axis lookup. Callers pass\n# `get_parameter_array_data(container)` once, then write into the underlying Array\n# by integer indices. The (i, t) layout matches the canonical (component, time) axis\n# order produced by `add_param_container!`.\n#\n# 2D scalar path: covers Float64 and Tuple{Vararg{Float64}} eltypes (the latter is\n# used by piecewise-cost MarketBid parameters whose storage is a Matrix of tuples).\n@inline function _set_parameter_at!(\n    parent_param::Array{T, 2},\n    ::JuMP.Model,\n    value::T,\n    i::Int,\n    t::Int,\n) where {T <: ValidDataParamEltypes}\n    parent_param[i, t] = value\n    return\nend\n\n# 2D recurrent-rebuild paths: param storage is `Array{JuMP.VariableRef, 2}`. Either we\n# need a fresh JuMP parameter (Float64 input) or we reuse one created by an earlier\n# parallel branch type (VariableRef input).\n@inline function _set_parameter_at!(\n    parent_param::Array{JuMP.VariableRef, 2},\n    jump_model::JuMP.Model,\n    value::Float64,\n    i::Int,\n    t::Int,\n)\n    parent_param[i, t] = add_jump_parameter(jump_model, value)\n    return\nend\n\n@inline function _set_parameter_at!(\n    parent_param::Array{JuMP.VariableRef, 2},\n    ::JuMP.Model,\n    parameter::JuMP.VariableRef,\n    i::Int,\n    t::Int,\n)\n    parent_param[i, t] = parameter\n    return\nend\n\n# 3D fast paths (parameter container with a middle additional axis, e.g. piecewise\n# tranches). The supplied `value` is a length-(size(parent_param, 2)) vector that fills\n# the middle axis at position (i, :, t). Eltype constrained to `ValidDataParamEltypes`\n# so tuples-of-floats are also accepted (piecewise breakpoint storage).\n@inline function _set_parameter_at!(\n    parent_param::Array{T, 3},\n    ::JuMP.Model,\n    value::AbstractVector{<:T},\n    i::Int,\n    t::Int,\n) where {T <: ValidDataParamEltypes}\n    @views parent_param[i, :, t] .= value\n    return\nend\n\n@inline function _set_parameter_at!(\n    parent_param::Array{JuMP.VariableRef, 3},\n    jump_model::JuMP.Model,\n    value::AbstractVector{Float64},\n    i::Int,\n    t::Int,\n)\n    for k in 1:size(parent_param, 2)\n        parent_param[i, k, t] = add_jump_parameter(jump_model, value[k])\n    end\n    return\nend\n\n# Fast-path setters for the multiplier array, mirroring `_set_parameter_at!`.\n# Multipliers are always Float64-valued (or tuples-of-floats for piecewise\n# parameters), so a single typed family covers every call site. Callers should\n# hoist `parent_mult = get_multiplier_array_data(parameter_container)` once\n# above the device loop and pass the integer device row index.\n\n# 2D row fill: assigns `value` across the whole time slice for component `i`\n# (the canonical pattern at parameter-creation time, where the multiplier is\n# constant per device).\n@inline function _set_multiplier_at!(\n    parent_mult::Array{T, 2},\n    value::T,\n    i::Int,\n) where {T <: ValidDataParamEltypes}\n    @views parent_mult[i, :] .= value\n    return\nend\n\n# 2D scalar write at a single (component, time) cell.\n@inline function _set_multiplier_at!(\n    parent_mult::Array{T, 2},\n    value::T,\n    i::Int,\n    t::Int,\n) where {T <: ValidDataParamEltypes}\n    parent_mult[i, t] = value\n    return\nend\n\n# 3D row fill: assigns `value` across all (tranche, time) for component `i`.\n@inline function _set_multiplier_at!(\n    parent_mult::Array{T, 3},\n    value::T,\n    i::Int,\n) where {T <: ValidDataParamEltypes}\n    @views parent_mult[i, :, :] .= value\n    return\nend\n\n# 3D point write at a single (component, tranche, time) cell.\n@inline function _set_multiplier_at!(\n    parent_mult::Array{T, 3},\n    value::T,\n    i::Int,\n    j::Int,\n    t::Int,\n) where {T <: ValidDataParamEltypes}\n    parent_mult[i, j, t] = value\n    return\nend\n\n# Fast-path setters for the simulation-step parameter-VALUE update path.\n# Used by `_update_parameter_values!` and `_fix_parameter_value!` overloads\n# where the parameter container already exists and we are pushing new values\n# into it from upstream model results or time series. Caller hoists\n# `parent_param = get_parameter_array_data(parameter_container)` (and an\n# optional `parent_mult` / `parent_var`) above the device loop, then writes\n# by integer (i, t) — bypassing DenseAxisArray's string-keyed axis lookup.\n#\n# For Float64-typed storage we write directly; for `JuMP.VariableRef` storage\n# we update the JuMP parameter's bound via `JuMP.fix(...; force=true)`.\n@inline function _set_param_value_at!(\n    parent_param::Array{T, 2},\n    value::T,\n    i::Int,\n    t::Int,\n) where {T <: ValidDataParamEltypes}\n    @inbounds parent_param[i, t] = value\n    return\nend\n\n@inline function _set_param_value_at!(\n    parent_param::Array{JuMP.VariableRef, 2},\n    value::Float64,\n    i::Int,\n    t::Int,\n)\n    @inbounds JuMP.fix(parent_param[i, t], value; force = true)\n    return\nend\n\n# 3D paths for piecewise-tranche updates.\n@inline function _set_param_value_at!(\n    parent_param::Array{T, 3},\n    value::AbstractVector{<:T},\n    i::Int,\n    t::Int,\n) where {T <: ValidDataParamEltypes}\n    @inbounds @views parent_param[i, :, t] .= value\n    return\nend\n\n@inline function _set_param_value_at!(\n    parent_param::Array{JuMP.VariableRef, 3},\n    value::AbstractVector{Float64},\n    i::Int,\n    t::Int,\n)\n    @inbounds for k in 1:size(parent_param, 2)\n        JuMP.fix(parent_param[i, k, t], value[k]; force = true)\n    end\n    return\nend\n\n\"\"\"\nParameter to define active power time series\n\"\"\"\nstruct ActivePowerTimeSeriesParameter <: TimeSeriesParameter end\n\n\"\"\"\nParameter to define reactive power time series\n\"\"\"\nstruct ReactivePowerTimeSeriesParameter <: TimeSeriesParameter end\n\n\"\"\"\nParameter to define active power out time series\n\"\"\"\nstruct ActivePowerOutTimeSeriesParameter <: TimeSeriesParameter end\n\n\"\"\"\nParameter to define active power in time series\n\"\"\"\nstruct ActivePowerInTimeSeriesParameter <: TimeSeriesParameter end\n\n\"\"\"\nParameter to define active power in time series\n\"\"\"\nstruct ShiftUpActivePowerTimeSeriesParameter <: TimeSeriesParameter end\n\n\"\"\"\nParameter to define active power in time series\n\"\"\"\nstruct ShiftDownActivePowerTimeSeriesParameter <: TimeSeriesParameter end\n\n\"\"\"\nParameter to define requirement time series\n\"\"\"\nstruct RequirementTimeSeriesParameter <: TimeSeriesParameter end\n\n\"\"\"\nAbstract type for dynamic ratings of AC branches\n\"\"\"\nabstract type AbstractDynamicBranchRatingTimeSeriesParameter <: TimeSeriesParameter end\n\n\"\"\"\nParameter to define the dynamic rating time series of a branch\n\"\"\"\nstruct DynamicBranchRatingTimeSeriesParameter <:\n       AbstractDynamicBranchRatingTimeSeriesParameter end\n\n\"\"\"\nParameter to define the dynamic ratings time series of an AC branch for post-contingency condition\n\"\"\"\nstruct PostContingencyDynamicBranchRatingTimeSeriesParameter <:\n       AbstractDynamicBranchRatingTimeSeriesParameter end\n\n\"\"\"\nParameter to define Flow From_To limit time series\n\"\"\"\nstruct FromToFlowLimitParameter <: TimeSeriesParameter end\n\n\"\"\"\nParameter to define Flow To_From limit time series\n\"\"\"\nstruct ToFromFlowLimitParameter <: TimeSeriesParameter end\n\n\"\"\"\nParameter to define Max Flow limit for interface time series\n\"\"\"\nstruct MaxInterfaceFlowLimitParameter <: TimeSeriesParameter end\n\n\"\"\"\nParameter to define Min Flow limit for interface time series\n\"\"\"\nstruct MinInterfaceFlowLimitParameter <: TimeSeriesParameter end\n\nabstract type VariableValueParameter <: RightHandSideParameter end\n\n\"\"\"\nParameter to define variable upper bound\n\"\"\"\nstruct UpperBoundValueParameter <: VariableValueParameter end\n\n\"\"\"\nParameter to define variable lower bound\n\"\"\"\nstruct LowerBoundValueParameter <: VariableValueParameter end\n\n\"\"\"\nParameter to define unit commitment status updated from the system state\n\"\"\"\nstruct OnStatusParameter <: VariableValueParameter end\n\n\"\"\"\nParameter to FixValueParameter\n\"\"\"\nstruct FixValueParameter <: VariableValueParameter end\n\n\"\"\"\nParameter to define cost function coefficient\n\"\"\"\nstruct CostFunctionParameter <: ObjectiveFunctionParameter end\n\n\"\"\"\nParameter to define fuel cost time series\n\"\"\"\nstruct FuelCostParameter <: ObjectiveFunctionParameter end\n\n\"Parameter to define startup cost time series\"\nstruct StartupCostParameter <: ObjectiveFunctionParameter end\n\n\"Parameter to define shutdown cost time series\"\nstruct ShutdownCostParameter <: ObjectiveFunctionParameter end\n\n\"Parameters to define the cost at the minimum available power\"\nabstract type AbstractCostAtMinParameter <: ObjectiveFunctionParameter end\n\n\"[`AbstractCostAtMinParameter`](@ref) for the incremental case (power source)\"\nstruct IncrementalCostAtMinParameter <: AbstractCostAtMinParameter end\n\n\"[`AbstractCostAtMinParameter`](@ref) for the decremental case (power sink)\"\nstruct DecrementalCostAtMinParameter <: AbstractCostAtMinParameter end\n\n\"Parameters to define the slopes of a piecewise linear cost function\"\nabstract type AbstractPiecewiseLinearSlopeParameter <: ObjectiveFunctionParameter end\n\n\"[`AbstractPiecewiseLinearSlopeParameter`](@ref) for the incremental case (power source)\"\nstruct IncrementalPiecewiseLinearSlopeParameter <: AbstractPiecewiseLinearSlopeParameter end\n\n\"[`AbstractPiecewiseLinearSlopeParameter`](@ref) for the decremental case (power sink)\"\nstruct DecrementalPiecewiseLinearSlopeParameter <: AbstractPiecewiseLinearSlopeParameter end\n\n\"Parameters to define the breakpoints of a piecewise linear function\"\nabstract type AbstractPiecewiseLinearBreakpointParameter <: TimeSeriesParameter end\n\n\"[`AbstractPiecewiseLinearBreakpointParameter`](@ref) for the incremental case (power source)\"\nstruct IncrementalPiecewiseLinearBreakpointParameter <:\n       AbstractPiecewiseLinearBreakpointParameter end\n\n\"[`AbstractPiecewiseLinearBreakpointParameter`](@ref) for the decremental case (power sink)\"\nstruct DecrementalPiecewiseLinearBreakpointParameter <:\n       AbstractPiecewiseLinearBreakpointParameter end\n\nabstract type AuxVariableValueParameter <: RightHandSideParameter end\n\nabstract type EventParameter <: ParameterType end\n\n\"\"\"\nParameter to define component availability status updated from the system state\n\"\"\"\nstruct AvailableStatusParameter <: EventParameter end\n\n\"\"\"\nParameter to define active power offset during an event.\n\"\"\"\nstruct ActivePowerOffsetParameter <: EventParameter end\n\n\"\"\"\nParameter to define reactive power offset during an event.\n\"\"\"\nstruct ReactivePowerOffsetParameter <: EventParameter end\n\n\"\"\"\nParameter to record that the component changed in the availability status\n\"\"\"\nstruct AvailableStatusChangeCountdownParameter <: EventParameter end\n\nshould_write_resulting_value(::Type{<:RightHandSideParameter}) = true\nshould_write_resulting_value(::Type{<:EventParameter}) = true\n\nshould_write_resulting_value(::Type{<:FuelCostParameter}) = true\nshould_write_resulting_value(::Type{<:ShutdownCostParameter}) = true\nshould_write_resulting_value(::Type{<:AbstractCostAtMinParameter}) = true\nshould_write_resulting_value(::Type{<:AbstractPiecewiseLinearSlopeParameter}) = true\nshould_write_resulting_value(::Type{<:AbstractPiecewiseLinearBreakpointParameter}) = true\n\nconvert_result_to_natural_units(::Type{DynamicBranchRatingTimeSeriesParameter}) = true\nconvert_result_to_natural_units(\n    ::Type{PostContingencyDynamicBranchRatingTimeSeriesParameter},\n) = true\nconvert_result_to_natural_units(::Type{ActivePowerTimeSeriesParameter}) = true\nconvert_result_to_natural_units(::Type{ReactivePowerTimeSeriesParameter}) = true\nconvert_result_to_natural_units(::Type{ActivePowerOutTimeSeriesParameter}) = true\nconvert_result_to_natural_units(::Type{ActivePowerInTimeSeriesParameter}) = true\nconvert_result_to_natural_units(::Type{RequirementTimeSeriesParameter}) = true\nconvert_result_to_natural_units(::Type{UpperBoundValueParameter}) = true\nconvert_result_to_natural_units(::Type{LowerBoundValueParameter}) = true\nconvert_result_to_natural_units(::Type{ShiftUpActivePowerTimeSeriesParameter}) = true\nconvert_result_to_natural_units(::Type{ShiftDownActivePowerTimeSeriesParameter}) = true\n"
  },
  {
    "path": "src/core/power_flow_data_wrapper.jl",
    "content": "mutable struct PowerFlowEvaluationData{T <: PFS.PowerFlowContainer}\n    power_flow_data::T\n    \"\"\"\n    Records which PSI keys are read as input to the power flow and how the data are mapped.\n    The Symbol is a category of data: `:active_power`, `:reactive_power`, etc. The\n    `OptimizationContainerKey` is a source of that data in the `OptimizationContainer`. For\n    `PowerFlowData`, leaf values are `Dict{String, Int64}` mapping component name to matrix\n    index of bus; for `SystemPowerFlowContainer`, leaf values are Dict{Union{String, Int64},\n    Union{String, Int64}} mapping component name/bus number to component name/bus number.\n    \"\"\"\n    input_key_map::Dict{Symbol, <:Dict{<:OptimizationContainerKey, <:Any}}\n    is_solved::Bool\nend\n\ncheck_network_reduction(::PFS.SystemPowerFlowContainer) = nothing\n\nfunction check_network_reduction(pfd::PFS.PowerFlowData)\n    nrd = PFS.get_network_reduction_data(pfd)\n    if !isempty(PNM.get_reductions(nrd))\n        throw(\n            IS.NotImplementedError(\n                \"Power flow in-the-loop on reduced networks isn't supported. Network \" *\n                \"reductions of types $(PNM.get_reductions(nrd)) present.\",\n            ),\n        )\n    end\n    return\nend\n\nfunction PowerFlowEvaluationData(power_flow_data::T) where {T <: PFS.PowerFlowContainer}\n    check_network_reduction(power_flow_data)\n    return PowerFlowEvaluationData{T}(\n        power_flow_data,\n        Dict{Symbol, Dict{OptimizationContainerKey, <:Any}}(),\n        false,\n    )\nend\n\nget_power_flow_data(ped::PowerFlowEvaluationData) = ped.power_flow_data\nget_input_key_map(ped::PowerFlowEvaluationData) = ped.input_key_map\n"
  },
  {
    "path": "src/core/results_by_time.jl",
    "content": "mutable struct ResultsByTime{T, N}\n    key::OptimizationContainerKey\n    data::SortedDict{Dates.DateTime, T}\n    resolution::Dates.Period\n    column_names::NTuple{N, Vector{String}}\nend\n\nfunction ResultsByTime(\n    key::OptimizationContainerKey,\n    data::SortedDict{Dates.DateTime, T},\n    resolution::Dates.Period,\n    column_names,\n) where {T}\n    _check_column_consistency(data, column_names)\n    ResultsByTime(key, data, resolution, column_names)\nend\n\nfunction _check_column_consistency(\n    data::SortedDict{Dates.DateTime, DenseAxisArray{Float64, 2}},\n    cols::Tuple{Vector{String}},\n)\n    for val in values(data)\n        if axes(val)[1] != cols[1]\n            error(\"Mismatch in DenseAxisArray column names: $(axes(val)[1]) $cols\")\n        end\n    end\nend\n\nfunction _check_column_consistency(\n    data::SortedDict{Dates.DateTime, Matrix{Float64}},\n    cols::Tuple{Vector{String}},\n)\n    for val in values(data)\n        if size(val)[2] != length(cols[1])\n            error(\n                \"Mismatch in length of Matrix columns: $(size(val)[2]) $(length(cols[1]))\",\n            )\n        end\n    end\nend\n\nfunction _check_column_consistency(\n    data::SortedDict{Dates.DateTime, DenseAxisArray{Float64, 2}},\n    cols::NTuple{N, Vector{String}},\n) where {N}\n    # TODO:\nend\n\nfunction _check_column_consistency(\n    data::SortedDict{Dates.DateTime, DataFrame},\n    cols::NTuple{N, Vector{String}},\n) where {N}\n    for df in values(data)\n        if DataFrames.ncol(df) != length(cols[1])\n            error(\n                \"Mismatch in length of DataFrame columns: $(DataFrames.ncol(df)) $(length(cols[1]))\",\n            )\n        end\n    end\nend\n\n# TODO: Implement consistency check for other sizes\n\n# This struct behaves like a dict, delegating to its 'data' field.\nBase.length(res::ResultsByTime) = length(res.data)\nBase.iterate(res::ResultsByTime) = iterate(res.data)\nBase.iterate(res::ResultsByTime, state) = iterate(res.data, state)\nBase.getindex(res::ResultsByTime, i) = getindex(res.data, i)\nBase.setindex!(res::ResultsByTime, v, i) = setindex!(res.data, v, i)\nBase.firstindex(res::ResultsByTime) = firstindex(res.data)\nBase.lastindex(res::ResultsByTime) = lastindex(res.data)\n\nget_column_names(x::ResultsByTime) = x.column_names\nget_num_rows(::ResultsByTime{DenseAxisArray{Float64, 2}}, data) = size(data, 2)\nget_num_rows(::ResultsByTime{DenseAxisArray{Float64, 3}}, data) = size(data, 3)\nget_num_rows(::ResultsByTime{Matrix{Float64}}, data) = size(data, 1)\nget_num_rows(::ResultsByTime{DataFrame}, data) = DataFrames.nrow(data)\n\nfunction _add_timestamps!(\n    df::DataFrames.DataFrame,\n    results::ResultsByTime,\n    timestamp::Dates.DateTime,\n    data,\n)\n    time_col = _get_timestamps(results, timestamp, get_num_rows(results, data))\n    if !isnothing(time_col)\n        DataFrames.insertcols!(df, 1, :DateTime => time_col)\n    end\n    return\nend\n\nfunction _get_timestamps(results::ResultsByTime, timestamp::Dates.DateTime, len::Int)\n    if results.resolution == Dates.Period(Dates.Millisecond(0))\n        return nothing\n    end\n    return range(timestamp; length = len, step = results.resolution)\nend\n\nfunction make_dataframe(\n    results::ResultsByTime{DenseAxisArray{Float64, 2}},\n    timestamp::Dates.DateTime;\n    table_format::TableFormat = TableFormat.LONG,\n)\n    array = results.data[timestamp]\n    timestamps = _get_timestamps(results, timestamp, get_num_rows(results, array))\n    return to_results_dataframe(array, timestamps, Val(table_format))\nend\n\nfunction make_dataframe(\n    results::ResultsByTime{DenseAxisArray{Float64, 3}},\n    timestamp::Dates.DateTime;\n    table_format::TableFormat = TableFormat.LONG,\n)\n    array = results.data[timestamp]\n    num_timestamps = get_num_rows(results, array)\n    timestamps = _get_timestamps(results, timestamp, num_timestamps)\n    return to_results_dataframe(array, timestamps, Val(table_format))\nend\n\nfunction make_dataframe(\n    results::ResultsByTime{Matrix{Float64}},\n    timestamp::Dates.DateTime;\n    table_format::TableFormat = TableFormat.LONG,\n)\n    array = results.data[timestamp]\n    df_wide = DataFrames.DataFrame(array, results.column_names)\n    _add_timestamps!(df_wide, results, timestamp, array)\n    return if table_format == TableFormat.LONG\n        measure_vars = [x for x in names(df_wide) if x != \"DateTime\"]\n        DataFrames.stack(\n            df_wide,\n            measure_vars;\n            variable_name = :name,\n            value_name = :value,\n        )\n    elseif table_format == TableFormat.WIDE\n        df_wide\n    else\n        error(\"Unsupported table format: $table_format\")\n    end\nend\n\nfunction make_dataframes(results::ResultsByTime; table_format::TableFormat = table_format)\n    return SortedDict(\n        k => make_dataframe(results, k; table_format = table_format) for\n        k in keys(results.data)\n    )\nend\n\nstruct ResultsByKeyAndTime\n    \"Contains all keys stored in the model.\"\n    result_keys::Vector{OptimizationContainerKey}\n    \"Contains the results that have been read from the store and cached.\"\n    cached_results::Dict{OptimizationContainerKey, ResultsByTime}\nend\n\nResultsByKeyAndTime(result_keys) = ResultsByKeyAndTime(\n    collect(result_keys),\n    Dict{OptimizationContainerKey, ResultsByTime}(),\n)\n\nBase.empty!(res::ResultsByKeyAndTime) = empty!(res.cached_results)\n"
  },
  {
    "path": "src/core/service_model.jl",
    "content": "function _check_service_formulation(\n    ::Type{D},\n) where {D <: Union{AbstractServiceFormulation, PSY.Service}}\n    if !isconcretetype(D)\n        throw(\n            ArgumentError(\n                \"The service model must contain only concrete types, $(D) is an Abstract Type\",\n            ),\n        )\n    end\nend\n\n\"\"\"\nEstablishes the model for a particular services specified by type. Uses the keyword argument\n`use_service_name` to assign the model to a service with the same name as the name in the\ntemplate. Uses the keyword argument feedforward to enable passing values between operation\nmodel at simulation time\n\n# Arguments\n\n-`::Type{D}`: Power System Service Type\n-`::Type{B}`: Abstract Service Formulation\n\n# Accepted Key Words\n\n  - `feedforward::Array{<:AbstractAffectFeedforward}` : use to pass parameters between models\n  - `use_service_name::Bool` : use the name as the name for the service\n\n# Example\n\nreserves = ServiceModel(PSY.VariableReserve{PSY.ReserveUp}, RangeReserve)\n\"\"\"\nmutable struct ServiceModel{D <: PSY.Service, B <: AbstractServiceFormulation}\n    feedforwards::Vector{<:AbstractAffectFeedforward}\n    service_name::String\n    use_slacks::Bool\n    duals::Vector{DataType}\n    time_series_names::Dict{Type{<:TimeSeriesParameter}, String}\n    attributes::Dict{String, Any}\n    contributing_devices_map::Dict{Type{<:PSY.Component}, Vector{<:PSY.Component}}\n    subsystem::Union{Nothing, String}\n    function ServiceModel(\n        ::Type{D},\n        ::Type{B},\n        service_name::String;\n        use_slacks = false,\n        feedforwards = Vector{AbstractAffectFeedforward}(),\n        duals = Vector{DataType}(),\n        time_series_names = get_default_time_series_names(D, B),\n        attributes = Dict{String, Any}(),\n        contributing_devices_map = Dict{Type{<:PSY.Component}, Vector{<:PSY.Component}}(),\n    ) where {D <: PSY.Service, B <: AbstractServiceFormulation}\n        attributes_for_model = get_default_attributes(D, B)\n        for (k, v) in attributes\n            attributes_for_model[k] = v\n        end\n\n        _check_service_formulation(D)\n        _check_service_formulation(B)\n        new{D, B}(\n            feedforwards,\n            service_name,\n            use_slacks,\n            duals,\n            time_series_names,\n            attributes_for_model,\n            contributing_devices_map,\n            nothing,\n        )\n    end\nend\n\nget_component_type(\n    ::ServiceModel{D, B},\n) where {D <: PSY.Service, B <: AbstractServiceFormulation} = D\nget_formulation(\n    ::ServiceModel{D, B},\n) where {D <: PSY.Service, B <: AbstractServiceFormulation} = B\nget_feedforwards(m::ServiceModel) = m.feedforwards\nget_service_name(m::ServiceModel) = m.service_name\nget_use_slacks(m::ServiceModel) = m.use_slacks\nget_duals(m::ServiceModel) = m.duals\nget_time_series_names(m::ServiceModel) = m.time_series_names\nget_attributes(m::ServiceModel) = m.attributes\nget_attribute(m::ServiceModel, key::String) = get(m.attributes, key, nothing)\nget_contributing_devices_map(m::ServiceModel) = m.contributing_devices_map\nget_contributing_devices_map(m::ServiceModel, key) =\n    get(m.contributing_devices_map, key, nothing)\nget_contributing_devices(m::ServiceModel) =\n    [z for x in values(m.contributing_devices_map) for z in x]\nget_subsystem(m::ServiceModel) = m.subsystem\n\nset_subsystem!(m::ServiceModel, id::String) = m.subsystem = id\n\nfunction ServiceModel(\n    service_type::Type{D},\n    formulation_type::Type{B};\n    use_slacks = false,\n    feedforwards = Vector{AbstractAffectFeedforward}(),\n    duals = Vector{DataType}(),\n    time_series_names = get_default_time_series_names(D, B),\n    attributes = get_default_attributes(D, B),\n) where {D <: PSY.Service, B <: AbstractServiceFormulation}\n    # If more attributes are used later, move free form string to const and organize\n    # attributes\n    attributes_for_model = get_default_attributes(D, B)\n    for (k, v) in attributes\n        attributes_for_model[k] = v\n    end\n    if !haskey(attributes_for_model, \"aggregated_service_model\")\n        push!(attributes_for_model, \"aggregated_service_model\" => true)\n    end\n    return ServiceModel(\n        service_type,\n        formulation_type,\n        NO_SERVICE_NAME_PROVIDED;\n        use_slacks,\n        feedforwards,\n        duals,\n        time_series_names,\n        attributes = attributes_for_model,\n    )\nend\n\nfunction _set_model!(dict::Dict, key::Tuple{String, Symbol}, model::ServiceModel)\n    if haskey(dict, key)\n        @warn \"Overwriting $(key) existing model\"\n    end\n    dict[key] = model\n    return\nend\n\nfunction _set_model!(\n    dict::Dict,\n    model::ServiceModel{D, B},\n) where {D <: PSY.Service, B <: AbstractServiceFormulation}\n    _set_model!(dict, (get_service_name(model), Symbol(D)), model)\n    return\nend\n"
  },
  {
    "path": "src/core/settings.jl",
    "content": "struct Settings\n    horizon::Base.RefValue{Dates.Millisecond}\n    resolution::Base.RefValue{Dates.Millisecond}\n    interval::Base.RefValue{Dates.Millisecond}\n    time_series_cache_size::Int\n    warm_start::Base.RefValue{Bool}\n    initial_time::Base.RefValue{Dates.DateTime}\n    optimizer::Union{Nothing, MOI.OptimizerWithAttributes}\n    direct_mode_optimizer::Bool\n    optimizer_solve_log_print::Bool\n    detailed_optimizer_stats::Bool\n    calculate_conflict::Bool\n    check_components::Bool\n    initialize_model::Bool\n    initialization_file::String\n    deserialize_initial_conditions::Bool\n    export_pwl_vars::Bool\n    allow_fails::Bool\n    rebuild_model::Bool\n    export_optimization_model::Bool\n    store_variable_names::Bool\n    check_numerical_bounds::Bool\n    ext::Dict{String, Any}\nend\n\nfunction Settings(\n    sys;\n    initial_time::Dates.DateTime = UNSET_INI_TIME,\n    time_series_cache_size::Int = IS.TIME_SERIES_CACHE_SIZE_BYTES,\n    warm_start::Bool = true,\n    horizon::Dates.Period = UNSET_HORIZON,\n    resolution::Dates.Period = UNSET_RESOLUTION,\n    interval::Dates.Period = UNSET_INTERVAL,\n    optimizer = nothing,\n    direct_mode_optimizer::Bool = false,\n    optimizer_solve_log_print::Bool = false,\n    detailed_optimizer_stats::Bool = false,\n    calculate_conflict::Bool = false,\n    check_components::Bool = true,\n    initialize_model::Bool = true,\n    initialization_file = \"\",\n    deserialize_initial_conditions::Bool = false,\n    export_pwl_vars::Bool = false,\n    allow_fails::Bool = false,\n    check_numerical_bounds = true,\n    rebuild_model = false,\n    export_optimization_model = false,\n    store_variable_names = false,\n    ext = Dict{String, Any}(),\n)\n    if time_series_cache_size > 0 && PSY.stores_time_series_in_memory(sys)\n        @info \"Overriding time_series_cache_size because time series is stored in memory\"\n        time_series_cache_size = 0\n    end\n\n    if isa(optimizer, MOI.OptimizerWithAttributes) || optimizer === nothing\n        optimizer_ = optimizer\n    elseif isa(optimizer, DataType)\n        optimizer_ = MOI.OptimizerWithAttributes(optimizer)\n    else\n        error(\n            \"The provided input for optimizer is invalid. Provide a JuMP.OptimizerWithAttributes object or a valid Optimizer constructor (e.g. HiGHS.Optimizer).\",\n        )\n    end\n\n    return Settings(\n        Ref(IS.time_period_conversion(horizon)),\n        Ref(IS.time_period_conversion(resolution)),\n        Ref(IS.time_period_conversion(interval)),\n        time_series_cache_size,\n        Ref(warm_start),\n        Ref(initial_time),\n        optimizer_,\n        direct_mode_optimizer,\n        optimizer_solve_log_print,\n        detailed_optimizer_stats,\n        calculate_conflict,\n        check_components,\n        initialize_model,\n        initialization_file,\n        deserialize_initial_conditions,\n        export_pwl_vars,\n        allow_fails,\n        rebuild_model,\n        export_optimization_model,\n        store_variable_names,\n        check_numerical_bounds,\n        ext,\n    )\nend\n\nfunction log_values(settings::Settings)\n    text = Vector{String}()\n    for (name, type) in zip(fieldnames(Settings), fieldtypes(Settings))\n        val = getfield(settings, name)\n        if type <: Base.RefValue\n            val = val[]\n        end\n        push!(text, \"$name = $val\")\n    end\n\n    @debug \"Settings: $(join(text, \", \"))\" _group = LOG_GROUP_OPTIMIZATION_CONTAINER\nend\n\nget_horizon(settings::Settings) = settings.horizon[]\nget_resolution(settings::Settings) = settings.resolution[]\nget_interval(settings::Settings) = settings.interval[]\nget_initial_time(settings::Settings)::Dates.DateTime = settings.initial_time[]\nget_optimizer(settings::Settings) = settings.optimizer\nget_ext(settings::Settings) = settings.ext\nget_warm_start(settings::Settings) = settings.warm_start[]\nget_check_components(settings::Settings) = settings.check_components\nget_initialize_model(settings::Settings) = settings.initialize_model\nget_initialization_file(settings::Settings) = settings.initialization_file\nget_deserialize_initial_conditions(settings::Settings) =\n    settings.deserialize_initial_conditions\nget_export_pwl_vars(settings::Settings) = settings.export_pwl_vars\nget_check_numerical_bounds(settings::Settings) = settings.check_numerical_bounds\nget_allow_fails(settings::Settings) = settings.allow_fails\nget_optimizer_solve_log_print(settings::Settings) = settings.optimizer_solve_log_print\nget_calculate_conflict(settings::Settings) = settings.calculate_conflict\nget_detailed_optimizer_stats(settings::Settings) = settings.detailed_optimizer_stats\nget_direct_mode_optimizer(settings::Settings) = settings.direct_mode_optimizer\nget_store_variable_names(settings::Settings) = settings.store_variable_names\nget_rebuild_model(settings::Settings) = settings.rebuild_model\nget_export_optimization_model(settings::Settings) = settings.export_optimization_model\nuse_time_series_cache(settings::Settings) = settings.time_series_cache_size > 0\n\nfunction set_horizon!(settings::Settings, horizon::Dates.TimePeriod)\n    settings.horizon[] = IS.time_period_conversion(horizon)\n    return\nend\n\nfunction set_resolution!(settings::Settings, resolution::Dates.TimePeriod)\n    settings.resolution[] = IS.time_period_conversion(resolution)\n    return\nend\n\nfunction set_interval!(settings::Settings, interval::Dates.TimePeriod)\n    settings.interval[] = IS.time_period_conversion(interval)\n    return\nend\n\nfunction set_initial_time!(settings::Settings, initial_time::Dates.DateTime)\n    settings.initial_time[] = initial_time\n    return\nend\n\nfunction set_warm_start!(settings::Settings, warm_start::Bool)\n    settings.warm_start[] = warm_start\n    return\nend\n"
  },
  {
    "path": "src/core/store_common.jl",
    "content": "# Aliases used for clarity in the method dispatches so it is possible to know if writing to\n# DecisionModel data or EmulationModel data\nconst DecisionModelIndexType = Dates.DateTime\nconst EmulationModelIndexType = Int\n\nfunction write_results!(\n    store,\n    model::OperationModel,\n    index::Union{DecisionModelIndexType, EmulationModelIndexType},\n    update_timestamp::Dates.DateTime;\n    exports = nothing,\n)\n    if exports !== nothing\n        export_params = Dict{Symbol, Any}(\n            :exports => exports,\n            :exports_path => joinpath(exports.path, string(get_name(model))),\n            :file_type => get_export_file_type(exports),\n            :resolution => get_resolution(model),\n            :horizon_count => get_horizon(get_settings(model)) ÷ get_resolution(model),\n        )\n    else\n        export_params = nothing\n    end\n\n    write_model_dual_results!(store, model, index, update_timestamp, export_params)\n    write_model_parameter_results!(store, model, index, update_timestamp, export_params)\n    write_model_variable_results!(store, model, index, update_timestamp, export_params)\n    write_model_aux_variable_results!(store, model, index, update_timestamp, export_params)\n    write_model_expression_results!(store, model, index, update_timestamp, export_params)\n    return\nend\n\nfunction write_model_dual_results!(\n    store,\n    model::T,\n    index::Union{DecisionModelIndexType, EmulationModelIndexType},\n    update_timestamp::Dates.DateTime,\n    export_params::Union{Dict{Symbol, Any}, Nothing},\n) where {T <: OperationModel}\n    container = get_optimization_container(model)\n    model_name = get_name(model)\n    if export_params !== nothing\n        exports_path = joinpath(export_params[:exports_path], \"duals\")\n        mkpath(exports_path)\n    end\n\n    for (key, constraint) in get_duals(container)\n        !should_write_resulting_value(key) && continue\n        data = jump_value.(constraint)\n        write_result!(store, model_name, key, index, update_timestamp, data)\n\n        if export_params !== nothing &&\n           should_export_dual(export_params[:exports], update_timestamp, model_name, key)\n            horizon_count = export_params[:horizon_count]\n            resolution = export_params[:resolution]\n            file_type = export_params[:file_type]\n            df = to_dataframe(jump_value.(constraint), key)\n            time_col = range(index; length = horizon_count, step = resolution)\n            DataFrames.insertcols!(df, 1, :DateTime => time_col)\n            ISOPT.export_result(file_type, exports_path, key, index, df)\n        end\n    end\n    return\nend\n\nfunction write_model_parameter_results!(\n    store,\n    model::T,\n    index::Union{DecisionModelIndexType, EmulationModelIndexType},\n    update_timestamp::Dates.DateTime,\n    export_params::Union{Dict{Symbol, Any}, Nothing},\n) where {T <: OperationModel}\n    container = get_optimization_container(model)\n    model_name = get_name(model)\n    if export_params !== nothing\n        exports_path = joinpath(export_params[:exports_path], \"parameters\")\n        mkpath(exports_path)\n    end\n\n    horizon = get_horizon(get_settings(model))\n    resolution = get_resolution(get_settings(model))\n    horizon_count = horizon ÷ resolution\n\n    parameters = get_parameters(container)\n    for (key, container) in parameters\n        !should_write_resulting_value(key) && continue\n        data = calculate_parameter_values(container)\n        write_result!(store, model_name, key, index, update_timestamp, data)\n\n        if export_params !== nothing &&\n           should_export_parameter(\n            export_params[:exports],\n            update_timestamp,\n            model_name,\n            key,\n        )\n            resolution = export_params[:resolution]\n            file_type = export_params[:file_type]\n            df = to_dataframe(data, key)\n            time_col = range(index; length = horizon_count, step = resolution)\n            DataFrames.insertcols!(df, 1, :DateTime => time_col)\n            ISOPT.export_result(file_type, exports_path, key, index, df)\n        end\n    end\n    return\nend\n\nfunction write_model_variable_results!(\n    store,\n    model::T,\n    index::Union{DecisionModelIndexType, EmulationModelIndexType},\n    update_timestamp::Dates.DateTime,\n    export_params::Union{Dict{Symbol, Any}, Nothing},\n) where {T <: OperationModel}\n    container = get_optimization_container(model)\n    model_name = get_name(model)\n    if export_params !== nothing\n        exports_path = joinpath(export_params[:exports_path], \"variables\")\n        mkpath(exports_path)\n    end\n\n    if !isempty(container.primal_values_cache)\n        variables = container.primal_values_cache.variables_cache\n    else\n        variables = get_variables(container)\n    end\n\n    for (key, variable) in variables\n        !should_write_resulting_value(key) && continue\n        data = jump_value.(variable)\n        write_result!(store, model_name, key, index, update_timestamp, data)\n\n        if export_params !== nothing &&\n           should_export_variable(\n            export_params[:exports],\n            update_timestamp,\n            model_name,\n            key,\n        )\n            horizon_count = export_params[:horizon_count]\n            resolution = export_params[:resolution]\n            file_type = export_params[:file_type]\n            df = to_dataframe(data, key)\n            time_col = range(index; length = horizon_count, step = resolution)\n            DataFrames.insertcols!(df, 1, :DateTime => time_col)\n            ISOPT.export_result(file_type, exports_path, key, index, df)\n        end\n    end\n    return\nend\n\nfunction write_model_aux_variable_results!(\n    store,\n    model::T,\n    index::Union{DecisionModelIndexType, EmulationModelIndexType},\n    update_timestamp::Dates.DateTime,\n    export_params::Union{Dict{Symbol, Any}, Nothing},\n) where {T <: OperationModel}\n    container = get_optimization_container(model)\n    model_name = get_name(model)\n    if export_params !== nothing\n        exports_path = joinpath(export_params[:exports_path], \"aux_variables\")\n        mkpath(exports_path)\n    end\n\n    for (key, variable) in get_aux_variables(container)\n        !should_write_resulting_value(key) && continue\n        data = jump_value.(variable)\n        write_result!(store, model_name, key, index, update_timestamp, data)\n\n        if export_params !== nothing &&\n           should_export_aux_variable(\n            export_params[:exports],\n            update_timestamp,\n            model_name,\n            key,\n        )\n            horizon_count = export_params[:horizon_count]\n            resolution = export_params[:resolution]\n            file_type = export_params[:file_type]\n            df = to_dataframe(data, key)\n            time_col = range(index; length = horizon_count, step = resolution)\n            DataFrames.insertcols!(df, 1, :DateTime => time_col)\n            ISOPT.export_result(file_type, exports_path, key, index, df)\n        end\n    end\n    return\nend\n\nfunction write_model_expression_results!(\n    store,\n    model::T,\n    index::Union{DecisionModelIndexType, EmulationModelIndexType},\n    update_timestamp::Dates.DateTime,\n    export_params::Union{Dict{Symbol, Any}, Nothing},\n) where {T <: OperationModel}\n    container = get_optimization_container(model)\n    model_name = get_name(model)\n    if export_params !== nothing\n        exports_path = joinpath(export_params[:exports_path], \"expressions\")\n        mkpath(exports_path)\n    end\n\n    if !isempty(container.primal_values_cache)\n        expressions = container.primal_values_cache.expressions_cache\n    else\n        expressions = get_expressions(container)\n    end\n\n    for (key, expression) in expressions\n        !should_write_resulting_value(key) && continue\n        data = jump_value.(expression)\n        write_result!(store, model_name, key, index, update_timestamp, data)\n\n        if export_params !== nothing &&\n           should_export_expression(\n            export_params[:exports],\n            update_timestamp,\n            model_name,\n            key,\n        )\n            horizon_count = export_params[:horizon_count]\n            resolution = export_params[:resolution]\n            file_type = export_params[:file_type]\n            df = to_dataframe(data, key)\n            time_col = range(index; length = horizon_count, step = resolution)\n            DataFrames.insertcols!(df, 1, :DateTime => time_col)\n            ISOPT.export_result(file_type, exports_path, key, index, df)\n        end\n    end\n    return\nend\n"
  },
  {
    "path": "src/core/variables.jl",
    "content": "abstract type AbstractContingencyVariableType <: VariableType end\nabstract type SparseVariableType <: VariableType end\nabstract type InterpolationVariableType <: SparseVariableType end\nabstract type BinaryInterpolationVariableType <: SparseVariableType end\n\n\"\"\"\nStruct to dispatch the creation of Active Power Variables\n\nDocs abbreviation: ``p``\n\"\"\"\nstruct ActivePowerVariable <: VariableType end\n\n\"\"\"\nStruct to dispatch the creation of Post-Contingency Active Power Change Variables.\n\nDocs abbreviation: ``\\\\Delta p_{g,c}``\n\"\"\"\nstruct PostContingencyActivePowerChangeVariable <: AbstractContingencyVariableType end\n\n\"\"\"\nStruct to dispatch the creation of Post-Contingency Active Power Deployment Variable for mapping reserves deployment under contingencies.\n\nDocs abbreviation: ``\\\\Delta rsv_{r,g,c}``\n\"\"\"\nstruct PostContingencyActivePowerReserveDeploymentVariable <:\n       AbstractContingencyVariableType end\n\n\"\"\"\nStruct to dispatch the creation of Active Power Variables above minimum power for Thermal Compact formulations\n\nDocs abbreviation: ``\\\\Delta p``\n\"\"\"\nstruct PowerAboveMinimumVariable <: VariableType end\n\n\"\"\"\nStruct to dispatch the creation of Active Power Input Variables for 2-directional devices. For instance storage or pump-hydro\n\nDocs abbreviation: ``p^\\\\text{in}``\n\"\"\"\nstruct ActivePowerInVariable <: VariableType end\n\n\"\"\"\nStruct to dispatch the creation of Active Power Output Variables for 2-directional devices. For instance storage or pump-hydro\n\nDocs abbreviation: ``p^\\\\text{out}``\n\"\"\"\nstruct ActivePowerOutVariable <: VariableType end\n\n\"Multi-start startup variables\"\nabstract type MultiStartVariable <: VariableType end\n\n\"\"\"\nStruct to dispatch the creation of Hot Start Variable for Thermal units with temperature considerations\n\nDocs abbreviation: ``z^\\\\text{th}``\n\"\"\"\nstruct HotStartVariable <: MultiStartVariable end\n\n\"\"\"\nStruct to dispatch the creation of Warm Start Variable for Thermal units with temperature considerations\n\nDocs abbreviation: ``y^\\\\text{th}``\n\"\"\"\nstruct WarmStartVariable <: MultiStartVariable end\n\n\"\"\"\nStruct to dispatch the creation of Cold Start Variable for Thermal units with temperature considerations\n\nDocs abbreviation: ``x^\\\\text{th}``\n\"\"\"\nstruct ColdStartVariable <: MultiStartVariable end\n\n\"\"\"\nStruct to dispatch the creation of a variable for energy storage level (state of charge)\n\nDocs abbreviation: ``e``\n\"\"\"\nstruct EnergyVariable <: VariableType end\n\nstruct LiftVariable <: VariableType end\n\n\"\"\"\nStruct to dispatch the creation of a binary commitment status variable\n\nDocs abbreviation: ``u``\n\"\"\"\nstruct OnVariable <: VariableType end\n\n\"\"\"\nStruct to dispatch the creation of Reactive Power Variables\n\nDocs abbreviation: ``q``\n\"\"\"\nstruct ReactivePowerVariable <: VariableType end\n\n\"\"\"\nStruct to dispatch the creation of binary storage charge reservation variable\n\nDocs abbreviation: ``u^\\\\text{st}``\n\"\"\"\nstruct ReservationVariable <: VariableType end\n\n\"\"\"\nStruct to dispatch the creation of Active Power Reserve Variables\n\nDocs abbreviation: ``r``\n\"\"\"\nstruct ActivePowerReserveVariable <: VariableType end\n\n\"\"\"\nStruct to dispatch the creation of Service Requirement Variables\n\nDocs abbreviation: ``\\\\text{req}``\n\"\"\"\nstruct ServiceRequirementVariable <: VariableType end\n\n\"\"\"\nStruct to dispatch the creation of Binary Start Variables\n\nDocs abbreviation: ``v``\n\"\"\"\nstruct StartVariable <: VariableType end\n\n\"\"\"\nStruct to dispatch the creation of Binary Stop Variables\n\nDocs abbreviation: ``w``\n\"\"\"\nstruct StopVariable <: VariableType end\n\nstruct SteadyStateFrequencyDeviation <: VariableType end\n\nstruct AreaMismatchVariable <: VariableType end\n\nstruct DeltaActivePowerUpVariable <: VariableType end\n\nstruct DeltaActivePowerDownVariable <: VariableType end\n\nstruct AdditionalDeltaActivePowerUpVariable <: VariableType end\n\nstruct AdditionalDeltaActivePowerDownVariable <: VariableType end\n\nstruct SmoothACE <: VariableType end\n\n\"\"\"\nStruct to dispatch the creation of System-wide slack up variables. Used when there is not enough generation.\n\nDocs abbreviation: ``p^\\\\text{sl,up}``\n\"\"\"\nstruct SystemBalanceSlackUp <: VariableType end\n\n\"\"\"\nStruct to dispatch the creation of System-wide slack down variables. Used when there is not enough load curtailment.\n\nDocs abbreviation: ``p^\\\\text{sl,dn}``\n\"\"\"\nstruct SystemBalanceSlackDown <: VariableType end\n\n\"\"\"\nStruct to dispatch the creation of Reserve requirement slack variables. Used when there is not reserves in the system to satisfy the requirement.\n\nDocs abbreviation: ``r^\\\\text{sl}``\n\"\"\"\nstruct ReserveRequirementSlack <: VariableType end\n\n\"\"\"\nStruct to dispatch the creation of Voltage Magnitude Variables for AC formulations\n\nDocs abbreviation: ``v``\n\"\"\"\nstruct VoltageMagnitude <: VariableType end\n\n\"\"\"\nStruct to dispatch the creation of Voltage Angle Variables for AC/DC formulations\n\nDocs abbreviation: ``\\\\theta``\n\"\"\"\nstruct VoltageAngle <: VariableType end\n\n#########################################\n###### Power Load Shift Variables #######\n#########################################\n\n\"\"\"\nStruct to dispatch the creation of Shifted Active Power Variables\nDocs abbreviation: ``p^\\\\text{shift,up}``\n\"\"\"\nstruct ShiftUpActivePowerVariable <: VariableType end\n\n\"\"\"\nStruct to dispatch the creation of Shifted Down Active Power Variables\nDocs abbreviation: ``p^\\\\text{shift,dn}``\n\"\"\"\nstruct ShiftDownActivePowerVariable <: VariableType end\n\n#########################################\n####### DC Converter Variables ##########\n#########################################\n\n\"\"\"\nStruct to dispatch the variable of DC Current Variables for DC Lines formulations\nDocs abbreviation: ``i_l^{dc}``\n\"\"\"\nstruct DCLineCurrent <: VariableType end\n\n\"\"\"\nStruct to dispatch the creation of Voltage Variables for DC formulations\nDocs abbreviation: ``v^{dc}``\n\"\"\"\nstruct DCVoltage <: VariableType end\n\n\"\"\"\nStruct to dispatch the creation of Squared Voltage Variables for DC formulations\nDocs abbreviation: ``v^{sq,dc}``\n\"\"\"\nstruct SquaredDCVoltage <: VariableType end\n\n\"\"\"\nStruct to dispatch the creation of DC Converter Current Variables for DC formulations\nDocs abbreviation: ``i_c^{dc}``\n\"\"\"\nstruct ConverterCurrent <: VariableType end\n\n\"\"\"\nStruct to dispatch the creation of DC Converter Power Variables for DC formulations\nDocs abbreviation: ``p_c^{dc}``\n\"\"\"\nstruct ConverterDCPower <: VariableType end\n\n\"\"\"\nStruct to dispatch the creation of Squared DC Converter Current Variables for DC formulations\nDocs abbreviation: ``i_c^{sq,dc}``\n\"\"\"\nstruct SquaredConverterCurrent <: VariableType end\n\n\"\"\"\nStruct to dispatch the creation of DC Converter Positive Term Current Variables for DC formulations\nDocs abbreviation: ``i_c^{+,dc}``\n\"\"\"\nstruct ConverterPositiveCurrent <: VariableType end\n\n\"\"\"\nStruct to dispatch the creation of DC Converter Negative Term Current Variables for DC formulations\nDocs abbreviation: ``i_c^{-,dc}``\n\"\"\"\nstruct ConverterNegativeCurrent <: VariableType end\n\n\"\"\"\nStruct to dispatch the creation of DC Converter Binary for Absolute Value Current Variables for DC formulations\nDocs abbreviation: `\\\\nu_c``\n\"\"\"\nstruct ConverterCurrentDirection <: VariableType end\n\n\"\"\"\nStruct to dispatch the creation of Binary Variable for Converter Power Direction\nDocs abbreviation: ``\\\\kappa_c^{dc}``\n\"\"\"\nstruct ConverterPowerDirection <: VariableType end\n\n\"\"\"\nStruct to dispatch the creation of Auxiliary Variable for Converter Bilinear term: v * i\nDocs abbreviation: ``\\\\gamma_c^{dc}``\n\"\"\"\nstruct AuxBilinearConverterVariable <: VariableType end\n\n\"\"\"\nStruct to dispatch the creation of Auxiliary Variable for Squared Converter Bilinear term: v * i\n    \nDocs abbreviation: ``\\\\gamma_c^{sq,dc}``\n\"\"\"\nstruct AuxBilinearSquaredConverterVariable <: VariableType end\n\n\"\"\"\nStruct to dispatch the creation of Continuous Interpolation Variable for Squared Converter Voltage\n    \nDocs abbreviation: ``\\\\delta_c^{v}``\n\"\"\"\nstruct InterpolationSquaredVoltageVariable <: InterpolationVariableType end\n\n\"\"\"\nStruct to dispatch the creation of Binary Interpolation Variable for Squared Converter Voltage\n    \nDocs abbreviation: ``z_c^{v}``\n\"\"\"\nstruct InterpolationBinarySquaredVoltageVariable <: BinaryInterpolationVariableType end\n\n\"\"\"\nStruct to dispatch the creation of Continuous Interpolation Variable for Squared Converter Current\n    \nDocs abbreviation: ``\\\\delta_c^{i}``\n\"\"\"\nstruct InterpolationSquaredCurrentVariable <: InterpolationVariableType end\n\n\"\"\"\nStruct to dispatch the creation of Binary Interpolation Variable for Squared Converter Current\n    \nDocs abbreviation: ``z_c^{i}``\n\"\"\"\nstruct InterpolationBinarySquaredCurrentVariable <: BinaryInterpolationVariableType end\n\n\"\"\"\nStruct to dispatch the creation of Continuous Interpolation Variable for Squared Converter AuxVar\n    \nDocs abbreviation: ``\\\\delta_c^{\\\\gamma}``\n\"\"\"\nstruct InterpolationSquaredBilinearVariable <: InterpolationVariableType end\n\n\"\"\"\nStruct to dispatch the creation of Binary Interpolation Variable for Squared Converter AuxVar\n    \nDocs abbreviation: ``z_c^{\\\\gamma}``\n\"\"\"\nstruct InterpolationBinarySquaredBilinearVariable <: BinaryInterpolationVariableType end\n\n#########################################################\n#########################################################\n\nabstract type AbstractACActivePowerFlow <: VariableType end\n\nabstract type AbstractACReactivePowerFlow <: VariableType end\n\n\"\"\"\nStruct to dispatch the creation of bidirectional Active Power Flow Variables\n\nDocs abbreviation: ``f``\n\"\"\"\nstruct FlowActivePowerVariable <: AbstractACActivePowerFlow end\n\n# This Variable Type doesn't make sense since there are no lossless NetworkModels with ReactivePower.\n# struct FlowReactivePowerVariable <: VariableType end\n\n\"\"\"\nStruct to dispatch the creation of unidirectional Active Power Flow Variables\n\nDocs abbreviation: ``f^\\\\text{from-to}``\n\"\"\"\nstruct FlowActivePowerFromToVariable <: AbstractACActivePowerFlow end\n\n\"\"\"\nStruct to dispatch the creation of unidirectional Active Power Flow Variables\n\nDocs abbreviation: ``f^\\\\text{to-from}``\n\"\"\"\nstruct FlowActivePowerToFromVariable <: AbstractACActivePowerFlow end\n\n\"\"\"\nStruct to dispatch the creation of unidirectional Reactive Power Flow Variables\n\nDocs abbreviation: ``f^\\\\text{q,from-to}``\n\"\"\"\nstruct FlowReactivePowerFromToVariable <: AbstractACReactivePowerFlow end\n\n\"\"\"\nStruct to dispatch the creation of unidirectional Reactive Power Flow Variables\n\nDocs abbreviation: ``f^\\\\text{q,to-from}``\n\"\"\"\nstruct FlowReactivePowerToFromVariable <: AbstractACReactivePowerFlow end\n\n\"\"\"\nStruct 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.\n\nDocs abbreviation: ``f^\\\\text{sl,up}``\n\"\"\"\nstruct FlowActivePowerSlackUpperBound <: AbstractACActivePowerFlow end\n\n\"\"\"\nStruct 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.\n\nDocs abbreviation: ``f^\\\\text{sl,lo}``\n\"\"\"\nstruct FlowActivePowerSlackLowerBound <: AbstractACActivePowerFlow end\n\n\"\"\"\nStruct to dispatch the creation of Phase Shifters Variables\n\nDocs abbreviation: ``\\\\theta^\\\\text{shift}``\n\"\"\"\nstruct PhaseShifterAngle <: VariableType end\n\n# Necessary as a work around for HVDCTwoTerminal models with losses\n\"\"\"\nStruct to dispatch the creation of HVDC Losses Auxiliary Variables\n\nDocs abbreviation: ``\\\\ell``\n\"\"\"\nstruct HVDCLosses <: VariableType end\n\n\"\"\"\nStruct to dispatch the creation of HVDC Flow Direction Auxiliary Variables\n\nDocs abbreviation: ``u^\\\\text{dir}``\n\"\"\"\nstruct HVDCFlowDirectionVariable <: VariableType end\n\n\"\"\"\nStruct to dispatch the creation of HVDC Received Flow at From Bus Variables for PWL formulations\n\nDocs abbreviation: ``x``\n\"\"\"\nstruct HVDCActivePowerReceivedFromVariable <: VariableType end\n\n\"\"\"\nStruct to dispatch the creation of HVDC Received Flow at To Bus Variables for PWL formulations\n\nDocs abbreviation: ``y``\n\"\"\"\nstruct HVDCActivePowerReceivedToVariable <: VariableType end\n\n\"\"\"\nStruct to dispatch the creation of HVDC Received Reactive Flow From Bus Variables\n\nDocs abbreviation: ``x^r``\n\"\"\"\nstruct HVDCReactivePowerReceivedFromVariable <: VariableType end\n\n\"\"\"\nStruct to dispatch the creation of HVDC Received Reactive Flow To Bus Variables\n\nDocs abbreviation: ``y^i``\n\"\"\"\nstruct HVDCReactivePowerReceivedToVariable <: VariableType end\n\n\"\"\"\nStruct to define the creation of HVDC Rectifier Delay Angle Variable\n\nDocs abbreviation: ``\\\\alpha^r``\n\"\"\"\nstruct HVDCRectifierDelayAngleVariable <: VariableType end\n\n\"\"\"\nStruct to define the creation of HVDC Inverter Extinction Angle Variable\n\nDocs abbreviation: ``\\\\gamma^i``\n\"\"\"\nstruct HVDCInverterExtinctionAngleVariable <: VariableType end\n\n\"\"\"\nStruct to define the creation of HVDC Rectifier Power Factor Angle Variable\n\nDocs abbreviation: ``\\\\phi^r``\n\"\"\"\nstruct HVDCRectifierPowerFactorAngleVariable <: VariableType end\n\n\"\"\"\nStruct to define the creation of HVDC Inverter Power Factor Angle Variable\n\nDocs abbreviation: ``\\\\phi^i``\n\"\"\"\nstruct HVDCInverterPowerFactorAngleVariable <: VariableType end\n\n\"\"\"\nStruct to define the creation of HVDC Rectifier Overlap Angle Variable\n\nDocs abbreviation: ``\\\\mu^r``\n\"\"\"\nstruct HVDCRectifierOverlapAngleVariable <: VariableType end\n\n\"\"\"\nStruct to define the creation of HVDC Inverter Overlap Angle Variable\n\nDocs abbreviation: ``\\\\mu^i``\n\"\"\"\nstruct HVDCInverterOverlapAngleVariable <: VariableType end\n\n\"\"\"\nStruct to define the creation of HVDC DC Line Voltage at Rectifier Side\n\nDocs abbreviation: ``\\\\v_{d}^r``\n\"\"\"\nstruct HVDCRectifierDCVoltageVariable <: VariableType end\n\n\"\"\"\nStruct to define the creation of HVDC DC Line Voltage at Inverter Side\n\nDocs abbreviation: ``\\\\v_{d}^i``\n\"\"\"\nstruct HVDCInverterDCVoltageVariable <: VariableType end\n\n\"\"\"\nStruct to define the creation of HVDC AC Line Current flowing into the AC side of Rectifier\n\nDocs abbreviation: ``\\\\i_{ac}^r``\n\"\"\"\nstruct HVDCRectifierACCurrentVariable <: VariableType end\n\n\"\"\"\nStruct to define the creation of HVDC AC Line Current flowing into the AC side of Inverter\n\nDocs abbreviation: ``\\\\i_{ac}^i``\n\"\"\"\nstruct HVDCInverterACCurrentVariable <: VariableType end\n\n\"\"\"\nStruct to define the creation of HVDC DC Line Current Flow\n\nDocs abbreviation: ``\\\\i_{d}``\n\"\"\"\nstruct DCLineCurrentFlowVariable <: VariableType end\n\n\"\"\"\nStruct to define the creation of HVDC Tap Setting at Rectifier Transformer\n\nDocs abbreviation: ``\\\\t^r``\n\"\"\"\nstruct HVDCRectifierTapSettingVariable <: VariableType end\n\n\"\"\"\nStruct to define the creation of HVDC Tap Setting at Inverter Transformer\n\nDocs abbreviation: ``\\\\t^i``\n\"\"\"\nstruct HVDCInverterTapSettingVariable <: VariableType end\n\n\"\"\"\nStruct to dispatch the creation of HVDC Piecewise Loss Variables\n\nDocs abbreviation: ``h`` or ``w``\n\"\"\"\nstruct HVDCPiecewiseLossVariable <: SparseVariableType end\n\n\"\"\"\nStruct to dispatch the creation of HVDC Piecewise Binary Loss Variables\n\nDocs abbreviation: ``z``\n\"\"\"\nstruct HVDCPiecewiseBinaryLossVariable <: SparseVariableType end\n\n\"\"\"\nStruct to dispatch the creation of piecewise linear cost variables for objective function\n\nDocs abbreviation: ``\\\\delta``\n\"\"\"\nstruct PiecewiseLinearCostVariable <: SparseVariableType end\n\nabstract type AbstractPiecewiseLinearBlockOffer <: SparseVariableType end\n\n\"\"\"\nStruct to dispatch the creation of piecewise linear block incremental offer variables for objective function\n\nDocs abbreviation: ``\\\\delta``\n\"\"\"\nstruct PiecewiseLinearBlockIncrementalOffer <: AbstractPiecewiseLinearBlockOffer end\n\n\"\"\"\nStruct to dispatch the creation of piecewise linear block decremental offer variables for objective function\n\nDocs abbreviation: ``\\\\delta_d``\n\"\"\"\nstruct PiecewiseLinearBlockDecrementalOffer <: AbstractPiecewiseLinearBlockOffer end\n\n\"\"\"\nStruct to dispatch the creation of Interface Flow Slack Up variables\n\nDocs abbreviation: ``f^\\\\text{sl,up}``\n\"\"\"\nstruct InterfaceFlowSlackUp <: VariableType end\n\"\"\"\nStruct to dispatch the creation of Interface Flow Slack Down variables\n\nDocs abbreviation: ``f^\\\\text{sl,dn}``\n\"\"\"\nstruct InterfaceFlowSlackDown <: VariableType end\n\n\"\"\"\nStruct to dispatch the creation of Slack variables for UpperBoundFeedforward\n\nDocs abbreviation: ``p^\\\\text{ff,ubsl}``\n\"\"\"\nstruct UpperBoundFeedForwardSlack <: VariableType end\n\"\"\"\nStruct to dispatch the creation of Slack variables for LowerBoundFeedforward\n\nDocs abbreviation: ``p^\\\\text{ff,lbsl}``\n\"\"\"\nstruct LowerBoundFeedForwardSlack <: VariableType end\n\n\"\"\"\nStruct to dispatch the creation of Slack variables for rate of change constraints up limits\n\nDocs abbreviation: ``p^\\\\text{sl,up}``\n\"\"\"\nstruct RateofChangeConstraintSlackUp <: VariableType end\n\"\"\"\nStruct to dispatch the creation of Slack variables for rate of change constraints down limits\n\nDocs abbreviation: ``p^\\\\text{sl,dn}``\n\"\"\"\nstruct RateofChangeConstraintSlackDown <: VariableType end\n\nconst MULTI_START_VARIABLES = Tuple(IS.get_all_concrete_subtypes(PSI.MultiStartVariable))\n\nshould_write_resulting_value(::Type{PiecewiseLinearCostVariable}) = false\nshould_write_resulting_value(::Type{PiecewiseLinearBlockIncrementalOffer}) = false\nshould_write_resulting_value(::Type{PiecewiseLinearBlockDecrementalOffer}) = false\nshould_write_resulting_value(::Type{HVDCPiecewiseLossVariable}) = false\nshould_write_resulting_value(::Type{HVDCPiecewiseBinaryLossVariable}) = false\nfunction should_write_resulting_value(\n    ::Type{T},\n) where {T <: Union{InterpolationVariableType, BinaryInterpolationVariableType}}\n    return false\nend\nconvert_result_to_natural_units(::Type{ActivePowerVariable}) = true\nconvert_result_to_natural_units(::Type{PostContingencyActivePowerChangeVariable}) = true\nconvert_result_to_natural_units(::Type{PowerAboveMinimumVariable}) = true\nconvert_result_to_natural_units(::Type{ActivePowerInVariable}) = true\nconvert_result_to_natural_units(::Type{ActivePowerOutVariable}) = true\nconvert_result_to_natural_units(::Type{EnergyVariable}) = true\nconvert_result_to_natural_units(::Type{ReactivePowerVariable}) = true\nconvert_result_to_natural_units(::Type{ActivePowerReserveVariable}) = true\nconvert_result_to_natural_units(\n    ::Type{PostContingencyActivePowerReserveDeploymentVariable},\n) = true\nconvert_result_to_natural_units(::Type{ServiceRequirementVariable}) = true\nconvert_result_to_natural_units(::Type{RateofChangeConstraintSlackUp}) = true\nconvert_result_to_natural_units(::Type{RateofChangeConstraintSlackDown}) = true\nconvert_result_to_natural_units(::Type{AreaMismatchVariable}) = true\nconvert_result_to_natural_units(::Type{DeltaActivePowerUpVariable}) = true\nconvert_result_to_natural_units(::Type{DeltaActivePowerDownVariable}) = true\nconvert_result_to_natural_units(::Type{AdditionalDeltaActivePowerUpVariable}) = true\nconvert_result_to_natural_units(::Type{AdditionalDeltaActivePowerDownVariable}) = true\nconvert_result_to_natural_units(::Type{SmoothACE}) = true\nconvert_result_to_natural_units(::Type{SystemBalanceSlackUp}) = true\nconvert_result_to_natural_units(::Type{SystemBalanceSlackDown}) = true\nconvert_result_to_natural_units(::Type{ReserveRequirementSlack}) = true\nconvert_result_to_natural_units(::Type{FlowActivePowerVariable}) = true\nconvert_result_to_natural_units(::Type{FlowActivePowerFromToVariable}) = true\nconvert_result_to_natural_units(::Type{FlowActivePowerToFromVariable}) = true\nconvert_result_to_natural_units(::Type{FlowReactivePowerFromToVariable}) = true\nconvert_result_to_natural_units(::Type{FlowReactivePowerToFromVariable}) = true\nconvert_result_to_natural_units(::Type{HVDCLosses}) = true\nconvert_result_to_natural_units(::Type{InterfaceFlowSlackUp}) = true\nconvert_result_to_natural_units(::Type{InterfaceFlowSlackDown}) = true\n"
  },
  {
    "path": "src/devices_models/device_constructors/branch_constructor.jl",
    "content": "################################# Generic AC Branch  Models ################################\n# These 3 methods are defined on concrete formulations of the branches to avoid ambiguity\nfunction construct_device!(\n    ::OptimizationContainer,\n    ::PSY.System,\n    ::ArgumentConstructStage,\n    ::DeviceModel{T, StaticBranch},\n    ::Union{\n        NetworkModel{CopperPlatePowerModel},\n        NetworkModel{AreaBalancePowerModel},\n    },\n) where {T <: PSY.ACTransmission}\n    @debug \"No argument construction needed for CopperPlatePowerModel or AreaBalancePowerModel and DeviceModel{$T, StaticBranch}\" _group =\n        LOG_GROUP_BRANCH_CONSTRUCTIONS\n    return\nend\n\nfunction construct_device!(\n    ::OptimizationContainer,\n    ::PSY.System,\n    ::ModelConstructStage,\n    ::DeviceModel{T, StaticBranch},\n    ::Union{\n        NetworkModel{CopperPlatePowerModel},\n        NetworkModel{AreaBalancePowerModel},\n    },\n) where {T <: PSY.ACTransmission}\n    @debug \"No model construction needed for CopperPlatePowerModel or AreaBalancePowerModel and DeviceModel{$T, StaticBranch}\" _group =\n        LOG_GROUP_BRANCH_CONSTRUCTIONS\n    return\nend\n\nfunction construct_device!(\n    ::OptimizationContainer,\n    ::PSY.System,\n    ::ArgumentConstructStage,\n    ::DeviceModel{T, StaticBranchBounds},\n    ::Union{\n        NetworkModel{CopperPlatePowerModel},\n        NetworkModel{AreaBalancePowerModel},\n    },\n) where {T <: PSY.ACTransmission}\n    @debug \"No argument construction needed for CopperPlatePowerModel or AreaBalancePowerModel and DeviceModel{$T, StaticBranchBounds}\" _group =\n        LOG_GROUP_BRANCH_CONSTRUCTIONS\n    return\nend\n\nfunction construct_device!(\n    ::OptimizationContainer,\n    ::PSY.System,\n    ::ModelConstructStage,\n    ::DeviceModel{T, StaticBranchBounds},\n    ::Union{\n        NetworkModel{CopperPlatePowerModel},\n        NetworkModel{AreaBalancePowerModel},\n    },\n) where {T <: PSY.ACTransmission}\n    @debug \"No model construction needed for CopperPlatePowerModel or AreaBalancePowerModel and DeviceModel{$T, StaticBranchBounds}\" _group =\n        LOG_GROUP_BRANCH_CONSTRUCTIONS\n    return\nend\n\nconstruct_device!(\n    ::OptimizationContainer,\n    ::PSY.System,\n    ::ArgumentConstructStage,\n    ::DeviceModel{<:PSY.ACTransmission, StaticBranchUnbounded},\n    ::NetworkModel{<:PM.AbstractPowerModel},\n) = nothing\n\nconstruct_device!(\n    ::OptimizationContainer,\n    ::PSY.System,\n    ::ModelConstructStage,\n    ::DeviceModel{<:PSY.ACTransmission, StaticBranchUnbounded},\n    ::NetworkModel{<:PM.AbstractPowerModel},\n) = nothing\n\n# For DC Power only. Implements constraints\nfunction construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ArgumentConstructStage,\n    device_model::DeviceModel{T, StaticBranch},\n    network_model::NetworkModel{<:PM.AbstractActivePowerModel},\n) where {T <: PSY.ACTransmission}\n    devices = get_available_components(device_model, sys)\n    if get_use_slacks(device_model)\n        add_variables!(\n            container,\n            FlowActivePowerSlackUpperBound,\n            network_model,\n            devices,\n            StaticBranch(),\n        )\n        add_variables!(\n            container,\n            FlowActivePowerSlackLowerBound,\n            network_model,\n            devices,\n            StaticBranch(),\n        )\n    end\n    add_feedforward_arguments!(container, device_model, devices)\n    return\nend\n\n# For DC Power only. Implements constraints\nfunction construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ModelConstructStage,\n    device_model::DeviceModel{T, StaticBranch},\n    network_model::NetworkModel{U},\n) where {T <: PSY.ACTransmission, U <: PM.AbstractActivePowerModel}\n    @debug \"construct_device\" _group = LOG_GROUP_BRANCH_CONSTRUCTIONS\n\n    devices = get_available_components(device_model, sys)\n    add_constraints!(container, FlowRateConstraint, devices, device_model, network_model)\n    add_feedforward_constraints!(container, device_model, devices)\n    objective_function!(container, devices, device_model, U)\n    add_constraint_dual!(container, sys, device_model)\n    return\nend\n\n# For DC Power only\nfunction construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ArgumentConstructStage,\n    device_model::DeviceModel{T, StaticBranch},\n    network_model::NetworkModel{<:AbstractPTDFModel},\n) where {T <: PSY.ACTransmission}\n    devices = get_available_components(device_model, sys)\n    if get_use_slacks(device_model)\n        add_variables!(\n            container,\n            FlowActivePowerSlackUpperBound,\n            network_model,\n            devices,\n            StaticBranch(),\n        )\n        add_variables!(\n            container,\n            FlowActivePowerSlackLowerBound,\n            network_model,\n            devices,\n            StaticBranch(),\n        )\n    end\n\n    if haskey(get_time_series_names(device_model), DynamicBranchRatingTimeSeriesParameter)\n        add_branch_parameters!(\n            container,\n            DynamicBranchRatingTimeSeriesParameter,\n            devices,\n            device_model,\n            network_model,\n        )\n    end\n\n    if haskey(\n        get_time_series_names(device_model),\n        PostContingencyDynamicBranchRatingTimeSeriesParameter,\n    )\n        add_branch_parameters!(\n            container,\n            PostContingencyDynamicBranchRatingTimeSeriesParameter,\n            devices,\n            device_model,\n            network_model,\n        )\n    end\n\n    add_feedforward_arguments!(container, device_model, devices)\n    return\nend\n\nfunction construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ModelConstructStage,\n    device_model::DeviceModel{T, StaticBranch},\n    network_model::NetworkModel{<:AbstractPTDFModel},\n) where {T <: PSY.ACTransmission}\n    devices = get_available_components(device_model, sys)\n\n    # The order of these methods is important. The add_expressions! must be before the constraints\n    add_expressions!(\n        container,\n        PTDFBranchFlow,\n        devices,\n        device_model,\n        network_model,\n    )\n    if haskey(get_time_series_names(device_model), DynamicBranchRatingTimeSeriesParameter)\n        add_flow_rate_constraint_with_parameters!(\n            container,\n            FlowRateConstraint,\n            devices,\n            device_model,\n            network_model,\n        )\n    else\n        add_constraints!(\n            container,\n            FlowRateConstraint,\n            devices,\n            device_model,\n            network_model,\n        )\n    end\n    add_feedforward_constraints!(container, device_model, devices)\n    objective_function!(container, devices, device_model, PTDFPowerModel)\n    add_constraint_dual!(container, sys, device_model)\n    return\nend\n\nfunction construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ArgumentConstructStage,\n    device_model::DeviceModel{T, StaticBranchBounds},\n    network_model::NetworkModel{<:AbstractPTDFModel},\n) where {T <: PSY.ACTransmission}\n    devices = get_available_components(device_model, sys)\n\n    add_variables!(\n        container,\n        FlowActivePowerVariable,\n        network_model,\n        devices,\n        StaticBranchBounds(),\n    )\n\n    if get_use_slacks(device_model)\n        add_variables!(\n            container,\n            FlowActivePowerSlackUpperBound,\n            network_model,\n            devices,\n            StaticBranch(),\n        )\n        add_variables!(\n            container,\n            FlowActivePowerSlackLowerBound,\n            network_model,\n            devices,\n            StaticBranch(),\n        )\n    end\n\n    add_feedforward_arguments!(container, device_model, devices)\n    return\nend\n\nfunction construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ModelConstructStage,\n    device_model::DeviceModel{T, StaticBranchBounds},\n    network_model::NetworkModel{<:AbstractPTDFModel},\n) where {T <: PSY.ACTransmission}\n    devices = get_available_components(device_model, sys)\n    # The order of these methods is important. The add_expressions! must be before the constraints\n    add_expressions!(\n        container,\n        PTDFBranchFlow,\n        devices,\n        device_model,\n        network_model,\n    )\n\n    branch_rate_bounds!(container, device_model, network_model)\n    add_constraints!(container, NetworkFlowConstraint, devices, device_model, network_model)\n    add_feedforward_constraints!(container, device_model, devices)\n    objective_function!(container, devices, device_model, PTDFPowerModel)\n    add_constraint_dual!(container, sys, device_model)\n    return\nend\n\nfunction construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ArgumentConstructStage,\n    device_model::DeviceModel{T, StaticBranchUnbounded},\n    network_model::NetworkModel{<:AbstractPTDFModel},\n) where {T <: PSY.ACTransmission}\n    devices = get_available_components(device_model, sys)\n    add_feedforward_arguments!(container, device_model, devices)\n    return\nend\n\nfunction construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ModelConstructStage,\n    device_model::DeviceModel{T, StaticBranchUnbounded},\n    network_model::NetworkModel{<:AbstractPTDFModel},\n) where {T <: PSY.ACTransmission}\n    devices = get_available_components(device_model, sys)\n    # The order of these methods is important. The add_expressions! must be before the constraints\n    add_expressions!(\n        container,\n        PTDFBranchFlow,\n        devices,\n        device_model,\n        network_model,\n    )\n    add_feedforward_constraints!(container, device_model, devices)\n    add_constraints!(container, NetworkFlowConstraint, devices, device_model, network_model)\n    add_constraint_dual!(container, sys, device_model)\n    return\nend\n\n# For AC Power only. Implements Bounds on the active power and rating constraints on the aparent power\nfunction construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ArgumentConstructStage,\n    device_model::DeviceModel{T, StaticBranch},\n    network_model::NetworkModel{<:PM.AbstractPowerModel},\n) where {T <: PSY.ACTransmission}\n    devices = get_available_components(device_model, sys)\n\n    if get_use_slacks(device_model)\n        # Only one slack is needed for this formulations in AC\n        add_variables!(\n            container,\n            FlowActivePowerSlackUpperBound,\n            network_model,\n            devices,\n            StaticBranch(),\n        )\n    end\n    add_feedforward_arguments!(container, device_model, devices)\n    return\nend\n\nfunction construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ModelConstructStage,\n    device_model::DeviceModel{T, StaticBranch},\n    network_model::NetworkModel{<:PM.AbstractPowerModel},\n) where {T <: PSY.ACTransmission}\n    devices = get_available_components(device_model, sys)\n    add_feedforward_constraints!(container, device_model, devices)\n    add_constraints!(\n        container,\n        FlowRateConstraintFromTo,\n        devices,\n        device_model,\n        network_model,\n    )\n    add_constraints!(\n        container,\n        FlowRateConstraintToFrom,\n        devices,\n        device_model,\n        network_model,\n    )\n    add_constraint_dual!(container, sys, device_model)\n    return\nend\n\nfunction construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ArgumentConstructStage,\n    device_model::DeviceModel{T, StaticBranchBounds},\n    ::NetworkModel{U},\n) where {T <: PSY.ACTransmission, U <: PM.AbstractPowerModel}\n    if get_use_slacks(device_model)\n        throw(\n            ArgumentError(\n                \"StaticBranchBounds formulation and $U is not compatible with the use of slacks\",\n            ),\n        )\n    end\n    devices = get_available_components(device_model, sys)\n    add_feedforward_arguments!(container, device_model, devices)\n    return\nend\n\nfunction construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ModelConstructStage,\n    device_model::DeviceModel{T, StaticBranchBounds},\n    network_model::NetworkModel{<:PM.AbstractPowerModel},\n) where {T <: PSY.ACTransmission}\n    devices = get_available_components(device_model, sys)\n    branch_rate_bounds!(container, device_model, network_model)\n    add_constraints!(\n        container,\n        FlowRateConstraintFromTo,\n        devices,\n        device_model,\n        network_model,\n    )\n    add_constraints!(\n        container,\n        FlowRateConstraintToFrom,\n        devices,\n        device_model,\n        network_model,\n    )\n    add_constraint_dual!(container, sys, device_model)\n    add_feedforward_constraints!(container, device_model, devices)\n    return\nend\n\nfunction construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ModelConstructStage,\n    device_model::DeviceModel{T, StaticBranchBounds},\n    network_model::NetworkModel{<:PM.AbstractActivePowerModel},\n) where {T <: PSY.ACTransmission}\n    devices = get_available_components(device_model, sys)\n    branch_rate_bounds!(container, device_model, network_model)\n    add_constraint_dual!(container, sys, device_model)\n    add_feedforward_constraints!(container, device_model, devices)\n    return\nend\n\n################################### TwoTerminal HVDC Line Models ###################################\nfunction construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ArgumentConstructStage,\n    device_model::DeviceModel{T, HVDCTwoTerminalLossless},\n    network_model::NetworkModel{CopperPlatePowerModel},\n) where {T <: PSY.TwoTerminalHVDC}\n    if has_subnetworks(network_model)\n        devices = get_available_components(device_model, sys)\n        add_variables!(\n            container,\n            FlowActivePowerVariable,\n            network_model,\n            devices,\n            HVDCTwoTerminalLossless(),\n        )\n        add_to_expression!(\n            container,\n            ActivePowerBalance,\n            FlowActivePowerVariable,\n            devices,\n            device_model,\n            network_model,\n        )\n        add_feedforward_arguments!(container, device_model, devices)\n    end\n    return\nend\n\nfunction construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ModelConstructStage,\n    device_model::DeviceModel{T, HVDCTwoTerminalLossless},\n    network_model::NetworkModel{CopperPlatePowerModel},\n) where {T <: PSY.TwoTerminalHVDC}\n    if has_subnetworks(network_model)\n        devices =\n            get_available_components(device_model, sys)\n        add_constraints!(\n            container,\n            FlowRateConstraint,\n            devices,\n            device_model,\n            network_model,\n        )\n        add_constraint_dual!(container, sys, device_model)\n        add_feedforward_constraints!(container, device_model, devices)\n    end\n    return\nend\n\nfunction construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ArgumentConstructStage,\n    device_model::DeviceModel{T, HVDCTwoTerminalUnbounded},\n    ::NetworkModel{<:PM.AbstractPowerModel},\n) where {T <: PSY.TwoTerminalHVDC}\n    devices = get_available_components(device_model, sys)\n    add_feedforward_arguments!(container, device_model, devices)\n    return\nend\n\nfunction construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ModelConstructStage,\n    device_model::DeviceModel{<:PSY.TwoTerminalHVDC, HVDCTwoTerminalUnbounded},\n    ::NetworkModel{<:PM.AbstractPowerModel},\n)\n    devices = get_available_components(device_model, sys)\n    add_constraint_dual!(container, sys, device_model)\n    add_feedforward_constraints!(container, device_model, devices)\n    return\nend\n\nfunction construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ArgumentConstructStage,\n    device_model::DeviceModel{T, HVDCTwoTerminalUnbounded},\n    network_model::NetworkModel{CopperPlatePowerModel},\n) where {T <: PSY.TwoTerminalHVDC}\n    devices = get_available_components(device_model, sys)\n    add_variables!(container, FlowActivePowerVariable, devices, HVDCTwoTerminalUnbounded())\n    add_to_expression!(\n        container,\n        ActivePowerBalance,\n        FlowActivePowerVariable,\n        devices,\n        device_model,\n        network_model,\n    )\n    add_feedforward_arguments!(container, devicemodel, devices)\n    return\nend\n\nfunction construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ModelConstructStage,\n    device_model::DeviceModel{<:PSY.TwoTerminalHVDC, HVDCTwoTerminalUnbounded},\n    ::NetworkModel{CopperPlatePowerModel},\n)\n    devices = get_available_components(device_model, sys)\n    add_constraint_dual!(container, sys, device_model)\n    add_feedforward_constraints!(container, device_model, devices)\n    return\nend\n\nfunction construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ArgumentConstructStage,\n    device_model::DeviceModel{T, HVDCTwoTerminalDispatch},\n    ::NetworkModel{AreaBalancePowerModel},\n) where {T <: PSY.TwoTerminalHVDC}\n    @warn \"AreaBalancePowerModel doesn't model individual line flows for $T. Arguments not built\"\n    return\nend\n\nfunction construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ModelConstructStage,\n    device_model::DeviceModel{T, HVDCTwoTerminalDispatch},\n    ::NetworkModel{AreaBalancePowerModel},\n) where {T <: PSY.TwoTerminalHVDC}\n    @warn \"AreaBalancePowerModel doesn't model individual line flows for $T. Model not built\"\n    return\nend\n\n# Repeated method to avoid ambiguity between HVDCTwoTerminalUnbounded, HVDCTwoTerminalLossless and HVDCTwoTerminalDispatch\nfunction construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ArgumentConstructStage,\n    device_model::DeviceModel{T, HVDCTwoTerminalUnbounded},\n    ::NetworkModel{AreaBalancePowerModel},\n) where {T <: PSY.TwoTerminalHVDC}\n    @warn \"AreaBalancePowerModel doesn't model individual line flows for $T. Arguments not built\"\n    return\nend\n\nfunction construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ModelConstructStage,\n    device_model::DeviceModel{T, HVDCTwoTerminalUnbounded},\n    ::NetworkModel{AreaBalancePowerModel},\n) where {T <: PSY.TwoTerminalHVDC}\n    @warn \"AreaBalancePowerModel doesn't model individual line flows for $T. Model not built\"\n    return\nend\n\n# Repeated method to avoid ambiguity between HVDCTwoTerminalUnbounded, HVDCTwoTerminalLossless and HVDCTwoTerminalDispatch\nfunction construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ArgumentConstructStage,\n    device_model::DeviceModel{T, HVDCTwoTerminalLossless},\n    ::NetworkModel{AreaBalancePowerModel},\n) where {T <: PSY.TwoTerminalHVDC}\n    @warn \"AreaBalancePowerModel doesn't model individual line flows for $T. Arguments not built\"\n    return\nend\n\nfunction construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ModelConstructStage,\n    device_model::DeviceModel{T, HVDCTwoTerminalLossless},\n    ::NetworkModel{AreaBalancePowerModel},\n) where {T <: PSY.TwoTerminalHVDC}\n    @warn \"AreaBalancePowerModel doesn't model individual line flows for $T. Model not built\"\n    return\nend\n\n# Repeated method to avoid ambiguity between HVDCTwoTerminalUnbounded and HVDCTwoTerminalLossless\nfunction construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ArgumentConstructStage,\n    device_model::DeviceModel{T, HVDCTwoTerminalUnbounded},\n    network_model::NetworkModel{<:AbstractPTDFModel},\n) where {T <: PSY.TwoTerminalHVDC}\n    devices = get_available_components(device_model, sys)\n    add_variables!(container, FlowActivePowerVariable, devices, HVDCTwoTerminalUnbounded())\n    add_to_expression!(\n        container,\n        ActivePowerBalance,\n        FlowActivePowerVariable,\n        devices,\n        device_model,\n        network_model,\n    )\n    add_feedforward_arguments!(container, device_model, devices)\n    return\nend\n\n# Repeated method to avoid ambiguity between HVDCTwoTerminalUnbounded and HVDCTwoTerminalLossless\nfunction construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ModelConstructStage,\n    device_model::DeviceModel{<:PSY.TwoTerminalHVDC, HVDCTwoTerminalUnbounded},\n    network_model::NetworkModel{<:AbstractPTDFModel},\n)\n    devices = get_available_components(device_model, sys)\n    add_constraint_dual!(container, sys, device_model)\n    add_feedforward_constraints!(container, device_model, devices)\n    return\nend\n\nfunction construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ArgumentConstructStage,\n    device_model::DeviceModel{T, HVDCTwoTerminalLossless},\n    ::NetworkModel{<:PM.AbstractPowerModel},\n) where {T <: PSY.TwoTerminalHVDC}\n    devices = get_available_components(device_model, sys)\n    add_feedforward_arguments!(container, device_model, devices)\n    return\nend\n\nfunction construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ModelConstructStage,\n    device_model::DeviceModel{T, HVDCTwoTerminalLossless},\n    network_model::NetworkModel{<:PM.AbstractPowerModel},\n) where {T <: PSY.TwoTerminalHVDC}\n    devices = get_available_components(device_model, sys)\n    add_constraints!(container, FlowRateConstraint, devices, device_model, network_model)\n    add_constraint_dual!(container, sys, device_model)\n    add_feedforward_constraints!(container, device_model, devices)\n    return\nend\n\n# Repeated method to avoid ambiguity between HVDCTwoTerminalUnbounded and HVDCTwoTerminalLossless\nfunction construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ArgumentConstructStage,\n    device_model::DeviceModel{T, HVDCTwoTerminalLossless},\n    network_model::NetworkModel{<:AbstractPTDFModel},\n) where {T <: PSY.TwoTerminalHVDC}\n    devices = get_available_components(device_model, sys)\n    add_variables!(container, FlowActivePowerVariable, devices, HVDCTwoTerminalLossless())\n    add_to_expression!(\n        container,\n        ActivePowerBalance,\n        FlowActivePowerVariable,\n        devices,\n        device_model,\n        network_model,\n    )\n    add_feedforward_arguments!(container, device_model, devices)\n    return\nend\n\n# Repeated method to avoid ambiguity between HVDCTwoTerminalUnbounded and HVDCTwoTerminalLossless\nfunction construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ModelConstructStage,\n    device_model::DeviceModel{T, HVDCTwoTerminalLossless},\n    network_model::NetworkModel{PTDFPowerModel},\n) where {\n    T <: PSY.TwoTerminalHVDC,\n}\n    devices = get_available_components(device_model, sys)\n    add_constraints!(container, FlowRateConstraint, devices, device_model, network_model)\n    add_constraint_dual!(container, sys, device_model)\n    add_feedforward_constraints!(container, device_model, devices)\n    return\nend\n\nfunction construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ArgumentConstructStage,\n    device_model::DeviceModel{T, HVDCTwoTerminalDispatch},\n    network_model::NetworkModel{<:AbstractPTDFModel},\n) where {T <: PSY.TwoTerminalHVDC}\n    devices = get_available_components(device_model, sys)\n    add_variables!(\n        container,\n        FlowActivePowerToFromVariable,\n        devices,\n        HVDCTwoTerminalDispatch(),\n    )\n    add_variables!(\n        container,\n        FlowActivePowerFromToVariable,\n        devices,\n        HVDCTwoTerminalDispatch(),\n    )\n    add_variables!(container, HVDCLosses, devices, HVDCTwoTerminalDispatch())\n    add_variables!(container, HVDCFlowDirectionVariable, devices, HVDCTwoTerminalDispatch())\n    add_to_expression!(\n        container,\n        ActivePowerBalance,\n        FlowActivePowerToFromVariable,\n        devices,\n        device_model,\n        network_model,\n    )\n    add_to_expression!(\n        container,\n        ActivePowerBalance,\n        FlowActivePowerFromToVariable,\n        devices,\n        device_model,\n        network_model,\n    )\n    add_to_expression!(\n        container,\n        ActivePowerBalance,\n        HVDCLosses,\n        devices,\n        device_model,\n        network_model,\n    )\n    add_feedforward_arguments!(container, device_model, devices)\n    return\nend\n\nfunction construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ModelConstructStage,\n    device_model::DeviceModel{T, HVDCTwoTerminalDispatch},\n    network_model::NetworkModel{<:AbstractPTDFModel},\n) where {T <: PSY.TwoTerminalHVDC}\n    devices = get_available_components(device_model, sys)\n    add_constraints!(\n        container,\n        FlowRateConstraintFromTo,\n        devices,\n        device_model,\n        network_model,\n    )\n    add_constraints!(\n        container,\n        FlowRateConstraintToFrom,\n        devices,\n        device_model,\n        network_model,\n    )\n    add_constraints!(container, HVDCPowerBalance, devices, device_model, network_model)\n    add_constraint_dual!(container, sys, device_model)\n    add_feedforward_constraints!(container, device_model, devices)\n    return\nend\n\nfunction construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ArgumentConstructStage,\n    device_model::DeviceModel{T, HVDCTwoTerminalDispatch},\n    network_model::NetworkModel{<:PM.AbstractActivePowerModel},\n) where {T <: PSY.TwoTerminalHVDC}\n    devices = get_available_components(device_model, sys)\n    add_variables!(\n        container,\n        FlowActivePowerToFromVariable,\n        devices,\n        HVDCTwoTerminalDispatch(),\n    )\n    add_variables!(\n        container,\n        FlowActivePowerFromToVariable,\n        devices,\n        HVDCTwoTerminalDispatch(),\n    )\n    add_variables!(container, HVDCFlowDirectionVariable, devices, HVDCTwoTerminalDispatch())\n    add_variables!(container, HVDCLosses, devices, HVDCTwoTerminalDispatch())\n    add_to_expression!(\n        container,\n        ActivePowerBalance,\n        FlowActivePowerToFromVariable,\n        devices,\n        device_model,\n        network_model,\n    )\n    add_to_expression!(\n        container,\n        ActivePowerBalance,\n        FlowActivePowerFromToVariable,\n        devices,\n        device_model,\n        network_model,\n    )\n    add_feedforward_arguments!(container, device_model, devices)\n    return\nend\n\nfunction construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ModelConstructStage,\n    device_model::DeviceModel{T, HVDCTwoTerminalDispatch},\n    network_model::NetworkModel{CopperPlatePowerModel},\n) where {T <: PSY.TwoTerminalHVDC}\n    devices = get_available_components(device_model, sys)\n    @warn \"CopperPlatePowerModel models with HVDC ignores inter-area losses\"\n    add_constraints!(\n        container,\n        FlowRateConstraintFromTo,\n        devices,\n        device_model,\n        network_model,\n    )\n    add_constraints!(\n        container,\n        FlowRateConstraintToFrom,\n        devices,\n        device_model,\n        network_model,\n    )\n    add_constraint_dual!(container, sys, device_model)\n    add_feedforward_constraints!(container, device_model, devices)\n    return\nend\n\nfunction construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ArgumentConstructStage,\n    device_model::DeviceModel{T, U},\n    network_model::NetworkModel{<:AbstractPTDFModel},\n) where {\n    T <: PSY.TwoTerminalHVDC,\n    U <: HVDCTwoTerminalPiecewiseLoss,\n}\n    devices = get_available_components(device_model, sys)\n    add_variables!(\n        container,\n        HVDCActivePowerReceivedFromVariable,\n        devices,\n        HVDCTwoTerminalPiecewiseLoss(),\n    )\n    add_variables!(\n        container,\n        HVDCActivePowerReceivedToVariable,\n        devices,\n        HVDCTwoTerminalPiecewiseLoss(),\n    )\n    _add_sparse_pwl_loss_variables!(container, devices, device_model)\n    add_to_expression!(\n        container,\n        ActivePowerBalance,\n        HVDCActivePowerReceivedFromVariable,\n        devices,\n        device_model,\n        network_model,\n    )\n    add_to_expression!(\n        container,\n        ActivePowerBalance,\n        HVDCActivePowerReceivedToVariable,\n        devices,\n        device_model,\n        network_model,\n    )\n    add_feedforward_arguments!(container, device_model, devices)\n    return\nend\n\nfunction construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ModelConstructStage,\n    device_model::DeviceModel{T, U},\n    network_model::NetworkModel{<:AbstractPTDFModel},\n) where {\n    T <: PSY.TwoTerminalHVDC,\n    U <: HVDCTwoTerminalPiecewiseLoss,\n}\n    devices = get_available_components(device_model, sys)\n    add_constraints!(\n        container,\n        FlowRateConstraintFromTo,\n        devices,\n        device_model,\n        network_model,\n    )\n    add_constraints!(\n        container,\n        FlowRateConstraintToFrom,\n        devices,\n        device_model,\n        network_model,\n    )\n    add_constraints!(\n        container,\n        HVDCFlowCalculationConstraint,\n        devices,\n        device_model,\n        network_model,\n    )\n    add_feedforward_constraints!(container, device_model, devices)\n    return\nend\n\n# TODO: Other models besides PTDF\n#=\nfunction construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ArgumentConstructStage,\n    model::DeviceModel{T, HVDCTwoTerminalPiecewiseLoss},\n    network_model::NetworkModel{<:PM.AbstractActivePowerModel},\n) where {T <: PSY.TwoTerminalHVDC}\n    devices =\n        get_available_components(model, sys)\n    add_variables!(\n        container,\n        FlowActivePowerToFromVariable,\n        devices,\n        HVDCTwoTerminalDispatch(),\n    )\n    add_variables!(\n        container,\n        FlowActivePowerFromToVariable,\n        devices,\n        HVDCTwoTerminalDispatch(),\n    )\n    add_variables!(container, HVDCFlowDirectionVariable, devices, HVDCTwoTerminalDispatch())\n    add_to_expression!(\n        container,\n        ActivePowerBalance,\n        FlowActivePowerToFromVariable,\n        devices,\n        model,\n        network_model,\n    )\n    add_to_expression!(\n        container,\n        ActivePowerBalance,\n        FlowActivePowerFromToVariable,\n        devices,\n        model,\n        network_model,\n    )\n    add_feedforward_arguments!(container, model, devices)\n    return\nend\n\nfunction construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ModelConstructStage,\n    model::DeviceModel{T, HVDCTwoTerminalPiecewiseLoss},\n    network_model::NetworkModel{CopperPlatePowerModel},\n) where {T <: PSY.TwoTerminalHVDC}\n    devices =\n        get_available_components(model, sys)\n    @warn \"CopperPlatePowerModel models with HVDC ignores inter-area losses\"\n    add_constraints!(container, FlowRateConstraintFromTo, devices, model, network_model)\n    add_constraints!(container, FlowRateConstraintToFrom, devices, model, network_model)\n    add_constraint_dual!(container, sys, model)\n    return\nend\n=#\n\nfunction construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ModelConstructStage,\n    device_model::DeviceModel{T, HVDCTwoTerminalDispatch},\n    network_model::NetworkModel{<:PM.AbstractActivePowerModel},\n) where {T <: PSY.TwoTerminalHVDC}\n    devices = get_available_components(device_model, sys)\n    add_constraints!(\n        container,\n        FlowRateConstraintFromTo,\n        devices,\n        device_model,\n        network_model,\n    )\n    add_constraints!(\n        container,\n        FlowRateConstraintToFrom,\n        devices,\n        device_model,\n        network_model,\n    )\n    add_constraints!(container, HVDCPowerBalance, devices, device_model, network_model)\n    add_constraint_dual!(container, sys, device_model)\n    add_feedforward_constraints!(container, device_model, devices)\n    return\nend\n\n############################# NEW LCC HVDC NON-LINEAR MODEL #############################\n\nfunction construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ArgumentConstructStage,\n    device_model::DeviceModel{T, HVDCTwoTerminalLCC},\n    network_model::NetworkModel{<:PM.ACPPowerModel},\n) where {T <: PSY.TwoTerminalLCCLine}\n    devices = get_available_components(device_model, sys)\n\n    # Variables\n    add_variables!(\n        container,\n        HVDCActivePowerReceivedFromVariable,\n        devices,\n        HVDCTwoTerminalLCC(),\n    )\n    add_variables!(\n        container,\n        HVDCActivePowerReceivedToVariable,\n        devices,\n        HVDCTwoTerminalLCC(),\n    )\n    add_variables!(\n        container,\n        HVDCReactivePowerReceivedFromVariable,\n        devices,\n        HVDCTwoTerminalLCC(),\n    )\n    add_variables!(\n        container,\n        HVDCReactivePowerReceivedToVariable,\n        devices,\n        HVDCTwoTerminalLCC(),\n    )\n    add_variables!(\n        container,\n        HVDCRectifierDelayAngleVariable,\n        devices,\n        HVDCTwoTerminalLCC(),\n    )\n    add_variables!(\n        container,\n        HVDCInverterExtinctionAngleVariable,\n        devices,\n        HVDCTwoTerminalLCC(),\n    )\n    add_variables!(\n        container,\n        HVDCRectifierPowerFactorAngleVariable,\n        devices,\n        HVDCTwoTerminalLCC(),\n    )\n    add_variables!(\n        container,\n        HVDCInverterPowerFactorAngleVariable,\n        devices,\n        HVDCTwoTerminalLCC(),\n    )\n    add_variables!(\n        container,\n        HVDCRectifierOverlapAngleVariable,\n        devices,\n        HVDCTwoTerminalLCC(),\n    )\n    add_variables!(\n        container,\n        HVDCInverterOverlapAngleVariable,\n        devices,\n        HVDCTwoTerminalLCC(),\n    )\n    add_variables!(\n        container,\n        HVDCRectifierDCVoltageVariable,\n        devices,\n        HVDCTwoTerminalLCC(),\n    )\n    add_variables!(\n        container,\n        HVDCInverterDCVoltageVariable,\n        devices,\n        HVDCTwoTerminalLCC(),\n    )\n    add_variables!(\n        container,\n        HVDCRectifierACCurrentVariable,\n        devices,\n        HVDCTwoTerminalLCC(),\n    )\n    add_variables!(\n        container,\n        HVDCInverterACCurrentVariable,\n        devices,\n        HVDCTwoTerminalLCC(),\n    )\n    add_variables!(\n        container,\n        DCLineCurrentFlowVariable,\n        devices,\n        HVDCTwoTerminalLCC(),\n    )\n    add_variables!(\n        container,\n        HVDCRectifierTapSettingVariable,\n        devices,\n        HVDCTwoTerminalLCC(),\n    )\n    add_variables!(\n        container,\n        HVDCInverterTapSettingVariable,\n        devices,\n        HVDCTwoTerminalLCC(),\n    )\n\n    # Expressions\n    add_to_expression!(\n        container,\n        ActivePowerBalance,\n        HVDCActivePowerReceivedFromVariable,\n        devices,\n        device_model,\n        network_model,\n    )\n    add_to_expression!(\n        container,\n        ActivePowerBalance,\n        HVDCActivePowerReceivedToVariable,\n        devices,\n        device_model,\n        network_model,\n    )\n    add_to_expression!(\n        container,\n        ReactivePowerBalance,\n        HVDCReactivePowerReceivedFromVariable,\n        devices,\n        device_model,\n        network_model,\n    )\n    add_to_expression!(\n        container,\n        ReactivePowerBalance,\n        HVDCReactivePowerReceivedToVariable,\n        devices,\n        device_model,\n        network_model,\n    )\n\n    add_feedforward_arguments!(container, device_model, devices)\n    return\nend\n\nfunction construct_device!(\n    ::OptimizationContainer,\n    ::PSY.System,\n    ::ArgumentConstructStage,\n    ::DeviceModel{T, HVDCTwoTerminalLCC},\n    ::NetworkModel{N},\n) where {T <: PSY.TwoTerminalLCCLine, N <: PM.AbstractPowerModel}\n    throw(\n        ArgumentError(\n            \"HVDCTwoTerminalLCC formulation requires ACPPowerModel network. \" *\n            \"Got $N. Use HVDCTwoTerminalLossless, HVDCTwoTerminalDispatch, or \" *\n            \"HVDCTwoTerminalPiecewiseLoss for DC/PTDF networks.\",\n        ),\n    )\nend\n\nfunction construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ModelConstructStage,\n    device_model::DeviceModel{T, HVDCTwoTerminalLCC},\n    network_model::NetworkModel{<:PM.ACPPowerModel},\n) where {T <: PSY.TwoTerminalLCCLine}\n    devices = get_available_components(device_model, sys)\n    add_constraints!(\n        container,\n        HVDCRectifierDCLineVoltageConstraint,\n        devices,\n        device_model,\n        network_model,\n    )\n    add_constraints!(\n        container,\n        HVDCInverterDCLineVoltageConstraint,\n        devices,\n        device_model,\n        network_model,\n    )\n    add_constraints!(\n        container,\n        HVDCRectifierOverlapAngleConstraint,\n        devices,\n        device_model,\n        network_model,\n    )\n    add_constraints!(\n        container,\n        HVDCInverterOverlapAngleConstraint,\n        devices,\n        device_model,\n        network_model,\n    )\n    add_constraints!(\n        container,\n        HVDCRectifierPowerFactorAngleConstraint,\n        devices,\n        device_model,\n        network_model,\n    )\n    add_constraints!(\n        container,\n        HVDCInverterPowerFactorAngleConstraint,\n        devices,\n        device_model,\n        network_model,\n    )\n    add_constraints!(\n        container,\n        HVDCRectifierACCurrentFlowConstraint,\n        devices,\n        device_model,\n        network_model,\n    )\n    add_constraints!(\n        container,\n        HVDCInverterACCurrentFlowConstraint,\n        devices,\n        device_model,\n        network_model,\n    )\n    add_constraints!(\n        container,\n        HVDCRectifierPowerCalculationConstraint,\n        devices,\n        device_model,\n        network_model,\n    )\n    add_constraints!(\n        container,\n        HVDCInverterPowerCalculationConstraint,\n        devices,\n        device_model,\n        network_model,\n    )\n    add_constraints!(\n        container,\n        HVDCTransmissionDCLineConstraint,\n        devices,\n        device_model,\n        network_model,\n    )\n    return\nend\n\n############################# Phase Shifter Transformer Models #############################\n\nfunction construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ArgumentConstructStage,\n    device_model::DeviceModel{PSY.PhaseShiftingTransformer, PhaseAngleControl},\n    network_model::NetworkModel{PM.DCPPowerModel},\n)\n    devices = get_available_components(device_model, sys)\n    add_variables!(container, FlowActivePowerVariable, devices, PhaseAngleControl())\n    add_variables!(container, PhaseShifterAngle, devices, PhaseAngleControl())\n    add_to_expression!(\n        container,\n        ActivePowerBalance,\n        FlowActivePowerVariable,\n        devices,\n        device_model,\n        network_model,\n    )\n    add_feedforward_arguments!(container, device_model, devices)\n    return\nend\n\nfunction construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ArgumentConstructStage,\n    device_model::DeviceModel{PSY.PhaseShiftingTransformer, PhaseAngleControl},\n    network_model::NetworkModel{<:AbstractPTDFModel},\n)\n    devices = get_available_components(device_model, sys)\n    add_variables!(container, FlowActivePowerVariable, devices, PhaseAngleControl())\n    add_variables!(container, PhaseShifterAngle, devices, PhaseAngleControl())\n    add_to_expression!(\n        container,\n        ActivePowerBalance,\n        PhaseShifterAngle,\n        devices,\n        device_model,\n        network_model,\n    )\n    add_feedforward_arguments!(container, device_model, devices)\n    return\nend\n\nfunction construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ModelConstructStage,\n    device_model::DeviceModel{PSY.PhaseShiftingTransformer, PhaseAngleControl},\n    network_model::NetworkModel{PM.DCPPowerModel},\n)\n    devices = get_available_components(device_model, sys)\n    add_constraints!(container, FlowLimitConstraint, devices, device_model, network_model)\n    add_constraints!(\n        container,\n        PhaseAngleControlLimit,\n        devices,\n        device_model,\n        network_model,\n    )\n    add_constraints!(container, NetworkFlowConstraint, devices, device_model, network_model)\n    add_constraint_dual!(container, sys, device_model)\n    add_feedforward_constraints!(container, device_model, devices)\n    return\nend\n\nfunction construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ModelConstructStage,\n    device_model::DeviceModel{PSY.PhaseShiftingTransformer, PhaseAngleControl},\n    network_model::NetworkModel{<:AbstractPTDFModel},\n)\n    devices = get_available_components(device_model, sys)\n    add_constraints!(container, FlowLimitConstraint, devices, device_model, network_model)\n    add_constraints!(\n        container,\n        PhaseAngleControlLimit,\n        devices,\n        device_model,\n        network_model,\n    )\n    add_constraints!(container, NetworkFlowConstraint, devices, device_model, network_model)\n    add_constraint_dual!(container, sys, device_model)\n    add_feedforward_constraints!(container, device_model, devices)\n    return\nend\n\n################################# AreaInterchange Models ################################\nfunction construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ArgumentConstructStage,\n    device_model::DeviceModel{PSY.AreaInterchange, U},\n    network_model::NetworkModel{CopperPlatePowerModel},\n) where {U <: Union{StaticBranchUnbounded, StaticBranch}}\n    devices = get_available_components(device_model, sys)\n    add_feedforward_arguments!(container, device_model, devices)\n    return\nend\n\nfunction construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ModelConstructStage,\n    device_model::DeviceModel{PSY.AreaInterchange, StaticBranchUnbounded},\n    network_model::NetworkModel{T},\n) where {T <: PM.AbstractActivePowerModel}\n    devices = get_available_components(device_model, sys)\n    add_feedforward_constraints!(container, device_model, devices)\n    return\nend\n\nfunction construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ArgumentConstructStage,\n    device_model::DeviceModel{PSY.AreaInterchange, T},\n    network_model::NetworkModel{U},\n) where {\n    T <: Union{StaticBranchUnbounded, StaticBranch},\n    U <: PM.AbstractActivePowerModel,\n}\n    devices = get_available_components(device_model, sys)\n    has_ts = PSY.has_time_series.(devices)\n    if get_use_slacks(device_model)\n        add_variables!(\n            container,\n            FlowActivePowerSlackUpperBound,\n            network_model,\n            devices,\n            T(),\n        )\n        add_variables!(\n            container,\n            FlowActivePowerSlackLowerBound,\n            network_model,\n            devices,\n            T(),\n        )\n    end\n    if any(has_ts) && !all(has_ts)\n        error(\n            \"Not all AreaInterchange devices have time series. Check data to complete (or remove) time series.\",\n        )\n    end\n    add_variables!(\n        container,\n        FlowActivePowerVariable,\n        network_model,\n        devices,\n        T(),\n    )\n    add_to_expression!(\n        container,\n        ActivePowerBalance,\n        FlowActivePowerVariable,\n        devices,\n        device_model,\n        network_model,\n    )\n    if all(has_ts)\n        for device in devices\n            name = PSY.get_name(device)\n            num_ts = length(unique(PSY.get_name.(PSY.get_time_series_keys(device))))\n            if num_ts < 2\n                error(\n                    \"AreaInterchange $name has less than two time series. It is required to add both from_to and to_from time series.\",\n                )\n            end\n        end\n        add_parameters!(container, FromToFlowLimitParameter, devices, device_model)\n        add_parameters!(container, ToFromFlowLimitParameter, devices, device_model)\n    end\n    add_feedforward_arguments!(container, device_model, devices)\n    return\nend\n\nfunction construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ModelConstructStage,\n    device_model::DeviceModel{PSY.AreaInterchange, StaticBranch},\n    network_model::NetworkModel{T},\n) where {T <: PM.AbstractActivePowerModel}\n    devices = get_available_components(device_model, sys)\n    add_constraints!(container, FlowLimitConstraint, devices, device_model, network_model)\n    add_feedforward_constraints!(container, device_model, devices)\n    return\nend\n\nfunction _get_branch_map(network_model::NetworkModel)\n    @assert !isempty(network_model.modeled_ac_branch_types)\n    net_reduction_data = get_network_reduction(network_model)\n    all_branch_maps_by_type = net_reduction_data.all_branch_maps_by_type\n    inter_area_branch_map =\n    # This method uses ACBranch to support HVDC\n        Dict{Tuple{String, String}, Dict{DataType, Vector{String}}}()\n    name_to_arc_maps = PNM.get_name_to_arc_maps(net_reduction_data)\n    for br_type in network_model.modeled_ac_branch_types\n        !haskey(name_to_arc_maps, br_type) && continue\n        name_to_arc_map = PNM.get_name_to_arc_map(net_reduction_data, br_type)\n        for (name, (arc, reduction)) in name_to_arc_map\n            reduction_entry = all_branch_maps_by_type[reduction][br_type][arc]\n            area_from, area_to = _get_area_from_to(reduction_entry)\n            if area_from != area_to\n                branch_typed_dict = get!(\n                    inter_area_branch_map,\n                    (PSY.get_name(area_from), PSY.get_name(area_to)),\n                    Dict{DataType, Vector{String}}(),\n                )\n                _add_to_branch_map!(branch_typed_dict, reduction_entry, name)\n            end\n        end\n    end\n    return inter_area_branch_map\nend\n\nfunction _add_to_branch_map!(\n    branch_typed_dict::Dict{DataType, Vector{String}},\n    ::T,\n    name::String,\n) where {T <: PSY.ACBranch}\n    if !haskey(branch_typed_dict, T)\n        branch_typed_dict[T] = [name]\n    else\n        push!(branch_typed_dict[T], name)\n    end\nend\n\nfunction _add_to_branch_map!(\n    branch_typed_dict::Dict{DataType, Vector{String}},\n    reduction_entry::Union{PNM.BranchesParallel, PNM.BranchesSeries},\n    name::String,\n)\n    _add_to_branch_map!(branch_typed_dict, first(reduction_entry), name)\nend\n\n# This method uses ACBranch to support 2T - HVDC\nfunction _get_area_from_to(reduction_entry::PSY.ACBranch)\n    area_from = PSY.get_area(PSY.get_arc(reduction_entry).from)\n    area_to = PSY.get_area(PSY.get_arc(reduction_entry).to)\n    return area_from, area_to\nend\n\nfunction _get_area_from_to(reduction_entry::PNM.ThreeWindingTransformerWinding)\n    tfw = PNM.get_transformer(reduction_entry)\n    winding_int = PNM.get_winding_number(reduction_entry)\n    if winding_int == 1\n        area_from = PSY.get_area(PSY.get_primary_star_arc(tfw).from)\n        area_to = PSY.get_area(PSY.get_primary_star_arc(tfw).to)\n    elseif winding_int == 2\n        area_from = PSY.get_area(PSY.get_secondary_star_arc(tfw).from)\n        area_to = PSY.get_area(PSY.get_secondary_star_arc(tfw).to_index)\n    elseif winding_int == 3\n        area_from = PSY.get_area(PSY.get_tertiary_star_arc(tfw).from)\n        area_to = PSY.get_area(PSY.get_tertiary_star_arc(tfw).to)\n    else\n        @assert false \"Winding number $winding_int is not valid for three-winding transformer\"\n    end\n    return area_from, area_to\nend\n\nfunction _get_area_from_to(reduction_entry::PNM.BranchesParallel)\n    return _get_area_from_to(first(reduction_entry))\nend\n\nfunction _get_area_from_to(reduction_entry::PNM.BranchesSeries)\n    area_froms = [_get_area_from_to(x)[1] for x in reduction_entry]\n    area_tos = [_get_area_from_to(x)[2] for x in reduction_entry]\n    all_areas = vcat(area_froms, area_tos)\n    if length(unique(all_areas)) > 1\n        error(\n            \"Inter-area line found as part of a degree two chain reduction; this feature is not supported\",\n        )\n    end\n    return first(all_areas), first(all_areas)\nend\n\nfunction construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ModelConstructStage,\n    device_model::DeviceModel{PSY.AreaInterchange, StaticBranch},\n    network_model::NetworkModel{T},\n) where {T <: PSI.AbstractPTDFModel}\n    devices = get_available_components(device_model, sys)\n    add_constraints!(container, FlowLimitConstraint, devices, device_model, network_model)\n    # Not ideal to do this here, but it is a not terrible workaround\n    # The area interchanges are like a services/device mix.\n    # Doesn't include the possibility of Multi-terminal HVDC\n    inter_area_branch_map = _get_branch_map(network_model)\n\n    add_constraints!(\n        container,\n        LineFlowBoundConstraint,\n        devices,\n        device_model,\n        network_model,\n        inter_area_branch_map,\n    )\n    add_feedforward_constraints!(container, device_model, devices)\n    return\nend\n\nfunction construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ModelConstructStage,\n    device_model::DeviceModel{PSY.AreaInterchange, StaticBranchUnbounded},\n    network_model::NetworkModel{AreaBalancePowerModel},\n)\n    devices = get_available_components(device_model, sys)\n    add_feedforward_constraints!(container, device_model, devices)\n    return\nend\n\n#TODO Check if for SCUC AreaPTDF needs something else\nfunction construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ModelConstructStage,\n    device_model::DeviceModel{PSY.AreaInterchange, StaticBranchUnbounded},\n    network_model::NetworkModel{AreaPTDFPowerModel},\n)\n    devices = get_available_components(device_model, sys)\n    inter_area_branch_map = _get_branch_map(network_model)\n    # Not ideal to do this here, but it is a not terrible workaround\n    # The area interchanges are like a services/device mix.\n    # Doesn't include the possibility of Multi-terminal HVDC\n    add_constraints!(\n        container,\n        LineFlowBoundConstraint,\n        devices,\n        device_model,\n        network_model,\n        inter_area_branch_map,\n    )\n    add_feedforward_constraints!(container, device_model, devices)\n    return\nend\n"
  },
  {
    "path": "src/devices_models/device_constructors/hvdcsystems_constructor.jl",
    "content": "function construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ArgumentConstructStage,\n    model::DeviceModel{PSY.InterconnectingConverter, LosslessConverter},\n    network_model::NetworkModel{<:PM.AbstractActivePowerModel},\n)\n    devices = get_available_components(\n        model,\n        sys,\n    )\n    add_variables!(container, ActivePowerVariable, devices, LosslessConverter())\n    add_to_expression!(\n        container,\n        ActivePowerBalance,\n        ActivePowerVariable,\n        devices,\n        model,\n        network_model,\n    )\n    add_feedforward_arguments!(container, model, devices)\n    return\nend\n\nfunction construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ModelConstructStage,\n    model::DeviceModel{PSY.InterconnectingConverter, LosslessConverter},\n    network_model::NetworkModel{<:PM.AbstractActivePowerModel},\n)\n    devices = get_available_components(\n        model,\n        sys,\n    )\n    add_feedforward_constraints!(container, model, devices)\n    objective_function!(container, devices, model, get_network_formulation(network_model))\n    add_constraint_dual!(container, sys, model)\n    return\nend\n\nfunction construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ArgumentConstructStage,\n    model::DeviceModel{PSY.InterconnectingConverter, QuadraticLossConverter},\n    network_model::NetworkModel{<:PM.AbstractActivePowerModel},\n)\n    devices = get_available_components(\n        model,\n        sys,\n    )\n    #####################\n    ##### Variables #####\n    #####################\n\n    # Add Power Variable\n    add_variables!(container, ActivePowerVariable, devices, QuadraticLossConverter()) # p_c^{ac}\n    add_variables!(container, ConverterDCPower, devices, QuadraticLossConverter()) # p_c\n    # Add Current Variables: i, i+, i-\n    add_variables!(container, ConverterCurrent, devices, QuadraticLossConverter()) # i\n    add_variables!(container, SquaredConverterCurrent, devices, QuadraticLossConverter()) # i^sq\n    use_linear_loss = PSI.get_attribute(model, \"use_linear_loss\")\n    if use_linear_loss\n        add_variables!(\n            container,\n            ConverterPositiveCurrent,\n            devices,\n            QuadraticLossConverter(),\n        ) # i^+\n        add_variables!(\n            container,\n            ConverterNegativeCurrent,\n            devices,\n            QuadraticLossConverter(),\n        ) # i^-\n        add_variables!(\n            container,\n            ConverterCurrentDirection,\n            devices,\n            QuadraticLossConverter(),\n        ) # ν\n    end\n    # Add Voltage Variables: v^sq\n    add_variables!(container, SquaredDCVoltage, devices, QuadraticLossConverter())\n    # Add Bilinear Variables: γ, γ^{sq}\n    add_variables!(\n        container,\n        AuxBilinearConverterVariable,\n        devices,\n        QuadraticLossConverter(),\n    ) # γ\n    add_variables!(\n        container,\n        AuxBilinearSquaredConverterVariable,\n        devices,\n        QuadraticLossConverter(),\n    ) # γ^{sq}\n\n    #### Add Interpolation Variables ####\n\n    v_segments = PSI.get_attribute(model, \"voltage_segments\")\n    i_segments = PSI.get_attribute(model, \"current_segments\")\n    γ_segments = PSI.get_attribute(model, \"bilinear_segments\")\n\n    vars_vector = [\n        # Voltage v #\n        (InterpolationSquaredVoltageVariable, v_segments), # δ^v\n        (InterpolationBinarySquaredVoltageVariable, v_segments), # z^v\n        # Current i #\n        (InterpolationSquaredCurrentVariable, i_segments), # δ^i\n        (InterpolationBinarySquaredCurrentVariable, i_segments), # z^i\n        # Bilinear γ #\n        (InterpolationSquaredBilinearVariable, γ_segments), # δ^γ\n        (InterpolationBinarySquaredBilinearVariable, γ_segments), # z^γ\n    ]\n\n    for (T, len_segments) in vars_vector\n        add_sparse_pwl_interpolation_variables!(\n            container,\n            T(),\n            devices,\n            model,\n            len_segments,\n        )\n    end\n\n    #####################\n    #### Expressions ####\n    #####################\n\n    add_to_expression!(\n        container,\n        ActivePowerBalance,\n        ActivePowerVariable,\n        devices,\n        model,\n        network_model,\n    )\n    add_to_expression!(\n        container,\n        DCCurrentBalance,\n        ConverterCurrent,\n        devices,\n        model,\n        network_model,\n    )\n    add_feedforward_arguments!(container, model, devices)\n    return\nend\n\nfunction construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ModelConstructStage,\n    model::DeviceModel{PSY.InterconnectingConverter, QuadraticLossConverter},\n    network_model::NetworkModel{<:PM.AbstractActivePowerModel},\n)\n    devices = get_available_components(\n        model,\n        sys,\n    )\n\n    add_constraints!(\n        container,\n        ConverterPowerCalculationConstraint,\n        devices,\n        model,\n        network_model,\n    )\n    add_constraints!(\n        container,\n        ConverterMcCormickEnvelopes,\n        devices,\n        model,\n        network_model,\n    )\n    add_constraints!(\n        container,\n        ConverterLossConstraint,\n        devices,\n        model,\n        network_model,\n    )\n    use_linear_loss = PSI.get_attribute(model, \"use_linear_loss\")\n    if use_linear_loss\n        add_constraints!(\n            container,\n            CurrentAbsoluteValueConstraint,\n            devices,\n            model,\n            network_model,\n        )\n    end\n    add_constraints!(\n        container,\n        InterpolationVoltageConstraints,\n        devices,\n        model,\n        network_model,\n    )\n    add_constraints!(\n        container,\n        InterpolationCurrentConstraints,\n        devices,\n        model,\n        network_model,\n    )\n    add_constraints!(\n        container,\n        InterpolationBilinearConstraints,\n        devices,\n        model,\n        network_model,\n    )\n\n    add_feedforward_constraints!(container, model, devices)\n    objective_function!(container, devices, model, get_network_formulation(network_model))\n    #add_constraint_dual!(container, sys, model)\n    return\nend\n\nfunction construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ArgumentConstructStage,\n    model::DeviceModel{PSY.TModelHVDCLine, LosslessLine},\n    network_model::NetworkModel{<:PM.AbstractActivePowerModel},\n)\n    devices = get_available_components(\n        model,\n        sys,\n    )\n    add_variables!(container, FlowActivePowerVariable, devices, LosslessLine())\n    add_to_expression!(\n        container,\n        ActivePowerBalance,\n        FlowActivePowerVariable,\n        devices,\n        model,\n        network_model,\n    )\n    add_feedforward_arguments!(container, model, devices)\n    return\nend\n\nfunction construct_device!(\n    ::OptimizationContainer,\n    sys::PSY.System,\n    ::ModelConstructStage,\n    model::DeviceModel{PSY.TModelHVDCLine, LosslessLine},\n    ::NetworkModel{<:PM.AbstractActivePowerModel},\n)\nend\n\nfunction construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ArgumentConstructStage,\n    model::DeviceModel{PSY.TModelHVDCLine, DCLossyLine},\n    network_model::NetworkModel{<:PM.AbstractActivePowerModel},\n)\n    devices = get_available_components(\n        model,\n        sys,\n    )\n\n    add_variables!(container, DCLineCurrent, devices, DCLossyLine())\n    add_to_expression!(\n        container,\n        DCCurrentBalance,\n        DCLineCurrent,\n        devices,\n        model,\n        network_model,\n    )\n    add_feedforward_arguments!(container, model, devices)\n    return\nend\n\nfunction construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ModelConstructStage,\n    model::DeviceModel{PSY.TModelHVDCLine, DCLossyLine},\n    network_model::NetworkModel{<:PM.AbstractActivePowerModel},\n)\n    devices = get_available_components(\n        model,\n        sys,\n    )\n    add_constraints!(\n        container,\n        DCLineCurrentConstraint,\n        devices,\n        model,\n        network_model,\n    )\nend\n"
  },
  {
    "path": "src/devices_models/device_constructors/load_constructor.jl",
    "content": "# AbstractPowerModel + ControllableLoad device model\nfunction construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ArgumentConstructStage,\n    model::DeviceModel{L, D},\n    network_model::NetworkModel{<:PM.AbstractPowerModel},\n) where {\n    L <: PSY.ControllableLoad,\n    D <: AbstractControllablePowerLoadFormulation,\n}\n    devices =\n        get_available_components(model,\n            sys,\n        )\n\n    add_variables!(container, ActivePowerVariable, devices, D())\n    add_variables!(container, ReactivePowerVariable, devices, D())\n\n    process_market_bid_parameters!(container, devices, model, false, true)\n\n    # Add Variables to expressions\n    add_to_expression!(\n        container,\n        ActivePowerBalance,\n        ActivePowerVariable,\n        devices,\n        model,\n        network_model,\n    )\n\n    add_to_expression!(\n        container,\n        ReactivePowerBalance,\n        ReactivePowerVariable,\n        devices,\n        model,\n        network_model,\n    )\n\n    if haskey(get_time_series_names(model), ActivePowerTimeSeriesParameter)\n        add_parameters!(container, ActivePowerTimeSeriesParameter, devices, model)\n    end\n\n    add_cost_expressions!(container, devices, model)\n    add_event_arguments!(container, devices, model, network_model)\n    return\nend\n\nfunction construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ModelConstructStage,\n    model::DeviceModel{L, <:AbstractControllablePowerLoadFormulation},\n    network_model::NetworkModel{<:PM.AbstractPowerModel},\n) where {L <: PSY.ControllableLoad}\n    devices =\n        get_available_components(model,\n            sys,\n        )\n\n    add_constraints!(\n        container,\n        ActivePowerVariableLimitsConstraint,\n        ActivePowerVariable,\n        devices,\n        model,\n        network_model,\n    )\n    add_constraints!(\n        container,\n        ReactivePowerVariableLimitsConstraint,\n        ReactivePowerVariable,\n        devices,\n        model,\n        network_model,\n    )\n    add_feedforward_constraints!(container, model, devices)\n\n    objective_function!(container, devices, model, get_network_formulation(network_model))\n    add_event_constraints!(container, devices, model, network_model)\n    add_constraint_dual!(container, sys, model)\n    return\nend\n\n# AbstractActivePowerModel + ControllableLoad device model\nfunction construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ArgumentConstructStage,\n    model::DeviceModel{L, D},\n    network_model::NetworkModel{<:PM.AbstractActivePowerModel},\n) where {\n    L <: PSY.ControllableLoad,\n    D <: AbstractControllablePowerLoadFormulation,\n}\n    devices =\n        get_available_components(model,\n            sys,\n        )\n\n    add_variables!(container, ActivePowerVariable, devices, D())\n\n    # Add Variables to expressions\n    add_to_expression!(\n        container,\n        ActivePowerBalance,\n        ActivePowerVariable,\n        devices,\n        model,\n        network_model,\n    )\n\n    if haskey(get_time_series_names(model), ActivePowerTimeSeriesParameter)\n        add_parameters!(container, ActivePowerTimeSeriesParameter, devices, model)\n    end\n\n    process_market_bid_parameters!(container, devices, model, false, true)\n\n    add_cost_expressions!(container, devices, model)\n    add_event_arguments!(container, devices, model, network_model)\n    return\nend\n\nfunction construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ModelConstructStage,\n    model::DeviceModel{L, <:AbstractControllablePowerLoadFormulation},\n    network_model::NetworkModel{<:PM.AbstractActivePowerModel},\n) where {L <: PSY.ControllableLoad}\n    devices =\n        get_available_components(model,\n            sys,\n        )\n\n    add_constraints!(\n        container,\n        ActivePowerVariableLimitsConstraint,\n        ActivePowerVariable,\n        devices,\n        model,\n        network_model,\n    )\n    add_feedforward_constraints!(container, model, devices)\n\n    objective_function!(container, devices, model, get_network_formulation(network_model))\n    add_event_constraints!(container, devices, model, network_model)\n    add_constraint_dual!(container, sys, model)\n    return\nend\n\n# AbstractPowerModel + PowerLoadInterruption device model\nfunction construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ArgumentConstructStage,\n    model::DeviceModel{L, PowerLoadInterruption},\n    network_model::NetworkModel{<:PM.AbstractPowerModel},\n) where {L <: PSY.ControllableLoad}\n    devices =\n        get_available_components(model,\n            sys,\n        )\n\n    add_variables!(container, ActivePowerVariable, devices, PowerLoadInterruption())\n    add_variables!(container, ReactivePowerVariable, devices, PowerLoadInterruption())\n    add_variables!(container, OnVariable, devices, PowerLoadInterruption())\n\n    # Add Variables to expressions\n    add_to_expression!(\n        container,\n        ActivePowerBalance,\n        ActivePowerVariable,\n        devices,\n        model,\n        network_model,\n    )\n\n    add_to_expression!(\n        container,\n        ReactivePowerBalance,\n        ReactivePowerVariable,\n        devices,\n        model,\n        network_model,\n    )\n\n    if haskey(get_time_series_names(model), ActivePowerTimeSeriesParameter)\n        add_parameters!(container, ActivePowerTimeSeriesParameter, devices, model)\n    end\n\n    process_market_bid_parameters!(container, devices, model, false, true)\n\n    add_cost_expressions!(container, devices, model)\n    add_event_arguments!(container, devices, model, network_model)\n    return\nend\n\nfunction construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ModelConstructStage,\n    model::DeviceModel{L, PowerLoadInterruption},\n    network_model::NetworkModel{<:PM.AbstractPowerModel},\n) where {L <: PSY.ControllableLoad}\n    devices =\n        get_available_components(model,\n            sys,\n        )\n\n    add_constraints!(\n        container,\n        ActivePowerVariableLimitsConstraint,\n        ActivePowerVariable,\n        devices,\n        model,\n        network_model,\n    )\n    add_constraints!(\n        container,\n        ActivePowerVariableLimitsConstraint,\n        OnVariable,\n        devices,\n        model,\n        network_model,\n    )\n    add_constraints!(\n        container,\n        ReactivePowerVariableLimitsConstraint,\n        ReactivePowerVariable,\n        devices,\n        model,\n        network_model,\n    )\n    add_feedforward_constraints!(container, model, devices)\n\n    objective_function!(container, devices, model, get_network_formulation(network_model))\n    add_event_constraints!(container, devices, model, network_model)\n    add_constraint_dual!(container, sys, model)\n    return\nend\n\n# AbstractActivePowerModel + PowerLoadInterruption device model\nfunction construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ArgumentConstructStage,\n    model::DeviceModel{L, PowerLoadInterruption},\n    network_model::NetworkModel{<:PM.AbstractActivePowerModel},\n) where {L <: PSY.ControllableLoad}\n    devices =\n        get_available_components(model,\n            sys,\n        )\n\n    add_variables!(container, ActivePowerVariable, devices, PowerLoadInterruption())\n    add_variables!(container, OnVariable, devices, PowerLoadInterruption())\n\n    process_market_bid_parameters!(container, devices, model, false, true)\n    # Add Variables to expressions\n    add_to_expression!(\n        container,\n        ActivePowerBalance,\n        ActivePowerVariable,\n        devices,\n        model,\n        network_model,\n    )\n\n    if haskey(get_time_series_names(model), ActivePowerTimeSeriesParameter)\n        add_parameters!(container, ActivePowerTimeSeriesParameter, devices, model)\n    end\n\n    add_cost_expressions!(container, devices, model)\n    add_event_arguments!(container, devices, model, network_model)\n    return\nend\n\nfunction construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ModelConstructStage,\n    model::DeviceModel{L, PowerLoadInterruption},\n    network_model::NetworkModel{<:PM.AbstractActivePowerModel},\n) where {L <: PSY.ControllableLoad}\n    devices =\n        get_available_components(model,\n            sys,\n        )\n\n    add_constraints!(\n        container,\n        ActivePowerVariableLimitsConstraint,\n        ActivePowerVariable,\n        devices,\n        model,\n        network_model,\n    )\n    add_constraints!(\n        container,\n        ActivePowerVariableLimitsConstraint,\n        OnVariable,\n        devices,\n        model,\n        network_model,\n    )\n    add_feedforward_constraints!(container, model, devices)\n\n    objective_function!(container, devices, model, get_network_formulation(network_model))\n    add_event_constraints!(container, devices, model, network_model)\n    add_constraint_dual!(container, sys, model)\n    return\nend\n\n# AbstractPowerModel + StaticPowerLoad device model\nfunction construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ArgumentConstructStage,\n    model::DeviceModel{L, StaticPowerLoad},\n    network_model::NetworkModel{<:PM.AbstractPowerModel},\n) where {L <: PSY.ElectricLoad}\n    devices =\n        get_available_components(model,\n            sys,\n        )\n\n    if haskey(get_time_series_names(model), ActivePowerTimeSeriesParameter)\n        add_parameters!(container, ActivePowerTimeSeriesParameter, devices, model)\n    end\n    if haskey(get_time_series_names(model), ReactivePowerTimeSeriesParameter)\n        add_parameters!(container, ReactivePowerTimeSeriesParameter, devices, model)\n    end\n\n    add_to_expression!(\n        container,\n        ActivePowerBalance,\n        ActivePowerTimeSeriesParameter,\n        devices,\n        model,\n        network_model,\n    )\n    add_to_expression!(\n        container,\n        ReactivePowerBalance,\n        ReactivePowerTimeSeriesParameter,\n        devices,\n        model,\n        network_model,\n    )\n\n    add_event_arguments!(container, devices, model, network_model)\n    return\nend\n\n# AbstractActivePowerModel + StaticPowerLoad device model\nfunction construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ArgumentConstructStage,\n    model::DeviceModel{L, StaticPowerLoad},\n    network_model::NetworkModel{<:PM.AbstractActivePowerModel},\n) where {L <: PSY.ElectricLoad}\n    devices =\n        get_available_components(model,\n            sys,\n        )\n\n    if haskey(get_time_series_names(model), ActivePowerTimeSeriesParameter)\n        add_parameters!(container, ActivePowerTimeSeriesParameter, devices, model)\n    end\n\n    add_to_expression!(\n        container,\n        ActivePowerBalance,\n        ActivePowerTimeSeriesParameter,\n        devices,\n        model,\n        network_model,\n    )\n\n    add_event_arguments!(container, devices, model, network_model)\n    return\nend\n\nfunction construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ModelConstructStage,\n    model::DeviceModel{<:PSY.ElectricLoad, StaticPowerLoad},\n    network_model::NetworkModel{<:PM.AbstractPowerModel},\n)\n    # Static PowerLoad doesn't add any constraints to the model. This function covers\n    # AbstractPowerModel and AbtractActivePowerModel\n    return\nend\n\n# AbstractPowerModel + StaticLoad, but with non-StaticPowerLoad device models\nfunction construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ArgumentConstructStage,\n    model::DeviceModel{L, <:AbstractControllablePowerLoadFormulation},\n    network_model::NetworkModel{<:PM.AbstractPowerModel},\n) where {L <: PSY.StaticLoad}\n    devices =\n        get_available_components(model,\n            sys,\n        )\n\n    if haskey(get_time_series_names(model), ActivePowerTimeSeriesParameter)\n        add_parameters!(container, ActivePowerTimeSeriesParameter, devices, model)\n    end\n    if haskey(get_time_series_names(model), ReactivePowerTimeSeriesParameter)\n        add_parameters!(container, ReactivePowerTimeSeriesParameter, devices, model)\n    end\n\n    process_market_bid_parameters!(container, devices, model, false, true)\n    add_to_expression!(\n        container,\n        ActivePowerBalance,\n        ActivePowerTimeSeriesParameter,\n        devices,\n        model,\n        network_model,\n    )\n    add_to_expression!(\n        container,\n        ReactivePowerBalance,\n        ReactivePowerTimeSeriesParameter,\n        devices,\n        model,\n        network_model,\n    )\n    add_event_arguments!(container, devices, model, network_model)\n    return\nend\n\n# AbstractActivePowerModel + StaticLoad, but with non-StaticPowerLoad device models\nfunction construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ArgumentConstructStage,\n    model::DeviceModel{L, <:AbstractControllablePowerLoadFormulation},\n    network_model::NetworkModel{<:PM.AbstractActivePowerModel},\n) where {L <: PSY.StaticLoad}\n    devices =\n        get_available_components(model,\n            sys,\n        )\n\n    if haskey(get_time_series_names(model), ActivePowerTimeSeriesParameter)\n        add_parameters!(container, ActivePowerTimeSeriesParameter, devices, model)\n    end\n    add_to_expression!(\n        container,\n        ActivePowerBalance,\n        ActivePowerTimeSeriesParameter,\n        devices,\n        model,\n        network_model,\n    )\n\n    process_market_bid_parameters!(container, devices, model, false, true)\n    add_event_arguments!(container, devices, model, network_model)\n    return\nend\n\nfunction construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ccs::ModelConstructStage,\n    model::DeviceModel{L, D},\n    network_model::NetworkModel{<:PM.AbstractPowerModel},\n) where {\n    L <: PSY.StaticLoad,\n    D <: AbstractControllablePowerLoadFormulation,\n}\n    if D != StaticPowerLoad\n        @warn(\n            \"The Formulation $(D) only applies to FormulationControllable Loads, \\n Consider Changing the Device Formulation to StaticPowerLoad\"\n        )\n    end\n\n    # Makes a new model with the correct formulation of the type. Needs to recover all the other fields\n    # slacks, services and duals are not applicable to StaticPowerLoad so those are ignored\n    new_model = DeviceModel(\n        L,\n        StaticPowerLoad;\n        feedforwards = model.feedforwards,\n        time_series_names = model.time_series_names,\n        attributes = model.attributes,\n    )\n    construct_device!(container, sys, ccs, new_model, network_model)\n    return\nend\n\nfunction construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ArgumentConstructStage,\n    model::DeviceModel{PSY.ShiftablePowerLoad, PowerLoadShift},\n    network_model::NetworkModel{<:PM.AbstractPowerModel},\n)\n    devices = get_available_components(model, sys)\n\n    add_variables!(container, ShiftUpActivePowerVariable, devices, PowerLoadShift())\n    add_variables!(container, ShiftDownActivePowerVariable, devices, PowerLoadShift())\n    add_variables!(container, ReactivePowerVariable, devices, PowerLoadShift())\n\n    process_market_bid_parameters!(container, devices, model, false, true)\n\n    if haskey(get_time_series_names(model), ActivePowerTimeSeriesParameter)\n        add_parameters!(container, ActivePowerTimeSeriesParameter, devices, model)\n    end\n    if haskey(get_time_series_names(model), ShiftUpActivePowerTimeSeriesParameter)\n        add_parameters!(container, ShiftUpActivePowerTimeSeriesParameter, devices, model)\n    end\n    if haskey(get_time_series_names(model), ShiftDownActivePowerTimeSeriesParameter)\n        add_parameters!(container, ShiftDownActivePowerTimeSeriesParameter, devices, model)\n    end\n\n    # Add realized load expression\n    add_expressions!(container, RealizedShiftedLoad, devices, model)\n\n    # Add Parameters to expressions\n    add_to_expression!(\n        container,\n        ActivePowerBalance,\n        RealizedShiftedLoad,\n        devices,\n        model,\n        network_model,\n    )\n    add_to_expression!(\n        container,\n        ReactivePowerBalance,\n        ReactivePowerVariable,\n        devices,\n        model,\n        network_model,\n    )\n\n    add_expressions!(container, ProductionCostExpression, devices, model)\n    add_event_arguments!(container, devices, model, network_model)\n    return\nend\n\nfunction construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ModelConstructStage,\n    model::DeviceModel{PSY.ShiftablePowerLoad, PowerLoadShift},\n    network_model::NetworkModel{<:PM.AbstractPowerModel},\n)\n    devices =\n        get_available_components(model,\n            sys,\n        )\n\n    add_constraints!(\n        container,\n        ShiftedActivePowerBalanceConstraint,\n        devices,\n        model,\n        network_model,\n    )\n    add_constraints!(\n        container,\n        RealizedShiftedLoadMinimumBoundConstraint,\n        RealizedShiftedLoad,\n        devices,\n        model,\n        network_model,\n    )\n    add_constraints!(\n        container,\n        ShiftUpActivePowerVariableLimitsConstraint,\n        ShiftUpActivePowerVariable,\n        devices,\n        model,\n        network_model,\n    )\n    add_constraints!(\n        container,\n        ShiftDownActivePowerVariableLimitsConstraint,\n        ShiftDownActivePowerVariable,\n        devices,\n        model,\n        network_model,\n    )\n    add_constraints!(\n        container,\n        ReactivePowerVariableLimitsConstraint,\n        ReactivePowerVariable,\n        devices,\n        model,\n        network_model,\n    )\n    add_constraints!(\n        container,\n        NonAnticipativityConstraint,\n        devices,\n        model,\n        network_model,\n    )\n\n    add_feedforward_constraints!(container, model, devices)\n\n    objective_function!(container, devices, model, get_network_formulation(network_model))\n    add_event_constraints!(container, devices, model, network_model)\n    add_constraint_dual!(container, sys, model)\n    return\nend\n\n# AbstractActivePowerModel + PowerLoadShift device model\nfunction construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ArgumentConstructStage,\n    model::DeviceModel{PSY.ShiftablePowerLoad, PowerLoadShift},\n    network_model::NetworkModel{<:PM.AbstractActivePowerModel},\n)\n    devices = get_available_components(model, sys)\n\n    add_variables!(container, ShiftUpActivePowerVariable, devices, PowerLoadShift())\n    add_variables!(container, ShiftDownActivePowerVariable, devices, PowerLoadShift())\n\n    process_market_bid_parameters!(container, devices, model, false, true)\n\n    if haskey(get_time_series_names(model), ActivePowerTimeSeriesParameter)\n        add_parameters!(container, ActivePowerTimeSeriesParameter, devices, model)\n    end\n    if haskey(get_time_series_names(model), ShiftUpActivePowerTimeSeriesParameter)\n        add_parameters!(container, ShiftUpActivePowerTimeSeriesParameter, devices, model)\n    end\n    if haskey(get_time_series_names(model), ShiftDownActivePowerTimeSeriesParameter)\n        add_parameters!(container, ShiftDownActivePowerTimeSeriesParameter, devices, model)\n    end\n\n    # Add realized load expression\n    add_expressions!(container, RealizedShiftedLoad, devices, model)\n\n    # Add Parameters to expressions\n    add_to_expression!(\n        container,\n        ActivePowerBalance,\n        RealizedShiftedLoad,\n        devices,\n        model,\n        network_model,\n    )\n\n    add_expressions!(container, ProductionCostExpression, devices, model)\n    add_event_arguments!(container, devices, model, network_model)\n    return\nend\n\nfunction construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ModelConstructStage,\n    model::DeviceModel{PSY.ShiftablePowerLoad, PowerLoadShift},\n    network_model::NetworkModel{<:PM.AbstractActivePowerModel},\n)\n    devices =\n        get_available_components(model,\n            sys,\n        )\n\n    add_constraints!(\n        container,\n        ShiftedActivePowerBalanceConstraint,\n        devices,\n        model,\n        network_model,\n    )\n    add_constraints!(\n        container,\n        RealizedShiftedLoadMinimumBoundConstraint,\n        RealizedShiftedLoad,\n        devices,\n        model,\n        network_model,\n    )\n    add_constraints!(\n        container,\n        ShiftUpActivePowerVariableLimitsConstraint,\n        ShiftUpActivePowerVariable,\n        devices,\n        model,\n        network_model,\n    )\n    add_constraints!(\n        container,\n        ShiftDownActivePowerVariableLimitsConstraint,\n        ShiftDownActivePowerVariable,\n        devices,\n        model,\n        network_model,\n    )\n    add_constraints!(\n        container,\n        NonAnticipativityConstraint,\n        devices,\n        model,\n        network_model,\n    )\n\n    add_feedforward_constraints!(container, model, devices)\n\n    objective_function!(container, devices, model, get_network_formulation(network_model))\n    add_event_constraints!(container, devices, model, network_model)\n    add_constraint_dual!(container, sys, model)\n    return\nend\n"
  },
  {
    "path": "src/devices_models/device_constructors/reactivepowerdevice_constructor.jl",
    "content": "function construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ArgumentConstructStage,\n    model::DeviceModel{R, D},\n    network_model::NetworkModel{<:PM.AbstractPowerModel},\n) where {\n    R <: PSY.SynchronousCondenser,\n    D <: AbstractReactivePowerDeviceFormulation,\n}\n    devices = get_available_components(model, sys)\n    add_variables!(container, ReactivePowerVariable, devices, D())\n    add_to_expression!(\n        container,\n        ReactivePowerBalance,\n        ReactivePowerVariable,\n        devices,\n        model,\n        network_model,\n    )\n    add_feedforward_arguments!(container, model, devices)\nend\n\nfunction construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ModelConstructStage,\n    model::DeviceModel{R, D},\n    network_model::NetworkModel{<:PM.AbstractPowerModel},\n) where {\n    R <: PSY.SynchronousCondenser,\n    D <: AbstractReactivePowerDeviceFormulation,\n}\n    devices = get_available_components(model, sys)\n    # No constraints\n    # Add FFs\n    add_feedforward_constraints!(container, model, devices)\n    # No objective function\n    return\nend\n\nfunction construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ArgumentConstructStage,\n    model::DeviceModel{R, D},\n    network_model::NetworkModel{<:PM.AbstractActivePowerModel},\n) where {\n    R <: PSY.SynchronousCondenser,\n    D <: AbstractReactivePowerDeviceFormulation,\n}\n    # Do Nothing in Active Power Only Models\n    return\nend\n"
  },
  {
    "path": "src/devices_models/device_constructors/regulationdevice_constructor.jl",
    "content": "function construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ArgumentConstructStage,\n    model::DeviceModel{PSY.RegulationDevice{T}, U},\n    network_model::NetworkModel{AreaBalancePowerModel},\n) where {T <: PSY.StaticInjection, U <: DeviceLimitedRegulation}\n    devices = get_available_components(get_component_type(model), sys)\n    add_parameters!(container, ActivePowerTimeSeriesParameter, devices, model)\n\n    add_to_expression!(\n        container,\n        ActivePowerBalance,\n        ActivePowerTimeSeriesParameter,\n        devices,\n        model,\n        network_model,\n    )\n\n    add_variables!(container, DeltaActivePowerUpVariable, devices, U())\n    add_variables!(container, DeltaActivePowerDownVariable, devices, U())\n    add_variables!(container, AdditionalDeltaActivePowerUpVariable, devices, U())\n    add_variables!(container, AdditionalDeltaActivePowerDownVariable, devices, U())\n    return\nend\n\nfunction construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ModelConstructStage,\n    model::DeviceModel{PSY.RegulationDevice{T}, DeviceLimitedRegulation},\n    network_model::NetworkModel{AreaBalancePowerModel},\n) where {T <: PSY.StaticInjection}\n    devices = get_available_components(get_component_type(model), sys)\n\n    add_constraints!(\n        container,\n        RegulationLimitsConstraint,\n        DeltaActivePowerUpVariable,\n        devices,\n        model,\n        network_model,\n    )\n\n    add_constraints!(container, RampLimitConstraint, devices, model, network_model)\n    add_constraints!(\n        container,\n        ParticipationAssignmentConstraint,\n        devices,\n        model,\n        network_model,\n    )\n    objective_function!(container, devices, model)\n    return\nend\n\nfunction construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ArgumentConstructStage,\n    model::DeviceModel{PSY.RegulationDevice{T}, U},\n    network_model::NetworkModel{AreaBalancePowerModel},\n) where {T <: PSY.StaticInjection, U <: ReserveLimitedRegulation}\n    devices = get_available_components(get_component_type(model), sys)\n    add_parameters!(container, ActivePowerTimeSeriesParameter, devices, model)\n\n    add_to_expression!(\n        container,\n        ActivePowerBalance,\n        ActivePowerTimeSeriesParameter,\n        devices,\n        model,\n        network_model,\n    )\n\n    add_variables!(container, DeltaActivePowerUpVariable, devices, U())\n    add_variables!(container, DeltaActivePowerDownVariable, devices, U())\n    add_variables!(container, AdditionalDeltaActivePowerUpVariable, devices, U())\n    add_variables!(container, AdditionalDeltaActivePowerDownVariable, devices, U())\n    return\nend\n\nfunction construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ModelConstructStage,\n    model::DeviceModel{PSY.RegulationDevice{T}, ReserveLimitedRegulation},\n    network_model::NetworkModel{AreaBalancePowerModel},\n) where {T <: PSY.StaticInjection}\n    devices = get_available_components(get_component_type(model), sys)\n\n    add_constraints!(\n        container,\n        RegulationLimitsConstraint,\n        DeltaActivePowerUpVariable,\n        devices,\n        model,\n        network_model,\n    )\n\n    add_constraints!(\n        container,\n        ParticipationAssignmentConstraint,\n        devices,\n        model,\n        AreaBalancePowerModel,\n    )\n    objective_function!(container, devices, model)\n    return\nend\n\nfunction construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ArgumentConstructStage,\n    model::DeviceModel{PSY.RegulationDevice{<:PSY.StaticInjection}, FixedOutput},\n    network_model::NetworkModel{AreaBalancePowerModel},\n)\n    devices = get_available_components(get_component_type(model), sys)\n    add_parameters!(container, ActivePowerTimeSeriesParameter, devices, model)\n\n    add_to_expression!(\n        container,\n        ActivePowerBalance,\n        ActivePowerTimeSeriesParameter,\n        devices,\n        model,\n        network_model,\n    )\n    return\nend\n\nfunction construct_device!(\n    ::OptimizationContainer,\n    ::PSY.System,\n    ::ModelConstructStage,\n    ::DeviceModel{PSY.RegulationDevice{<:PSY.StaticInjection}, FixedOutput},\n    network_model::NetworkModel{AreaBalancePowerModel},\n)\n    # There is no-op under FixedOutput formulation\n    return\nend\n"
  },
  {
    "path": "src/devices_models/device_constructors/renewablegeneration_constructor.jl",
    "content": "function construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ArgumentConstructStage,\n    model::DeviceModel{R, D},\n    network_model::NetworkModel{<:PM.AbstractPowerModel},\n) where {\n    R <: PSY.RenewableGen,\n    D <: AbstractRenewableDispatchFormulation,\n}\n    devices = get_available_components(model, sys)\n\n    add_variables!(container, ActivePowerVariable, devices, D())\n    add_variables!(container, ReactivePowerVariable, devices, D())\n    if haskey(get_time_series_names(model), ActivePowerTimeSeriesParameter)\n        add_parameters!(container, ActivePowerTimeSeriesParameter, devices, model)\n    end\n    if haskey(get_time_series_names(model), ReactivePowerTimeSeriesParameter)\n        add_parameters!(container, ReactivePowerTimeSeriesParameter, devices, model)\n    end\n    process_market_bid_parameters!(container, devices, model)\n\n    add_cost_expressions!(container, devices, model)\n\n    # Expression\n    add_to_expression!(\n        container,\n        ActivePowerBalance,\n        ActivePowerVariable,\n        devices,\n        model,\n        network_model,\n    )\n    add_to_expression!(\n        container,\n        ReactivePowerBalance,\n        ReactivePowerVariable,\n        devices,\n        model,\n        network_model,\n    )\n    if has_service_model(model)\n        add_to_expression!(\n            container,\n            ActivePowerRangeExpressionLB,\n            ActivePowerVariable,\n            devices,\n            model,\n            network_model,\n        )\n        add_to_expression!(\n            container,\n            ActivePowerRangeExpressionUB,\n            ActivePowerVariable,\n            devices,\n            model,\n            network_model,\n        )\n    end\n    add_feedforward_arguments!(container, model, devices)\n    add_event_arguments!(container, devices, model, network_model)\nend\n\nfunction construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ModelConstructStage,\n    model::DeviceModel{R, <:AbstractRenewableDispatchFormulation},\n    network_model::NetworkModel{<:PM.AbstractPowerModel},\n) where {R <: PSY.RenewableGen}\n    devices = get_available_components(model, sys)\n\n    if has_service_model(model)\n        add_constraints!(\n            container,\n            ActivePowerVariableLimitsConstraint,\n            ActivePowerRangeExpressionLB,\n            devices,\n            model,\n            network_model,\n        )\n        add_constraints!(\n            container,\n            ActivePowerVariableLimitsConstraint,\n            ActivePowerRangeExpressionUB,\n            devices,\n            model,\n            network_model,\n        )\n    else\n        add_constraints!(\n            container,\n            ActivePowerVariableLimitsConstraint,\n            ActivePowerVariable,\n            devices,\n            model,\n            network_model,\n        )\n    end\n\n    add_constraints!(\n        container,\n        ReactivePowerVariableLimitsConstraint,\n        ReactivePowerVariable,\n        devices,\n        model,\n        network_model,\n    )\n    add_feedforward_constraints!(container, model, devices)\n\n    objective_function!(container, devices, model, get_network_formulation(network_model))\n\n    add_event_constraints!(container, devices, model, network_model)\n    add_constraint_dual!(container, sys, model)\n    return\nend\n\nfunction construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ArgumentConstructStage,\n    model::DeviceModel{R, D},\n    network_model::NetworkModel{<:PM.AbstractActivePowerModel},\n) where {\n    R <: PSY.RenewableGen,\n    D <: AbstractRenewableDispatchFormulation,\n}\n    devices = get_available_components(model, sys)\n\n    add_variables!(container, ActivePowerVariable, devices, D())\n    # this is always true!! see get_default_time_series_names in renewable_generation.jl\n    # and line 62 of device_model.jl\n    if haskey(get_time_series_names(model), ActivePowerTimeSeriesParameter)\n        add_parameters!(container, ActivePowerTimeSeriesParameter, devices, model)\n    end\n    process_market_bid_parameters!(container, devices, model)\n\n    add_cost_expressions!(container, devices, model)\n\n    # Expression\n    add_to_expression!(\n        container,\n        ActivePowerBalance,\n        ActivePowerVariable,\n        devices,\n        model,\n        network_model,\n    )\n    if has_service_model(model)\n        add_to_expression!(\n            container,\n            ActivePowerRangeExpressionLB,\n            ActivePowerVariable,\n            devices,\n            model,\n            network_model,\n        )\n        add_to_expression!(\n            container,\n            ActivePowerRangeExpressionUB,\n            ActivePowerVariable,\n            devices,\n            model,\n            network_model,\n        )\n    end\n    add_feedforward_arguments!(container, model, devices)\n    add_event_arguments!(container, devices, model, network_model)\nend\n\nfunction construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ModelConstructStage,\n    model::DeviceModel{R, <:AbstractRenewableDispatchFormulation},\n    network_model::NetworkModel{<:PM.AbstractActivePowerModel},\n) where {R <: PSY.RenewableGen}\n    devices = get_available_components(model, sys)\n\n    if has_service_model(model)\n        add_constraints!(\n            container,\n            ActivePowerVariableLimitsConstraint,\n            ActivePowerRangeExpressionLB,\n            devices,\n            model,\n            network_model,\n        )\n        add_constraints!(\n            container,\n            ActivePowerVariableLimitsConstraint,\n            ActivePowerRangeExpressionUB,\n            devices,\n            model,\n            network_model,\n        )\n    else\n        add_constraints!(\n            container,\n            ActivePowerVariableLimitsConstraint,\n            ActivePowerVariable,\n            devices,\n            model,\n            network_model,\n        )\n    end\n    add_feedforward_constraints!(container, model, devices)\n\n    objective_function!(container, devices, model, get_network_formulation(network_model))\n\n    add_event_constraints!(container, devices, model, network_model)\n    add_constraint_dual!(container, sys, model)\n\n    return\nend\n\nfunction construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ArgumentConstructStage,\n    model::DeviceModel{R, FixedOutput},\n    network_model::NetworkModel{<:PM.AbstractPowerModel},\n) where {R <: PSY.RenewableGen}\n    devices = get_available_components(model, sys)\n\n    add_parameters!(container, ActivePowerTimeSeriesParameter, devices, model)\n    add_parameters!(container, ReactivePowerTimeSeriesParameter, devices, model)\n    process_market_bid_parameters!(container, devices, model)\n\n    add_to_expression!(\n        container,\n        ActivePowerBalance,\n        ActivePowerTimeSeriesParameter,\n        devices,\n        model,\n        network_model,\n    )\n    add_to_expression!(\n        container,\n        ReactivePowerBalance,\n        ReactivePowerTimeSeriesParameter,\n        devices,\n        model,\n        network_model,\n    )\n    add_event_arguments!(container, devices, model, network_model)\n    return\nend\n\nfunction construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ArgumentConstructStage,\n    model::DeviceModel{R, FixedOutput},\n    network_model::NetworkModel{<:PM.AbstractActivePowerModel},\n) where {R <: PSY.RenewableGen}\n    devices = get_available_components(model, sys)\n\n    add_parameters!(container, ActivePowerTimeSeriesParameter, devices, model)\n    add_to_expression!(\n        container,\n        ActivePowerBalance,\n        ActivePowerTimeSeriesParameter,\n        devices,\n        model,\n        network_model,\n    )\n    process_market_bid_parameters!(container, devices, model)\n    add_event_arguments!(container, devices, model, network_model)\n    return\nend\n\nfunction construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ModelConstructStage,\n    model::DeviceModel{<:PSY.RenewableGen, FixedOutput},\n    network_model::NetworkModel{<:PM.AbstractPowerModel},\n)\n    # FixedOutput doesn't add any constraints to the model. This function covers\n    # AbstractPowerModel and AbtractActivePowerModel\n    return\nend\n"
  },
  {
    "path": "src/devices_models/device_constructors/source_constructor.jl",
    "content": "\"\"\"\nThis function creates the arguments for the model for an import/export formulation for Source devices\n\"\"\"\nfunction construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ArgumentConstructStage,\n    model::DeviceModel{T, D},\n    network_model::NetworkModel{<:PM.AbstractPowerModel},\n) where {\n    T <: PSY.Source,\n    D <: ImportExportSourceModel,\n}\n    devices = get_available_components(model, sys)\n\n    add_variables!(container, ActivePowerInVariable, devices, D())\n    add_variables!(container, ActivePowerOutVariable, devices, D())\n    add_variables!(container, ReactivePowerVariable, devices, D())\n    add_expressions!(container, NetActivePower, devices, model)\n\n    if haskey(get_time_series_names(model), ActivePowerOutTimeSeriesParameter)\n        add_parameters!(container, ActivePowerOutTimeSeriesParameter, devices, model)\n    end\n    if haskey(get_time_series_names(model), ActivePowerInTimeSeriesParameter)\n        add_parameters!(container, ActivePowerInTimeSeriesParameter, devices, model)\n    end\n\n    if get_attribute(model, \"reservation\")\n        add_variables!(container, ReservationVariable, devices, D())\n    end\n\n    process_import_export_parameters!(container, devices, model)\n\n    add_to_expression!(\n        container,\n        ActivePowerBalance,\n        ActivePowerInVariable,\n        devices,\n        model,\n        network_model,\n    )\n    add_to_expression!(\n        container,\n        ActivePowerBalance,\n        ActivePowerOutVariable,\n        devices,\n        model,\n        network_model,\n    )\n    add_to_expression!(\n        container,\n        ReactivePowerBalance,\n        ReactivePowerVariable,\n        devices,\n        model,\n        network_model,\n    )\n\n    add_cost_expressions!(container, devices, model)\n\n    add_to_expression!(\n        container,\n        ActivePowerRangeExpressionLB,\n        ActivePowerOutVariable,\n        devices,\n        model,\n        network_model,\n    )\n    add_to_expression!(\n        container,\n        ActivePowerRangeExpressionUB,\n        ActivePowerOutVariable,\n        devices,\n        model,\n        network_model,\n    )\n\n    add_feedforward_arguments!(container, model, devices)\n    return\nend\n\n\"\"\"\nThis function creates the constraints for the model for an import/export formulation for Source devices\n\"\"\"\nfunction construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ModelConstructStage,\n    model::DeviceModel{T, D},\n    network_model::NetworkModel{<:PM.AbstractPowerModel},\n) where {\n    T <: PSY.Source,\n    D <: ImportExportSourceModel,\n}\n    devices = get_available_components(model, sys)\n\n    add_constraints!(\n        container,\n        ActivePowerVariableLimitsConstraint,\n        ActivePowerRangeExpressionLB,\n        devices,\n        model,\n        network_model,\n    )\n    add_constraints!(\n        container,\n        ActivePowerVariableLimitsConstraint,\n        ActivePowerRangeExpressionUB,\n        devices,\n        model,\n        network_model,\n    )\n    add_constraints!(\n        container,\n        InputActivePowerVariableLimitsConstraint,\n        ActivePowerInVariable,\n        devices,\n        model,\n        network_model,\n    )\n    add_constraints!(\n        container,\n        ReactivePowerVariableLimitsConstraint,\n        ReactivePowerVariable,\n        devices,\n        model,\n        network_model,\n    )\n    add_constraints!(container, ImportExportBudgetConstraint, devices, model, network_model)\n\n    if haskey(get_time_series_names(model), ActivePowerOutTimeSeriesParameter)\n        add_constraints!(\n            container,\n            ActivePowerOutVariableTimeSeriesLimitsConstraint,\n            ActivePowerRangeExpressionUB,\n            devices,\n            model,\n            network_model,\n        )\n    end\n    if haskey(get_time_series_names(model), ActivePowerInTimeSeriesParameter)\n        add_constraints!(\n            container,\n            ActivePowerInVariableTimeSeriesLimitsConstraint,\n            ActivePowerInVariable,\n            devices,\n            model,\n            network_model,\n        )\n    end\n\n    add_feedforward_constraints!(container, model, devices)\n\n    objective_function!(container, devices, model, get_network_formulation(network_model))\n    add_constraint_dual!(container, sys, model)\n    return\nend\n\n\"\"\"\nThis function creates the arguments for the model for an import/export formulation for Source devices\n\"\"\"\nfunction construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ArgumentConstructStage,\n    model::DeviceModel{T, D},\n    network_model::NetworkModel{<:PM.AbstractActivePowerModel},\n) where {\n    T <: PSY.Source,\n    D <: ImportExportSourceModel,\n}\n    devices = get_available_components(model, sys)\n\n    add_variables!(container, ActivePowerInVariable, devices, D())\n    add_variables!(container, ActivePowerOutVariable, devices, D())\n    add_expressions!(container, NetActivePower, devices, model)\n\n    if haskey(get_time_series_names(model), ActivePowerOutTimeSeriesParameter)\n        add_parameters!(container, ActivePowerOutTimeSeriesParameter, devices, model)\n    end\n    if haskey(get_time_series_names(model), ActivePowerInTimeSeriesParameter)\n        add_parameters!(container, ActivePowerInTimeSeriesParameter, devices, model)\n    end\n\n    if get_attribute(model, \"reservation\")\n        add_variables!(container, ReservationVariable, devices, D())\n    end\n\n    process_import_export_parameters!(container, devices, model)\n\n    add_to_expression!(\n        container,\n        ActivePowerBalance,\n        ActivePowerInVariable,\n        devices,\n        model,\n        network_model,\n    )\n    add_to_expression!(\n        container,\n        ActivePowerBalance,\n        ActivePowerOutVariable,\n        devices,\n        model,\n        network_model,\n    )\n\n    add_to_expression!(\n        container,\n        NetActivePower,\n        ActivePowerInVariable(),\n        devices,\n        model,\n    )\n    add_to_expression!(\n        container,\n        NetActivePower,\n        ActivePowerOutVariable(),\n        devices,\n        model,\n    )\n\n    add_cost_expressions!(container, devices, model)\n\n    add_to_expression!(\n        container,\n        ActivePowerRangeExpressionLB,\n        ActivePowerOutVariable,\n        devices,\n        model,\n        network_model,\n    )\n    add_to_expression!(\n        container,\n        ActivePowerRangeExpressionUB,\n        ActivePowerOutVariable,\n        devices,\n        model,\n        network_model,\n    )\n\n    add_feedforward_arguments!(container, model, devices)\n    return\nend\n\n\"\"\"\nThis function creates the constraints for the model for an import/export formulation for Source devices\n\"\"\"\nfunction construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ModelConstructStage,\n    model::DeviceModel{T, D},\n    network_model::NetworkModel{<:PM.AbstractActivePowerModel},\n) where {\n    T <: PSY.Source,\n    D <: ImportExportSourceModel,\n}\n    devices = get_available_components(model, sys)\n\n    add_constraints!(\n        container,\n        ActivePowerVariableLimitsConstraint,\n        ActivePowerRangeExpressionLB,\n        devices,\n        model,\n        network_model,\n    )\n    add_constraints!(\n        container,\n        ActivePowerVariableLimitsConstraint,\n        ActivePowerRangeExpressionUB,\n        devices,\n        model,\n        network_model,\n    )\n    add_constraints!(\n        container,\n        InputActivePowerVariableLimitsConstraint,\n        ActivePowerInVariable,\n        devices,\n        model,\n        network_model,\n    )\n\n    add_constraints!(container, ImportExportBudgetConstraint, devices, model, network_model)\n\n    if haskey(get_time_series_names(model), ActivePowerOutTimeSeriesParameter)\n        add_constraints!(\n            container,\n            ActivePowerOutVariableTimeSeriesLimitsConstraint,\n            ActivePowerRangeExpressionUB,\n            devices,\n            model,\n            network_model,\n        )\n    end\n    if haskey(get_time_series_names(model), ActivePowerInTimeSeriesParameter)\n        add_constraints!(\n            container,\n            ActivePowerInVariableTimeSeriesLimitsConstraint,\n            ActivePowerInVariable,\n            devices,\n            model,\n            network_model,\n        )\n    end\n\n    add_feedforward_constraints!(container, model, devices)\n\n    objective_function!(container, devices, model, get_network_formulation(network_model))\n    add_constraint_dual!(container, sys, model)\n    return\nend\n\n\"\"\"\nThis function creates the arguments for the model for a FixedOutput formulation for Source devices\n\"\"\"\nfunction construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ArgumentConstructStage,\n    model::DeviceModel{T, D},\n    network_model::NetworkModel{<:PM.AbstractActivePowerModel},\n) where {\n    T <: PSY.Source,\n    D <: FixedOutput,\n}\n    devices = get_available_components(model, sys)\n    add_parameters!(container, ActivePowerInTimeSeriesParameter, devices, model)\n    add_parameters!(container, ActivePowerOutTimeSeriesParameter, devices, model)\n\n    add_to_expression!(\n        container,\n        ActivePowerBalance,\n        ActivePowerInTimeSeriesParameter,\n        devices,\n        model,\n        network_model,\n    )\n    add_to_expression!(\n        container,\n        ActivePowerBalance,\n        ActivePowerOutTimeSeriesParameter,\n        devices,\n        model,\n        network_model,\n    )\n    add_event_arguments!(container, devices, model, network_model)\n    return\nend\n\n\"\"\"\nThis function creates the constraints for the model for a FixedOutput formulation for Source devices (no constraints added for FixedOutput)\n\"\"\"\nfunction construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ModelConstructStage,\n    model::DeviceModel{T, D},\n    network_model::NetworkModel{<:PM.AbstractActivePowerModel},\n) where {\n    T <: PSY.Source,\n    D <: FixedOutput,\n}\n    # FixedOutput doesn't add any constraints to the model.\n    return\nend\n"
  },
  {
    "path": "src/devices_models/device_constructors/thermalgeneration_constructor.jl",
    "content": "# TODO: Security constrained models implement the correct functions for the model\nfunction has_security_arguments(device_model::DeviceModel)::Bool\n    return true\nend\n\n# TODO: Security constrained models implement the correct functions for the model\nfunction has_security_model(device_model::DeviceModel)::Bool\n    return true\nend\n\n@inline function _handle_common_thermal_parameters!(\n    container::OptimizationContainer,\n    devices::IS.FlattenIteratorWrapper{T},\n    model::DeviceModel{T},\n) where {T <: PSY.ThermalGen}\n    if haskey(get_time_series_names(model), FuelCostParameter)\n        add_parameters!(container, FuelCostParameter, devices, model)\n    end\n\n    process_market_bid_parameters!(container, devices, model)\nend\n\nfunction construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ArgumentConstructStage,\n    device_model::DeviceModel{T, FixedOutput},\n    network_model::NetworkModel{<:PM.AbstractActivePowerModel},\n) where {T <: PSY.ThermalGen}\n    devices = get_available_components(device_model, sys)\n    add_parameters!(container, ActivePowerTimeSeriesParameter, devices, device_model)\n    add_to_expression!(\n        container,\n        ActivePowerBalance,\n        ActivePowerTimeSeriesParameter,\n        devices,\n        device_model,\n        network_model,\n    )\n    add_event_arguments!(container, devices, device_model, network_model)\n    return\nend\n\nfunction construct_device!(\n    ::OptimizationContainer,\n    ::PSY.System,\n    ::ModelConstructStage,\n    ::DeviceModel{<:PSY.ThermalGen, FixedOutput},\n    network_model::NetworkModel{<:PM.AbstractPowerModel},\n)\n    # FixedOutput doesn't add any constraints to the model. This function covers\n    # AbstractPowerModel and AbtractActivePowerModel\n    return\nend\n\n\"\"\"\nThis function creates the arguments for the model for a full thermal dispatch formulation depending on combination of devices, device_formulation and system_formulation\n\"\"\"\nfunction construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ArgumentConstructStage,\n    device_model::DeviceModel{T, D},\n    network_model::NetworkModel{<:PM.AbstractPowerModel},\n) where {\n    T <: PSY.ThermalGen,\n    D <: AbstractStandardUnitCommitment,\n}\n    devices = get_available_components(device_model, sys)\n\n    add_variables!(container, ActivePowerVariable, devices, D())\n    add_variables!(container, ReactivePowerVariable, devices, D())\n    add_variables!(container, OnVariable, devices, D())\n    add_variables!(container, StartVariable, devices, D())\n    add_variables!(container, StopVariable, devices, D())\n\n    add_variables!(container, TimeDurationOn, devices, D())\n    add_variables!(container, TimeDurationOff, devices, D())\n\n    initial_conditions!(container, devices, D())\n\n    if haskey(get_time_series_names(device_model), ActivePowerTimeSeriesParameter)\n        add_parameters!(container, ActivePowerTimeSeriesParameter, devices, device_model)\n    end\n\n    _handle_common_thermal_parameters!(container, devices, device_model)\n\n    add_to_expression!(\n        container,\n        ActivePowerBalance,\n        ActivePowerVariable,\n        devices,\n        device_model,\n        network_model,\n    )\n    add_to_expression!(\n        container,\n        ReactivePowerBalance,\n        ReactivePowerVariable,\n        devices,\n        device_model,\n        network_model,\n    )\n\n    add_cost_expressions!(container, devices, device_model)\n\n    add_to_expression!(\n        container,\n        ActivePowerRangeExpressionLB,\n        ActivePowerVariable,\n        devices,\n        device_model,\n        network_model,\n    )\n    add_to_expression!(\n        container,\n        ActivePowerRangeExpressionUB,\n        ActivePowerVariable,\n        devices,\n        device_model,\n        network_model,\n    )\n    add_to_expression!(\n        container,\n        FuelConsumptionExpression,\n        ActivePowerVariable,\n        devices,\n        device_model,\n    )\n    if get_use_slacks(device_model)\n        add_variables!(container, RateofChangeConstraintSlackUp, devices, D())\n        add_variables!(container, RateofChangeConstraintSlackDown, devices, D())\n    end\n    add_feedforward_arguments!(container, device_model, devices)\n    add_event_arguments!(container, devices, device_model, network_model)\n    return\nend\n\n\"\"\"\nThis function creates the constraints for the model for a full thermal dispatch formulation depending on combination of devices, device_formulation and system_formulation\n\"\"\"\nfunction construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ModelConstructStage,\n    device_model::DeviceModel{T, U},\n    network_model::NetworkModel{<:PM.AbstractPowerModel},\n) where {\n    T <: PSY.ThermalGen,\n    U <: AbstractStandardUnitCommitment,\n}\n    devices = get_available_components(device_model, sys)\n\n    add_constraints!(\n        container,\n        ActivePowerVariableLimitsConstraint,\n        ActivePowerRangeExpressionLB,\n        devices,\n        device_model,\n        network_model,\n    )\n    add_constraints!(\n        container,\n        ActivePowerVariableLimitsConstraint,\n        ActivePowerRangeExpressionUB,\n        devices,\n        device_model,\n        network_model,\n    )\n    add_constraints!(\n        container,\n        ReactivePowerVariableLimitsConstraint,\n        ReactivePowerVariable,\n        devices,\n        device_model,\n        network_model,\n    )\n    add_constraints!(container, CommitmentConstraint, devices, device_model, network_model)\n    add_constraints!(container, RampConstraint, devices, device_model, network_model)\n    add_constraints!(container, DurationConstraint, devices, device_model, network_model)\n\n    if haskey(get_time_series_names(device_model), ActivePowerTimeSeriesParameter)\n        add_constraints!(\n            container,\n            ActivePowerVariableTimeSeriesLimitsConstraint,\n            ActivePowerRangeExpressionUB,\n            devices,\n            device_model,\n            network_model,\n        )\n    end\n\n    add_feedforward_constraints!(container, device_model, devices)\n    add_event_constraints!(container, devices, device_model, network_model)\n\n    objective_function!(\n        container,\n        devices,\n        device_model,\n        get_network_formulation(network_model),\n    )\n\n    if has_security_arguments(device_model)\n        # TODO: SecurityConstrainedModels Implemenation of G-1\n        #add_constraints!(\n        #    container,\n        #    SecurityConstraint,\n        #    devices,\n        #    device_model,\n        #    network_model,\n        #)\n    end\n\n    add_constraint_dual!(container, sys, device_model)\n    return\nend\n\n\"\"\"\nThis function creates the arguments model for a full thermal dispatch formulation depending on combination of devices, device_formulation and system_formulation\n\"\"\"\nfunction construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ArgumentConstructStage,\n    device_model::DeviceModel{T, D},\n    network_model::NetworkModel{<:PM.AbstractActivePowerModel},\n) where {T <: PSY.ThermalGen, D <: AbstractStandardUnitCommitment}\n    devices = get_available_components(device_model, sys)\n\n    add_variables!(container, ActivePowerVariable, devices, D())\n    add_variables!(container, OnVariable, devices, D())\n    add_variables!(container, StartVariable, devices, D())\n    add_variables!(container, StopVariable, devices, D())\n\n    add_variables!(container, TimeDurationOn, devices, D())\n    add_variables!(container, TimeDurationOff, devices, D())\n\n    initial_conditions!(container, devices, D())\n\n    if haskey(get_time_series_names(device_model), ActivePowerTimeSeriesParameter)\n        add_parameters!(container, ActivePowerTimeSeriesParameter, devices, device_model)\n    end\n\n    _handle_common_thermal_parameters!(container, devices, device_model)\n\n    add_to_expression!(\n        container,\n        ActivePowerBalance,\n        ActivePowerVariable,\n        devices,\n        device_model,\n        network_model,\n    )\n\n    add_cost_expressions!(container, devices, device_model)\n\n    add_to_expression!(\n        container,\n        ActivePowerRangeExpressionLB,\n        ActivePowerVariable,\n        devices,\n        device_model,\n        network_model,\n    )\n    add_to_expression!(\n        container,\n        ActivePowerRangeExpressionUB,\n        ActivePowerVariable,\n        devices,\n        device_model,\n        network_model,\n    )\n    add_to_expression!(\n        container,\n        FuelConsumptionExpression,\n        ActivePowerVariable,\n        devices,\n        device_model,\n    )\n    if get_use_slacks(device_model)\n        add_variables!(container, RateofChangeConstraintSlackUp, devices, D())\n        add_variables!(container, RateofChangeConstraintSlackDown, devices, D())\n    end\n\n    add_feedforward_arguments!(container, device_model, devices)\n    add_event_arguments!(container, devices, device_model, network_model)\n    return\nend\n\n\"\"\"\nThis function creates the constraints for the model for a full thermal dispatch formulation depending on combination of devices, device_formulation and system_formulation\n\"\"\"\nfunction construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ModelConstructStage,\n    device_model::DeviceModel{T, <:AbstractStandardUnitCommitment},\n    network_model::NetworkModel{<:PM.AbstractActivePowerModel},\n) where {T <: PSY.ThermalGen}\n    devices = get_available_components(device_model, sys)\n    add_constraints!(\n        container,\n        ActivePowerVariableLimitsConstraint,\n        ActivePowerRangeExpressionLB,\n        devices,\n        device_model,\n        network_model,\n    )\n    add_constraints!(\n        container,\n        ActivePowerVariableLimitsConstraint,\n        ActivePowerRangeExpressionUB,\n        devices,\n        device_model,\n        network_model,\n    )\n\n    add_constraints!(container, CommitmentConstraint, devices, device_model, network_model)\n    add_constraints!(container, RampConstraint, devices, device_model, network_model)\n    add_constraints!(container, DurationConstraint, devices, device_model, network_model)\n    if haskey(get_time_series_names(device_model), ActivePowerTimeSeriesParameter)\n        add_constraints!(\n            container,\n            ActivePowerVariableTimeSeriesLimitsConstraint,\n            ActivePowerRangeExpressionUB,\n            devices,\n            device_model,\n            network_model,\n        )\n    end\n\n    add_feedforward_constraints!(container, device_model, devices)\n\n    objective_function!(\n        container,\n        devices,\n        device_model,\n        get_network_formulation(network_model),\n    )\n    add_event_constraints!(container, devices, device_model, network_model)\n    add_constraint_dual!(container, sys, device_model)\n    return\nend\n\n\"\"\"\nThis function creates the model for a full thermal dispatch formulation depending on combination of devices, device_formulation and system_formulation\n\"\"\"\nfunction construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ArgumentConstructStage,\n    device_model::DeviceModel{T, ThermalBasicUnitCommitment},\n    network_model::NetworkModel{<:PM.AbstractPowerModel},\n) where {T <: PSY.ThermalGen}\n    devices = get_available_components(device_model, sys)\n\n    add_variables!(container, ActivePowerVariable, devices, ThermalBasicUnitCommitment())\n    add_variables!(container, ReactivePowerVariable, devices, ThermalBasicUnitCommitment())\n    add_variables!(container, OnVariable, devices, ThermalBasicUnitCommitment())\n    add_variables!(container, StartVariable, devices, ThermalBasicUnitCommitment())\n    add_variables!(container, StopVariable, devices, ThermalBasicUnitCommitment())\n\n    add_variables!(container, TimeDurationOn, devices, ThermalBasicUnitCommitment())\n    add_variables!(container, TimeDurationOff, devices, ThermalBasicUnitCommitment())\n    initial_conditions!(container, devices, ThermalBasicUnitCommitment())\n\n    if haskey(get_time_series_names(device_model), ActivePowerTimeSeriesParameter)\n        add_parameters!(container, ActivePowerTimeSeriesParameter, devices, device_model)\n    end\n\n    _handle_common_thermal_parameters!(container, devices, device_model)\n\n    add_to_expression!(\n        container,\n        ActivePowerBalance,\n        ActivePowerVariable,\n        devices,\n        device_model,\n        network_model,\n    )\n    add_to_expression!(\n        container,\n        ReactivePowerBalance,\n        ReactivePowerVariable,\n        devices,\n        device_model,\n        network_model,\n    )\n\n    add_cost_expressions!(container, devices, device_model)\n\n    add_to_expression!(\n        container,\n        ActivePowerRangeExpressionLB,\n        ActivePowerVariable,\n        devices,\n        device_model,\n        network_model,\n    )\n    add_to_expression!(\n        container,\n        ActivePowerRangeExpressionUB,\n        ActivePowerVariable,\n        devices,\n        device_model,\n        network_model,\n    )\n    add_to_expression!(\n        container,\n        FuelConsumptionExpression,\n        ActivePowerVariable,\n        devices,\n        device_model,\n    )\n    if get_use_slacks(device_model)\n        add_variables!(\n            container,\n            RateofChangeConstraintSlackUp,\n            devices,\n            ThermalBasicUnitCommitment(),\n        )\n        add_variables!(\n            container,\n            RateofChangeConstraintSlackDown,\n            devices,\n            ThermalBasicUnitCommitment(),\n        )\n    end\n\n    add_feedforward_arguments!(container, device_model, devices)\n    add_event_arguments!(container, devices, device_model, network_model)\n    return\nend\n\n\"\"\"\nThis function creates the model for a full thermal dispatch formulation depending on combination of devices, device_formulation and system_formulation\n\"\"\"\nfunction construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ModelConstructStage,\n    device_model::DeviceModel{T, ThermalBasicUnitCommitment},\n    network_model::NetworkModel{<:PM.AbstractPowerModel},\n) where {T <: PSY.ThermalGen}\n    devices = get_available_components(device_model, sys)\n\n    add_constraints!(\n        container,\n        ActivePowerVariableLimitsConstraint,\n        ActivePowerRangeExpressionLB,\n        devices,\n        device_model,\n        network_model,\n    )\n    add_constraints!(\n        container,\n        ActivePowerVariableLimitsConstraint,\n        ActivePowerRangeExpressionUB,\n        devices,\n        device_model,\n        network_model,\n    )\n\n    add_constraints!(\n        container,\n        ReactivePowerVariableLimitsConstraint,\n        ReactivePowerVariable,\n        devices,\n        device_model,\n        network_model,\n    )\n    add_constraints!(container, CommitmentConstraint, devices, device_model, network_model)\n\n    if haskey(get_time_series_names(device_model), ActivePowerTimeSeriesParameter)\n        add_constraints!(\n            container,\n            ActivePowerVariableTimeSeriesLimitsConstraint,\n            ActivePowerRangeExpressionUB,\n            devices,\n            device_model,\n            network_model,\n        )\n    end\n\n    add_feedforward_constraints!(container, device_model, devices)\n\n    objective_function!(\n        container,\n        devices,\n        device_model,\n        get_network_formulation(network_model),\n    )\n    add_event_constraints!(container, devices, device_model, network_model)\n    add_constraint_dual!(container, sys, device_model)\n    return\nend\n\n\"\"\"\nThis function creates the arguments for the model for a full thermal dispatch formulation depending on combination of devices, device_formulation and system_formulation\n\"\"\"\nfunction construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ArgumentConstructStage,\n    device_model::DeviceModel{T, ThermalBasicUnitCommitment},\n    network_model::NetworkModel{<:PM.AbstractActivePowerModel},\n) where {T <: PSY.ThermalGen}\n    devices = get_available_components(device_model, sys)\n\n    add_variables!(container, ActivePowerVariable, devices, ThermalBasicUnitCommitment())\n    add_variables!(container, OnVariable, devices, ThermalBasicUnitCommitment())\n    add_variables!(container, StartVariable, devices, ThermalBasicUnitCommitment())\n    add_variables!(container, StopVariable, devices, ThermalBasicUnitCommitment())\n\n    add_variables!(container, TimeDurationOn, devices, ThermalBasicUnitCommitment())\n    add_variables!(container, TimeDurationOff, devices, ThermalBasicUnitCommitment())\n    initial_conditions!(container, devices, ThermalBasicUnitCommitment())\n\n    if haskey(get_time_series_names(device_model), ActivePowerTimeSeriesParameter)\n        add_parameters!(container, ActivePowerTimeSeriesParameter, devices, device_model)\n    end\n\n    _handle_common_thermal_parameters!(container, devices, device_model)\n\n    add_to_expression!(\n        container,\n        ActivePowerBalance,\n        ActivePowerVariable,\n        devices,\n        device_model,\n        network_model,\n    )\n\n    add_cost_expressions!(container, devices, device_model)\n\n    add_to_expression!(\n        container,\n        ActivePowerRangeExpressionLB,\n        ActivePowerVariable,\n        devices,\n        device_model,\n        network_model,\n    )\n    add_to_expression!(\n        container,\n        ActivePowerRangeExpressionUB,\n        ActivePowerVariable,\n        devices,\n        device_model,\n        network_model,\n    )\n    add_to_expression!(\n        container,\n        FuelConsumptionExpression,\n        ActivePowerVariable,\n        devices,\n        device_model,\n    )\n    if get_use_slacks(device_model)\n        add_variables!(\n            container,\n            RateofChangeConstraintSlackUp,\n            devices,\n            ThermalBasicUnitCommitment(),\n        )\n        add_variables!(\n            container,\n            RateofChangeConstraintSlackDown,\n            devices,\n            ThermalBasicUnitCommitment(),\n        )\n    end\n\n    add_feedforward_arguments!(container, device_model, devices)\n    add_event_arguments!(container, devices, device_model, network_model)\n    return\nend\n\n\"\"\"\nThis function creates the constraints for the model for a full thermal dispatch formulation depending on combination of devices, device_formulation and system_formulation\n\"\"\"\nfunction construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ModelConstructStage,\n    device_model::DeviceModel{T, ThermalBasicUnitCommitment},\n    network_model::NetworkModel{<:PM.AbstractActivePowerModel},\n) where {T <: PSY.ThermalGen}\n    devices = get_available_components(device_model, sys)\n\n    add_constraints!(\n        container,\n        ActivePowerVariableLimitsConstraint,\n        ActivePowerRangeExpressionLB,\n        devices,\n        device_model,\n        network_model,\n    )\n    add_constraints!(\n        container,\n        ActivePowerVariableLimitsConstraint,\n        ActivePowerRangeExpressionUB,\n        devices,\n        device_model,\n        network_model,\n    )\n\n    add_constraints!(container, CommitmentConstraint, devices, device_model, network_model)\n    if haskey(get_time_series_names(device_model), ActivePowerTimeSeriesParameter)\n        add_constraints!(\n            container,\n            ActivePowerVariableTimeSeriesLimitsConstraint,\n            ActivePowerRangeExpressionUB,\n            devices,\n            device_model,\n            network_model,\n        )\n    end\n\n    add_feedforward_constraints!(container, device_model, devices)\n\n    objective_function!(\n        container,\n        devices,\n        device_model,\n        get_network_formulation(network_model),\n    )\n    add_event_constraints!(container, devices, device_model, network_model)\n    add_constraint_dual!(container, sys, device_model)\n    return\nend\n\n\"\"\"\nThis function creates the model for a full thermal dispatch formulation depending on combination of devices, device_formulation and system_formulation\n\"\"\"\nfunction construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ArgumentConstructStage,\n    device_model::DeviceModel{T, ThermalStandardDispatch},\n    network_model::NetworkModel{<:PM.AbstractPowerModel},\n) where {T <: PSY.ThermalGen}\n    devices = get_available_components(device_model, sys)\n\n    add_variables!(container, ActivePowerVariable, devices, ThermalStandardDispatch())\n    add_variables!(container, ReactivePowerVariable, devices, ThermalStandardDispatch())\n\n    initial_conditions!(container, devices, ThermalStandardDispatch())\n\n    _handle_common_thermal_parameters!(container, devices, device_model)\n\n    add_to_expression!(\n        container,\n        ActivePowerBalance,\n        ActivePowerVariable,\n        devices,\n        device_model,\n        network_model,\n    )\n    add_to_expression!(\n        container,\n        ReactivePowerBalance,\n        ReactivePowerVariable,\n        devices,\n        device_model,\n        network_model,\n    )\n\n    add_cost_expressions!(container, devices, device_model)\n\n    add_to_expression!(\n        container,\n        ActivePowerRangeExpressionLB,\n        ActivePowerVariable,\n        devices,\n        device_model,\n        network_model,\n    )\n    add_to_expression!(\n        container,\n        ActivePowerRangeExpressionUB,\n        ActivePowerVariable,\n        devices,\n        device_model,\n        network_model,\n    )\n    add_to_expression!(\n        container,\n        FuelConsumptionExpression,\n        ActivePowerVariable,\n        devices,\n        device_model,\n    )\n    if get_use_slacks(device_model)\n        add_variables!(\n            container,\n            RateofChangeConstraintSlackUp,\n            devices,\n            ThermalStandardDispatch(),\n        )\n        add_variables!(\n            container,\n            RateofChangeConstraintSlackDown,\n            devices,\n            ThermalStandardDispatch(),\n        )\n    end\n\n    add_feedforward_arguments!(container, device_model, devices)\n    add_event_arguments!(container, devices, device_model, network_model)\n    return\nend\n\n\"\"\"\nThis function creates the model for a full thermal dispatch formulation depending on combination of devices, device_formulation and system_formulation\n\"\"\"\nfunction construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ModelConstructStage,\n    device_model::DeviceModel{T, ThermalStandardDispatch},\n    network_model::NetworkModel{<:PM.AbstractPowerModel},\n) where {T <: PSY.ThermalGen}\n    devices = get_available_components(device_model, sys)\n\n    add_constraints!(\n        container,\n        ActivePowerVariableLimitsConstraint,\n        ActivePowerRangeExpressionLB,\n        devices,\n        device_model,\n        network_model,\n    )\n    add_constraints!(\n        container,\n        ActivePowerVariableLimitsConstraint,\n        ActivePowerRangeExpressionUB,\n        devices,\n        device_model,\n        network_model,\n    )\n\n    add_constraints!(\n        container,\n        ReactivePowerVariableLimitsConstraint,\n        ReactivePowerVariable,\n        devices,\n        device_model,\n        network_model,\n    )\n    add_constraints!(container, RampConstraint, devices, device_model, network_model)\n\n    add_feedforward_constraints!(container, device_model, devices)\n\n    objective_function!(\n        container,\n        devices,\n        device_model,\n        get_network_formulation(network_model),\n    )\n    add_event_constraints!(container, devices, device_model, network_model)\n    add_constraint_dual!(container, sys, device_model)\n    return\nend\n\n\"\"\"\nThis function creates the arguments for the model for a full thermal dispatch formulation depending on combination of devices, device_formulation and system_formulation\n\"\"\"\nfunction construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ArgumentConstructStage,\n    device_model::DeviceModel{T, ThermalStandardDispatch},\n    network_model::NetworkModel{<:PM.AbstractActivePowerModel},\n) where {T <: PSY.ThermalGen}\n    devices = get_available_components(device_model, sys)\n\n    add_variables!(container, ActivePowerVariable, devices, ThermalStandardDispatch())\n\n    initial_conditions!(container, devices, ThermalStandardDispatch())\n\n    _handle_common_thermal_parameters!(container, devices, device_model)\n\n    add_to_expression!(\n        container,\n        ActivePowerBalance,\n        ActivePowerVariable,\n        devices,\n        device_model,\n        network_model,\n    )\n\n    add_cost_expressions!(container, devices, device_model)\n\n    add_to_expression!(\n        container,\n        ActivePowerRangeExpressionLB,\n        ActivePowerVariable,\n        devices,\n        device_model,\n        network_model,\n    )\n    add_to_expression!(\n        container,\n        ActivePowerRangeExpressionUB,\n        ActivePowerVariable,\n        devices,\n        device_model,\n        network_model,\n    )\n    add_to_expression!(\n        container,\n        FuelConsumptionExpression,\n        ActivePowerVariable,\n        devices,\n        device_model,\n    )\n    if get_use_slacks(device_model)\n        add_variables!(\n            container,\n            RateofChangeConstraintSlackUp,\n            devices,\n            ThermalStandardDispatch(),\n        )\n        add_variables!(\n            container,\n            RateofChangeConstraintSlackDown,\n            devices,\n            ThermalStandardDispatch(),\n        )\n    end\n\n    add_feedforward_arguments!(container, device_model, devices)\n    add_event_arguments!(container, devices, device_model, network_model)\n    return\nend\n\n\"\"\"\nThis function creates the constraints for the model for a full thermal dispatch formulation depending on combination of devices, device_formulation and system_formulation\n\"\"\"\nfunction construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ModelConstructStage,\n    device_model::DeviceModel{T, ThermalStandardDispatch},\n    network_model::NetworkModel{<:PM.AbstractActivePowerModel},\n) where {T <: PSY.ThermalGen}\n    devices = get_available_components(device_model, sys)\n\n    add_constraints!(\n        container,\n        ActivePowerVariableLimitsConstraint,\n        ActivePowerRangeExpressionLB,\n        devices,\n        device_model,\n        network_model,\n    )\n    add_constraints!(\n        container,\n        ActivePowerVariableLimitsConstraint,\n        ActivePowerRangeExpressionUB,\n        devices,\n        device_model,\n        network_model,\n    )\n\n    add_constraints!(container, RampConstraint, devices, device_model, network_model)\n\n    add_feedforward_constraints!(container, device_model, devices)\n\n    objective_function!(\n        container,\n        devices,\n        device_model,\n        get_network_formulation(network_model),\n    )\n    add_event_constraints!(container, devices, device_model, network_model)\n    add_constraint_dual!(container, sys, device_model)\n    return\nend\n\nfunction construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ArgumentConstructStage,\n    device_model::DeviceModel{T, D},\n    network_model::NetworkModel{<:PM.AbstractPowerModel},\n) where {\n    T <: PSY.ThermalGen,\n    D <: AbstractThermalDispatchFormulation,\n}\n    devices = get_available_components(device_model, sys)\n\n    add_variables!(container, ActivePowerVariable, devices, D())\n    add_variables!(container, ReactivePowerVariable, devices, D())\n\n    _handle_common_thermal_parameters!(container, devices, device_model)\n\n    add_to_expression!(\n        container,\n        ActivePowerBalance,\n        ActivePowerVariable,\n        devices,\n        device_model,\n        network_model,\n    )\n    add_to_expression!(\n        container,\n        ReactivePowerBalance,\n        ReactivePowerVariable,\n        devices,\n        device_model,\n        network_model,\n    )\n\n    add_cost_expressions!(container, devices, device_model)\n\n    add_to_expression!(\n        container,\n        ActivePowerRangeExpressionLB,\n        ActivePowerVariable,\n        devices,\n        device_model,\n        network_model,\n    )\n    add_to_expression!(\n        container,\n        ActivePowerRangeExpressionUB,\n        ActivePowerVariable,\n        devices,\n        device_model,\n        network_model,\n    )\n    add_to_expression!(\n        container,\n        FuelConsumptionExpression,\n        ActivePowerVariable,\n        devices,\n        device_model,\n    )\n    if get_use_slacks(device_model)\n        add_variables!(container, RateofChangeConstraintSlackUp, devices, D())\n        add_variables!(container, RateofChangeConstraintSlackDown, devices, D())\n    end\n\n    add_feedforward_arguments!(container, device_model, devices)\n    add_event_arguments!(container, devices, device_model, network_model)\n    return\nend\n\nfunction construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ModelConstructStage,\n    device_model::DeviceModel{T, <:AbstractThermalDispatchFormulation},\n    network_model::NetworkModel{<:PM.AbstractPowerModel},\n) where {T <: PSY.ThermalGen}\n    devices = get_available_components(device_model, sys)\n\n    add_constraints!(\n        container,\n        ActivePowerVariableLimitsConstraint,\n        ActivePowerRangeExpressionLB,\n        devices,\n        device_model,\n        network_model,\n    )\n    add_constraints!(\n        container,\n        ActivePowerVariableLimitsConstraint,\n        ActivePowerRangeExpressionUB,\n        devices,\n        device_model,\n        network_model,\n    )\n\n    add_constraints!(\n        container,\n        ReactivePowerVariableLimitsConstraint,\n        ReactivePowerVariable,\n        devices,\n        device_model,\n        network_model,\n    )\n\n    add_feedforward_constraints!(container, device_model, devices)\n\n    objective_function!(\n        container,\n        devices,\n        device_model,\n        get_network_formulation(network_model),\n    )\n    add_event_constraints!(container, devices, device_model, network_model)\n    add_constraint_dual!(container, sys, device_model)\n    return\nend\n\nfunction construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ArgumentConstructStage,\n    device_model::DeviceModel{T, D},\n    network_model::NetworkModel{<:PM.AbstractActivePowerModel},\n) where {\n    T <: PSY.ThermalGen,\n    D <: AbstractThermalDispatchFormulation,\n}\n    devices = get_available_components(device_model, sys)\n\n    add_variables!(container, ActivePowerVariable, devices, D())\n\n    _handle_common_thermal_parameters!(container, devices, device_model)\n\n    add_to_expression!(\n        container,\n        ActivePowerBalance,\n        ActivePowerVariable,\n        devices,\n        device_model,\n        network_model,\n    )\n\n    add_cost_expressions!(container, devices, device_model)\n\n    add_to_expression!(\n        container,\n        ActivePowerRangeExpressionLB,\n        ActivePowerVariable,\n        devices,\n        device_model,\n        network_model,\n    )\n    add_to_expression!(\n        container,\n        ActivePowerRangeExpressionUB,\n        ActivePowerVariable,\n        devices,\n        device_model,\n        network_model,\n    )\n    add_to_expression!(\n        container,\n        FuelConsumptionExpression,\n        ActivePowerVariable,\n        devices,\n        device_model,\n    )\n    if get_use_slacks(device_model)\n        add_variables!(container, RateofChangeConstraintSlackUp, devices, D())\n        add_variables!(container, RateofChangeConstraintSlackDown, devices, D())\n    end\n\n    add_feedforward_arguments!(container, device_model, devices)\n    add_event_arguments!(container, devices, device_model, network_model)\n    return\nend\n\nfunction construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ModelConstructStage,\n    device_model::DeviceModel{T, <:AbstractThermalDispatchFormulation},\n    network_model::NetworkModel{<:PM.AbstractActivePowerModel},\n) where {T <: PSY.ThermalGen}\n    devices = get_available_components(device_model, sys)\n\n    add_constraints!(\n        container,\n        ActivePowerVariableLimitsConstraint,\n        ActivePowerRangeExpressionLB,\n        devices,\n        device_model,\n        network_model,\n    )\n    add_constraints!(\n        container,\n        ActivePowerVariableLimitsConstraint,\n        ActivePowerRangeExpressionUB,\n        devices,\n        device_model,\n        network_model,\n    )\n\n    add_feedforward_constraints!(container, device_model, devices)\n    add_event_constraints!(container, devices, device_model, network_model)\n\n    objective_function!(\n        container,\n        devices,\n        device_model,\n        get_network_formulation(network_model),\n    )\n    return\nend\n\nfunction construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ArgumentConstructStage,\n    device_model::DeviceModel{PSY.ThermalMultiStart, ThermalMultiStartUnitCommitment},\n    network_model::NetworkModel{<:PM.AbstractPowerModel},\n)\n    devices = get_available_components(device_model, sys)\n\n    add_variables!(\n        container,\n        PowerAboveMinimumVariable,\n        devices,\n        ThermalMultiStartUnitCommitment(),\n    )\n    add_variables!(\n        container,\n        ReactivePowerVariable,\n        devices,\n        ThermalMultiStartUnitCommitment(),\n    )\n    add_variables!(container, OnVariable, devices, ThermalMultiStartUnitCommitment())\n    add_variables!(container, StopVariable, devices, ThermalMultiStartUnitCommitment())\n    add_variables!(container, StartVariable, devices, ThermalMultiStartUnitCommitment())\n    add_variables!(container, ColdStartVariable, devices, ThermalMultiStartUnitCommitment())\n    add_variables!(container, WarmStartVariable, devices, ThermalMultiStartUnitCommitment())\n    add_variables!(container, HotStartVariable, devices, ThermalMultiStartUnitCommitment())\n\n    add_variables!(container, TimeDurationOn, devices, ThermalMultiStartUnitCommitment())\n    add_variables!(container, TimeDurationOff, devices, ThermalMultiStartUnitCommitment())\n    add_variables!(container, PowerOutput, devices, ThermalMultiStartUnitCommitment())\n\n    initial_conditions!(container, devices, ThermalMultiStartUnitCommitment())\n\n    if haskey(get_time_series_names(device_model), ActivePowerTimeSeriesParameter)\n        add_parameters!(container, ActivePowerTimeSeriesParameter, devices, device_model)\n    end\n\n    _handle_common_thermal_parameters!(container, devices, device_model)\n\n    add_to_expression!(\n        container,\n        ActivePowerBalance,\n        PowerAboveMinimumVariable,\n        devices,\n        device_model,\n        network_model,\n    )\n    add_to_expression!(\n        container,\n        ActivePowerBalance,\n        OnVariable,\n        devices,\n        device_model,\n        network_model,\n    )\n\n    add_to_expression!(\n        container,\n        ReactivePowerBalance,\n        ReactivePowerVariable,\n        devices,\n        device_model,\n        network_model,\n    )\n\n    add_cost_expressions!(container, devices, device_model)\n\n    add_to_expression!(\n        container,\n        ActivePowerRangeExpressionLB,\n        PowerAboveMinimumVariable,\n        devices,\n        device_model,\n        network_model,\n    )\n    add_to_expression!(\n        container,\n        ActivePowerRangeExpressionUB,\n        PowerAboveMinimumVariable,\n        devices,\n        device_model,\n        network_model,\n    )\n    add_to_expression!(\n        container,\n        FuelConsumptionExpression,\n        PowerAboveMinimumVariable,\n        devices,\n        device_model,\n    )\n    if get_use_slacks(device_model)\n        add_variables!(\n            container,\n            RateofChangeConstraintSlackUp,\n            devices,\n            ThermalMultiStartUnitCommitment(),\n        )\n        add_variables!(\n            container,\n            RateofChangeConstraintSlackDown,\n            devices,\n            ThermalMultiStartUnitCommitment(),\n        )\n    end\n\n    add_feedforward_arguments!(container, device_model, devices)\n    add_event_arguments!(container, devices, device_model, network_model)\n    return\nend\n\nfunction construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ModelConstructStage,\n    device_model::DeviceModel{PSY.ThermalMultiStart, ThermalMultiStartUnitCommitment},\n    network_model::NetworkModel{<:PM.AbstractPowerModel},\n)\n    devices = get_available_components(device_model, sys)\n\n    add_constraints!(\n        container,\n        ActivePowerVariableLimitsConstraint,\n        ActivePowerRangeExpressionLB,\n        devices,\n        device_model,\n        network_model,\n    )\n    add_constraints!(\n        container,\n        ActivePowerVariableLimitsConstraint,\n        ActivePowerRangeExpressionUB,\n        devices,\n        device_model,\n        network_model,\n    )\n\n    add_constraints!(\n        container,\n        ReactivePowerVariableLimitsConstraint,\n        ReactivePowerVariable,\n        devices,\n        device_model,\n        network_model,\n    )\n    add_constraints!(container, CommitmentConstraint, devices, device_model, network_model)\n    add_constraints!(container, RampConstraint, devices, device_model, network_model)\n    add_constraints!(container, DurationConstraint, devices, device_model, network_model)\n    add_constraints!(\n        container,\n        StartupTimeLimitTemperatureConstraint,\n        devices,\n        device_model,\n        network_model,\n    )\n    add_constraints!(container, StartTypeConstraint, devices, device_model, network_model)\n    add_constraints!(\n        container,\n        StartupInitialConditionConstraint,\n        devices,\n        device_model,\n        network_model,\n    )\n    add_constraints!(\n        container,\n        ActiveRangeICConstraint,\n        devices,\n        device_model,\n        network_model,\n    )\n\n    if haskey(get_time_series_names(device_model), ActivePowerTimeSeriesParameter)\n        add_constraints!(\n            container,\n            ActivePowerVariableTimeSeriesLimitsConstraint,\n            ActivePowerRangeExpressionUB,\n            devices,\n            device_model,\n            network_model,\n        )\n    end\n\n    add_feedforward_constraints!(container, device_model, devices)\n\n    objective_function!(\n        container,\n        devices,\n        device_model,\n        get_network_formulation(network_model),\n    )\n    add_event_constraints!(container, devices, device_model, network_model)\n    add_constraint_dual!(container, sys, device_model)\n    return\nend\n\nfunction construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ArgumentConstructStage,\n    device_model::DeviceModel{PSY.ThermalMultiStart, ThermalMultiStartUnitCommitment},\n    network_model::NetworkModel{<:PM.AbstractActivePowerModel},\n)\n    devices = get_available_components(device_model, sys)\n\n    add_variables!(\n        container,\n        PowerAboveMinimumVariable,\n        devices,\n        ThermalMultiStartUnitCommitment(),\n    )\n    add_variables!(container, OnVariable, devices, ThermalMultiStartUnitCommitment())\n    add_variables!(container, StopVariable, devices, ThermalMultiStartUnitCommitment())\n    add_variables!(container, StartVariable, devices, ThermalMultiStartUnitCommitment())\n    add_variables!(container, ColdStartVariable, devices, ThermalMultiStartUnitCommitment())\n    add_variables!(container, WarmStartVariable, devices, ThermalMultiStartUnitCommitment())\n    add_variables!(container, HotStartVariable, devices, ThermalMultiStartUnitCommitment())\n\n    add_variables!(container, TimeDurationOn, devices, ThermalMultiStartUnitCommitment())\n    add_variables!(container, TimeDurationOff, devices, ThermalMultiStartUnitCommitment())\n    add_variables!(container, PowerOutput, devices, ThermalMultiStartUnitCommitment())\n\n    if haskey(get_time_series_names(device_model), ActivePowerTimeSeriesParameter)\n        add_parameters!(container, ActivePowerTimeSeriesParameter, devices, device_model)\n    end\n\n    _handle_common_thermal_parameters!(container, devices, device_model)\n\n    add_to_expression!(\n        container,\n        ActivePowerBalance,\n        PowerAboveMinimumVariable,\n        devices,\n        device_model,\n        network_model,\n    )\n\n    add_to_expression!(\n        container,\n        ActivePowerBalance,\n        OnVariable,\n        devices,\n        device_model,\n        network_model,\n    )\n\n    add_cost_expressions!(container, devices, device_model)\n\n    add_to_expression!(\n        container,\n        ActivePowerRangeExpressionLB,\n        PowerAboveMinimumVariable,\n        devices,\n        device_model,\n        network_model,\n    )\n    add_to_expression!(\n        container,\n        ActivePowerRangeExpressionUB,\n        PowerAboveMinimumVariable,\n        devices,\n        device_model,\n        network_model,\n    )\n    add_to_expression!(\n        container,\n        FuelConsumptionExpression,\n        PowerAboveMinimumVariable,\n        devices,\n        device_model,\n    )\n    if get_use_slacks(device_model)\n        add_variables!(\n            container,\n            RateofChangeConstraintSlackUp,\n            devices,\n            ThermalMultiStartUnitCommitment(),\n        )\n        add_variables!(\n            container,\n            RateofChangeConstraintSlackDown,\n            devices,\n            ThermalMultiStartUnitCommitment(),\n        )\n    end\n\n    add_feedforward_arguments!(container, device_model, devices)\n    add_event_arguments!(container, devices, device_model, network_model)\n    return\nend\n\nfunction construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ModelConstructStage,\n    device_model::DeviceModel{PSY.ThermalMultiStart, ThermalMultiStartUnitCommitment},\n    network_model::NetworkModel{<:PM.AbstractActivePowerModel},\n)\n    devices = get_available_components(device_model, sys)\n\n    initial_conditions!(container, devices, ThermalMultiStartUnitCommitment())\n\n    add_constraints!(\n        container,\n        ActivePowerVariableLimitsConstraint,\n        ActivePowerRangeExpressionLB,\n        devices,\n        device_model,\n        network_model,\n    )\n    add_constraints!(\n        container,\n        ActivePowerVariableLimitsConstraint,\n        ActivePowerRangeExpressionUB,\n        devices,\n        device_model,\n        network_model,\n    )\n\n    add_constraints!(container, CommitmentConstraint, devices, device_model, network_model)\n    add_constraints!(container, RampConstraint, devices, device_model, network_model)\n    add_constraints!(container, DurationConstraint, devices, device_model, network_model)\n    add_constraints!(\n        container,\n        StartupTimeLimitTemperatureConstraint,\n        devices,\n        device_model,\n        network_model,\n    )\n    add_constraints!(container, StartTypeConstraint, devices, device_model, network_model)\n    add_constraints!(\n        container,\n        StartupInitialConditionConstraint,\n        devices,\n        device_model,\n        network_model,\n    )\n    add_constraints!(\n        container,\n        ActiveRangeICConstraint,\n        devices,\n        device_model,\n        network_model,\n    )\n\n    if haskey(get_time_series_names(device_model), ActivePowerTimeSeriesParameter)\n        add_constraints!(\n            container,\n            ActivePowerVariableTimeSeriesLimitsConstraint,\n            ActivePowerRangeExpressionUB,\n            devices,\n            device_model,\n            network_model,\n        )\n    end\n\n    add_feedforward_constraints!(container, device_model, devices)\n\n    objective_function!(\n        container,\n        devices,\n        device_model,\n        get_network_formulation(network_model),\n    )\n    add_event_constraints!(container, devices, device_model, network_model)\n    add_constraint_dual!(container, sys, device_model)\n    return\nend\n\nfunction construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ArgumentConstructStage,\n    device_model::DeviceModel{T, ThermalCompactUnitCommitment},\n    network_model::NetworkModel{<:PM.AbstractPowerModel},\n) where {T <: PSY.ThermalGen}\n    devices = get_available_components(device_model, sys)\n\n    add_variables!(\n        container,\n        PowerAboveMinimumVariable,\n        devices,\n        ThermalCompactUnitCommitment(),\n    )\n    add_variables!(\n        container,\n        ReactivePowerVariable,\n        devices,\n        ThermalCompactUnitCommitment(),\n    )\n    add_variables!(container, OnVariable, devices, ThermalCompactUnitCommitment())\n    add_variables!(container, StartVariable, devices, ThermalCompactUnitCommitment())\n    add_variables!(container, StopVariable, devices, ThermalCompactUnitCommitment())\n\n    add_variables!(container, TimeDurationOn, devices, ThermalCompactUnitCommitment())\n    add_variables!(container, TimeDurationOff, devices, ThermalCompactUnitCommitment())\n    add_variables!(container, PowerOutput, devices, ThermalCompactUnitCommitment())\n\n    initial_conditions!(container, devices, ThermalCompactUnitCommitment())\n\n    if haskey(get_time_series_names(device_model), ActivePowerTimeSeriesParameter)\n        add_parameters!(container, ActivePowerTimeSeriesParameter, devices, device_model)\n    end\n\n    _handle_common_thermal_parameters!(container, devices, device_model)\n\n    add_to_expression!(\n        container,\n        ActivePowerBalance,\n        PowerAboveMinimumVariable,\n        devices,\n        device_model,\n        network_model,\n    )\n    add_to_expression!(\n        container,\n        ActivePowerBalance,\n        OnVariable,\n        devices,\n        device_model,\n        network_model,\n    )\n\n    add_cost_expressions!(container, devices, device_model)\n\n    add_to_expression!(\n        container,\n        ActivePowerRangeExpressionLB,\n        PowerAboveMinimumVariable,\n        devices,\n        device_model,\n        network_model,\n    )\n    add_to_expression!(\n        container,\n        ActivePowerRangeExpressionUB,\n        PowerAboveMinimumVariable,\n        devices,\n        device_model,\n        network_model,\n    )\n    add_to_expression!(\n        container,\n        FuelConsumptionExpression,\n        PowerAboveMinimumVariable,\n        devices,\n        device_model,\n    )\n    if get_use_slacks(device_model)\n        add_variables!(\n            container,\n            RateofChangeConstraintSlackUp,\n            devices,\n            ThermalCompactUnitCommitment(),\n        )\n        add_variables!(\n            container,\n            RateofChangeConstraintSlackDown,\n            devices,\n            ThermalCompactUnitCommitment(),\n        )\n    end\n\n    add_feedforward_arguments!(container, device_model, devices)\n    add_event_arguments!(container, devices, device_model, network_model)\n    return\nend\n\nfunction construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ModelConstructStage,\n    device_model::DeviceModel{T, ThermalCompactUnitCommitment},\n    network_model::NetworkModel{<:PM.AbstractPowerModel},\n) where {T <: PSY.ThermalGen}\n    devices = get_available_components(device_model, sys)\n\n    add_constraints!(\n        container,\n        ActivePowerVariableLimitsConstraint,\n        ActivePowerRangeExpressionLB,\n        devices,\n        device_model,\n        network_model,\n    )\n    add_constraints!(\n        container,\n        ActivePowerVariableLimitsConstraint,\n        ActivePowerRangeExpressionUB,\n        devices,\n        device_model,\n        network_model,\n    )\n\n    add_constraints!(\n        container,\n        ReactivePowerVariableLimitsConstraint,\n        ReactivePowerVariable,\n        devices,\n        device_model,\n        network_model,\n    )\n    add_constraints!(container, CommitmentConstraint, devices, device_model, network_model)\n    add_constraints!(container, RampConstraint, devices, device_model, network_model)\n    add_constraints!(container, DurationConstraint, devices, device_model, network_model)\n    if haskey(get_time_series_names(device_model), ActivePowerTimeSeriesParameter)\n        add_constraints!(\n            container,\n            ActivePowerVariableTimeSeriesLimitsConstraint,\n            ActivePowerRangeExpressionUB,\n            devices,\n            device_model,\n            network_model,\n        )\n    end\n\n    add_feedforward_constraints!(container, device_model, devices)\n\n    objective_function!(\n        container,\n        devices,\n        device_model,\n        get_network_formulation(network_model),\n    )\n    add_event_constraints!(container, devices, device_model, network_model)\n    add_constraint_dual!(container, sys, device_model)\n    return\nend\n\nfunction construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ArgumentConstructStage,\n    device_model::DeviceModel{T, ThermalCompactUnitCommitment},\n    network_model::NetworkModel{<:PM.AbstractActivePowerModel},\n) where {T <: PSY.ThermalGen}\n    devices = get_available_components(device_model, sys)\n\n    add_variables!(\n        container,\n        PowerAboveMinimumVariable,\n        devices,\n        ThermalCompactUnitCommitment(),\n    )\n    add_variables!(container, OnVariable, devices, ThermalCompactUnitCommitment())\n    add_variables!(container, StartVariable, devices, ThermalCompactUnitCommitment())\n    add_variables!(container, StopVariable, devices, ThermalCompactUnitCommitment())\n\n    add_variables!(container, TimeDurationOn, devices, ThermalCompactUnitCommitment())\n    add_variables!(container, TimeDurationOff, devices, ThermalCompactUnitCommitment())\n    add_variables!(container, PowerOutput, devices, ThermalCompactUnitCommitment())\n\n    initial_conditions!(container, devices, ThermalCompactUnitCommitment())\n\n    if haskey(get_time_series_names(device_model), ActivePowerTimeSeriesParameter)\n        add_parameters!(container, ActivePowerTimeSeriesParameter, devices, device_model)\n    end\n\n    _handle_common_thermal_parameters!(container, devices, device_model)\n\n    add_to_expression!(\n        container,\n        ActivePowerBalance,\n        PowerAboveMinimumVariable,\n        devices,\n        device_model,\n        network_model,\n    )\n    add_to_expression!(\n        container,\n        ActivePowerBalance,\n        OnVariable,\n        devices,\n        device_model,\n        network_model,\n    )\n\n    add_cost_expressions!(container, devices, device_model)\n\n    add_to_expression!(\n        container,\n        ActivePowerRangeExpressionLB,\n        PowerAboveMinimumVariable,\n        devices,\n        device_model,\n        network_model,\n    )\n    add_to_expression!(\n        container,\n        ActivePowerRangeExpressionUB,\n        PowerAboveMinimumVariable,\n        devices,\n        device_model,\n        network_model,\n    )\n    add_to_expression!(\n        container,\n        FuelConsumptionExpression,\n        PowerAboveMinimumVariable,\n        devices,\n        device_model,\n    )\n    if get_use_slacks(device_model)\n        add_variables!(\n            container,\n            RateofChangeConstraintSlackUp,\n            devices,\n            ThermalCompactUnitCommitment(),\n        )\n        add_variables!(\n            container,\n            RateofChangeConstraintSlackDown,\n            devices,\n            ThermalCompactUnitCommitment(),\n        )\n    end\n\n    add_feedforward_arguments!(container, device_model, devices)\n    add_event_arguments!(container, devices, device_model, network_model)\n    return\nend\n\nfunction construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ModelConstructStage,\n    device_model::DeviceModel{T, ThermalCompactUnitCommitment},\n    network_model::NetworkModel{<:PM.AbstractActivePowerModel},\n) where {T <: PSY.ThermalGen}\n    devices = get_available_components(device_model, sys)\n\n    add_constraints!(\n        container,\n        ActivePowerVariableLimitsConstraint,\n        ActivePowerRangeExpressionLB,\n        devices,\n        device_model,\n        network_model,\n    )\n    add_constraints!(\n        container,\n        ActivePowerVariableLimitsConstraint,\n        ActivePowerRangeExpressionUB,\n        devices,\n        device_model,\n        network_model,\n    )\n\n    add_constraints!(container, CommitmentConstraint, devices, device_model, network_model)\n    add_constraints!(container, RampConstraint, devices, device_model, network_model)\n    add_constraints!(container, DurationConstraint, devices, device_model, network_model)\n    if haskey(get_time_series_names(device_model), ActivePowerTimeSeriesParameter)\n        add_constraints!(\n            container,\n            ActivePowerVariableTimeSeriesLimitsConstraint,\n            ActivePowerRangeExpressionUB,\n            devices,\n            device_model,\n            network_model,\n        )\n    end\n\n    add_feedforward_constraints!(container, device_model, devices)\n\n    objective_function!(\n        container,\n        devices,\n        device_model,\n        get_network_formulation(network_model),\n    )\n    add_event_constraints!(container, devices, device_model, network_model)\n    add_constraint_dual!(container, sys, device_model)\n    return\nend\n\nfunction construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ArgumentConstructStage,\n    device_model::DeviceModel{T, ThermalBasicCompactUnitCommitment},\n    network_model::NetworkModel{<:PM.AbstractPowerModel},\n) where {T <: PSY.ThermalGen}\n    devices = get_available_components(device_model, sys)\n\n    add_variables!(\n        container,\n        PowerAboveMinimumVariable,\n        devices,\n        ThermalBasicCompactUnitCommitment(),\n    )\n    add_variables!(\n        container,\n        ReactivePowerVariable,\n        devices,\n        ThermalBasicCompactUnitCommitment(),\n    )\n    add_variables!(container, OnVariable, devices, ThermalBasicCompactUnitCommitment())\n    add_variables!(container, StartVariable, devices, ThermalBasicCompactUnitCommitment())\n    add_variables!(container, StopVariable, devices, ThermalBasicCompactUnitCommitment())\n\n    add_variables!(container, PowerOutput, devices, ThermalBasicCompactUnitCommitment())\n    add_variables!(container, TimeDurationOn, devices, ThermalBasicCompactUnitCommitment())\n    add_variables!(container, TimeDurationOff, devices, ThermalBasicCompactUnitCommitment())\n\n    initial_conditions!(container, devices, ThermalBasicCompactUnitCommitment())\n\n    if haskey(get_time_series_names(device_model), ActivePowerTimeSeriesParameter)\n        add_parameters!(container, ActivePowerTimeSeriesParameter, devices, device_model)\n    end\n\n    _handle_common_thermal_parameters!(container, devices, device_model)\n\n    add_to_expression!(\n        container,\n        ActivePowerBalance,\n        PowerAboveMinimumVariable,\n        devices,\n        device_model,\n        network_model,\n    )\n    add_to_expression!(\n        container,\n        ActivePowerBalance,\n        OnVariable,\n        devices,\n        device_model,\n        network_model,\n    )\n\n    add_cost_expressions!(container, devices, device_model)\n\n    add_to_expression!(\n        container,\n        ActivePowerRangeExpressionLB,\n        PowerAboveMinimumVariable,\n        devices,\n        device_model,\n        network_model,\n    )\n    add_to_expression!(\n        container,\n        ActivePowerRangeExpressionUB,\n        PowerAboveMinimumVariable,\n        devices,\n        device_model,\n        network_model,\n    )\n    add_to_expression!(\n        container,\n        FuelConsumptionExpression,\n        PowerAboveMinimumVariable,\n        devices,\n        device_model,\n    )\n    if get_use_slacks(device_model)\n        add_variables!(\n            container,\n            RateofChangeConstraintSlackUp,\n            devices,\n            ThermalBasicCompactUnitCommitment(),\n        )\n        add_variables!(\n            container,\n            RateofChangeConstraintSlackDown,\n            devices,\n            ThermalBasicCompactUnitCommitment(),\n        )\n    end\n\n    add_feedforward_arguments!(container, device_model, devices)\n    add_event_arguments!(container, devices, device_model, network_model)\n    return\nend\n\nfunction construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ModelConstructStage,\n    device_model::DeviceModel{T, ThermalBasicCompactUnitCommitment},\n    network_model::NetworkModel{<:PM.AbstractPowerModel},\n) where {T <: PSY.ThermalGen}\n    devices = get_available_components(device_model, sys)\n\n    add_constraints!(\n        container,\n        ActivePowerVariableLimitsConstraint,\n        ActivePowerRangeExpressionLB,\n        devices,\n        device_model,\n        network_model,\n    )\n    add_constraints!(\n        container,\n        ActivePowerVariableLimitsConstraint,\n        ActivePowerRangeExpressionUB,\n        devices,\n        device_model,\n        network_model,\n    )\n\n    add_constraints!(\n        container,\n        ReactivePowerVariableLimitsConstraint,\n        ReactivePowerVariable,\n        devices,\n        device_model,\n        network_model,\n    )\n    add_constraints!(container, CommitmentConstraint, devices, device_model, network_model)\n    if haskey(get_time_series_names(device_model), ActivePowerTimeSeriesParameter)\n        add_constraints!(\n            container,\n            ActivePowerVariableTimeSeriesLimitsConstraint,\n            ActivePowerRangeExpressionUB,\n            devices,\n            device_model,\n            network_model,\n        )\n    end\n\n    add_feedforward_constraints!(container, device_model, devices)\n\n    objective_function!(\n        container,\n        devices,\n        device_model,\n        get_network_formulation(network_model),\n    )\n    add_event_constraints!(container, devices, device_model, network_model)\n    add_constraint_dual!(container, sys, device_model)\n    return\nend\n\nfunction construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ArgumentConstructStage,\n    device_model::DeviceModel{T, ThermalBasicCompactUnitCommitment},\n    network_model::NetworkModel{<:PM.AbstractActivePowerModel},\n) where {T <: PSY.ThermalGen}\n    devices = get_available_components(device_model, sys)\n\n    add_variables!(\n        container,\n        PowerAboveMinimumVariable,\n        devices,\n        ThermalBasicCompactUnitCommitment(),\n    )\n    add_variables!(container, OnVariable, devices, ThermalBasicCompactUnitCommitment())\n    add_variables!(container, StartVariable, devices, ThermalBasicCompactUnitCommitment())\n    add_variables!(container, StopVariable, devices, ThermalBasicCompactUnitCommitment())\n\n    add_variables!(container, PowerOutput, devices, ThermalBasicCompactUnitCommitment())\n    add_variables!(container, TimeDurationOn, devices, ThermalBasicCompactUnitCommitment())\n    add_variables!(container, TimeDurationOff, devices, ThermalBasicCompactUnitCommitment())\n    initial_conditions!(container, devices, ThermalBasicCompactUnitCommitment())\n\n    if haskey(get_time_series_names(device_model), ActivePowerTimeSeriesParameter)\n        add_parameters!(container, ActivePowerTimeSeriesParameter, devices, device_model)\n    end\n\n    _handle_common_thermal_parameters!(container, devices, device_model)\n\n    add_to_expression!(\n        container,\n        ActivePowerBalance,\n        PowerAboveMinimumVariable,\n        devices,\n        device_model,\n        network_model,\n    )\n    add_to_expression!(\n        container,\n        ActivePowerBalance,\n        OnVariable,\n        devices,\n        device_model,\n        network_model,\n    )\n\n    add_cost_expressions!(container, devices, device_model)\n\n    add_to_expression!(\n        container,\n        ActivePowerRangeExpressionLB,\n        PowerAboveMinimumVariable,\n        devices,\n        device_model,\n        network_model,\n    )\n    add_to_expression!(\n        container,\n        ActivePowerRangeExpressionUB,\n        PowerAboveMinimumVariable,\n        devices,\n        device_model,\n        network_model,\n    )\n    add_to_expression!(\n        container,\n        FuelConsumptionExpression,\n        PowerAboveMinimumVariable,\n        devices,\n        device_model,\n    )\n    if get_use_slacks(device_model)\n        add_variables!(\n            container,\n            RateofChangeConstraintSlackUp,\n            devices,\n            ThermalBasicCompactUnitCommitment(),\n        )\n        add_variables!(\n            container,\n            RateofChangeConstraintSlackDown,\n            devices,\n            ThermalBasicCompactUnitCommitment(),\n        )\n    end\n    add_feedforward_arguments!(container, device_model, devices)\n    add_event_arguments!(container, devices, device_model, network_model)\n    return\nend\n\nfunction construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ModelConstructStage,\n    device_model::DeviceModel{T, ThermalBasicCompactUnitCommitment},\n    network_model::NetworkModel{<:PM.AbstractActivePowerModel},\n) where {T <: PSY.ThermalGen}\n    devices = get_available_components(device_model, sys)\n\n    add_constraints!(\n        container,\n        ActivePowerVariableLimitsConstraint,\n        ActivePowerRangeExpressionLB,\n        devices,\n        device_model,\n        network_model,\n    )\n    add_constraints!(\n        container,\n        ActivePowerVariableLimitsConstraint,\n        ActivePowerRangeExpressionUB,\n        devices,\n        device_model,\n        network_model,\n    )\n\n    add_constraints!(container, CommitmentConstraint, devices, device_model, network_model)\n    if haskey(get_time_series_names(device_model), ActivePowerTimeSeriesParameter)\n        add_constraints!(\n            container,\n            ActivePowerVariableTimeSeriesLimitsConstraint,\n            ActivePowerRangeExpressionUB,\n            devices,\n            device_model,\n            network_model,\n        )\n    end\n\n    add_feedforward_constraints!(container, device_model, devices)\n\n    objective_function!(\n        container,\n        devices,\n        device_model,\n        get_network_formulation(network_model),\n    )\n    add_event_constraints!(container, devices, device_model, network_model)\n    add_constraint_dual!(container, sys, device_model)\n    return\nend\n\nfunction construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ArgumentConstructStage,\n    device_model::DeviceModel{T, ThermalCompactDispatch},\n    network_model::NetworkModel{<:PM.AbstractPowerModel},\n) where {T <: PSY.ThermalGen}\n    devices = get_available_components(device_model, sys)\n\n    add_variables!(container, PowerAboveMinimumVariable, devices, ThermalCompactDispatch())\n    add_variables!(container, ReactivePowerVariable, devices, ThermalCompactDispatch())\n\n    add_variables!(container, PowerOutput, devices, ThermalCompactDispatch())\n\n    add_parameters!(container, OnStatusParameter, devices, device_model)\n\n    _handle_common_thermal_parameters!(container, devices, device_model)\n\n    add_feedforward_arguments!(container, device_model, devices)\n\n    initial_conditions!(container, devices, ThermalCompactDispatch())\n\n    add_to_expression!(\n        container,\n        ActivePowerBalance,\n        PowerAboveMinimumVariable,\n        devices,\n        device_model,\n        network_model,\n    )\n\n    add_cost_expressions!(container, devices, device_model)\n\n    add_to_expression!(\n        container,\n        ReactivePowerBalance,\n        ReactivePowerVariable,\n        devices,\n        device_model,\n        network_model,\n    )\n    add_to_expression!(\n        container,\n        ActivePowerBalance,\n        OnStatusParameter,\n        devices,\n        device_model,\n        network_model,\n    )\n    add_to_expression!(\n        container,\n        ActivePowerRangeExpressionLB,\n        PowerAboveMinimumVariable,\n        devices,\n        device_model,\n        network_model,\n    )\n    add_to_expression!(\n        container,\n        ActivePowerRangeExpressionUB,\n        PowerAboveMinimumVariable,\n        devices,\n        device_model,\n        network_model,\n    )\n    add_to_expression!(\n        container,\n        FuelConsumptionExpression,\n        PowerAboveMinimumVariable,\n        devices,\n        device_model,\n    )\n    if get_use_slacks(device_model)\n        add_variables!(\n            container,\n            RateofChangeConstraintSlackUp,\n            devices,\n            ThermalCompactDispatch(),\n        )\n        add_variables!(\n            container,\n            RateofChangeConstraintSlackDown,\n            devices,\n            ThermalCompactDispatch(),\n        )\n    end\n    add_event_arguments!(container, devices, device_model, network_model)\n    return\nend\n\nfunction construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ModelConstructStage,\n    device_model::DeviceModel{T, ThermalCompactDispatch},\n    network_model::NetworkModel{<:PM.AbstractPowerModel},\n) where {T <: PSY.ThermalGen}\n    devices = get_available_components(device_model, sys)\n\n    add_constraints!(\n        container,\n        ActivePowerVariableLimitsConstraint,\n        ActivePowerRangeExpressionLB,\n        devices,\n        device_model,\n        network_model,\n    )\n    add_constraints!(\n        container,\n        ActivePowerVariableLimitsConstraint,\n        ActivePowerRangeExpressionUB,\n        devices,\n        device_model,\n        network_model,\n    )\n\n    add_constraints!(\n        container,\n        ReactivePowerVariableLimitsConstraint,\n        ReactivePowerVariable,\n        devices,\n        device_model,\n        network_model,\n    )\n    add_constraints!(container, RampConstraint, devices, device_model, network_model)\n\n    add_feedforward_constraints!(container, device_model, devices)\n\n    objective_function!(\n        container,\n        devices,\n        device_model,\n        get_network_formulation(network_model),\n    )\n    add_event_constraints!(container, devices, device_model, network_model)\n    add_constraint_dual!(container, sys, device_model)\n    return\nend\n\nfunction construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ArgumentConstructStage,\n    device_model::DeviceModel{T, ThermalCompactDispatch},\n    network_model::NetworkModel{<:PM.AbstractActivePowerModel},\n) where {T <: PSY.ThermalGen}\n    devices = get_available_components(device_model, sys)\n\n    add_variables!(container, PowerAboveMinimumVariable, devices, ThermalCompactDispatch())\n\n    add_variables!(container, PowerOutput, devices, ThermalCompactDispatch())\n\n    add_parameters!(container, OnStatusParameter, devices, device_model)\n\n    _handle_common_thermal_parameters!(container, devices, device_model)\n\n    add_feedforward_arguments!(container, device_model, devices)\n\n    add_to_expression!(\n        container,\n        ActivePowerBalance,\n        PowerAboveMinimumVariable,\n        devices,\n        device_model,\n        network_model,\n    )\n\n    add_to_expression!(\n        container,\n        ActivePowerBalance,\n        OnStatusParameter,\n        devices,\n        device_model,\n        network_model,\n    )\n\n    initial_conditions!(container, devices, ThermalCompactDispatch())\n\n    add_cost_expressions!(container, devices, device_model)\n\n    add_to_expression!(\n        container,\n        ActivePowerRangeExpressionLB,\n        PowerAboveMinimumVariable,\n        devices,\n        device_model,\n        network_model,\n    )\n    add_to_expression!(\n        container,\n        ActivePowerRangeExpressionUB,\n        PowerAboveMinimumVariable,\n        devices,\n        device_model,\n        network_model,\n    )\n    add_to_expression!(\n        container,\n        FuelConsumptionExpression,\n        PowerAboveMinimumVariable,\n        devices,\n        device_model,\n    )\n    if get_use_slacks(device_model)\n        add_variables!(\n            container,\n            RateofChangeConstraintSlackUp,\n            devices,\n            ThermalCompactDispatch(),\n        )\n        add_variables!(\n            container,\n            RateofChangeConstraintSlackDown,\n            devices,\n            ThermalCompactDispatch(),\n        )\n    end\n    add_event_arguments!(container, devices, device_model, network_model)\n    return\nend\n\nfunction construct_device!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ModelConstructStage,\n    device_model::DeviceModel{T, ThermalCompactDispatch},\n    network_model::NetworkModel{<:PM.AbstractActivePowerModel},\n) where {T <: PSY.ThermalGen}\n    devices = get_available_components(device_model, sys)\n\n    add_constraints!(\n        container,\n        ActivePowerVariableLimitsConstraint,\n        ActivePowerRangeExpressionLB,\n        devices,\n        device_model,\n        network_model,\n    )\n    add_constraints!(\n        container,\n        ActivePowerVariableLimitsConstraint,\n        ActivePowerRangeExpressionUB,\n        devices,\n        device_model,\n        network_model,\n    )\n\n    add_constraints!(container, RampConstraint, devices, device_model, network_model)\n\n    add_feedforward_constraints!(container, device_model, devices)\n\n    objective_function!(\n        container,\n        devices,\n        device_model,\n        get_network_formulation(network_model),\n    )\n\n    add_event_constraints!(container, devices, device_model, network_model)\n    add_constraint_dual!(container, sys, device_model)\n    return\nend\n"
  },
  {
    "path": "src/devices_models/devices/AC_branches.jl",
    "content": "\n# Note: Any future concrete formulation requires the definition of\n\n# construct_device!(\n#     ::OptimizationContainer,\n#     ::PSY.System,\n#     ::DeviceModel{<:PSY.ACTransmission, MyNewFormulation},\n#     ::Union{Type{CopperPlatePowerModel}, Type{AreaBalancePowerModel}},\n# ) = nothing\n\n#\n\n# Not implemented yet\n# struct TapControl <: AbstractBranchFormulation end\n\n#################################### Branch Variables ##################################################\n# Because of the way we integrate with PowerModels, most of the time PowerSimulations will create variables\n# for the branch flows either in AC or DC.\n\n#! format: off\nget_variable_binary(::FlowActivePowerVariable, ::Type{<:PSY.ACTransmission}, ::AbstractBranchFormulation,) = false\nget_variable_binary(::PhaseShifterAngle, ::Type{PSY.PhaseShiftingTransformer}, ::AbstractBranchFormulation,) = false\n\nget_parameter_multiplier(::FixValueParameter, ::PSY.ACTransmission, ::AbstractBranchFormulation) = 1.0\nget_parameter_multiplier(::LowerBoundValueParameter, ::PSY.ACTransmission, ::AbstractBranchFormulation) = 1.0\nget_parameter_multiplier(::UpperBoundValueParameter, ::PSY.ACTransmission, ::AbstractBranchFormulation) = 1.0\n\nget_variable_multiplier(::PhaseShifterAngle, d::PSY.PhaseShiftingTransformer, ::PhaseAngleControl) = 1.0/PSY.get_x(d)\n\nget_multiplier_value(::AbstractDynamicBranchRatingTimeSeriesParameter, d::PSY.ACTransmission, ::StaticBranch) = PSY.get_rating(d)\nget_multiplier_value(::AbstractDynamicBranchRatingTimeSeriesParameter, d::PNM.BranchesParallel, ::StaticBranch) = PNM.get_equivalent_rating(d)\n\n\nget_initial_conditions_device_model(::OperationModel, ::DeviceModel{T, U}) where {T <: PSY.ACTransmission, U <: AbstractBranchFormulation} = DeviceModel(T, U)\n\n#### Properties of slack variables\nget_variable_binary(::FlowActivePowerSlackUpperBound, ::Type{<:PSY.ACTransmission}, ::AbstractBranchFormulation,) = false\nget_variable_binary(::FlowActivePowerSlackLowerBound, ::Type{<:PSY.ACTransmission}, ::AbstractBranchFormulation,) = false\n# These two methods are defined to avoid ambiguities\nget_variable_upper_bound(::FlowActivePowerSlackUpperBound, ::PSY.ACTransmission, ::AbstractBranchFormulation) = nothing\nget_variable_lower_bound(::FlowActivePowerSlackUpperBound, ::PSY.ACTransmission, ::AbstractBranchFormulation) = 0.0\nget_variable_upper_bound(::FlowActivePowerSlackLowerBound, ::PSY.ACTransmission, ::AbstractBranchFormulation) = nothing\nget_variable_lower_bound(::FlowActivePowerSlackLowerBound, ::PSY.ACTransmission, ::AbstractBranchFormulation) = 0.0\nget_variable_upper_bound(::FlowActivePowerVariable, ::PNM.BranchesSeries, ::AbstractBranchFormulation) = nothing\nget_variable_lower_bound(::FlowActivePowerVariable, ::PNM.BranchesSeries, ::AbstractBranchFormulation) = nothing\nget_variable_upper_bound(::FlowActivePowerVariable, ::PNM.BranchesParallel, ::AbstractBranchFormulation) = nothing\nget_variable_lower_bound(::FlowActivePowerVariable, ::PNM.BranchesParallel, ::AbstractBranchFormulation) = nothing\nget_variable_upper_bound(::FlowActivePowerVariable, ::PNM.ThreeWindingTransformerWinding, ::AbstractBranchFormulation) = nothing\nget_variable_lower_bound(::FlowActivePowerVariable, ::PNM.ThreeWindingTransformerWinding, ::AbstractBranchFormulation) = nothing\n\n#! format: on\nfunction get_default_time_series_names(\n    ::Type{U},\n    ::Type{V},\n) where {U <: PSY.ACTransmission, V <: AbstractBranchFormulation}\n    return Dict{Type{<:TimeSeriesParameter}, String}(\n        DynamicBranchRatingTimeSeriesParameter => \"dynamic_line_ratings\",\n    )\nend\n\nfunction get_default_attributes(\n    ::Type{U},\n    ::Type{V},\n) where {U <: PSY.ACTransmission, V <: AbstractBranchFormulation}\n    return Dict{String, Any}()\nend\n#################################### Flow Variable Bounds ##################################################\n\nfunction add_variables!(\n    container::OptimizationContainer,\n    ::Type{T},\n    network_model::NetworkModel{<:AbstractPTDFModel},\n    devices::IS.FlattenIteratorWrapper{U},\n    formulation::AbstractBranchFormulation,\n) where {\n    T <: AbstractACActivePowerFlow,\n    U <: PSY.ACTransmission}\n    time_steps = get_time_steps(container)\n    net_reduction_data = network_model.network_reduction\n    branch_names = get_branch_argument_variable_axis(net_reduction_data, devices)\n    reduced_branch_tracker = get_reduced_branch_tracker(network_model)\n    all_branch_maps_by_type = PNM.get_all_branch_maps_by_type(net_reduction_data)\n\n    variable_container = add_variable_container!(\n        container,\n        T(),\n        U,\n        branch_names,\n        time_steps,\n    )\n\n    for (name, (arc, reduction)) in PNM.get_name_to_arc_map(net_reduction_data, U)\n        # TODO: entry is not type stable here, it can return any type ACTransmission.\n        # It might have performance implications. Possibly separate this into other functions\n        reduction_entry = all_branch_maps_by_type[reduction][U][arc]\n        has_entry, tracker_container = search_for_reduced_branch_argument!(\n            reduced_branch_tracker,\n            arc,\n            T,\n        )\n        if has_entry\n            @assert !isempty(tracker_container) name arc reduction\n        end\n        ub = get_variable_upper_bound(T(), reduction_entry, formulation)\n        lb = get_variable_lower_bound(T(), reduction_entry, formulation)\n        for t in time_steps\n            if !has_entry\n                tracker_container[t] = JuMP.@variable(\n                    get_jump_model(container),\n                    base_name = \"$(T)_$(U)_$(reduction)_{$(name), $(t)}\",\n                )\n                ub !== nothing && JuMP.set_upper_bound(tracker_container[t], ub)\n                lb !== nothing && JuMP.set_lower_bound(tracker_container[t], lb)\n            end\n            variable_container[name, t] = tracker_container[t]\n        end\n    end\n    return\nend\n\nfunction add_variables!(\n    container::OptimizationContainer,\n    ::Type{T},\n    network_model::NetworkModel{<:PM.AbstractPowerModel},\n    devices::IS.FlattenIteratorWrapper{U},\n    formulation::AbstractBranchFormulation,\n) where {\n    T <: AbstractACActivePowerFlow,\n    U <: PSY.ACTransmission}\n    net_reduction_data = network_model.network_reduction\n    time_steps = get_time_steps(container)\n    branch_names = get_branch_argument_variable_axis(net_reduction_data, devices)\n    reduced_branch_tracker = get_reduced_branch_tracker(network_model)\n    all_branch_maps_by_type = PNM.get_all_branch_maps_by_type(net_reduction_data)\n\n    variable_container = add_variable_container!(\n        container,\n        T(),\n        U,\n        branch_names,\n        time_steps,\n    )\n\n    for (name, (arc, reduction)) in PNM.get_name_to_arc_map(net_reduction_data, U)\n        reduction_entry = all_branch_maps_by_type[reduction][U][arc]\n        has_entry, tracker_container = search_for_reduced_branch_argument!(\n            reduced_branch_tracker,\n            arc,\n            T,\n        )\n        if has_entry\n            @assert !isempty(tracker_container) name arc reduction\n        end\n        ub = get_variable_upper_bound(T(), reduction_entry, formulation)\n        lb = get_variable_lower_bound(T(), reduction_entry, formulation)\n        for t in time_steps\n            if !has_entry\n                tracker_container[t] = JuMP.@variable(\n                    get_jump_model(container),\n                    base_name = \"$(T)_$(U)_$(reduction)_{$(name), $(t)}\",\n                )\n                ub !== nothing && JuMP.set_upper_bound(tracker_container[t], ub)\n                lb !== nothing && JuMP.set_lower_bound(tracker_container[t], lb)\n            end\n            variable_container[name, t] = tracker_container[t]\n        end\n    end\n    return\nend\n\nfunction add_variables!(\n    ::OptimizationContainer,\n    ::Type{T},\n    network_model::NetworkModel{<:AbstractPTDFModel},\n    devices::IS.FlattenIteratorWrapper{U},\n    formulation::StaticBranchUnbounded,\n) where {\n    T <: AbstractACActivePowerFlow,\n    U <: PSY.ACTransmission}\n    @debug \"PTDF Branch Flows with StaticBranchUnbounded do not require flow variables $T. Flow values are given by PTDFBranchFlow expression.\"\n    return\nend\n\nfunction add_variables!(\n    container::OptimizationContainer,\n    ::Type{S},\n    network_model::NetworkModel{CopperPlatePowerModel},\n    devices::IS.FlattenIteratorWrapper{T},\n    formulation::U,\n) where {\n    S <: AbstractACActivePowerFlow,\n    T <: PSY.ACTransmission,\n    U <: AbstractBranchFormulation,\n}\n    @debug \"AC Branches of type $(T) do not require flow variables $S in CopperPlatePowerModel.\"\n    return\nend\n\nfunction _get_flow_variable_vector(\n    container::OptimizationContainer,\n    ::NetworkModel{<:PM.AbstractDCPModel},\n    ::Type{B},\n) where {B <: PSY.ACTransmission}\n    return [get_variable(container, FlowActivePowerVariable(), B)]\nend\n\nfunction _get_flow_variable_vector(\n    container::OptimizationContainer,\n    ::NetworkModel{<:PM.AbstractPowerModel},\n    ::Type{B},\n) where {B <: PSY.ACTransmission}\n    return [\n        get_variable(container, FlowActivePowerFromToVariable(), B),\n        get_variable(container, FlowActivePowerToFromVariable(), B),\n    ]\nend\n\nfunction branch_rate_bounds!(\n    container::OptimizationContainer,\n    ::DeviceModel{B, T},\n    network_model::NetworkModel{<:PM.AbstractPowerModel},\n) where {B <: PSY.ACTransmission, T <: AbstractBranchFormulation}\n    time_steps = get_time_steps(container)\n    net_reduction_data = get_network_reduction(network_model)\n    all_branch_maps_by_type = net_reduction_data.all_branch_maps_by_type\n    for var in _get_flow_variable_vector(container, network_model, B)\n        for (name, (arc, reduction)) in PNM.get_name_to_arc_map(net_reduction_data, B)\n            # TODO: entry is not type stable here, it can return any type ACTransmission.\n            # It might have performance implications. Possibly separate this into other functions\n            reduction_entry = all_branch_maps_by_type[reduction][B][arc]\n            # Use the same limit values as FlowRateConstraint for consistency.\n            limits = get_min_max_limits(reduction_entry, FlowRateConstraint, T)\n            for t in time_steps\n                @assert limits.min <= limits.max \"Infeasible rate limits for branch $(name)\"\n                JuMP.set_upper_bound(var[name, t], limits.max)\n                JuMP.set_lower_bound(var[name, t], limits.min)\n            end\n        end\n    end\n    return\nend\n\n################################## PWL Loss Variables ##################################\n\nfunction _check_pwl_loss_model(devices)\n    first_loss = PSY.get_loss(first(devices))\n    first_loss_type = typeof(first_loss)\n    for d in devices\n        loss = PSY.get_loss(d)\n        if !isa(loss, first_loss_type)\n            error(\n                \"Not all TwoTerminal HVDC lines have the same loss model data. Check that all loss models are LinearCurve or PiecewiseIncrementalCurve\",\n            )\n        end\n        if isa(first_loss, PSY.PiecewiseIncrementalCurve)\n            len_first_loss = length(PSY.get_slopes(first_loss))\n            len_loss = length(PSY.get_slopes(loss))\n            if len_first_loss != len_loss\n                error(\n                    \"Different length of PWL segments for TwoTerminal HVDC losses are not supported. Check that all HVDC data have the same amount of PWL segments.\",\n                )\n            end\n        end\n    end\n    return\nend\n\n################################## Rate Limits constraint_infos ############################\n\nfunction get_rating(double_circuit::PNM.BranchesParallel)\n    return sum([PSY.get_rating(circuit) for circuit in double_circuit])\nend\nfunction get_rating(series_chain::PNM.BranchesSeries)\n    return minimum([get_rating(segment) for segment in series_chain])\nend\nfunction get_rating(device::T) where {T <: PSY.ACTransmission}\n    return PSY.get_rating(device)\nend\nfunction get_rating(\n    device::PNM.ThreeWindingTransformerWinding{T},\n) where {T <: PSY.ThreeWindingTransformer}\n    return PNM.get_equivalent_rating(device)\nend\n\n\"\"\"\nMin and max limits for Abstract Branch Formulation\n\"\"\"\nfunction get_min_max_limits(\n    double_circuit::PNM.BranchesParallel{<:PSY.ACTransmission},\n    constraint_type::Type{<:ConstraintType},\n    branch_formulation::Type{<:AbstractBranchFormulation},\n) #  -> Union{Nothing, NamedTuple{(:min, :max), Tuple{Float64, Float64}}}\n    min_max_by_circuit = [\n        get_min_max_limits(device, constraint_type, branch_formulation) for\n        device in double_circuit\n    ]\n    min_by_circuit = [x.min for x in min_max_by_circuit]\n    max_by_circuit = [x.max for x in min_max_by_circuit]\n    # Limit by most restictive circuit:\n    return (min = maximum(min_by_circuit), max = minimum(max_by_circuit))\nend\n\n\"\"\"\nMin and max limits for Abstract Branch Formulation\n\"\"\"\nfunction get_min_max_limits(\n    transformer_entry::PNM.ThreeWindingTransformerWinding,\n    constraint_type::Type{<:ConstraintType},\n    branch_formulation::Type{<:AbstractBranchFormulation},\n) #  -> Union{Nothing, NamedTuple{(:min, :max), Tuple{Float64, Float64}}}\n    transformer = PNM.get_transformer(transformer_entry)\n    winding_number = PNM.get_winding_number(transformer_entry)\n    if winding_number == 1\n        limits = (\n            min = -1 * PSY.get_rating_primary(transformer),\n            max = PSY.get_rating_primary(transformer),\n        )\n    elseif winding_number == 2\n        limits = (\n            min = -1 * PSY.get_rating_secondary(transformer),\n            max = PSY.get_rating_secondary(transformer),\n        )\n    elseif winding_number == 3\n        limits = (\n            min = -1 * PSY.get_rating_tertiary(transformer),\n            max = PSY.get_rating_tertiary(transformer),\n        )\n    end\n    return limits\nend\n\n\"\"\"\nMin and max limits for Abstract Branch Formulation\n\"\"\"\nfunction get_min_max_limits(\n    series_chain::PNM.BranchesSeries,\n    constraint_type::Type{<:ConstraintType},\n    branch_formulation::Type{<:AbstractBranchFormulation},\n) #  -> Union{Nothing, NamedTuple{(:min, :max), Tuple{Float64, Float64}}}\n    min_max_by_segment = [\n        get_min_max_limits(segment, constraint_type, branch_formulation) for\n        segment in series_chain\n    ]\n    min_by_segment = [x.min for x in min_max_by_segment]\n    max_by_segment = [x.max for x in min_max_by_segment]\n    # Limit by most restictive segment:\n    return (min = maximum(min_by_segment), max = minimum(max_by_segment))\nend\n\n\"\"\"\nMin and max limits for Abstract Branch Formulation\n\"\"\"\nfunction get_min_max_limits(\n    device::PSY.ACTransmission,\n    ::Type{<:ConstraintType},\n    ::Type{<:AbstractBranchFormulation},\n) #  -> Union{Nothing, NamedTuple{(:min, :max), Tuple{Float64, Float64}}}\n    return (\n        min = -1 * PNM.get_equivalent_rating(device),\n        max = PNM.get_equivalent_rating(device),\n    )\nend\n\n\"\"\"\nMin and max limits for Abstract Branch Formulation\n\"\"\"\nfunction get_min_max_limits(\n    ::PSY.PhaseShiftingTransformer,\n    ::Type{PhaseAngleControlLimit},\n    ::Type{PhaseAngleControl},\n) #  -> Union{Nothing, NamedTuple{(:min, :max), Tuple{Float64, Float64}}}\n    return (min = -π / 2, max = π / 2)\nend\n\nfunction _add_flow_rate_constraint!(\n    container::OptimizationContainer,\n    ::Type{T},\n    arc::Tuple{Int, Int},\n    use_slacks::Bool,\n    con_lb::DenseAxisArray,\n    con_ub::DenseAxisArray,\n    var::DenseAxisArray,\n    branch_maps_by_type::Dict,\n    name::String,\n) where {T <: PSY.ACTransmission}\n    reduction_entry = branch_maps_by_type[arc]\n    time_steps = get_time_steps(container)\n    if use_slacks\n        slack_ub = get_variable(container, FlowActivePowerSlackUpperBound(), T)[name, :]\n        slack_lb = get_variable(container, FlowActivePowerSlackLowerBound(), T)[name, :]\n    end\n    limits = get_min_max_limits(reduction_entry, FlowRateConstraint, StaticBranch)\n    for t in time_steps\n        con_ub[name, t] =\n            JuMP.@constraint(\n                get_jump_model(container),\n                var[name, t] - (use_slacks ? slack_ub[t] : 0.0) <= limits.max\n            )\n        con_lb[name, t] =\n            JuMP.@constraint(\n                get_jump_model(container),\n                var[name, t] + (use_slacks ? slack_lb[t] : 0.0) >= limits.min\n            )\n    end\n    return\nend\n\n\"\"\"\nAdd branch rate limit constraints for ACBranch with AbstractActivePowerModel\n\"\"\"\nfunction add_constraints!(\n    container::OptimizationContainer,\n    cons_type::Type{FlowRateConstraint},\n    devices::IS.FlattenIteratorWrapper{T},\n    device_model::DeviceModel{T, U},\n    network_model::NetworkModel{V},\n) where {\n    T <: PSY.ACTransmission,\n    U <: AbstractBranchFormulation,\n    V <: PM.AbstractActivePowerModel,\n}\n    time_steps = get_time_steps(container)\n    net_reduction_data = network_model.network_reduction\n    reduced_branch_tracker = get_reduced_branch_tracker(network_model)\n    branch_names = get_branch_argument_constraint_axis(\n        net_reduction_data,\n        reduced_branch_tracker,\n        devices,\n        cons_type,\n    )\n    all_branch_maps_by_type = PNM.get_all_branch_maps_by_type(net_reduction_data)\n\n    con_lb =\n        add_constraints_container!(\n            container,\n            cons_type(),\n            T,\n            branch_names,\n            time_steps;\n            meta = \"lb\",\n        )\n    con_ub =\n        add_constraints_container!(\n            container,\n            cons_type(),\n            T,\n            branch_names,\n            time_steps;\n            meta = \"ub\",\n        )\n\n    array = get_variable(container, FlowActivePowerVariable(), T)\n\n    use_slacks = get_use_slacks(device_model)\n    if use_slacks\n        slack_ub = get_variable(container, FlowActivePowerSlackUpperBound(), T)\n        slack_lb = get_variable(container, FlowActivePowerSlackLowerBound(), T)\n    end\n    for (name, (arc, reduction)) in\n        get_constraint_map_by_type(reduced_branch_tracker)[FlowRateConstraint][T]\n        _add_flow_rate_constraint!(\n            container,\n            T,\n            arc,\n            use_slacks,\n            con_lb,\n            con_ub,\n            array,\n            all_branch_maps_by_type[reduction][T],\n            name,\n        )\n    end\n    return\nend\n\nfunction _add_flow_rate_constraint_with_parameters!(\n    container::OptimizationContainer,\n    ::Type{T},\n    arc::Tuple{Int, Int},\n    use_slacks::Bool,\n    con_lb::DenseAxisArray,\n    con_ub::DenseAxisArray,\n    var::DenseAxisArray,\n    branch_maps_by_type::Dict,\n    name::String,\n    ts_name::String,\n) where {T <: PSY.ACTransmission}\n    time_steps = get_time_steps(container)\n    if use_slacks\n        slack_ub = get_variable(container, FlowActivePowerSlackUpperBound(), T)[name, :]\n        slack_lb = get_variable(container, FlowActivePowerSlackLowerBound(), T)[name, :]\n    end\n    param_container =\n        get_parameter(container, DynamicBranchRatingTimeSeriesParameter(), T)\n    param = get_parameter_column_refs(param_container, name)\n    mult = get_multiplier_array(param_container)[name, :]\n\n    for t in time_steps\n        @debug \"Dynamic Branch Rating applied for branch $(name) at time step $(t)\"\n        con_ub[name, t] =\n            JuMP.@constraint(\n                get_jump_model(container),\n                var[name, t] - (use_slacks ? slack_ub[t] : 0.0) <= param[t] * mult[t]\n            )\n        con_lb[name, t] =\n            JuMP.@constraint(\n                get_jump_model(container),\n                var[name, t] + (use_slacks ? slack_lb[t] : 0.0) >=\n                -1.0 * param[t] * mult[t]\n            )\n    end\n    return\nend\n\nfunction add_constraints!(\n    container::OptimizationContainer,\n    cons_type::Type{FlowRateConstraint},\n    devices::IS.FlattenIteratorWrapper{T},\n    device_model::DeviceModel{T, U},\n    network_model::NetworkModel{V},\n) where {\n    T <: PSY.ACTransmission,\n    U <: AbstractBranchFormulation,\n    V <: AbstractPTDFModel,\n}\n    time_steps = get_time_steps(container)\n    net_reduction_data = network_model.network_reduction\n    reduced_branch_tracker = get_reduced_branch_tracker(network_model)\n    branch_names = get_branch_argument_constraint_axis(\n        net_reduction_data,\n        reduced_branch_tracker,\n        devices,\n        cons_type,\n    )\n    all_branch_maps_by_type = PNM.get_all_branch_maps_by_type(net_reduction_data)\n\n    con_lb =\n        add_constraints_container!(\n            container,\n            cons_type(),\n            T,\n            branch_names,\n            time_steps;\n            meta = \"lb\",\n        )\n    con_ub =\n        add_constraints_container!(\n            container,\n            cons_type(),\n            T,\n            branch_names,\n            time_steps;\n            meta = \"ub\",\n        )\n\n    array = get_expression(container, PTDFBranchFlow(), T)\n\n    use_slacks = get_use_slacks(device_model)\n    if use_slacks\n        slack_ub = get_variable(container, FlowActivePowerSlackUpperBound(), T)\n        slack_lb = get_variable(container, FlowActivePowerSlackLowerBound(), T)\n    end\n    for (name, (arc, reduction)) in\n        get_constraint_map_by_type(reduced_branch_tracker)[FlowRateConstraint][T]\n        # TODO: entry is not type stable here, it can return any type ACTransmission.\n        # It might have performance implications. Possibly separate this into other functions\n        reduction_entry = all_branch_maps_by_type[reduction][T][arc]\n        limits = get_min_max_limits(reduction_entry, FlowRateConstraint, U)\n        for t in time_steps\n            con_ub[name, t] =\n                JuMP.@constraint(get_jump_model(container),\n                    array[name, t] -\n                    (use_slacks ? slack_ub[name, t] : 0.0) <=\n                    limits.max)\n            con_lb[name, t] =\n                JuMP.@constraint(get_jump_model(container),\n                    array[name, t] +\n                    (use_slacks ? slack_lb[name, t] : 0.0) >=\n                    limits.min)\n        end\n    end\n    return\nend\n\nfunction add_flow_rate_constraint_with_parameters!(\n    container::OptimizationContainer,\n    cons_type::Type{FlowRateConstraint},\n    devices::IS.FlattenIteratorWrapper{T},\n    device_model::DeviceModel{T, U},\n    network_model::NetworkModel{V},\n) where {\n    T <: PSY.ACTransmission,\n    U <: StaticBranch,\n    V <: AbstractPTDFModel,\n}\n    time_steps = get_time_steps(container)\n    net_reduction_data = network_model.network_reduction\n    reduced_branch_tracker = get_reduced_branch_tracker(network_model)\n\n    branch_names = get_branch_argument_constraint_axis(\n        net_reduction_data,\n        reduced_branch_tracker,\n        devices,\n        cons_type,\n    )\n\n    all_branch_maps_by_type = PNM.get_all_branch_maps_by_type(net_reduction_data)\n\n    con_lb =\n        add_constraints_container!(\n            container,\n            cons_type(),\n            T,\n            branch_names,\n            time_steps;\n            meta = \"lb\",\n        )\n    con_ub =\n        add_constraints_container!(\n            container,\n            cons_type(),\n            T,\n            branch_names,\n            time_steps;\n            meta = \"ub\",\n        )\n\n    var_array = get_expression(container, PTDFBranchFlow(), T)\n\n    ts_name = get_time_series_names(device_model)[DynamicBranchRatingTimeSeriesParameter]\n    ts_type = get_default_time_series_type(container)\n    use_slacks = get_use_slacks(device_model)\n    for (name, (arc, reduction)) in\n        get_constraint_map_by_type(reduced_branch_tracker)[FlowRateConstraint][T]\n        if PNM.has_time_series(\n            all_branch_maps_by_type[reduction][T][arc],\n            ts_type,\n            ts_name,\n        )\n            _add_flow_rate_constraint_with_parameters!(\n                container,\n                T,\n                arc,\n                use_slacks,\n                con_lb,\n                con_ub,\n                var_array,\n                all_branch_maps_by_type[reduction][T],\n                name,\n                ts_name,\n            )\n        else\n            _add_flow_rate_constraint!(\n                container,\n                T,\n                arc,\n                use_slacks,\n                con_lb,\n                con_ub,\n                var_array,\n                all_branch_maps_by_type[reduction][T],\n                name,\n            )\n        end\n    end\n    return\nend\n\n\"\"\"\nAdd rate limit from to constraints for ACBranch with AbstractPowerModel\n\"\"\"\nfunction add_constraints!(\n    container::OptimizationContainer,\n    cons_type::Type{FlowRateConstraintFromTo},\n    devices::IS.FlattenIteratorWrapper{B},\n    device_model::DeviceModel{B, <:AbstractBranchFormulation},\n    network_model::NetworkModel{T},\n) where {B <: PSY.ACTransmission, T <: PM.AbstractPowerModel}\n    reduced_branch_tracker = get_reduced_branch_tracker(network_model)\n    net_reduction_data = get_network_reduction(network_model)\n    all_branch_maps_by_type = net_reduction_data.all_branch_maps_by_type\n    device_names = get_branch_argument_constraint_axis(\n        net_reduction_data,\n        reduced_branch_tracker,\n        devices,\n        cons_type,\n    )\n    time_steps = get_time_steps(container)\n    var1 = get_variable(container, FlowActivePowerFromToVariable(), B)\n    var2 = get_variable(container, FlowReactivePowerFromToVariable(), B)\n    add_constraints_container!(\n        container,\n        cons_type(),\n        B,\n        device_names,\n        time_steps,\n    )\n    constraint = get_constraint(container, cons_type(), B)\n\n    use_slacks = get_use_slacks(device_model)\n    if use_slacks\n        slack_ub = get_variable(container, FlowActivePowerSlackUpperBound(), B)\n    end\n    for (name, (arc, reduction)) in\n        get_constraint_map_by_type(reduced_branch_tracker)[FlowRateConstraintFromTo][B]\n        # TODO: entry is not type stable here, it can return any type ACTransmission.\n        # It might have performance implications. Possibly separate this into other functions\n        reduction_entry = all_branch_maps_by_type[reduction][B][arc]\n        branch_rate = get_rating(reduction_entry)\n        for t in time_steps\n            constraint[name, t] = JuMP.@constraint(\n                get_jump_model(container),\n                var1[name, t]^2 + var2[name, t]^2 -\n                (use_slacks ? slack_ub[name, t] : 0.0) <= branch_rate^2\n            )\n        end\n    end\n    return\nend\n\n\"\"\"\nAdd rate limit to from constraints for ACBranch with AbstractPowerModel\n\"\"\"\nfunction add_constraints!(\n    container::OptimizationContainer,\n    cons_type::Type{FlowRateConstraintToFrom},\n    devices::IS.FlattenIteratorWrapper{B},\n    device_model::DeviceModel{B, <:AbstractBranchFormulation},\n    network_model::NetworkModel{T},\n) where {B <: PSY.ACTransmission, T <: PM.AbstractPowerModel}\n    reduced_branch_tracker = get_reduced_branch_tracker(network_model)\n    net_reduction_data = get_network_reduction(network_model)\n    all_branch_maps_by_type = net_reduction_data.all_branch_maps_by_type\n    time_steps = get_time_steps(container)\n    device_names = get_branch_argument_constraint_axis(\n        net_reduction_data,\n        reduced_branch_tracker,\n        devices,\n        cons_type,\n    )\n    var1 = get_variable(container, FlowActivePowerToFromVariable(), B)\n    var2 = get_variable(container, FlowReactivePowerToFromVariable(), B)\n    add_constraints_container!(\n        container,\n        cons_type(),\n        B,\n        device_names,\n        time_steps,\n    )\n    constraint = get_constraint(container, cons_type(), B)\n    use_slacks = get_use_slacks(device_model)\n    if use_slacks\n        slack_ub = get_variable(container, FlowActivePowerSlackUpperBound(), B)\n    end\n    for (name, (arc, reduction)) in\n        get_constraint_map_by_type(reduced_branch_tracker)[FlowRateConstraintToFrom][B]\n        # TODO: entry is not type stable here, it can return any type ACTransmission.\n        # It might have performance implications. Possibly separate this into other functions\n        reduction_entry = all_branch_maps_by_type[reduction][B][arc]\n        branch_rate = get_rating(reduction_entry)\n        for t in time_steps\n            constraint[name, t] = JuMP.@constraint(\n                get_jump_model(container),\n                var1[name, t]^2 + var2[name, t]^2 -\n                (use_slacks ? slack_ub[name, t] : 0.0) <= branch_rate^2\n            )\n        end\n    end\n    return\nend\n\nfunction _make_flow_expressions!(\n    name::String,\n    time_steps::UnitRange{Int},\n    ptdf_col::Vector{Float64},\n    nodal_balance_expressions::Matrix{JuMP.AffExpr},\n)\n    @debug \"Making Flow Expression on thread $(Threads.threadid()) for branch $name\"\n    nz_idx = [i for i in eachindex(ptdf_col) if abs(ptdf_col[i]) > PTDF_ZERO_TOL]\n    hint = length(nz_idx)\n    expressions = Vector{JuMP.AffExpr}(undef, length(time_steps))\n    for t in time_steps\n        acc = get_hinted_aff_expr(hint)\n        @inbounds for i in nz_idx\n            JuMP.add_to_expression!(acc, ptdf_col[i], nodal_balance_expressions[i, t])\n        end\n        expressions[t] = acc\n    end\n    return name, expressions\nend\n\nfunction _make_flow_expressions!(\n    name::String,\n    time_steps::UnitRange{Int},\n    ptdf_col::SparseArrays.SparseVector{Float64, Int},\n    nodal_balance_expressions::Matrix{JuMP.AffExpr},\n)\n    @debug \"Making Flow Expression on thread $(Threads.threadid()) for branch $name\"\n    nz_idx = SparseArrays.nonzeroinds(ptdf_col)\n    nz_val = SparseArrays.nonzeros(ptdf_col)\n    hint = length(nz_idx)\n    expressions = Vector{JuMP.AffExpr}(undef, length(time_steps))\n    for t in time_steps\n        acc = get_hinted_aff_expr(hint)\n        @inbounds for k in eachindex(nz_idx)\n            JuMP.add_to_expression!(\n                acc,\n                nz_val[k],\n                nodal_balance_expressions[nz_idx[k], t],\n            )\n        end\n        expressions[t] = acc\n    end\n    return name, expressions\nend\n\nfunction _add_expression_to_container!(\n    branch_flow_expr::JuMPAffineExpressionDArrayStringInt,\n    jump_model::JuMP.Model,\n    time_steps::UnitRange{Int},\n    ptdf_col::AbstractVector{Float64},\n    nodal_balance_expressions::JuMPAffineExpressionDArrayIntInt,\n    reduction_entry::T,\n    branches::Vector{String},\n) where {T <: PSY.ACTransmission}\n    name = PSY.get_name(reduction_entry)\n    if name in branches\n        branch_flow_expr[name, :] .= _make_flow_expressions!(\n            name,\n            time_steps,\n            ptdf_col,\n            nodal_balance_expressions.data,\n        )\n    end\n    return\nend\n\nfunction _add_expression_to_container!(\n    branch_flow_expr::JuMPAffineExpressionDArrayStringInt,\n    jump_model::JuMP.Model,\n    time_steps::UnitRange{Int},\n    ptdf_col::AbstractVector{Float64},\n    nodal_balance_expressions::JuMPAffineExpressionDArrayIntInt,\n    reduction_entry::Vector{Any},\n    branches::Vector{String},\n)\n    names = _get_branch_names(reduction_entry)\n    for name in names\n        if name in branches\n            branch_flow_expr[name, :] .= _make_flow_expressions!(\n                name,\n                time_steps,\n                ptdf_col,\n                nodal_balance_expressions.data,\n            )\n            #Only one constraint added per arc; once it is found can return\n            return\n        end\n    end\nend\n\nfunction _add_expression_to_container!(\n    branch_flow_expr::JuMPAffineExpressionDArrayStringInt,\n    jump_model::JuMP.Model,\n    time_steps::UnitRange{Int},\n    ptdf_col::AbstractVector{Float64},\n    nodal_balance_expressions::JuMPAffineExpressionDArrayIntInt,\n    reduction_entry::Set{PSY.ACTransmission},\n    branches::Vector{String},\n)\n    names = _get_branch_names(reduction_entry)\n    for name in names\n        if name in branches\n            branch_flow_expr[name, :] .= _make_flow_expressions!(\n                name,\n                time_steps,\n                ptdf_col,\n                nodal_balance_expressions.data,\n            )\n            #Only one constraint added per arc; once it is found can return\n            return\n        end\n    end\nend\n\nfunction add_expressions!(\n    container::OptimizationContainer,\n    ::Type{PTDFBranchFlow},\n    devices::IS.FlattenIteratorWrapper{B},\n    model::DeviceModel{B, <:AbstractBranchFormulation},\n    network_model::NetworkModel{<:AbstractPTDFModel},\n) where {B <: PSY.ACTransmission}\n    time_steps = get_time_steps(container)\n    ptdf = get_PTDF_matrix(network_model)\n    net_reduction_data = network_model.network_reduction\n    # This might need to be changed to something else\n    branch_names = get_branch_argument_variable_axis(net_reduction_data, devices)\n    # Needs to be a vector to use multi-threading\n    name_to_arc_map = collect(PNM.get_name_to_arc_map(net_reduction_data, B))\n    nodal_balance_expressions = get_expression(\n        container,\n        ActivePowerBalance(),\n        PSY.ACBus,\n    )\n\n    branch_flow_expr = add_expression_container!(container,\n        PTDFBranchFlow(),\n        B,\n        branch_names,\n        time_steps,\n    )\n\n    jump_model = get_jump_model(container)\n\n    tasks = map(name_to_arc_map) do pair\n        (name, (arc, _)) = pair\n        ptdf_col = ptdf[arc, :]\n        Threads.@spawn _make_flow_expressions!(\n            name,\n            time_steps,\n            ptdf_col,\n            nodal_balance_expressions.data,\n        )\n    end\n    for task in tasks\n        name, expressions = fetch(task)\n        branch_flow_expr[name, :] .= expressions\n    end\n    #= Leaving serial code commented out for debugging purposes in the future\n    for (name, (arc, reduction)) in name_to_arc_map\n        reduction_entry = all_branch_maps_by_type[reduction][B][arc]\n        network_reduction_map = all_branch_maps_by_type[map]\n        !haskey(network_reduction_map, branch_Type) && continue\n        for (arc_tuple, reduction_entry) in network_reduction_map[branch_Type]\n            ptdf_col = ptdf[arc_tuple, :]\n            _add_expression_to_container!(\n                branch_flow_expr,\n                jump_model,\n                time_steps,\n                ptdf_col,\n                nodal_balance_expressions,\n                reduction_entry,\n                name,\n            )\n        end\n    end\n    =#\n    return\nend\n\n\"\"\"\nAdd network flow constraints for ACBranch and NetworkModel with <: AbstractPTDFModel\n\"\"\"\nfunction add_constraints!(\n    container::OptimizationContainer,\n    cons_type::Type{NetworkFlowConstraint},\n    devices::IS.FlattenIteratorWrapper{T},\n    device_model::DeviceModel{T, StaticBranchBounds},\n    network_model::NetworkModel{<:AbstractPTDFModel},\n) where {T <: PSY.ACTransmission}\n    time_steps = get_time_steps(container)\n    branch_flow_expr = get_expression(container, PTDFBranchFlow(), T)\n    flow_variables = get_variable(container, FlowActivePowerVariable(), T)\n    net_reduction_data = network_model.network_reduction\n    reduced_branch_tracker = get_reduced_branch_tracker(network_model)\n    branches = get_branch_argument_constraint_axis(\n        net_reduction_data,\n        reduced_branch_tracker,\n        devices,\n        cons_type,\n    )\n    branch_flow = add_constraints_container!(\n        container,\n        NetworkFlowConstraint(),\n        T,\n        branches,\n        time_steps,\n    )\n    jump_model = get_jump_model(container)\n\n    use_slacks = get_use_slacks(device_model)\n    if use_slacks\n        slack_ub = get_variable(container, FlowActivePowerSlackUpperBound(), T)\n        slack_lb = get_variable(container, FlowActivePowerSlackLowerBound(), T)\n    end\n\n    for name in branches\n        for t in time_steps\n            branch_flow[name, t] = JuMP.@constraint(\n                jump_model,\n                branch_flow_expr[name, t] -\n                flow_variables[name, t]\n                ==\n                (use_slacks ? slack_ub[name, t] - slack_lb[name, t] : 0.0)\n            )\n        end\n    end\n    return\nend\n\nfunction add_constraints!(\n    ::OptimizationContainer,\n    cons_type::Type{NetworkFlowConstraint},\n    ::IS.FlattenIteratorWrapper{B},\n    ::DeviceModel{B, T},\n    ::NetworkModel{<:AbstractPTDFModel},\n) where {B <: PSY.ACTransmission, T <: Union{StaticBranchUnbounded, StaticBranch}}\n    @debug \"PTDF Branch Flows with $T do not require network flow constraints $cons_type. Flow values are given by PTDFBranchFlow.\"\n    return\nend\n\n\"\"\"\nAdd network flow constraints for PhaseShiftingTransformer and NetworkModel with <: AbstractPTDFModel\n\"\"\"\nfunction add_constraints!(\n    container::OptimizationContainer,\n    ::Type{NetworkFlowConstraint},\n    devices::IS.FlattenIteratorWrapper{T},\n    model::DeviceModel{T, PhaseAngleControl},\n    network_model::NetworkModel{<:AbstractPTDFModel},\n) where {T <: PSY.PhaseShiftingTransformer}\n    ptdf = get_PTDF_matrix(network_model)\n    branches = PSY.get_name.(devices)\n    time_steps = get_time_steps(container)\n    branch_flow = add_constraints_container!(\n        container,\n        NetworkFlowConstraint(),\n        T,\n        branches,\n        time_steps,\n    )\n    nodal_balance_expressions = get_expression(container, ActivePowerBalance(), PSY.ACBus)\n    flow_variables = get_variable(container, FlowActivePowerVariable(), T)\n    angle_variables = get_variable(container, PhaseShifterAngle(), T)\n    jump_model = get_jump_model(container)\n    for br in devices\n        arc = PNM.get_arc_tuple(br)\n        name = PSY.get_name(br)\n        ptdf_col = ptdf[arc, :]\n        inv_x = 1 / PSY.get_x(br)\n        for t in time_steps\n            branch_flow[name, t] = JuMP.@constraint(\n                jump_model,\n                sum(\n                    ptdf_col[i] * nodal_balance_expressions.data[i, t] for\n                    i in 1:length(ptdf_col)\n                ) + inv_x * angle_variables[name, t] - flow_variables[name, t] == 0.0\n            )\n        end\n    end\n    return\nend\n\n\"\"\"\nMin and max limits for monitored line\n\"\"\"\nfunction get_min_max_limits(\n    device::PSY.MonitoredLine,\n    ::Type{<:ConstraintType},\n    ::Type{T},\n) where {T <: AbstractBranchFormulation}\n    if PSY.get_flow_limits(device).to_from != PSY.get_flow_limits(device).from_to\n        @warn(\n            \"Flow limits in Line $(PSY.get_name(device)) aren't equal. The minimum will be used in formulation $(T)\"\n        )\n    end\n    limit = min(\n        PSY.get_rating(device),\n        PSY.get_flow_limits(device).to_from,\n        PSY.get_flow_limits(device).from_to,\n    )\n    minmax = (min = -1 * limit, max = limit)\n    return minmax\nend\n\n############################## Flow Limits Constraints #####################################\n\"\"\"\nAdd branch flow constraints for monitored lines with DC Power Model\n\"\"\"\nfunction add_constraints!(\n    container::OptimizationContainer,\n    ::Type{FlowLimitConstraint},\n    devices::IS.FlattenIteratorWrapper{T},\n    model::DeviceModel{T, U},\n    ::NetworkModel{V},\n) where {\n    T <: Union{PSY.PhaseShiftingTransformer, PSY.MonitoredLine},\n    U <: AbstractBranchFormulation,\n    V <: PM.AbstractDCPModel,\n}\n    add_range_constraints!(\n        container,\n        FlowLimitConstraint,\n        FlowActivePowerVariable,\n        devices,\n        model,\n        V,\n    )\n    return\nend\n\n\"\"\"\nDon't add branch flow constraints for monitored lines if formulation is StaticBranchUnbounded\n\"\"\"\nfunction add_constraints!(\n    ::OptimizationContainer,\n    ::Type{FlowRateConstraintFromTo},\n    devices::IS.FlattenIteratorWrapper{T},\n    model::DeviceModel{T, U},\n    ::NetworkModel{V},\n) where {\n    T <: PSY.MonitoredLine,\n    U <: StaticBranchUnbounded,\n    V <: PM.AbstractActivePowerModel,\n}\n    return\nend\n\n\"\"\"\nMin and max limits for flow limit from-to constraint\n\"\"\"\nfunction get_min_max_limits(\n    device::PSY.MonitoredLine,\n    ::Type{FlowLimitFromToConstraint},\n    ::Type{<:AbstractBranchFormulation},\n)\n    if PSY.get_flow_limits(device).to_from != PSY.get_flow_limits(device).from_to\n        @warn(\n            \"Flow limits in Line $(PSY.get_name(device)) aren't equal. The minimum will be used in formulation $(T)\"\n        )\n    end\n    return (\n        min = -1 * PSY.get_flow_limits(device).from_to,\n        max = PSY.get_flow_limits(device).from_to,\n    )\nend\n\n\"\"\"\nMin and max limits for flow limit to-from constraint\n\"\"\"\nfunction get_min_max_limits(\n    device::PSY.MonitoredLine,\n    ::Type{FlowLimitToFromConstraint},\n    ::Type{<:AbstractBranchFormulation},\n)\n    if PSY.get_flow_limits(device).to_from != PSY.get_flow_limits(device).from_to\n        @warn(\n            \"Flow limits in Line $(PSY.get_name(device)) aren't equal. The minimum will be used in formulation $(T)\"\n        )\n    end\n    return (\n        min = -1 * PSY.get_flow_limits(device).to_from,\n        max = PSY.get_flow_limits(device).to_from,\n    )\nend\n\n\"\"\"\nDon't add branch flow constraints for monitored lines if formulation is StaticBranchUnbounded\n\"\"\"\nfunction add_constraints!(\n    ::OptimizationContainer,\n    ::Type{FlowLimitToFromConstraint},\n    devices::IS.FlattenIteratorWrapper{T},\n    model::DeviceModel{T, U},\n    ::NetworkModel{V},\n) where {\n    T <: PSY.MonitoredLine,\n    U <: StaticBranchUnbounded,\n    V <: PM.AbstractActivePowerModel,\n}\n    return\nend\n\n\"\"\"\nAdd phase angle limits for phase shifters\n\"\"\"\nfunction add_constraints!(\n    container::OptimizationContainer,\n    ::Type{PhaseAngleControlLimit},\n    devices::IS.FlattenIteratorWrapper{T},\n    model::DeviceModel{T, PhaseAngleControl},\n    ::NetworkModel{U},\n) where {T <: PSY.PhaseShiftingTransformer, U <: PM.AbstractActivePowerModel}\n    add_range_constraints!(\n        container,\n        PhaseAngleControlLimit,\n        PhaseShifterAngle,\n        devices,\n        model,\n        U,\n    )\n    return\nend\n\n\"\"\"\nAdd network flow constraints for PhaseShiftingTransformer and NetworkModel with PM.DCPPowerModel\n\"\"\"\nfunction add_constraints!(\n    container::OptimizationContainer,\n    ::Type{NetworkFlowConstraint},\n    devices::IS.FlattenIteratorWrapper{T},\n    model::DeviceModel{T, PhaseAngleControl},\n    ::NetworkModel{PM.DCPPowerModel},\n) where {T <: PSY.PhaseShiftingTransformer}\n    time_steps = get_time_steps(container)\n    flow_variables = get_variable(container, FlowActivePowerVariable(), T)\n    ps_angle_variables = get_variable(container, PhaseShifterAngle(), T)\n    bus_angle_variables = get_variable(container, VoltageAngle(), PSY.ACBus)\n    jump_model = get_jump_model(container)\n    branch_flow = add_constraints_container!(\n        container,\n        NetworkFlowConstraint(),\n        T,\n        axes(flow_variables)[1],\n        time_steps,\n    )\n\n    for br in devices\n        name = PSY.get_name(br)\n        inv_x = 1.0 / PSY.get_x(br)\n        flow_variables_ = flow_variables[name, :]\n        from_bus = PSY.get_name(PSY.get_from(PSY.get_arc(br)))\n        to_bus = PSY.get_name(PSY.get_to(PSY.get_arc(br)))\n        angle_variables_ = ps_angle_variables[name, :]\n        bus_angle_from = bus_angle_variables[from_bus, :]\n        bus_angle_to = bus_angle_variables[to_bus, :]\n        @assert inv_x > 0.0\n        for t in time_steps\n            branch_flow[name, t] = JuMP.@constraint(\n                jump_model,\n                flow_variables_[t] ==\n                inv_x * (bus_angle_from[t] - bus_angle_to[t] + angle_variables_[t])\n            )\n        end\n    end\n    return\nend\n\nfunction objective_function!(\n    container::OptimizationContainer,\n    ::IS.FlattenIteratorWrapper{T},\n    device_model::DeviceModel{T, <:AbstractBranchFormulation},\n    ::Type{<:PM.AbstractPowerModel},\n) where {T <: PSY.ACTransmission}\n    if get_use_slacks(device_model)\n        variable_up = get_variable(container, FlowActivePowerSlackUpperBound(), T)\n        # Use device names because there might be a network reduction\n        for name in axes(variable_up, 1)\n            for t in get_time_steps(container)\n                add_to_objective_invariant_expression!(\n                    container,\n                    variable_up[name, t] * CONSTRAINT_VIOLATION_SLACK_COST,\n                )\n            end\n        end\n    end\n    return\nend\n\nfunction objective_function!(\n    container::OptimizationContainer,\n    ::IS.FlattenIteratorWrapper{T},\n    device_model::DeviceModel{T, <:AbstractBranchFormulation},\n    ::Type{<:PM.AbstractActivePowerModel},\n) where {T <: PSY.ACTransmission}\n    if get_use_slacks(device_model)\n        variable_up = get_variable(container, FlowActivePowerSlackUpperBound(), T)\n        variable_dn = get_variable(container, FlowActivePowerSlackLowerBound(), T)\n        # Use device names because there might be a network reduction\n        for name in axes(variable_up, 1)\n            for t in get_time_steps(container)\n                add_to_objective_invariant_expression!(\n                    container,\n                    (variable_dn[name, t] + variable_up[name, t]) *\n                    CONSTRAINT_VIOLATION_SLACK_COST,\n                )\n            end\n        end\n    end\n    return\nend\n"
  },
  {
    "path": "src/devices_models/devices/HVDCsystems.jl",
    "content": "#! format: off\nget_variable_binary(::ActivePowerVariable, ::Type{PSY.InterconnectingConverter}, ::AbstractConverterFormulation) = false\nget_variable_warm_start_value(::ActivePowerVariable, d::PSY.InterconnectingConverter, ::AbstractConverterFormulation) = PSY.get_active_power(d)\nget_variable_lower_bound(::ActivePowerVariable, d::PSY.InterconnectingConverter, ::AbstractConverterFormulation) = PSY.get_active_power_limits(d).min\nget_variable_upper_bound(::ActivePowerVariable, d::PSY.InterconnectingConverter, ::AbstractConverterFormulation) = PSY.get_active_power_limits(d).max\nget_variable_multiplier(_, ::Type{PSY.InterconnectingConverter}, ::AbstractConverterFormulation) = 1.0\n\n\nfunction _get_flow_bounds(d::PSY.TModelHVDCLine)\n    check_hvdc_line_limits_consistency(d)\n    from_min = PSY.get_active_power_limits_from(d).min\n    to_min = PSY.get_active_power_limits_to(d).min\n    from_max = PSY.get_active_power_limits_from(d).max\n    to_max = PSY.get_active_power_limits_to(d).max\n\n    if from_min >= 0.0 && to_min >= 0.0\n        min_rate = min(from_min, to_min)\n    elseif from_min <= 0.0 && to_min <= 0.0\n        min_rate = max(from_min, to_min)\n    elseif from_min <= 0.0 && to_min >= 0.0\n        min_rate = from_min\n    elseif to_min <= 0.0 && from_min >= 0.0\n        min_rate = to_min\n    else\n        @assert false\n    end\n\n    if from_max >= 0.0 && to_max >= 0.0\n        max_rate = min(from_max, to_max)\n    elseif from_max <= 0.0 && to_max <= 0.0\n        max_rate = max(from_max, to_max)\n    elseif from_max <= 0.0 && to_max >= 0.0\n        max_rate = from_max\n    elseif from_max >= 0.0 && to_max <= 0.0\n        max_rate = to_max\n    else\n        @assert false\n    end\n\n    return min_rate, max_rate\nend\n\n\nget_variable_binary(::FlowActivePowerVariable, ::Type{PSY.TModelHVDCLine}, ::AbstractBranchFormulation) = false\nget_variable_binary(::DCLineCurrent, ::Type{PSY.TModelHVDCLine}, ::AbstractBranchFormulation) = false\nget_variable_warm_start_value(::FlowActivePowerVariable, d::PSY.TModelHVDCLine, ::AbstractBranchFormulation) = PSY.get_active_power_flow(d)\nget_variable_lower_bound(::FlowActivePowerVariable, d::PSY.TModelHVDCLine, ::AbstractBranchFormulation) = _get_flow_bounds(d)[1]\nget_variable_upper_bound(::FlowActivePowerVariable, d::PSY.TModelHVDCLine, ::AbstractBranchFormulation) = _get_flow_bounds(d)[2]\n\nget_parameter_multiplier(::FixValueParameter, ::PSY.DCBranch, ::AbstractBranchFormulation) = 1.0\nget_parameter_multiplier(::LowerBoundValueParameter, ::PSY.DCBranch, ::AbstractBranchFormulation) = 1.0\nget_parameter_multiplier(::UpperBoundValueParameter, ::PSY.DCBranch, ::AbstractBranchFormulation) = 1.0\n\n# This is an approximation for DC lines since the actual current limit depends on the voltage, that is a variable in the optimization problem\nfunction get_variable_lower_bound(::DCLineCurrent, d::PSY.TModelHVDCLine, ::AbstractBranchFormulation)\n    p_min_flow = _get_flow_bounds(d)[1]\n    arc = PSY.get_arc(d)\n    bus_from = arc.from\n    bus_to = arc.to\n    max_v = max(PSY.get_magnitude(bus_from), PSY.get_magnitude(bus_to))\n    return p_min_flow / max_v\nend\n# This is an approximation for DC lines since the actual current limit depends on the voltage, that is a variable in the optimization problem\nfunction get_variable_upper_bound(::DCLineCurrent, d::PSY.TModelHVDCLine, ::AbstractBranchFormulation)\n    p_max_flow = _get_flow_bounds(d)[2]\n    arc = PSY.get_arc(d)\n    bus_from = arc.from\n    bus_to = arc.to\n    max_v = max(PSY.get_magnitude(bus_from), PSY.get_magnitude(bus_to))\n    return p_max_flow / max_v\nend\nget_variable_multiplier(_, ::Type{PSY.TModelHVDCLine}, ::AbstractBranchFormulation) = 1.0\n\nrequires_initialization(::AbstractConverterFormulation) = false\nrequires_initialization(::LosslessLine) = false\n\nfunction get_initial_conditions_device_model(\n    ::OperationModel,\n    model::DeviceModel{PSY.InterconnectingConverter, <:AbstractConverterFormulation},\n)\n    return model\nend\n\nfunction get_initial_conditions_device_model(\n    ::OperationModel,\n    model::DeviceModel{PSY.TModelHVDCLine, D},\n) where {D <: AbstractDCLineFormulation}\n    return model\nend\n\n\nfunction get_default_time_series_names(\n    ::Type{PSY.InterconnectingConverter},\n    ::Type{<:AbstractConverterFormulation},\n)\n    return Dict{Type{<:TimeSeriesParameter}, String}()\nend\n\nfunction get_default_time_series_names(\n    ::Type{PSY.TModelHVDCLine},\n    ::Type{<:AbstractBranchFormulation},\n)\n    return Dict{Type{<:TimeSeriesParameter}, String}()\nend\n\nfunction get_default_attributes(\n    ::Type{PSY.InterconnectingConverter},\n    ::Type{<:AbstractConverterFormulation},\n)\n    return Dict{String, Any}()\nend\n\nfunction get_default_attributes(\n    ::Type{PSY.TModelHVDCLine},\n    ::Type{<:AbstractBranchFormulation},\n)\n    return Dict{String, Any}()\nend\n\n\n############################################\n######## Quadratic Converter Model #########\n############################################\n\n## Binaries ###\nget_variable_binary(::ConverterDCPower, ::Type{PSY.InterconnectingConverter}, ::AbstractConverterFormulation) = false\nget_variable_binary(::ConverterPowerDirection, ::Type{PSY.InterconnectingConverter}, ::AbstractConverterFormulation) = true\nget_variable_binary(::ConverterCurrent, ::Type{PSY.InterconnectingConverter}, ::AbstractConverterFormulation) = false\nget_variable_binary(::ConverterPositiveCurrent, ::Type{PSY.InterconnectingConverter}, ::AbstractConverterFormulation) = false\nget_variable_binary(::ConverterNegativeCurrent, ::Type{PSY.InterconnectingConverter}, ::AbstractConverterFormulation) = false\nget_variable_binary(::ConverterCurrentDirection, ::Type{PSY.InterconnectingConverter}, ::AbstractConverterFormulation) = true\nget_variable_binary(::SquaredConverterCurrent, ::Type{PSY.InterconnectingConverter}, ::AbstractConverterFormulation) = false\nget_variable_binary(::SquaredDCVoltage, ::Type{PSY.InterconnectingConverter}, ::AbstractConverterFormulation) = false\nget_variable_binary(::AuxBilinearConverterVariable, ::Type{PSY.InterconnectingConverter}, ::AbstractConverterFormulation) = false\nget_variable_binary(::AuxBilinearSquaredConverterVariable, ::Type{PSY.InterconnectingConverter}, ::AbstractConverterFormulation) = false\nfunction get_variable_binary(\n    ::W,\n    ::Type{PSY.InterconnectingConverter},\n    ::AbstractConverterFormulation\n) where W <: InterpolationVariableType\n    return false\nend\nfunction get_variable_binary(\n    ::W,\n    ::Type{PSY.InterconnectingConverter},\n    ::AbstractConverterFormulation\n) where W <: BinaryInterpolationVariableType\n    return true\nend\n\n\n### Warm Start ###\nget_variable_warm_start_value(::ConverterCurrent, d::PSY.InterconnectingConverter, ::AbstractConverterFormulation) = PSY.get_dc_current(d)\n\n### Lower Bounds ###\nget_variable_lower_bound(::ConverterDCPower, d::PSY.InterconnectingConverter, ::AbstractConverterFormulation) = PSY.get_active_power_limits(d).min\nget_variable_lower_bound(::ConverterCurrent, d::PSY.InterconnectingConverter, ::AbstractConverterFormulation) = -PSY.get_max_dc_current(d)\nget_variable_lower_bound(::SquaredConverterCurrent, d::PSY.InterconnectingConverter, ::AbstractConverterFormulation) = 0.0\nget_variable_lower_bound(::SquaredDCVoltage, d::PSY.InterconnectingConverter, ::AbstractConverterFormulation) = PSY.get_voltage_limits(d.dc_bus).min^2\nget_variable_lower_bound(::InterpolationVariableType, d::PSY.InterconnectingConverter, ::AbstractConverterFormulation) = 0.0\nget_variable_lower_bound(::ConverterPositiveCurrent, d::PSY.InterconnectingConverter,::AbstractConverterFormulation) = 0.0\nget_variable_lower_bound(::ConverterNegativeCurrent, d::PSY.InterconnectingConverter,::AbstractConverterFormulation) = 0.0\n\n### Upper Bounds ###\nget_variable_upper_bound(::ConverterDCPower, d::PSY.InterconnectingConverter, ::AbstractConverterFormulation) = PSY.get_active_power_limits(d).max\nget_variable_upper_bound(::ConverterCurrent, d::PSY.InterconnectingConverter, ::AbstractConverterFormulation) = PSY.get_max_dc_current(d)\nget_variable_upper_bound(::SquaredConverterCurrent, d::PSY.InterconnectingConverter, ::AbstractConverterFormulation) = PSY.get_max_dc_current(d)^2\nget_variable_upper_bound(::SquaredDCVoltage, d::PSY.InterconnectingConverter, ::AbstractConverterFormulation) = PSY.get_voltage_limits(d.dc_bus).max^2\nget_variable_upper_bound(::InterpolationVariableType, d::PSY.InterconnectingConverter, ::AbstractConverterFormulation) = 1.0\nget_variable_upper_bound(::ConverterPositiveCurrent, d::PSY.InterconnectingConverter,::AbstractConverterFormulation) = PSY.get_max_dc_current(d)\nget_variable_upper_bound(::ConverterNegativeCurrent, d::PSY.InterconnectingConverter,::AbstractConverterFormulation) = PSY.get_max_dc_current(d)\n\n\nfunction get_default_attributes(\n    ::Type{PSY.InterconnectingConverter},\n    ::Type{QuadraticLossConverter},\n)\n    return Dict{String, Any}(\n        \"voltage_segments\" => 3,\n        \"current_segments\" => 6,\n        \"bilinear_segments\" => 10,\n        \"use_linear_loss\" => true,\n    )\nend\n\n#! format: on\n\n############################################\n############## Expressions #################\n############################################\n\nfunction add_to_expression!(\n    container::OptimizationContainer,\n    ::Type{T},\n    ::Type{U},\n    devices::IS.FlattenIteratorWrapper{V},\n    ::DeviceModel{V, W},\n    network_model::NetworkModel{X},\n) where {\n    T <: Union{ActivePowerBalance, DCCurrentBalance},\n    U <: Union{FlowActivePowerVariable, DCLineCurrent},\n    V <: PSY.TModelHVDCLine,\n    W <: AbstractDCLineFormulation,\n    X <: PM.AbstractPowerModel,\n}\n    variable = get_variable(container, U(), V)\n    expression = get_expression(container, T(), PSY.DCBus)\n    for d in devices\n        arc = PSY.get_arc(d)\n        to_bus_number = PSY.get_number(PSY.get_to(arc))\n        from_bus_number = PSY.get_number(PSY.get_from(arc))\n        for t in get_time_steps(container)\n            name = PSY.get_name(d)\n            _add_to_jump_expression!(\n                expression[to_bus_number, t],\n                variable[name, t],\n                1.0,\n            )\n            _add_to_jump_expression!(\n                expression[from_bus_number, t],\n                variable[name, t],\n                -1.0,\n            )\n        end\n    end\n    return\nend\n\nfunction add_to_expression!(\n    container::OptimizationContainer,\n    ::Type{T},\n    ::Type{U},\n    devices::IS.FlattenIteratorWrapper{V},\n    device_model::DeviceModel{V, W},\n    network_model::NetworkModel{X},\n) where {\n    T <: ActivePowerBalance,\n    U <: ActivePowerVariable,\n    V <: PSY.InterconnectingConverter,\n    W <: AbstractConverterFormulation,\n    X <: AreaPTDFPowerModel,\n}\n    _add_to_expression!(\n        container,\n        T,\n        U,\n        devices,\n        device_model,\n        network_model,\n    )\n    return\nend\n\nfunction add_to_expression!(\n    container::OptimizationContainer,\n    ::Type{T},\n    ::Type{U},\n    devices::IS.FlattenIteratorWrapper{V},\n    device_model::DeviceModel{V, W},\n    network_model::NetworkModel{X},\n) where {\n    T <: ActivePowerBalance,\n    U <: ActivePowerVariable,\n    V <: PSY.InterconnectingConverter,\n    W <: AbstractConverterFormulation,\n    X <: PM.AbstractPowerModel,\n}\n    _add_to_expression!(\n        container,\n        T,\n        U,\n        devices,\n        device_model,\n        network_model,\n    )\n    return\nend\n\nfunction _add_to_expression!(\n    container::OptimizationContainer,\n    ::Type{T},\n    ::Type{U},\n    devices::IS.FlattenIteratorWrapper{V},\n    ::DeviceModel{V, W},\n    network_model::NetworkModel{X},\n) where {\n    T <: ActivePowerBalance,\n    U <: ActivePowerVariable,\n    V <: PSY.InterconnectingConverter,\n    W <: AbstractConverterFormulation,\n    X <: PM.AbstractPowerModel,\n}\n    variable = get_variable(container, U(), V)\n    expression_dc = get_expression(container, T(), PSY.DCBus)\n    expression_ac = get_expression(container, T(), PSY.ACBus)\n    for d in devices, t in get_time_steps(container)\n        name = PSY.get_name(d)\n        bus_number_dc = PSY.get_number(PSY.get_dc_bus(d))\n        bus_number_ac = PSY.get_number(PSY.get_bus(d))\n        _add_to_jump_expression!(\n            expression_ac[bus_number_ac, t],\n            variable[name, t],\n            1.0,\n        )\n        _add_to_jump_expression!(\n            expression_dc[bus_number_dc, t],\n            variable[name, t],\n            -1.0,\n        )\n    end\n    return\nend\n\nfunction add_to_expression!(\n    container::OptimizationContainer,\n    ::Type{T},\n    ::Type{U},\n    devices::IS.FlattenIteratorWrapper{V},\n    ::DeviceModel{V, W},\n    network_model::NetworkModel{AreaPTDFPowerModel},\n) where {\n    T <: ActivePowerBalance,\n    U <: ActivePowerVariable,\n    V <: PSY.InterconnectingConverter,\n    W <: AbstractConverterFormulation,\n}\n    variable = get_variable(container, U(), V)\n    expression_dc = get_expression(container, T(), PSY.DCBus)\n    expression_ac = get_expression(container, T(), PSY.ACBus)\n    area_expr = get_expression(container, T(), PSY.Area)\n    network_reduction = get_network_reduction(network_model)\n    for d in devices\n        name = PSY.get_name(d)\n        device_bus = PSY.get_bus(d)\n        area_name = PSY.get_name(PSY.get_area(device_bus))\n        bus_number_ac = PNM.get_mapped_bus_number(network_reduction, device_bus)\n        bus_number_dc = PSY.get_number(PSY.get_dc_bus(d))\n        for t in get_time_steps(container)\n            _add_to_jump_expression!(\n                area_expr[area_name, t],\n                variable[name, t],\n                get_variable_multiplier(U(), V, W()),\n            )\n            _add_to_jump_expression!(\n                expression_ac[bus_number_ac, t],\n                variable[name, t],\n                get_variable_multiplier(U(), V, W()),\n            )\n            _add_to_jump_expression!(\n                expression_dc[bus_number_dc, t],\n                variable[name, t],\n                -1.0,\n            )\n        end\n    end\n    return\nend\n\nfunction add_to_expression!(\n    container::OptimizationContainer,\n    ::Type{T},\n    ::Type{U},\n    devices::IS.FlattenIteratorWrapper{V},\n    ::DeviceModel{V, W},\n    network_model::NetworkModel{PTDFPowerModel},\n) where {\n    T <: ActivePowerBalance,\n    U <: ActivePowerVariable,\n    V <: PSY.InterconnectingConverter,\n    W <: AbstractConverterFormulation,\n}\n    variable = get_variable(container, U(), V)\n    expression_dc = get_expression(container, T(), PSY.DCBus)\n    expression_ac = get_expression(container, T(), PSY.ACBus)\n    sys_expr = get_expression(container, T(), PSY.System)\n    for d in devices\n        name = PSY.get_name(d)\n        device_bus = PSY.get_bus(d)\n        bus_number_ac = PSY.get_number(device_bus)\n        ref_bus = get_reference_bus(network_model, device_bus)\n        bus_number_dc = PSY.get_number(PSY.get_dc_bus(d))\n        for t in get_time_steps(container)\n            _add_to_jump_expression!(\n                sys_expr[ref_bus, t],\n                variable[name, t],\n                get_variable_multiplier(U(), V, W()),\n            )\n            _add_to_jump_expression!(\n                expression_ac[bus_number_ac, t],\n                variable[name, t],\n                get_variable_multiplier(U(), V, W()),\n            )\n            _add_to_jump_expression!(\n                expression_dc[bus_number_dc, t],\n                variable[name, t],\n                -1.0,\n            )\n        end\n    end\n    return\nend\n\nfunction add_to_expression!(\n    ::OptimizationContainer,\n    ::Type{T},\n    ::Type{U},\n    devices::IS.FlattenIteratorWrapper{V},\n    ::DeviceModel{V, W},\n    network_model::NetworkModel{AreaBalancePowerModel},\n) where {\n    T <: ActivePowerBalance,\n    U <: ActivePowerVariable,\n    V <: PSY.InterconnectingConverter,\n    W <: AbstractConverterFormulation,\n}\n    return\nend\n\nfunction add_to_expression!(\n    container::OptimizationContainer,\n    ::Type{T},\n    ::Type{U},\n    devices::IS.FlattenIteratorWrapper{V},\n    model::DeviceModel{V, W},\n    network_model::NetworkModel{CopperPlatePowerModel},\n) where {\n    T <: ActivePowerBalance,\n    U <: ActivePowerVariable,\n    V <: PSY.InterconnectingConverter,\n    W <: AbstractConverterFormulation,\n}\n    variable = get_variable(container, U(), V)\n    expression_dc = get_expression(container, T(), PSY.DCBus)\n    sys_expr = get_expression(container, T(), PSY.System)\n    for d in devices\n        name = PSY.get_name(d)\n        device_bus = PSY.get_bus(d)\n        ref_bus = get_reference_bus(network_model, device_bus)\n        bus_number_dc = PSY.get_number(PSY.get_dc_bus(d))\n        for t in get_time_steps(container)\n            _add_to_jump_expression!(\n                sys_expr[ref_bus, t],\n                variable[name, t],\n                get_variable_multiplier(U(), V, W()),\n            )\n            _add_to_jump_expression!(\n                expression_dc[bus_number_dc, t],\n                variable[name, t],\n                -1.0,\n            )\n        end\n    end\n    return\nend\n\nfunction add_to_expression!(\n    container::OptimizationContainer,\n    ::Type{T},\n    ::Type{U},\n    devices::IS.FlattenIteratorWrapper{V},\n    model::DeviceModel{V, W},\n    network_model::NetworkModel{CopperPlatePowerModel},\n) where {\n    T <: ActivePowerBalance,\n    U <: ActivePowerVariable,\n    V <: PSY.InterconnectingConverter,\n    W <: QuadraticLossConverter,\n}\n    variable = get_variable(container, U(), V)\n    sys_expr = get_expression(container, T(), PSY.System)\n    for d in devices\n        name = PSY.get_name(d)\n        device_bus = PSY.get_bus(d)\n        ref_bus = get_reference_bus(network_model, device_bus)\n        for t in get_time_steps(container)\n            _add_to_jump_expression!(\n                sys_expr[ref_bus, t],\n                variable[name, t],\n                get_variable_multiplier(U(), V, W()),\n            )\n        end\n    end\n    return\nend\n\nfunction add_to_expression!(\n    container::OptimizationContainer,\n    ::Type{T},\n    ::Type{U},\n    devices::IS.FlattenIteratorWrapper{V},\n    model::DeviceModel{V, W},\n    network_model::NetworkModel{CopperPlatePowerModel},\n) where {\n    T <: DCCurrentBalance,\n    U <: ConverterCurrent,\n    V <: PSY.InterconnectingConverter,\n    W <: QuadraticLossConverter,\n}\n    variable = get_variable(container, U(), V)\n    expression_dc = get_expression(container, T(), PSY.DCBus)\n    for d in devices\n        name = PSY.get_name(d)\n        bus_number_dc = PSY.get_number(PSY.get_dc_bus(d))\n        for t in get_time_steps(container)\n            _add_to_jump_expression!(\n                expression_dc[bus_number_dc, t],\n                variable[name, t],\n                -1.0,\n            )\n        end\n    end\n    return\nend\n\nfunction add_to_expression!(\n    container::OptimizationContainer,\n    ::Type{T},\n    ::Type{U},\n    devices::IS.FlattenIteratorWrapper{V},\n    ::DeviceModel{V, W},\n    network_model::NetworkModel{X},\n) where {\n    T <: ActivePowerBalance,\n    U <: ActivePowerVariable,\n    V <: PSY.InterconnectingConverter,\n    W <: AbstractConverterFormulation,\n    X <: PTDFPowerModel,\n}\n    variable = get_variable(container, U(), V)\n    expression_dc = get_expression(container, T(), PSY.DCBus)\n    expression_ac = get_expression(container, T(), PSY.ACBus)\n    sys_expr = get_expression(container, T(), PSY.System)\n    for d in devices\n        name = PSY.get_name(d)\n        device_bus = PSY.get_bus(d)\n        bus_number_ac = PSY.get_number(device_bus)\n        ref_bus = get_reference_bus(network_model, device_bus)\n        bus_number_dc = PSY.get_number(PSY.get_dc_bus(d))\n        for t in get_time_steps(container)\n            _add_to_jump_expression!(\n                sys_expr[ref_bus, t],\n                variable[name, t],\n                get_variable_multiplier(U(), V, W()),\n            )\n            _add_to_jump_expression!(\n                expression_ac[bus_number_ac, t],\n                variable[name, t],\n                get_variable_multiplier(U(), V, W()),\n            )\n            _add_to_jump_expression!(\n                expression_dc[bus_number_dc, t],\n                variable[name, t],\n                -1.0,\n            )\n        end\n    end\n\n    return\nend\n\n############################################\n############## Constraints #################\n############################################\n\n############## HVDC Lines ##################\nfunction add_constraints!(\n    container::OptimizationContainer,\n    ::Type{DCLineCurrentConstraint},\n    devices::IS.FlattenIteratorWrapper{T},\n    model::DeviceModel{T, U},\n    network_model::NetworkModel{V},\n) where {T <: PSY.TModelHVDCLine, U <: DCLossyLine, V <: PM.AbstractPowerModel}\n    variable = get_variable(container, DCLineCurrent(), T)\n    dc_voltage = get_variable(container, DCVoltage(), PSY.DCBus)\n    time_steps = get_time_steps(container)\n    constraints = add_constraints_container!(\n        container,\n        DCLineCurrentConstraint(),\n        T,\n        PSY.get_name.(devices),\n        time_steps,\n    )\n\n    for d in devices\n        arc = PSY.get_arc(d)\n        from_bus_name = PSY.get_name(arc.from)\n        to_bus_name = PSY.get_name(arc.to)\n        name = PSY.get_name(d)\n        r = PSY.get_r(d)\n        if iszero(r)\n            for t in time_steps\n                constraints[name, t] = JuMP.@constraint(\n                    get_jump_model(container),\n                    dc_voltage[from_bus_name, t] == dc_voltage[to_bus_name, t]\n                )\n            end\n        else\n            for t in get_time_steps(container)\n                constraints[name, t] = JuMP.@constraint(\n                    get_jump_model(container),\n                    variable[name, t] ==\n                    (dc_voltage[from_bus_name, t] - dc_voltage[to_bus_name, t]) / r\n                )\n            end\n        end\n    end\n    return\nend\n\n############## Converters ##################\nfunction add_constraints!(\n    container::OptimizationContainer,\n    ::Type{ConverterPowerCalculationConstraint},\n    devices::IS.FlattenIteratorWrapper{U},\n    model::DeviceModel{U, V},\n    network_model::NetworkModel{X},\n) where {\n    U <: PSY.InterconnectingConverter,\n    V <: QuadraticLossConverter,\n    X <: PM.AbstractActivePowerModel,\n}\n    time_steps = get_time_steps(container)\n    varcurrent = get_variable(container, ConverterCurrent(), U)\n    var_dcvoltage = get_variable(container, DCVoltage(), PSY.DCBus)\n    var_sq_current = get_variable(container, SquaredConverterCurrent(), U)\n    var_sq_voltage = get_variable(container, SquaredDCVoltage(), U)\n    var_bilinear = get_variable(container, AuxBilinearConverterVariable(), U)\n    var_sq_bilinear = get_variable(container, AuxBilinearSquaredConverterVariable(), U)\n    var_dc_power = get_variable(container, ConverterDCPower(), U)\n    ipc_names = axes(varcurrent, 1)\n    constraint =\n        add_constraints_container!(\n            container,\n            ConverterPowerCalculationConstraint(),\n            U,\n            ipc_names,\n            time_steps,\n        )\n    constraint_aux =\n        add_constraints_container!(\n            container,\n            ConverterPowerCalculationConstraint(),\n            U,\n            ipc_names,\n            time_steps;\n            meta = \"aux\",\n        )\n\n    for device in devices\n        name = PSY.get_name(device)\n        dc_bus_name = PSY.get_name(PSY.get_dc_bus(device))\n        for t in time_steps\n            # p_dc = v_dc * i_dc = 0.5 * (bilinear - v_dc^2 - i_dc^2)\n            constraint[name, t] = JuMP.@constraint(\n                get_jump_model(container),\n                var_dc_power[name, t] ==\n                0.5 * (\n                    var_sq_bilinear[name, t] - var_sq_voltage[name, t] -\n                    var_sq_current[name, t]\n                )\n            )\n            constraint_aux[name, t] = JuMP.@constraint(\n                get_jump_model(container),\n                var_bilinear[name, t] ==\n                var_dcvoltage[dc_bus_name, t] + varcurrent[name, t]\n            )\n        end\n    end\n    return\nend\n\nfunction add_constraints!(\n    container::OptimizationContainer,\n    ::Type{ConverterMcCormickEnvelopes},\n    devices::IS.FlattenIteratorWrapper{U},\n    model::DeviceModel{U, V},\n    network_model::NetworkModel{X},\n) where {\n    U <: PSY.InterconnectingConverter,\n    V <: QuadraticLossConverter,\n    X <: PM.AbstractActivePowerModel,\n}\n    time_steps = get_time_steps(container)\n    varcurrent = get_variable(container, ConverterCurrent(), U)\n    var_dcvoltage = get_variable(container, DCVoltage(), PSY.DCBus)\n    var_dc_power = get_variable(container, ConverterDCPower(), U)\n    ipc_names = axes(varcurrent, 1)\n    constraint1_under =\n        add_constraints_container!(\n            container,\n            ConverterMcCormickEnvelopes(),\n            U,\n            ipc_names,\n            time_steps;\n            meta = \"under_1\",\n        )\n    constraint2_under =\n        add_constraints_container!(\n            container,\n            ConverterMcCormickEnvelopes(),\n            U,\n            ipc_names,\n            time_steps;\n            meta = \"under_2\",\n        )\n    constraint1_over =\n        add_constraints_container!(\n            container,\n            ConverterMcCormickEnvelopes(),\n            U,\n            ipc_names,\n            time_steps;\n            meta = \"over_1\",\n        )\n    constraint2_over =\n        add_constraints_container!(\n            container,\n            ConverterMcCormickEnvelopes(),\n            U,\n            ipc_names,\n            time_steps;\n            meta = \"over_2\",\n        )\n\n    for device in devices\n        name = PSY.get_name(device)\n        dc_bus = PSY.get_dc_bus(device)\n        dc_bus_name = PSY.get_name(dc_bus)\n        V_min, V_max = PSY.get_voltage_limits(dc_bus)\n        I_max = PSY.get_max_dc_current(device)\n        I_min = -I_max\n        for t in time_steps\n            constraint1_under[name, t] = JuMP.@constraint(\n                get_jump_model(container),\n                var_dc_power[name, t] >=\n                V_min * varcurrent[name, t] + var_dcvoltage[dc_bus_name, t] * I_min -\n                I_min * V_min\n            )\n            constraint2_under[name, t] = JuMP.@constraint(\n                get_jump_model(container),\n                var_dc_power[name, t] >=\n                V_max * varcurrent[name, t] + var_dcvoltage[dc_bus_name, t] * I_max -\n                I_max * V_max\n            )\n            constraint1_over[name, t] = JuMP.@constraint(\n                get_jump_model(container),\n                var_dc_power[name, t] <=\n                V_max * varcurrent[name, t] + var_dcvoltage[dc_bus_name, t] * I_min -\n                I_min * V_max\n            )\n            constraint2_over[name, t] = JuMP.@constraint(\n                get_jump_model(container),\n                var_dc_power[name, t] <=\n                V_min * varcurrent[name, t] + var_dcvoltage[dc_bus_name, t] * I_max -\n                I_max * V_min\n            )\n        end\n    end\n    return\nend\n\nfunction add_constraints!(\n    container::OptimizationContainer,\n    ::Type{ConverterLossConstraint},\n    devices::IS.FlattenIteratorWrapper{U},\n    model::DeviceModel{U, V},\n    network_model::NetworkModel{X},\n) where {\n    U <: PSY.InterconnectingConverter,\n    V <: QuadraticLossConverter,\n    X <: PM.AbstractActivePowerModel,\n}\n    time_steps = get_time_steps(container)\n    var_sq_current = get_variable(container, SquaredConverterCurrent(), U)\n    var_ac_power = get_variable(container, ActivePowerVariable(), U)\n    var_dc_power = get_variable(container, ConverterDCPower(), U)\n    ipc_names = axes(var_sq_current, 1)\n    constraint =\n        add_constraints_container!(\n            container,\n            ConverterLossConstraint(),\n            U,\n            ipc_names,\n            time_steps,\n        )\n\n    use_linear_loss = PSI.get_attribute(model, \"use_linear_loss\")\n    if use_linear_loss\n        pos_current = get_variable(container, ConverterPositiveCurrent(), U)\n        neg_current = get_variable(container, ConverterNegativeCurrent(), U)\n    end\n\n    for device in devices\n        name = PSY.get_name(device)\n        loss_function = PSY.get_loss_function(device)\n        if isa(loss_function, PSY.QuadraticCurve)\n            a = PSY.get_quadratic_term(loss_function)\n            b = PSY.get_proportional_term(loss_function)\n            c = PSY.get_constant_term(loss_function)\n        else\n            a = 0.0\n            b = PSY.get_proportional_term(loss_function)\n            c = PSY.get_constant_term(loss_function)\n        end\n        for t in time_steps\n            if use_linear_loss\n                loss =\n                    a * var_sq_current[name, t] +\n                    b * (pos_current[name, t] + neg_current[name, t]) + c\n            else\n                loss = a * var_sq_current[name, t] + c\n            end\n            constraint[name, t] = JuMP.@constraint(\n                get_jump_model(container),\n                var_ac_power[name, t] == var_dc_power[name, t] - loss\n            )\n        end\n    end\n    return\nend\n\nfunction add_constraints!(\n    container::OptimizationContainer,\n    ::Type{T},\n    devices::IS.FlattenIteratorWrapper{U},\n    ::DeviceModel{U, V},\n    ::NetworkModel{<:PM.AbstractPowerModel},\n) where {\n    T <: CurrentAbsoluteValueConstraint,\n    U <: PSY.InterconnectingConverter,\n    V <: QuadraticLossConverter,\n}\n    time_steps = get_time_steps(container)\n    names = [PSY.get_name(d) for d in devices]\n    JuMPmodel = get_jump_model(container)\n    # current vars #\n    current_var = get_variable(container, ConverterCurrent(), U) # From direction\n    current_var_pos = get_variable(container, ConverterPositiveCurrent(), U) # From direction\n    current_var_neg = get_variable(container, ConverterNegativeCurrent(), U) # From direction\n    current_dir = get_variable(container, ConverterCurrentDirection(), U)\n\n    constraint =\n        add_constraints_container!(\n            container,\n            CurrentAbsoluteValueConstraint(),\n            U,\n            names,\n            time_steps,\n        )\n    constraint_pos_ub =\n        add_constraints_container!(\n            container,\n            CurrentAbsoluteValueConstraint(),\n            U,\n            names,\n            time_steps;\n            meta = \"pos_ub\",\n        )\n    constraint_neg_ub =\n        add_constraints_container!(\n            container,\n            CurrentAbsoluteValueConstraint(),\n            U,\n            names,\n            time_steps;\n            meta = \"neg_ub\",\n        )\n\n    for d in devices\n        name = PSY.get_name(d)\n        I_max = PSY.get_max_dc_current(d)\n        for t in time_steps\n            constraint[name, t] = JuMP.@constraint(\n                JuMPmodel,\n                current_var[name, t] == current_var_pos[name, t] - current_var_neg[name, t]\n            )\n            constraint_pos_ub[name, t] = JuMP.@constraint(\n                JuMPmodel,\n                current_var_pos[name, t] <= I_max * current_dir[name, t]\n            )\n            constraint_neg_ub[name, t] = JuMP.@constraint(\n                JuMPmodel,\n                current_var_neg[name, t] <= I_max * (1 - current_dir[name, t])\n            )\n        end\n    end\n    return\nend\n\nfunction add_constraints!(\n    container::OptimizationContainer,\n    ::Type{T},\n    devices::IS.FlattenIteratorWrapper{U},\n    model::DeviceModel{U, V},\n    ::NetworkModel{<:PM.AbstractPowerModel},\n) where {\n    T <: InterpolationVoltageConstraints,\n    U <: PSY.InterconnectingConverter,\n    V <: QuadraticLossConverter,\n}\n    dic_var_bkpts = Dict{String, Vector{Float64}}()\n    dic_function_bkpts = Dict{String, Vector{Float64}}()\n    num_segments = get_attribute(model, \"voltage_segments\")\n    for d in devices\n        name = PSY.get_name(d)\n        vmin, vmax = PSY.get_voltage_limits(d.dc_bus)\n        var_bkpts, function_bkpts =\n            _get_breakpoints_for_pwl_function(vmin, vmax, x -> x^2; num_segments)\n        dic_var_bkpts[name] = var_bkpts\n        dic_function_bkpts[name] = function_bkpts\n    end\n\n    _add_generic_incremental_interpolation_constraint!(\n        container,\n        DCVoltage(),\n        SquaredDCVoltage(),\n        InterpolationSquaredVoltageVariable(),\n        InterpolationBinarySquaredVoltageVariable(),\n        InterpolationVoltageConstraints(),\n        devices,\n        dic_var_bkpts,\n        dic_function_bkpts,\n    )\n    return\nend\n\nfunction add_constraints!(\n    container::OptimizationContainer,\n    ::Type{T},\n    devices::IS.FlattenIteratorWrapper{U},\n    model::DeviceModel{U, V},\n    ::NetworkModel{<:PM.AbstractPowerModel},\n) where {\n    T <: InterpolationCurrentConstraints,\n    U <: PSY.InterconnectingConverter,\n    V <: QuadraticLossConverter,\n}\n    dic_var_bkpts = Dict{String, Vector{Float64}}()\n    dic_function_bkpts = Dict{String, Vector{Float64}}()\n    num_segments = get_attribute(model, \"current_segments\")\n    for d in devices\n        name = PSY.get_name(d)\n        Imax = PSY.get_max_dc_current(d)\n        Imin = -Imax\n        var_bkpts, function_bkpts =\n            _get_breakpoints_for_pwl_function(Imin, Imax, x -> x^2; num_segments)\n        dic_var_bkpts[name] = var_bkpts\n        dic_function_bkpts[name] = function_bkpts\n    end\n\n    _add_generic_incremental_interpolation_constraint!(\n        container,\n        ConverterCurrent(),\n        SquaredConverterCurrent(),\n        InterpolationSquaredCurrentVariable(),\n        InterpolationBinarySquaredCurrentVariable(),\n        InterpolationCurrentConstraints(),\n        devices,\n        dic_var_bkpts,\n        dic_function_bkpts,\n    )\n    return\nend\n\nfunction add_constraints!(\n    container::OptimizationContainer,\n    ::Type{T},\n    devices::IS.FlattenIteratorWrapper{U},\n    model::DeviceModel{U, V},\n    ::NetworkModel{<:PM.AbstractPowerModel},\n) where {\n    T <: InterpolationBilinearConstraints,\n    U <: PSY.InterconnectingConverter,\n    V <: QuadraticLossConverter,\n}\n    dic_var_bkpts = Dict{String, Vector{Float64}}()\n    dic_function_bkpts = Dict{String, Vector{Float64}}()\n    num_segments = get_attribute(model, \"bilinear_segments\")\n    for d in devices\n        name = PSY.get_name(d)\n        vmin, vmax = PSY.get_voltage_limits(d.dc_bus)\n        Imax = PSY.get_max_dc_current(d)\n        Imin = -Imax\n        γ_min = vmin * Imin\n        γ_max = vmax * Imax\n        var_bkpts, function_bkpts =\n            _get_breakpoints_for_pwl_function(γ_min, γ_max, x -> x^2; num_segments)\n        dic_var_bkpts[name] = var_bkpts\n        dic_function_bkpts[name] = function_bkpts\n    end\n\n    _add_generic_incremental_interpolation_constraint!(\n        container,\n        AuxBilinearConverterVariable(),\n        AuxBilinearSquaredConverterVariable(),\n        InterpolationSquaredBilinearVariable(),\n        InterpolationBinarySquaredBilinearVariable(),\n        InterpolationBilinearConstraints(),\n        devices,\n        dic_var_bkpts,\n        dic_function_bkpts,\n    )\n    return\nend\n\n############################################\n########### Objective Function #############\n############################################\n\nfunction objective_function!(\n    ::OptimizationContainer,\n    ::IS.FlattenIteratorWrapper{PSY.InterconnectingConverter},\n    ::DeviceModel{PSY.InterconnectingConverter, D},\n    ::Type{<:PM.AbstractPowerModel},\n) where {D <: AbstractConverterFormulation}\n    return\nend\n"
  },
  {
    "path": "src/devices_models/devices/TwoTerminalDC_branches.jl",
    "content": "#################################### Branch Variables ##################################################\n#! format: off\nget_variable_binary(::FlowActivePowerSlackUpperBound, ::Type{<:PSY.TwoTerminalHVDC}, ::AbstractTwoTerminalDCLineFormulation,) = false\nget_variable_binary(::FlowActivePowerSlackLowerBound, ::Type{<:PSY.TwoTerminalHVDC}, ::AbstractTwoTerminalDCLineFormulation,) = false\nget_variable_binary(::HVDCPiecewiseLossVariable, ::Type{<:PSY.TwoTerminalHVDC}, ::AbstractTwoTerminalDCLineFormulation,) = false\nget_variable_binary(::HVDCActivePowerReceivedFromVariable, ::Type{<:PSY.TwoTerminalHVDC}, ::AbstractTwoTerminalDCLineFormulation,) = false\nget_variable_binary(::HVDCActivePowerReceivedToVariable, ::Type{<:PSY.TwoTerminalHVDC}, ::AbstractTwoTerminalDCLineFormulation,) = false\nget_variable_binary(::HVDCPiecewiseBinaryLossVariable, ::Type{<:PSY.TwoTerminalHVDC}, ::AbstractTwoTerminalDCLineFormulation,) = true\nget_variable_binary(_, ::Type{<:PSY.TwoTerminalHVDC}, ::AbstractTwoTerminalDCLineFormulation) = false\nget_variable_binary(::FlowActivePowerVariable, ::Type{<:PSY.TwoTerminalHVDC}, ::AbstractTwoTerminalDCLineFormulation) = false\nget_variable_binary(::HVDCFlowDirectionVariable, ::Type{<:PSY.TwoTerminalHVDC}, ::AbstractTwoTerminalDCLineFormulation) = true\nget_variable_multiplier(::FlowActivePowerVariable, ::Type{<:PSY.TwoTerminalHVDC}, _) = NaN\nget_parameter_multiplier(::FixValueParameter, ::PSY.TwoTerminalHVDC, ::AbstractTwoTerminalDCLineFormulation) = 1.0\nget_variable_multiplier(::FlowActivePowerFromToVariable, ::Type{<:PSY.TwoTerminalHVDC}, ::AbstractTwoTerminalDCLineFormulation) = -1.0\nget_variable_multiplier(::FlowActivePowerToFromVariable, ::Type{<:PSY.TwoTerminalHVDC}, ::AbstractTwoTerminalDCLineFormulation) = -1.0\n\nfunction get_variable_multiplier(\n    ::HVDCLosses,\n    d::PSY.TwoTerminalHVDC,\n    ::HVDCTwoTerminalDispatch,\n)\n    loss = PSY.get_loss(d)\n    if !isa(loss, PSY.LinearCurve)\n        error(\n            \"HVDCTwoTerminalDispatch of branch $(PSY.get_name(d)) only accepts LinearCurve for loss models.\",\n        )\n    end\n    l1 = PSY.get_proportional_term(loss)\n    l0 = PSY.get_constant_term(loss)\n    if l1 == 0.0 && l0 == 0.0\n        return 0.0\n    else\n        return -1.0\n    end\nend\n\nget_variable_lower_bound(::FlowActivePowerVariable, d::PSY.TwoTerminalHVDC, ::HVDCTwoTerminalUnbounded) = nothing\nget_variable_upper_bound(::FlowActivePowerVariable, d::PSY.TwoTerminalHVDC, ::HVDCTwoTerminalUnbounded) = nothing\nget_variable_lower_bound(::FlowActivePowerVariable, d::PSY.TwoTerminalHVDC, ::AbstractTwoTerminalDCLineFormulation) = nothing\nget_variable_upper_bound(::FlowActivePowerVariable, d::PSY.TwoTerminalHVDC, ::AbstractTwoTerminalDCLineFormulation) = nothing\nget_variable_lower_bound(::HVDCLosses, d::PSY.TwoTerminalHVDC, ::HVDCTwoTerminalDispatch) = 0.0\nget_variable_upper_bound(::FlowActivePowerFromToVariable, d::PSY.TwoTerminalHVDC, ::HVDCTwoTerminalDispatch) = PSY.get_active_power_limits_from(d).max\nget_variable_lower_bound(::FlowActivePowerFromToVariable, d::PSY.TwoTerminalHVDC, ::HVDCTwoTerminalDispatch) = PSY.get_active_power_limits_from(d).min\nget_variable_upper_bound(::FlowActivePowerToFromVariable, d::PSY.TwoTerminalHVDC, ::HVDCTwoTerminalDispatch) = PSY.get_active_power_limits_to(d).max\nget_variable_lower_bound(::FlowActivePowerToFromVariable, d::PSY.TwoTerminalHVDC, ::HVDCTwoTerminalDispatch) = PSY.get_active_power_limits_to(d).min\nget_variable_upper_bound(::HVDCActivePowerReceivedFromVariable, d::PSY.TwoTerminalHVDC, ::AbstractTwoTerminalDCLineFormulation) = PSY.get_active_power_limits_from(d).max\nget_variable_lower_bound(::HVDCActivePowerReceivedFromVariable, d::PSY.TwoTerminalHVDC, ::AbstractTwoTerminalDCLineFormulation) = PSY.get_active_power_limits_from(d).min\nget_variable_upper_bound(::HVDCActivePowerReceivedToVariable, d::PSY.TwoTerminalHVDC, ::AbstractTwoTerminalDCLineFormulation) = PSY.get_active_power_limits_to(d).max\nget_variable_lower_bound(::HVDCActivePowerReceivedToVariable, d::PSY.TwoTerminalHVDC, ::AbstractTwoTerminalDCLineFormulation) = PSY.get_active_power_limits_to(d).min\n\nfunction get_variable_upper_bound(\n    ::HVDCLosses,\n    d::PSY.TwoTerminalHVDC,\n    ::HVDCTwoTerminalDispatch,\n)\n    loss = PSY.get_loss(d)\n    if !isa(loss, PSY.LinearCurve)\n        error(\n            \"HVDCTwoTerminalDispatch of branch $(PSY.get_name(d)) only accepts LinearCurve for loss models.\",\n        )\n    end\n    l1 = PSY.get_proportional_term(loss)\n    l0 = PSY.get_constant_term(loss)\n    if l1 == 0.0 && l0 == 0.0\n        return 0.0\n    else\n        return nothing\n    end\nend\n\nget_variable_upper_bound(::HVDCPiecewiseLossVariable, d::PSY.TwoTerminalHVDC, ::Union{HVDCTwoTerminalDispatch, HVDCTwoTerminalPiecewiseLoss}) = 1.0\nget_variable_lower_bound(::HVDCPiecewiseLossVariable, d::PSY.TwoTerminalHVDC, ::Union{HVDCTwoTerminalDispatch, HVDCTwoTerminalPiecewiseLoss}) = 0.0\n\n#################################### LCC ##################################################\nget_variable_binary(::HVDCActivePowerReceivedFromVariable, ::Type{PSY.TwoTerminalLCCLine}, ::HVDCTwoTerminalLCC) = false\nget_variable_binary(::HVDCActivePowerReceivedToVariable, ::Type{PSY.TwoTerminalLCCLine}, ::HVDCTwoTerminalLCC) = false\nget_variable_binary(::HVDCReactivePowerReceivedFromVariable, ::Type{PSY.TwoTerminalLCCLine}, ::HVDCTwoTerminalLCC) = false\nget_variable_binary(::HVDCReactivePowerReceivedToVariable, ::Type{PSY.TwoTerminalLCCLine}, ::HVDCTwoTerminalLCC) = false\nget_variable_binary(::HVDCRectifierDelayAngleVariable, ::Type{PSY.TwoTerminalLCCLine}, ::HVDCTwoTerminalLCC) = false\nget_variable_binary(::HVDCInverterExtinctionAngleVariable, ::Type{PSY.TwoTerminalLCCLine}, ::HVDCTwoTerminalLCC) = false\nget_variable_binary(::HVDCRectifierPowerFactorAngleVariable, ::Type{PSY.TwoTerminalLCCLine}, ::HVDCTwoTerminalLCC) = false\nget_variable_binary(::HVDCInverterPowerFactorAngleVariable, ::Type{PSY.TwoTerminalLCCLine}, ::HVDCTwoTerminalLCC) = false\nget_variable_binary(::HVDCRectifierOverlapAngleVariable, ::Type{PSY.TwoTerminalLCCLine}, ::HVDCTwoTerminalLCC) = false\nget_variable_binary(::HVDCInverterOverlapAngleVariable, ::Type{PSY.TwoTerminalLCCLine}, ::HVDCTwoTerminalLCC) = false\nget_variable_binary(::HVDCRectifierDCVoltageVariable, ::Type{PSY.TwoTerminalLCCLine}, ::HVDCTwoTerminalLCC) = false\nget_variable_binary(::HVDCInverterDCVoltageVariable, ::Type{PSY.TwoTerminalLCCLine}, ::HVDCTwoTerminalLCC) = false\nget_variable_binary(::HVDCRectifierACCurrentVariable, ::Type{PSY.TwoTerminalLCCLine}, ::HVDCTwoTerminalLCC) = false\nget_variable_binary(::HVDCInverterACCurrentVariable, ::Type{PSY.TwoTerminalLCCLine}, ::HVDCTwoTerminalLCC) = false\nget_variable_binary(::DCLineCurrentFlowVariable, ::Type{PSY.TwoTerminalLCCLine}, ::HVDCTwoTerminalLCC) = false\nget_variable_binary(::HVDCRectifierTapSettingVariable, ::Type{PSY.TwoTerminalLCCLine}, ::HVDCTwoTerminalLCC) = false\nget_variable_binary(::HVDCInverterTapSettingVariable, ::Type{PSY.TwoTerminalLCCLine}, ::HVDCTwoTerminalLCC) = false\nget_variable_upper_bound(::HVDCRectifierDelayAngleVariable, d::PSY.TwoTerminalLCCLine, ::HVDCTwoTerminalLCC) = PSY.get_rectifier_delay_angle_limits(d).max\nget_variable_lower_bound(::HVDCRectifierDelayAngleVariable, d::PSY.TwoTerminalLCCLine, ::HVDCTwoTerminalLCC) = PSY.get_rectifier_delay_angle_limits(d).min\nget_variable_upper_bound(::HVDCInverterExtinctionAngleVariable, d::PSY.TwoTerminalLCCLine, ::HVDCTwoTerminalLCC) = PSY.get_inverter_extinction_angle_limits(d).max\nget_variable_lower_bound(::HVDCInverterExtinctionAngleVariable, d::PSY.TwoTerminalLCCLine, ::HVDCTwoTerminalLCC) = PSY.get_inverter_extinction_angle_limits(d).min\nget_variable_upper_bound(::HVDCRectifierTapSettingVariable, d::PSY.TwoTerminalLCCLine, ::HVDCTwoTerminalLCC) = PSY.get_rectifier_tap_limits(d).max\nget_variable_lower_bound(::HVDCRectifierTapSettingVariable, d::PSY.TwoTerminalLCCLine, ::HVDCTwoTerminalLCC) = PSY.get_rectifier_tap_limits(d).min\nget_variable_upper_bound(::HVDCInverterTapSettingVariable, d::PSY.TwoTerminalLCCLine, ::HVDCTwoTerminalLCC) = PSY.get_inverter_tap_limits(d).max\nget_variable_lower_bound(::HVDCInverterTapSettingVariable, d::PSY.TwoTerminalLCCLine, ::HVDCTwoTerminalLCC) = PSY.get_inverter_tap_limits(d).min\n#! format: on\n##########################################################\nfunction get_default_time_series_names(\n    ::Type{U},\n    ::Type{V},\n) where {U <: PSY.TwoTerminalHVDC, V <: AbstractTwoTerminalDCLineFormulation}\n    return Dict{Type{<:TimeSeriesParameter}, String}()\nend\n\nfunction get_default_attributes(\n    ::Type{U},\n    ::Type{V},\n) where {U <: PSY.TwoTerminalHVDC, V <: AbstractTwoTerminalDCLineFormulation}\n    return Dict{String, Any}()\nend\n\nget_initial_conditions_device_model(\n    ::OperationModel,\n    ::DeviceModel{T, U},\n) where {T <: PSY.TwoTerminalHVDC, U <: AbstractTwoTerminalDCLineFormulation} =\n    DeviceModel(T, U)\n\n####################################### PWL Constraints #######################################################\n\nfunction _get_range_segments(::PSY.TwoTerminalHVDC, loss::PSY.LinearCurve)\n    return 1:4\nend\n\nfunction _get_range_segments(\n    ::PSY.TwoTerminalHVDC,\n    loss::PSY.PiecewiseIncrementalCurve,\n)\n    loss_factors = PSY.get_slopes(loss)\n    return 1:(2 * length(loss_factors) + 2)\nend\n\nfunction _add_dense_pwl_loss_variables!(\n    container::OptimizationContainer,\n    devices,\n    model::DeviceModel{D, HVDCTwoTerminalPiecewiseLoss},\n) where {D <: PSY.TwoTerminalHVDC}\n    # Check if type and length of PWL loss model are the same for all devices\n    _check_pwl_loss_model(devices)\n\n    # Create Variables\n    time_steps = get_time_steps(container)\n    settings = get_settings(container)\n    formulation = HVDCTwoTerminalPiecewiseLoss()\n    T = HVDCPiecewiseLossVariable\n    binary = get_variable_binary(T(), D, formulation)\n    first_loss = PSY.get_loss(first(devices))\n    if isa(first_loss, PSY.LinearCurve)\n        len_segments = 4 # 2*1 + 2\n    elseif isa(first_loss, PSY.PiecewiseIncrementalCurve)\n        len_segments = 2 * length(PSY.get_slopes(first_loss)) + 2\n    else\n        error(\"Should not be here\")\n    end\n\n    segments = [\"pwl_$i\" for i in 1:len_segments]\n    T = HVDCPiecewiseLossVariable\n    variable = add_variable_container!(\n        container,\n        T(),\n        D,\n        PSY.get_name.(devices),\n        segments,\n        time_steps,\n    )\n\n    for t in time_steps, s in segments, d in devices\n        name = PSY.get_name(d)\n        variable[name, s, t] = JuMP.@variable(\n            get_jump_model(container),\n            base_name = \"$(T)_$(D)_{$(name), $(s), $(t)}\",\n            binary = binary\n        )\n        ub = get_variable_upper_bound(T(), d, formulation)\n        ub !== nothing && JuMP.set_upper_bound(variable[name, s, t], ub)\n\n        lb = get_variable_lower_bound(T(), d, formulation)\n        lb !== nothing && JuMP.set_lower_bound(variable[name, s, t], lb)\n\n        if get_warm_start(settings)\n            init = get_variable_warm_start_value(T(), d, formulation)\n            init !== nothing && JuMP.set_start_value(variable[name, s, t], init)\n        end\n    end\nend\n\n# Full Binary\nfunction _add_sparse_pwl_loss_variables!(\n    container::OptimizationContainer,\n    devices,\n    ::DeviceModel{D, HVDCTwoTerminalPiecewiseLoss},\n) where {D <: PSY.TwoTerminalHVDC}\n    # Check if type and length of PWL loss model are the same for all devices\n    #_check_pwl_loss_model(devices)\n\n    # Create Variables\n    time_steps = get_time_steps(container)\n    settings = get_settings(container)\n    formulation = HVDCTwoTerminalPiecewiseLoss()\n    T = HVDCPiecewiseLossVariable\n    binary_T = get_variable_binary(T(), D, formulation)\n    U = HVDCPiecewiseBinaryLossVariable\n    binary_U = get_variable_binary(U(), D, formulation)\n    first_loss = PSY.get_loss(first(devices))\n    if isa(first_loss, PSY.LinearCurve)\n        len_segments = 3 # 2*1 + 1\n    elseif isa(first_loss, PSY.PiecewiseIncrementalCurve)\n        len_segments = 2 * length(PSY.get_slopes(first_loss)) + 1\n    else\n        error(\"Should not be here\")\n    end\n\n    var_container = lazy_container_addition!(container, T(), D)\n    var_container_binary = lazy_container_addition!(container, U(), D)\n\n    for d in devices\n        name = PSY.get_name(d)\n        for t in time_steps\n            pwlvars = Array{JuMP.VariableRef}(undef, len_segments)\n            pwlvars_bin = Array{JuMP.VariableRef}(undef, len_segments)\n            for i in 1:len_segments\n                pwlvars[i] =\n                    var_container[(name, i, t)] = JuMP.@variable(\n                        get_jump_model(container),\n                        base_name = \"$(T)_$(name)_{pwl_$(i), $(t)}\",\n                        binary = binary_T\n                    )\n                ub = get_variable_upper_bound(T(), d, formulation)\n                ub !== nothing && JuMP.set_upper_bound(var_container[name, i, t], ub)\n\n                lb = get_variable_lower_bound(T(), d, formulation)\n                lb !== nothing && JuMP.set_lower_bound(var_container[name, i, t], lb)\n\n                pwlvars_bin[i] =\n                    var_container_binary[(name, i, t)] = JuMP.@variable(\n                        get_jump_model(container),\n                        base_name = \"$(U)_$(name)_{pwl_$(i), $(t)}\",\n                        binary = binary_U\n                    )\n            end\n        end\n    end\nend\n\nfunction _get_pwl_loss_params(d::PSY.TwoTerminalHVDC, loss::PSY.LinearCurve)\n    from_to_loss_params = Vector{Float64}(undef, 4)\n    to_from_loss_params = Vector{Float64}(undef, 4)\n    loss_factor = PSY.get_proportional_term(loss)\n    P_send0 = PSY.get_constant_term(loss)\n    P_max_ft = PSY.get_active_power_limits_from(d).max\n    P_max_tf = PSY.get_active_power_limits_to(d).max\n    if P_max_ft != P_max_tf\n        error(\n            \"HVDC Line $(PSY.get_name(d)) has non-symmetrical limits for from and to, that are not supported in the HVDCTwoTerminalPiecewiseLoss formulation\",\n        )\n    end\n    P_sendS = P_max_ft\n    ### Update Params Vectors ###\n    from_to_loss_params[1] = -P_sendS - P_send0\n    from_to_loss_params[2] = -P_send0\n    from_to_loss_params[3] = 0.0\n    from_to_loss_params[4] = P_sendS * (1 - loss_factor)\n\n    to_from_loss_params[1] = P_sendS * (1 - loss_factor)\n    to_from_loss_params[2] = 0.0\n    to_from_loss_params[3] = -P_send0\n    to_from_loss_params[4] = -P_sendS - P_send0\n\n    return from_to_loss_params, to_from_loss_params\nend\n\nfunction _get_pwl_loss_params(\n    d::PSY.TwoTerminalHVDC,\n    loss::PSY.PiecewiseIncrementalCurve,\n)\n    p_breakpoints = PSY.get_x_coords(loss)\n    loss_factors = PSY.get_slopes(loss)\n    len_segments = length(loss_factors)\n    len_variables = 2 * len_segments + 2\n    from_to_loss_params = Vector{Float64}(undef, len_variables)\n    to_from_loss_params = similar(from_to_loss_params)\n    P_max_ft = PSY.get_active_power_limits_from(d).max\n    P_max_tf = PSY.get_active_power_limits_to(d).max\n    if P_max_ft != P_max_tf\n        error(\n            \"HVDC Line $(PSY.get_name(d)) has non-symmetrical limits for from and to, that are not supported in the HVDCTwoTerminalPiecewiseLoss formulation\",\n        )\n    end\n    if P_max_ft != last(p_breakpoints)\n        error(\n            \"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)).\",\n        )\n    end\n    ### Update Params Vectors ###\n    ## Update from 1 to S\n    for i in 1:len_segments\n        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\n        to_from_loss_params[i] =\n            p_breakpoints[2 + len_segments - i] * (1 - loss_factors[len_segments + 1 - i])\n    end\n    ## Update from S+1 and S+2\n    from_to_loss_params[len_segments + 1] = -p_breakpoints[1] # P_send0\n    from_to_loss_params[len_segments + 2] = 0.0\n    to_from_loss_params[len_segments + 1] = 0.0\n    to_from_loss_params[len_segments + 2] = -p_breakpoints[1] # P_send0\n    ## Update from S+3 to 2S+2\n    for i in 1:len_segments\n        from_to_loss_params[2 + len_segments + i] =\n            p_breakpoints[i + 1] * (1 - loss_factors[i])\n        to_from_loss_params[2 + len_segments + i] = -p_breakpoints[i + 1] - p_breakpoints[1]\n    end\n\n    return from_to_loss_params, to_from_loss_params\nend\n\nfunction add_variables!(\n    container::OptimizationContainer,\n    ::Type{FlowActivePowerVariable},\n    network_model::NetworkModel{CopperPlatePowerModel},\n    devices::IS.FlattenIteratorWrapper{T},\n    formulation::U,\n) where {T <: PSY.TwoTerminalHVDC, U <: AbstractBranchFormulation}\n    inter_network_branches = T[]\n    for d in devices\n        ref_bus_from = get_reference_bus(network_model, PSY.get_arc(d).from)\n        ref_bus_to = get_reference_bus(network_model, PSY.get_arc(d).to)\n        if ref_bus_from != ref_bus_to\n            push!(inter_network_branches, d)\n        else\n            @warn(\n                \"HVDC Line $(PSY.get_name(d)) is in the same subnetwork, so the line will not be modeled.\"\n            )\n        end\n    end\n    if !isempty(inter_network_branches)\n        add_variables!(container, FlowActivePowerVariable, inter_network_branches, U())\n    end\n    return\nend\n\nfunction add_constraints!(\n    container::OptimizationContainer,\n    ::Type{T},\n    devices::Union{Vector{U}, IS.FlattenIteratorWrapper{U}},\n    ::DeviceModel{U, HVDCTwoTerminalPiecewiseLoss},\n    ::NetworkModel{<:PM.AbstractPowerModel},\n) where {T <: HVDCFlowCalculationConstraint, U <: PSY.TwoTerminalHVDC}\n    var_pwl = get_variable(container, HVDCPiecewiseLossVariable(), U)\n    var_pwl_bin = get_variable(container, HVDCPiecewiseBinaryLossVariable(), U)\n    names = PSY.get_name.(devices)\n    time_steps = get_time_steps(container)\n    flow_ft = get_variable(container, HVDCActivePowerReceivedFromVariable(), U)\n    flow_tf = get_variable(container, HVDCActivePowerReceivedToVariable(), U)\n\n    constraint_from_to =\n        add_constraints_container!(container, T(), U, names, time_steps; meta = \"ft\")\n    constraint_to_from =\n        add_constraints_container!(container, T(), U, names, time_steps; meta = \"tf\")\n    constraint_binary =\n        add_constraints_container!(container, T(), U, names, time_steps; meta = \"bin\")\n    for d in devices\n        name = PSY.get_name(d)\n        loss = PSY.get_loss(d)\n        from_to_params, to_from_params = _get_pwl_loss_params(d, loss)\n        range_segments = 1:(length(from_to_params) - 1) # 1:(2S+1)\n        for t in time_steps\n            ## Add Equality Constraints ##\n            constraint_from_to[name, t] = JuMP.@constraint(\n                get_jump_model(container),\n                flow_ft[name, t] ==\n                sum(\n                    var_pwl_bin[name, ix, t] * from_to_params[ix] for\n                    ix in range_segments\n                ) + sum(\n                    var_pwl[name, ix, t] * (from_to_params[ix + 1] - from_to_params[ix]) for\n                    ix in range_segments\n                )\n            )\n            constraint_to_from[name, t] = JuMP.@constraint(\n                get_jump_model(container),\n                flow_tf[name, t] ==\n                sum(\n                    var_pwl_bin[name, ix, t] * to_from_params[ix] for\n                    ix in range_segments\n                ) + sum(\n                    var_pwl[name, ix, t] * (to_from_params[ix + 1] - to_from_params[ix]) for\n                    ix in range_segments\n                )\n            )\n            ## Add Binary Bound ###\n            constraint_binary[name, t] = JuMP.@constraint(\n                get_jump_model(container),\n                sum(var_pwl_bin[name, ix, t] for ix in range_segments) == 1.0\n            )\n            ## Add Bounds for Continuous ##\n            for ix in range_segments\n                JuMP.@constraint(\n                    get_jump_model(container),\n                    var_pwl[name, ix, t] <= var_pwl_bin[name, ix, t]\n                )\n                if ix == div(length(range_segments) + 1, 2)\n                    JuMP.fix(var_pwl[name, ix, t], 0.0; force = true)\n                end\n            end\n        end\n    end\n    return\nend\n\n#################################### Rate Limits Constraints ##################################################\nfunction _get_flow_bounds(d::PSY.TwoTerminalHVDC)\n    check_hvdc_line_limits_consistency(d)\n    from_min = PSY.get_active_power_limits_from(d).min\n    to_min = PSY.get_active_power_limits_to(d).min\n    from_max = PSY.get_active_power_limits_from(d).max\n    to_max = PSY.get_active_power_limits_to(d).max\n\n    if from_min >= 0.0 && to_min >= 0.0\n        min_rate = min(from_min, to_min)\n    elseif from_min <= 0.0 && to_min <= 0.0\n        min_rate = max(from_min, to_min)\n    elseif from_min <= 0.0 && to_min >= 0.0\n        min_rate = from_min\n    elseif to_min <= 0.0 && from_min >= 0.0\n        min_rate = to_min\n    end\n\n    if from_max >= 0.0 && to_max >= 0.0\n        max_rate = min(from_max, to_max)\n    elseif from_max <= 0.0 && to_max <= 0.0\n        max_rate = max(from_max, to_max)\n    elseif from_max <= 0.0 && to_max >= 0.0\n        max_rate = from_max\n    elseif from_max >= 0.0 && to_max <= 0.0\n        max_rate = to_max\n    end\n\n    return min_rate, max_rate\nend\n\nadd_constraints!(\n    ::OptimizationContainer,\n    ::Type{<:Union{FlowRateConstraintFromTo, FlowRateConstraintToFrom}},\n    ::IS.FlattenIteratorWrapper{T},\n    ::DeviceModel{T, HVDCTwoTerminalUnbounded},\n    ::NetworkModel{<:PM.AbstractPowerModel},\n) where {T <: PSY.TwoTerminalHVDC} = nothing\n\nadd_constraints!(\n    ::OptimizationContainer,\n    ::Type{FlowRateConstraint},\n    ::IS.FlattenIteratorWrapper{T},\n    ::DeviceModel{T, HVDCTwoTerminalUnbounded},\n    ::NetworkModel{<:PM.AbstractPowerModel},\n) where {T <: PSY.TwoTerminalHVDC} = nothing\n\nfunction add_constraints!(\n    container::OptimizationContainer,\n    ::Type{T},\n    devices::Union{Vector{U}, IS.FlattenIteratorWrapper{U}},\n    ::DeviceModel{U, HVDCTwoTerminalLossless},\n    ::NetworkModel{<:PM.AbstractPowerModel},\n) where {T <: FlowRateConstraint, U <: PSY.TwoTerminalHVDC}\n    time_steps = get_time_steps(container)\n    names = PSY.get_name.(devices)\n\n    var = get_variable(container, FlowActivePowerVariable(), U)\n    constraint_ub =\n        add_constraints_container!(container, T(), U, names, time_steps; meta = \"ub\")\n    constraint_lb =\n        add_constraints_container!(container, T(), U, names, time_steps; meta = \"lb\")\n    for d in devices\n        min_rate, max_rate = _get_flow_bounds(d)\n        for t in time_steps\n            constraint_ub[PSY.get_name(d), t] = JuMP.@constraint(\n                get_jump_model(container),\n                var[PSY.get_name(d), t] <= max_rate\n            )\n            constraint_lb[PSY.get_name(d), t] = JuMP.@constraint(\n                get_jump_model(container),\n                min_rate <= var[PSY.get_name(d), t]\n            )\n        end\n    end\n    return\nend\n\nfunction add_constraints!(\n    container::OptimizationContainer,\n    ::Type{T},\n    devices::Union{Vector{U}, IS.FlattenIteratorWrapper{U}},\n    ::DeviceModel{U, HVDCTwoTerminalLossless},\n    network_model::NetworkModel{CopperPlatePowerModel},\n) where {T <: FlowRateConstraint, U <: PSY.TwoTerminalHVDC}\n    time_steps = get_time_steps(container)\n    names = String[]\n    modeled_devices = U[]\n\n    for d in devices\n        ref_bus_from = get_reference_bus(network_model, PSY.get_arc(d).from)\n        ref_bus_to = get_reference_bus(network_model, PSY.get_arc(d).to)\n        if ref_bus_from != ref_bus_to\n            push!(names, PSY.get_name(d))\n            push!(modeled_devices, d)\n        end\n    end\n\n    var = get_variable(container, FlowActivePowerVariable(), U)\n    constraint_ub =\n        add_constraints_container!(container, T(), U, names, time_steps; meta = \"ub\")\n    constraint_lb =\n        add_constraints_container!(container, T(), U, names, time_steps; meta = \"lb\")\n    for d in modeled_devices\n        min_rate, max_rate = _get_flow_bounds(d)\n        for t in time_steps\n            constraint_ub[PSY.get_name(d), t] = JuMP.@constraint(\n                get_jump_model(container),\n                var[PSY.get_name(d), t] <= max_rate\n            )\n            constraint_lb[PSY.get_name(d), t] = JuMP.@constraint(\n                get_jump_model(container),\n                min_rate <= var[PSY.get_name(d), t]\n            )\n        end\n    end\n    return\nend\n\nfunction _add_hvdc_flow_constraints!(\n    container::OptimizationContainer,\n    devices::Union{Vector{T}, IS.FlattenIteratorWrapper{T}},\n    constraint::FlowRateConstraintFromTo,\n) where {T <: PSY.TwoTerminalHVDC}\n    _add_hvdc_flow_constraints!(\n        container,\n        devices,\n        FlowActivePowerFromToVariable(),\n        constraint,\n    )\nend\n\nfunction _add_hvdc_flow_constraints!(\n    container::OptimizationContainer,\n    devices::Union{Vector{T}, IS.FlattenIteratorWrapper{T}},\n    constraint::FlowRateConstraintToFrom,\n) where {T <: PSY.TwoTerminalHVDC}\n    _add_hvdc_flow_constraints!(\n        container,\n        devices,\n        FlowActivePowerToFromVariable(),\n        constraint,\n    )\nend\n\nfunction _add_hvdc_flow_constraints!(\n    container::OptimizationContainer,\n    devices::Union{Vector{T}, IS.FlattenIteratorWrapper{T}},\n    var::Union{\n        FlowActivePowerFromToVariable,\n        FlowActivePowerToFromVariable,\n        HVDCActivePowerReceivedFromVariable,\n        HVDCActivePowerReceivedToVariable,\n    },\n    constraint::Union{FlowRateConstraintFromTo, FlowRateConstraintToFrom},\n) where {T <: PSY.TwoTerminalHVDC}\n    time_steps = get_time_steps(container)\n    names = PSY.get_name.(devices)\n\n    variable = get_variable(container, var, T)\n    constraint_ub =\n        add_constraints_container!(container, constraint, T, names, time_steps; meta = \"ub\")\n    constraint_lb =\n        add_constraints_container!(container, constraint, T, names, time_steps; meta = \"lb\")\n    for d in devices\n        check_hvdc_line_limits_consistency(d)\n        max_rate = get_variable_upper_bound(var, d, HVDCTwoTerminalDispatch())\n        min_rate = get_variable_lower_bound(var, d, HVDCTwoTerminalDispatch())\n        name = PSY.get_name(d)\n        for t in time_steps\n            constraint_ub[name, t] = JuMP.@constraint(\n                get_jump_model(container),\n                variable[name, t] <= max_rate\n            )\n            constraint_lb[name, t] = JuMP.@constraint(\n                get_jump_model(container),\n                min_rate <= variable[name, t]\n            )\n        end\n    end\n    return\nend\n\nfunction add_constraints!(\n    container::OptimizationContainer,\n    ::Type{T},\n    devices::IS.FlattenIteratorWrapper{U},\n    model::DeviceModel{U, HVDCTwoTerminalDispatch},\n    network_model::NetworkModel{CopperPlatePowerModel},\n) where {\n    T <: Union{FlowRateConstraintFromTo, FlowRateConstraintToFrom},\n    U <: PSY.TwoTerminalHVDC,\n}\n    inter_network_branches = U[]\n    for d in devices\n        ref_bus_from = get_reference_bus(network_model, PSY.get_arc(d).from)\n        ref_bus_to = get_reference_bus(network_model, PSY.get_arc(d).to)\n        if ref_bus_from != ref_bus_to\n            push!(inter_network_branches, d)\n        end\n    end\n    if !isempty(inter_network_branches)\n        _add_hvdc_flow_constraints!(container, devices, T())\n    end\n    return\nend\n\nfunction add_constraints!(\n    container::OptimizationContainer,\n    ::Type{T},\n    devices::IS.FlattenIteratorWrapper{U},\n    ::DeviceModel{U, HVDCTwoTerminalDispatch},\n    ::NetworkModel{<:PM.AbstractDCPModel},\n) where {\n    T <: Union{FlowRateConstraintToFrom, FlowRateConstraintFromTo},\n    U <: PSY.TwoTerminalHVDC,\n}\n    _add_hvdc_flow_constraints!(container, devices, T())\n    return\nend\n\nfunction add_constraints!(\n    container::OptimizationContainer,\n    ::Type{T},\n    devices::IS.FlattenIteratorWrapper{U},\n    ::DeviceModel{U, HVDCTwoTerminalDispatch},\n    ::NetworkModel{<:AbstractPTDFModel},\n) where {\n    T <: Union{FlowRateConstraintToFrom, FlowRateConstraintFromTo},\n    U <: PSY.TwoTerminalHVDC,\n}\n    _add_hvdc_flow_constraints!(container, devices, T())\n    return\nend\n\nfunction add_constraints!(\n    container::OptimizationContainer,\n    ::Type{T},\n    devices::IS.FlattenIteratorWrapper{U},\n    model::DeviceModel{U, V},\n    network_model::NetworkModel{CopperPlatePowerModel},\n) where {\n    T <: Union{FlowRateConstraintFromTo, FlowRateConstraintToFrom},\n    U <: PSY.TwoTerminalHVDC,\n    V <: HVDCTwoTerminalPiecewiseLoss,\n}\n    inter_network_branches = U[]\n    for d in devices\n        ref_bus_from = get_reference_bus(network_model, PSY.get_arc(d).from)\n        ref_bus_to = get_reference_bus(network_model, PSY.get_arc(d).to)\n        if ref_bus_from != ref_bus_to\n            push!(inter_network_branches, d)\n        end\n    end\n    if !isempty(inter_network_branches)\n        if T <: FlowRateConstraintFromTo\n            _add_hvdc_flow_constraints!(\n                container,\n                devices,\n                HVDCActivePowerReceivedFromVariable(),\n                T(),\n            )\n        else\n            _add_hvdc_flow_constraints!(\n                container,\n                devices,\n                HVDCActivePowerReceivedToVariable(),\n                T(),\n            )\n        end\n    end\n    return\nend\n\nfunction add_constraints!(\n    container::OptimizationContainer,\n    ::Type{T},\n    devices::IS.FlattenIteratorWrapper{U},\n    ::DeviceModel{U, V},\n    ::NetworkModel{<:AbstractPTDFModel},\n) where {\n    T <: Union{FlowRateConstraintFromTo, FlowRateConstraintToFrom},\n    U <: PSY.TwoTerminalHVDC,\n    V <: HVDCTwoTerminalPiecewiseLoss,\n}\n    if T <: FlowRateConstraintFromTo\n        _add_hvdc_flow_constraints!(\n            container,\n            devices,\n            HVDCActivePowerReceivedFromVariable(),\n            T(),\n        )\n    else\n        _add_hvdc_flow_constraints!(\n            container,\n            devices,\n            HVDCActivePowerReceivedToVariable(),\n            T(),\n        )\n    end\n    return\nend\n\nfunction add_constraints!(\n    container::OptimizationContainer,\n    ::Type{HVDCPowerBalance},\n    devices::IS.FlattenIteratorWrapper{T},\n    ::DeviceModel{T, <:AbstractTwoTerminalDCLineFormulation},\n    ::NetworkModel{<:PM.AbstractDCPModel},\n) where {T <: PSY.TwoTerminalHVDC}\n    time_steps = get_time_steps(container)\n    names = PSY.get_name.(devices)\n    tf_var = get_variable(container, FlowActivePowerToFromVariable(), T)\n    ft_var = get_variable(container, FlowActivePowerFromToVariable(), T)\n    direction_var = get_variable(container, HVDCFlowDirectionVariable(), T)\n    losses = get_variable(container, HVDCLosses(), T)\n\n    constraint_ft_ub = add_constraints_container!(\n        container,\n        HVDCPowerBalance(),\n        T,\n        names,\n        time_steps;\n        meta = \"ft_ub\",\n    )\n    constraint_tf_ub = add_constraints_container!(\n        container,\n        HVDCPowerBalance(),\n        T,\n        names,\n        time_steps;\n        meta = \"tf_ub\",\n    )\n    constraint_ft_lb = add_constraints_container!(\n        container,\n        HVDCPowerBalance(),\n        T,\n        names,\n        time_steps;\n        meta = \"tf_lb\",\n    )\n    constraint_tf_lb = add_constraints_container!(\n        container,\n        HVDCPowerBalance(),\n        T,\n        names,\n        time_steps;\n        meta = \"ft_lb\",\n    )\n    constraint_loss = add_constraints_container!(\n        container,\n        HVDCPowerBalance(),\n        T,\n        names,\n        time_steps;\n        meta = \"loss\",\n    )\n    constraint_loss_aux1 = add_constraints_container!(\n        container,\n        HVDCPowerBalance(),\n        T,\n        names,\n        time_steps;\n        meta = \"loss_aux1\",\n    )\n    constraint_loss_aux2 = add_constraints_container!(\n        container,\n        HVDCPowerBalance(),\n        T,\n        names,\n        time_steps;\n        meta = \"loss_aux2\",\n    )\n    constraint_loss_aux3 = add_constraints_container!(\n        container,\n        HVDCPowerBalance(),\n        T,\n        names,\n        time_steps;\n        meta = \"loss_aux3\",\n    )\n    constraint_loss_aux4 = add_constraints_container!(\n        container,\n        HVDCPowerBalance(),\n        T,\n        names,\n        time_steps;\n        meta = \"loss_aux4\",\n    )\n    for d in devices\n        name = PSY.get_name(d)\n        loss = PSY.get_loss(d)\n        if !isa(loss, PSY.LinearCurve)\n            error(\n                \"HVDCTwoTerminalDispatch of branch $(name) only accepts LinearCurve for loss models.\",\n            )\n        end\n        l1 = PSY.get_proportional_term(loss)\n        l0 = PSY.get_constant_term(loss)\n        R_min_from, R_max_from = PSY.get_active_power_limits_from(d)\n        R_min_to, R_max_to = PSY.get_active_power_limits_to(d)\n        for t in get_time_steps(container)\n            constraint_tf_ub[name, t] = JuMP.@constraint(\n                get_jump_model(container),\n                tf_var[name, t] <= R_max_to * direction_var[name, t]\n            )\n            constraint_tf_lb[name, t] = JuMP.@constraint(\n                get_jump_model(container),\n                tf_var[name, t] >= R_min_to * (1 - direction_var[name, t])\n            )\n            constraint_ft_ub[name, t] = JuMP.@constraint(\n                get_jump_model(container),\n                ft_var[name, t] <= R_max_from * (1 - direction_var[name, t])\n            )\n            constraint_ft_lb[name, t] = JuMP.@constraint(\n                get_jump_model(container),\n                ft_var[name, t] >= R_min_from * direction_var[name, t]\n            )\n            constraint_loss[name, t] = JuMP.@constraint(\n                get_jump_model(container),\n                tf_var[name, t] + ft_var[name, t] == losses[name, t]\n            )\n            constraint_loss_aux1[name, t] = JuMP.@constraint(\n                get_jump_model(container),\n                losses[name, t] >= l0 + l1 * ft_var[name, t]\n            )\n            constraint_loss_aux2[name, t] = JuMP.@constraint(\n                get_jump_model(container),\n                losses[name, t] >= l0 + l1 * tf_var[name, t]\n            )\n            constraint_loss_aux3[name, t] = JuMP.@constraint(\n                get_jump_model(container),\n                losses[name, t] <=\n                l0 + l1 * ft_var[name, t] + M_VALUE * direction_var[name, t]\n            )\n            constraint_loss_aux4[name, t] = JuMP.@constraint(\n                get_jump_model(container),\n                losses[name, t] <=\n                l0 + l1 * tf_var[name, t] + M_VALUE * (1 - direction_var[name, t])\n            )\n        end\n    end\n    return\nend\n\nfunction add_constraints!(\n    container::OptimizationContainer,\n    ::Type{HVDCRectifierDCLineVoltageConstraint},\n    devices::IS.FlattenIteratorWrapper{T},\n    ::DeviceModel{T, <:HVDCTwoTerminalLCC},\n    ::NetworkModel{PM.ACPPowerModel},\n) where {T <: PSY.TwoTerminalLCCLine}\n    time_steps = get_time_steps(container)\n    names = PSY.get_name.(devices)\n    rect_dc_voltage_var = get_variable(container, HVDCRectifierDCVoltageVariable(), T)\n    rect_ac_voltage_bus_var = get_variable(container, VoltageMagnitude(), PSY.ACBus)\n    rect_delay_angle_var = get_variable(container, HVDCRectifierDelayAngleVariable(), T)\n    rect_tap_setting_var = get_variable(container, HVDCRectifierTapSettingVariable(), T)\n    dc_line_current_var = get_variable(container, DCLineCurrentFlowVariable(), T)\n\n    constraint_rect_dc_volt = add_constraints_container!(\n        container,\n        HVDCRectifierDCLineVoltageConstraint(),\n        T,\n        names,\n        time_steps;\n    )\n\n    for d in devices\n        name = PSY.get_name(d)\n        rect_bridges = PSY.get_rectifier_bridges(d)\n        dc_rect_com_reactance = PSY.get_rectifier_xc(d)\n        rect_tap_ratio = PSY.get_rectifier_transformer_ratio(d)\n        bus_from = PSY.get_arc(d).from\n        bus_from_name = PSY.get_name(bus_from)\n\n        for t in get_time_steps(container)\n            constraint_rect_dc_volt[name, t] = JuMP.@constraint(\n                get_jump_model(container),\n                rect_dc_voltage_var[name, t] ==\n                (3 * rect_bridges / pi) * (\n                    sqrt(2) * (\n                        rect_tap_ratio *\n                        rect_ac_voltage_bus_var[bus_from_name, t] *\n                        cos(rect_delay_angle_var[name, t])\n                    ) / rect_tap_setting_var[name, t] -\n                    dc_rect_com_reactance * dc_line_current_var[name, t]\n                )\n            )\n        end\n    end\n    return\nend\n\nfunction add_constraints!(\n    container::OptimizationContainer,\n    ::Type{HVDCInverterDCLineVoltageConstraint},\n    devices::IS.FlattenIteratorWrapper{T},\n    ::DeviceModel{T, <:HVDCTwoTerminalLCC},\n    ::NetworkModel{PM.ACPPowerModel},\n) where {T <: PSY.TwoTerminalLCCLine}\n    time_steps = get_time_steps(container)\n    names = PSY.get_name.(devices)\n    inv_dc_voltage_var = get_variable(container, HVDCInverterDCVoltageVariable(), T)\n    inv_ac_voltage_bus_var = get_variable(container, VoltageMagnitude(), PSY.ACBus)\n    inv_extinction_angle_var =\n        get_variable(container, HVDCInverterExtinctionAngleVariable(), T)\n    inv_tap_setting_var = get_variable(container, HVDCInverterTapSettingVariable(), T)\n    dc_line_current_var = get_variable(container, DCLineCurrentFlowVariable(), T)\n\n    constraint_inv_dc_volt = add_constraints_container!(\n        container,\n        HVDCInverterDCLineVoltageConstraint(),\n        T,\n        names,\n        time_steps;\n    )\n\n    for d in devices\n        name = PSY.get_name(d)\n        inv_bridges = PSY.get_inverter_bridges(d)\n        dc_inv_com_reactance = PSY.get_inverter_xc(d)\n        inv_tap_ratio = PSY.get_inverter_transformer_ratio(d)\n        bus_to = PSY.get_arc(d).to\n        bus_to_name = PSY.get_name(bus_to)\n\n        for t in get_time_steps(container)\n            constraint_inv_dc_volt[name, t] = JuMP.@constraint(\n                get_jump_model(container),\n                inv_dc_voltage_var[name, t] ==\n                (3 * inv_bridges / pi) * (\n                    sqrt(2) * (\n                        inv_tap_ratio *\n                        inv_ac_voltage_bus_var[bus_to_name, t] *\n                        cos(inv_extinction_angle_var[name, t])\n                    ) / inv_tap_setting_var[name, t] -\n                    dc_inv_com_reactance * dc_line_current_var[name, t]\n                )\n            )\n        end\n    end\n    return\nend\n\nfunction add_constraints!(\n    container::OptimizationContainer,\n    ::Type{HVDCRectifierOverlapAngleConstraint},\n    devices::IS.FlattenIteratorWrapper{T},\n    ::DeviceModel{T, <:HVDCTwoTerminalLCC},\n    ::NetworkModel{PM.ACPPowerModel},\n) where {T <: PSY.TwoTerminalLCCLine}\n    time_steps = get_time_steps(container)\n    names = PSY.get_name.(devices)\n    rect_ac_voltage_bus_var = get_variable(container, VoltageMagnitude(), PSY.ACBus)\n    rect_delay_angle_var = get_variable(container, HVDCRectifierDelayAngleVariable(), T)\n    rect_overlap_angle_var = get_variable(container, HVDCRectifierOverlapAngleVariable(), T)\n    rect_tap_setting_var = get_variable(container, HVDCRectifierTapSettingVariable(), T)\n    dc_line_current_var = get_variable(container, DCLineCurrentFlowVariable(), T)\n\n    constraint_rect_over_ang = add_constraints_container!(\n        container,\n        HVDCRectifierOverlapAngleConstraint(),\n        T,\n        names,\n        time_steps;\n    )\n\n    for d in devices\n        name = PSY.get_name(d)\n        dc_rect_com_reactance = PSY.get_rectifier_xc(d)\n        rect_tap_ratio = PSY.get_rectifier_transformer_ratio(d)\n        bus_from = PSY.get_arc(d).from\n        bus_from_name = PSY.get_name(bus_from)\n\n        for t in get_time_steps(container)\n            constraint_rect_over_ang[name, t] = JuMP.@constraint(\n                get_jump_model(container),\n                rect_overlap_angle_var[name, t] == (\n                    acos(\n                        cos(rect_delay_angle_var[name, t])\n                        -\n                        (\n                            (\n                                sqrt(2) * dc_rect_com_reactance *\n                                dc_line_current_var[name, t] *\n                                rect_tap_setting_var[name, t]\n                            )\n                            /\n                            (\n                                rect_tap_ratio *\n                                rect_ac_voltage_bus_var[bus_from_name, t]\n                            )\n                        ),\n                    )\n                    -\n                    rect_delay_angle_var[name, t]\n                )\n            )\n        end\n    end\n    return\nend\n\nfunction add_constraints!(\n    container::OptimizationContainer,\n    ::Type{HVDCInverterOverlapAngleConstraint},\n    devices::IS.FlattenIteratorWrapper{T},\n    ::DeviceModel{T, <:HVDCTwoTerminalLCC},\n    ::NetworkModel{PM.ACPPowerModel},\n) where {T <: PSY.TwoTerminalLCCLine}\n    time_steps = get_time_steps(container)\n    names = PSY.get_name.(devices)\n    inv_ac_voltage_bus_var = get_variable(container, VoltageMagnitude(), PSY.ACBus)\n    inv_extinction_angle_var =\n        get_variable(container, HVDCInverterExtinctionAngleVariable(), T)\n    inv_overlap_angle_var = get_variable(container, HVDCInverterOverlapAngleVariable(), T)\n    inv_tap_setting_var = get_variable(container, HVDCInverterTapSettingVariable(), T)\n    dc_line_current_var = get_variable(container, DCLineCurrentFlowVariable(), T)\n\n    constraint_inv_over_ang = add_constraints_container!(\n        container,\n        HVDCInverterOverlapAngleConstraint(),\n        T,\n        names,\n        time_steps;\n    )\n\n    for d in devices\n        name = PSY.get_name(d)\n        dc_inv_com_reactance = PSY.get_inverter_xc(d)\n        inv_tap_ratio = PSY.get_inverter_transformer_ratio(d)\n        bus_to = PSY.get_arc(d).to\n        bus_to_name = PSY.get_name(bus_to)\n\n        for t in get_time_steps(container)\n            constraint_inv_over_ang[name, t] = JuMP.@constraint(\n                get_jump_model(container),\n                inv_overlap_angle_var[name, t] == (\n                    acos(\n                        cos(inv_extinction_angle_var[name, t])\n                        -\n                        (\n                            (\n                                sqrt(2) * dc_inv_com_reactance *\n                                dc_line_current_var[name, t] *\n                                inv_tap_setting_var[name, t]\n                            )\n                            /\n                            (\n                                inv_tap_ratio *\n                                inv_ac_voltage_bus_var[bus_to_name, t]\n                            )\n                        ),\n                    )\n                    -\n                    inv_extinction_angle_var[name, t]\n                )\n            )\n        end\n    end\n    return\nend\n\nfunction add_constraints!(\n    container::OptimizationContainer,\n    ::Type{HVDCRectifierPowerFactorAngleConstraint},\n    devices::IS.FlattenIteratorWrapper{T},\n    ::DeviceModel{T, <:HVDCTwoTerminalLCC},\n    ::NetworkModel{PM.ACPPowerModel},\n) where {T <: PSY.TwoTerminalLCCLine}\n    time_steps = get_time_steps(container)\n    names = PSY.get_name.(devices)\n    rect_delay_angle_var = get_variable(container, HVDCRectifierDelayAngleVariable(), T)\n    rect_overlap_angle_var = get_variable(container, HVDCRectifierOverlapAngleVariable(), T)\n    rect_power_factor_var =\n        get_variable(container, HVDCRectifierPowerFactorAngleVariable(), T)\n\n    constraint_rect_power_factor_ang = add_constraints_container!(\n        container,\n        HVDCRectifierPowerFactorAngleConstraint(),\n        T,\n        names,\n        time_steps;\n    )\n\n    for d in devices\n        name = PSY.get_name(d)\n\n        for t in get_time_steps(container)\n            constraint_rect_power_factor_ang[name, t] = JuMP.@constraint(\n                get_jump_model(container),\n                # Full equation not working with Ipopt\n                # rect_power_factor_var[name, t] *\n                #     (\n                #         cos(2 * rect_delay_angle_var[name, t]) - cos(\n                #             2(\n                #                 rect_overlap_angle_var[name, t] +\n                #                 rect_delay_angle_var[name, t]\n                #             ),\n                #         )\n                #     ) == atan(\n                #     (\n                #         - 2 * rect_overlap_angle_var[name, t] +\n                #         - sin(2 * rect_delay_angle_var[name, t]) + sin(\n                #             2 * (\n                #                 rect_overlap_angle_var[name, t] +\n                #                 rect_delay_angle_var[name, t]\n                #             ),\n                #         )\n                #     )\n                # )\n\n                # Approximation of rectifier power factor calculation\n                rect_power_factor_var[name, t] == acos(\n                    0.5 * cos(rect_delay_angle_var[name, t]) +\n                    0.5 * cos(\n                        rect_delay_angle_var[name, t] + rect_overlap_angle_var[name, t],\n                    ),\n                )\n            )\n        end\n    end\n    return\nend\n\nfunction add_constraints!(\n    container::OptimizationContainer,\n    ::Type{HVDCInverterPowerFactorAngleConstraint},\n    devices::IS.FlattenIteratorWrapper{T},\n    ::DeviceModel{T, <:HVDCTwoTerminalLCC},\n    ::NetworkModel{PM.ACPPowerModel},\n) where {T <: PSY.TwoTerminalLCCLine}\n    time_steps = get_time_steps(container)\n    names = PSY.get_name.(devices)\n    inv_extinction_angle_var =\n        get_variable(container, HVDCInverterExtinctionAngleVariable(), T)\n    inv_overlap_angle_var = get_variable(container, HVDCInverterOverlapAngleVariable(), T)\n    inv_power_factor_var =\n        get_variable(container, HVDCInverterPowerFactorAngleVariable(), T)\n\n    constraint_inv_power_factor_ang = add_constraints_container!(\n        container,\n        HVDCInverterPowerFactorAngleConstraint(),\n        T,\n        names,\n        time_steps;\n    )\n\n    for d in devices\n        name = PSY.get_name(d)\n\n        for t in get_time_steps(container)\n            constraint_inv_power_factor_ang[name, t] = JuMP.@constraint(\n                get_jump_model(container),\n                # Full equation not working with Ipopt\n                # inv_power_factor_var[name, t] *\n                #     (\n                #         cos(2 * inv_extinction_angle_var[name, t]) - cos(\n                #             2(\n                #                 inv_overlap_angle_var[name, t] +\n                #                 inv_extinction_angle_var[name, t]\n                #             ),\n                #         )\n                #     ) == atan(\n                #     (\n                #         - 2 * inv_overlap_angle_var[name, t] +\n                #         - sin(2 * inv_extinction_angle_var[name, t]) + sin(\n                #             2 * (\n                #                 inv_overlap_angle_var[name, t] +\n                #                 inv_extinction_angle_var[name, t]\n                #             ),\n                #         )\n                #     )\n                # )\n\n                # Approximation of inverter power factor calculation\n                inv_power_factor_var[name, t] == acos(\n                    0.5 * cos(inv_extinction_angle_var[name, t]) +\n                    0.5 * cos(\n                        inv_extinction_angle_var[name, t] + inv_overlap_angle_var[name, t],\n                    ),\n                )\n            )\n        end\n    end\n    return\nend\n\nfunction add_constraints!(\n    container::OptimizationContainer,\n    ::Type{HVDCRectifierACCurrentFlowConstraint},\n    devices::IS.FlattenIteratorWrapper{T},\n    ::DeviceModel{T, <:HVDCTwoTerminalLCC},\n    ::NetworkModel{PM.ACPPowerModel},\n) where {T <: PSY.TwoTerminalLCCLine}\n    time_steps = get_time_steps(container)\n    names = PSY.get_name.(devices)\n    rect_ac_current_var = get_variable(container, HVDCRectifierACCurrentVariable(), T)\n    dc_line_current_var = get_variable(container, DCLineCurrentFlowVariable(), T)\n\n    constraint_rect_ac_current = add_constraints_container!(\n        container,\n        HVDCRectifierACCurrentFlowConstraint(),\n        T,\n        names,\n        time_steps;\n    )\n\n    for d in devices\n        name = PSY.get_name(d)\n        rect_bridges = PSY.get_rectifier_bridges(d)\n\n        for t in get_time_steps(container)\n            constraint_rect_ac_current[name, t] = JuMP.@constraint(\n                get_jump_model(container),\n                rect_ac_current_var[name, t] ==\n                sqrt(6) * rect_bridges * dc_line_current_var[name, t] / pi\n            )\n        end\n    end\n    return\nend\n\nfunction add_constraints!(\n    container::OptimizationContainer,\n    ::Type{HVDCInverterACCurrentFlowConstraint},\n    devices::IS.FlattenIteratorWrapper{T},\n    ::DeviceModel{T, <:HVDCTwoTerminalLCC},\n    ::NetworkModel{PM.ACPPowerModel},\n) where {T <: PSY.TwoTerminalLCCLine}\n    time_steps = get_time_steps(container)\n    names = PSY.get_name.(devices)\n    inv_ac_current_var = get_variable(container, HVDCInverterACCurrentVariable(), T)\n    dc_line_current_var = get_variable(container, DCLineCurrentFlowVariable(), T)\n\n    constraint_inv_ac_current = add_constraints_container!(\n        container,\n        HVDCInverterACCurrentFlowConstraint(),\n        T,\n        names,\n        time_steps;\n    )\n\n    for d in devices\n        name = PSY.get_name(d)\n        inv_bridges = PSY.get_inverter_bridges(d)\n\n        for t in get_time_steps(container)\n            constraint_inv_ac_current[name, t] = JuMP.@constraint(\n                get_jump_model(container),\n                inv_ac_current_var[name, t] ==\n                sqrt(6) * inv_bridges * dc_line_current_var[name, t] / pi\n            )\n        end\n    end\n    return\nend\n\nfunction add_constraints!(\n    container::OptimizationContainer,\n    ::Type{HVDCRectifierPowerCalculationConstraint},\n    devices::IS.FlattenIteratorWrapper{T},\n    ::DeviceModel{T, <:HVDCTwoTerminalLCC},\n    ::NetworkModel{PM.ACPPowerModel},\n) where {T <: PSY.TwoTerminalLCCLine}\n    time_steps = get_time_steps(container)\n    names = PSY.get_name.(devices)\n    rect_ac_ppower_var = get_variable(container, HVDCActivePowerReceivedFromVariable(), T)\n    rect_ac_qpower_var = get_variable(container, HVDCReactivePowerReceivedFromVariable(), T)\n    rect_ac_current_var = get_variable(container, HVDCRectifierACCurrentVariable(), T)\n    rect_ac_voltage_bus_var = get_variable(container, VoltageMagnitude(), PSY.ACBus)\n    rect_power_factor_var =\n        get_variable(container, HVDCRectifierPowerFactorAngleVariable(), T)\n    rect_tap_setting_var = get_variable(container, HVDCRectifierTapSettingVariable(), T)\n\n    constraint_ft_p = add_constraints_container!(\n        container,\n        HVDCRectifierPowerCalculationConstraint(),\n        T,\n        names,\n        time_steps;\n        meta = \"active\",\n    )\n    constraint_ft_q = add_constraints_container!(\n        container,\n        HVDCRectifierPowerCalculationConstraint(),\n        T,\n        names,\n        time_steps;\n        meta = \"reactive\",\n    )\n\n    for d in devices\n        name = PSY.get_name(d)\n        rect_tap_ratio = PSY.get_rectifier_transformer_ratio(d)\n        bus_from = PSY.get_arc(d).from\n        bus_from_name = PSY.get_name(bus_from)\n\n        for t in get_time_steps(container)\n            constraint_ft_p[name, t] = JuMP.@constraint(\n                get_jump_model(container),\n                rect_ac_ppower_var[name, t] ==\n                (\n                    rect_tap_ratio * sqrt(3) * rect_ac_current_var[name, t]\n                    * rect_ac_voltage_bus_var[bus_from_name, t] *\n                    cos(rect_power_factor_var[name, t])\n                ) / rect_tap_setting_var[name, t],\n            )\n            constraint_ft_q[name, t] = JuMP.@constraint(\n                get_jump_model(container),\n                rect_ac_qpower_var[name, t] ==\n                (\n                    rect_tap_ratio * sqrt(3) * rect_ac_current_var[name, t]\n                    * rect_ac_voltage_bus_var[bus_from_name, t] *\n                    sin(rect_power_factor_var[name, t])\n                ) / rect_tap_setting_var[name, t],\n            )\n        end\n    end\n    return\nend\n\nfunction add_constraints!(\n    container::OptimizationContainer,\n    ::Type{HVDCInverterPowerCalculationConstraint},\n    devices::IS.FlattenIteratorWrapper{T},\n    ::DeviceModel{T, <:HVDCTwoTerminalLCC},\n    ::NetworkModel{PM.ACPPowerModel},\n) where {T <: PSY.TwoTerminalLCCLine}\n    time_steps = get_time_steps(container)\n    names = PSY.get_name.(devices)\n    inv_ac_ppower_var = get_variable(container, HVDCActivePowerReceivedToVariable(), T)\n    inv_ac_qpower_var = get_variable(container, HVDCReactivePowerReceivedToVariable(), T)\n    inv_ac_current_var = get_variable(container, HVDCInverterACCurrentVariable(), T)\n    inv_ac_voltage_bus_var = get_variable(container, VoltageMagnitude(), PSY.ACBus)\n    inv_power_factor_var =\n        get_variable(container, HVDCInverterPowerFactorAngleVariable(), T)\n    inv_tap_setting_var = get_variable(container, HVDCInverterTapSettingVariable(), T)\n\n    constraint_ft_p = add_constraints_container!(\n        container,\n        HVDCInverterPowerCalculationConstraint(),\n        T,\n        names,\n        time_steps;\n        meta = \"active\",\n    )\n    constraint_ft_q = add_constraints_container!(\n        container,\n        HVDCInverterPowerCalculationConstraint(),\n        T,\n        names,\n        time_steps;\n        meta = \"reactive\",\n    )\n\n    for d in devices\n        name = PSY.get_name(d)\n        inv_tap_ratio = PSY.get_inverter_transformer_ratio(d)\n        bus_to = PSY.get_arc(d).to\n        bus_to_name = PSY.get_name(bus_to)\n\n        for t in get_time_steps(container)\n            constraint_ft_p[name, t] = JuMP.@constraint(\n                get_jump_model(container),\n                inv_ac_ppower_var[name, t] ==\n                (\n                    inv_tap_ratio * sqrt(3) * inv_ac_current_var[name, t]\n                    * inv_ac_voltage_bus_var[bus_to_name, t] *\n                    cos(inv_power_factor_var[name, t])\n                ) / inv_tap_setting_var[name, t],\n            )\n            constraint_ft_q[name, t] = JuMP.@constraint(\n                get_jump_model(container),\n                inv_ac_qpower_var[name, t] ==\n                (\n                    inv_tap_ratio * sqrt(3) * inv_ac_current_var[name, t]\n                    * inv_ac_voltage_bus_var[bus_to_name, t] *\n                    sin(inv_power_factor_var[name, t])\n                ) / inv_tap_setting_var[name, t],\n            )\n        end\n    end\n    return\nend\n\nfunction add_constraints!(\n    container::OptimizationContainer,\n    ::Type{HVDCTransmissionDCLineConstraint},\n    devices::IS.FlattenIteratorWrapper{T},\n    ::DeviceModel{T, <:HVDCTwoTerminalLCC},\n    ::NetworkModel{PM.ACPPowerModel},\n) where {T <: PSY.TwoTerminalLCCLine}\n    time_steps = get_time_steps(container)\n    names = PSY.get_name.(devices)\n    rect_dc_voltage_var = get_variable(container, HVDCRectifierDCVoltageVariable(), T)\n    inv_dc_voltage_var = get_variable(container, HVDCInverterDCVoltageVariable(), T)\n    dc_line_current_var = get_variable(container, DCLineCurrentFlowVariable(), T)\n\n    constraint_tl_c = add_constraints_container!(\n        container,\n        HVDCTransmissionDCLineConstraint(),\n        T,\n        names,\n        time_steps;\n    )\n\n    for d in devices\n        name = PSY.get_name(d)\n        dc_line_resistance = PSY.get_r(d)\n\n        for t in get_time_steps(container)\n            constraint_tl_c[name, t] = JuMP.@constraint(\n                get_jump_model(container),\n                inv_dc_voltage_var[name, t] ==\n                rect_dc_voltage_var[name, t] -\n                dc_line_resistance * dc_line_current_var[name, t]\n            )\n        end\n    end\n    return\nend\n"
  },
  {
    "path": "src/devices_models/devices/area_interchange.jl",
    "content": "#! format: off\nget_multiplier_value(::FromToFlowLimitParameter, d::PSY.AreaInterchange, ::AbstractBranchFormulation) = -1.0 * PSY.get_from_to_flow_limit(d)\nget_multiplier_value(::ToFromFlowLimitParameter, d::PSY.AreaInterchange, ::AbstractBranchFormulation) = PSY.get_to_from_flow_limit(d)\n\nget_parameter_multiplier(::FixValueParameter, ::PSY.AreaInterchange, ::AbstractBranchFormulation) = 1.0\nget_parameter_multiplier(::LowerBoundValueParameter, ::PSY.AreaInterchange, ::AbstractBranchFormulation) = 1.0\nget_parameter_multiplier(::UpperBoundValueParameter, ::PSY.AreaInterchange, ::AbstractBranchFormulation) = 1.0\n\nget_initial_conditions_device_model(\n    ::OperationModel,\n    model::DeviceModel{PSY.AreaInterchange, T},\n) where {T <: AbstractBranchFormulation} = DeviceModel(PSY.AreaInterchange, T)\n\n#! format: on\n\nfunction get_default_time_series_names(\n    ::Type{PSY.AreaInterchange},\n    ::Type{V},\n) where {V <: AbstractBranchFormulation}\n    return Dict{Type{<:TimeSeriesParameter}, String}(\n        FromToFlowLimitParameter => \"from_to_flow_limit\",\n        ToFromFlowLimitParameter => \"to_from_flow_limit\",\n    )\nend\n\nfunction get_default_attributes(\n    ::Type{PSY.AreaInterchange},\n    ::Type{V},\n) where {V <: AbstractBranchFormulation}\n    return Dict{String, Any}()\nend\n\nfunction add_variables!(\n    container::OptimizationContainer,\n    ::Type{FlowActivePowerVariable},\n    model::NetworkModel{T},\n    devices::IS.FlattenIteratorWrapper{PSY.AreaInterchange},\n    formulation::AbstractBranchFormulation,\n) where {T <: PM.AbstractPowerModel}\n    time_steps = get_time_steps(container)\n\n    variable = add_variable_container!(\n        container,\n        FlowActivePowerVariable(),\n        PSY.AreaInterchange,\n        PSY.get_name.(devices),\n        time_steps,\n    )\n\n    for device in devices, t in time_steps\n        device_name = get_name(device)\n        variable[device_name, t] = JuMP.@variable(\n            get_jump_model(container),\n            base_name = \"FlowActivePowerVariable_AreaInterchange_{$(device_name), $(t)}\",\n        )\n    end\n    return\nend\n\nfunction add_variables!(\n    container::OptimizationContainer,\n    ::Type{FlowActivePowerVariable},\n    model::NetworkModel{CopperPlatePowerModel},\n    devices::IS.FlattenIteratorWrapper{PSY.AreaInterchange},\n    formulation::AbstractBranchFormulation,\n)\n    @warn(\n        \"CopperPlatePowerModel ignores AreaInterchanges. Instead use AreaBalancePowerModel.\"\n    )\n    return\nend\n\n\"\"\"\nAdd flow constraints for area interchanges\n\"\"\"\nfunction add_constraints!(\n    container::OptimizationContainer,\n    ::Type{FlowLimitConstraint},\n    devices::IS.FlattenIteratorWrapper{PSY.AreaInterchange},\n    model::DeviceModel{PSY.AreaInterchange, StaticBranch},\n    ::NetworkModel{T},\n) where {T <: PM.AbstractActivePowerModel}\n    time_steps = get_time_steps(container)\n    device_names = PSY.get_name.(devices)\n\n    con_ub = add_constraints_container!(\n        container,\n        FlowLimitConstraint(),\n        PSY.AreaInterchange,\n        device_names,\n        time_steps;\n        meta = \"ub\",\n    )\n\n    con_lb = add_constraints_container!(\n        container,\n        FlowLimitConstraint(),\n        PSY.AreaInterchange,\n        device_names,\n        time_steps;\n        meta = \"lb\",\n    )\n\n    var_array = get_variable(container, FlowActivePowerVariable(), PSY.AreaInterchange)\n    if !all(PSY.has_time_series.(devices))\n        for device in devices\n            ci_name = PSY.get_name(device)\n            to_from_limit = PSY.get_flow_limits(device).to_from\n            from_to_limit = PSY.get_flow_limits(device).from_to\n            for t in time_steps\n                con_lb[ci_name, t] =\n                    JuMP.@constraint(\n                        get_jump_model(container),\n                        var_array[ci_name, t] >= -1.0 * from_to_limit\n                    )\n                con_ub[ci_name, t] =\n                    JuMP.@constraint(\n                        get_jump_model(container),\n                        var_array[ci_name, t] <= to_from_limit\n                    )\n            end\n        end\n    else\n        param_container_from_to =\n            get_parameter(container, FromToFlowLimitParameter(), PSY.AreaInterchange)\n        param_multiplier_from_to = get_parameter_multiplier_array(\n            container,\n            FromToFlowLimitParameter(),\n            PSY.AreaInterchange,\n        )\n        param_container_to_from =\n            get_parameter(container, ToFromFlowLimitParameter(), PSY.AreaInterchange)\n        param_multiplier_to_from = get_parameter_multiplier_array(\n            container,\n            ToFromFlowLimitParameter(),\n            PSY.AreaInterchange,\n        )\n        jump_model = get_jump_model(container)\n        for device in devices\n            name = PSY.get_name(device)\n            param_from_to = get_parameter_column_refs(param_container_from_to, name)\n            param_to_from = get_parameter_column_refs(param_container_to_from, name)\n            for t in time_steps\n                con_lb[name, t] = JuMP.@constraint(\n                    jump_model,\n                    var_array[name, t] >=\n                    param_multiplier_from_to[name, t] * param_from_to[t]\n                )\n                con_ub[name, t] = JuMP.@constraint(\n                    jump_model,\n                    var_array[name, t] <=\n                    param_multiplier_to_from[name, t] * param_to_from[t]\n                )\n            end\n        end\n    end\n    return\nend\n\nfunction add_constraints!(\n    container::OptimizationContainer,\n    ::Type{LineFlowBoundConstraint},\n    devices::IS.FlattenIteratorWrapper{PSY.AreaInterchange},\n    model::DeviceModel{PSY.AreaInterchange, <:AbstractBranchFormulation},\n    network_model::NetworkModel{T},\n    inter_area_branch_map::Dict{\n        Tuple{String, String},\n        Dict{DataType, Vector{String}},\n    },\n) where {T <: AbstractPTDFModel}\n    @assert !isempty(inter_area_branch_map)\n    time_steps = get_time_steps(container)\n    device_names_with_branches = Vector{String}()\n    interchange_direction_branch_map =\n        Dict{String, Dict{Float64, Dict{DataType, Vector{String}}}}()\n    for area_interchange in devices\n        inter_change_name = PSY.get_name(area_interchange)\n        area_from_name = PSY.get_name(PSY.get_from_area(area_interchange))\n        area_to_name = PSY.get_name(PSY.get_to_area(area_interchange))\n        interchange_direction_branch_map[inter_change_name] =\n            Dict{Float64, Dict{DataType, Vector{String}}}()\n        if haskey(inter_area_branch_map, (area_from_name, area_to_name))\n            # 1 is the multiplier\n            interchange_direction_branch_map[inter_change_name][1.0] =\n                inter_area_branch_map[(area_from_name, area_to_name)]\n        end\n        if haskey(inter_area_branch_map, (area_to_name, area_from_name))\n            # -1 is the multiplier because the direction is reversed\n            interchange_direction_branch_map[inter_change_name][-1.0] =\n                inter_area_branch_map[(area_to_name, area_from_name)]\n        end\n        if isempty(interchange_direction_branch_map[inter_change_name])\n            @warn(\n                \"There are no branches modeled in Area InterChange $(summary(area_interchange)) \\\n          LineFlowBoundConstraint not created\"\n            )\n        else\n            push!(device_names_with_branches, inter_change_name)\n        end\n    end\n    con_ub = add_constraints_container!(\n        container,\n        LineFlowBoundConstraint(),\n        PSY.AreaInterchange,\n        device_names_with_branches,\n        time_steps;\n        meta = \"ub\",\n    )\n\n    con_lb = add_constraints_container!(\n        container,\n        LineFlowBoundConstraint(),\n        PSY.AreaInterchange,\n        device_names_with_branches,\n        time_steps;\n        meta = \"lb\",\n    )\n\n    area_ex_var = get_variable(container, FlowActivePowerVariable(), PSY.AreaInterchange)\n    jm = get_jump_model(container)\n    for area_interchange in devices\n        inter_change_name = PSY.get_name(area_interchange)\n        (inter_change_name ∉ device_names_with_branches) && continue\n        direction_branch_map = interchange_direction_branch_map[inter_change_name]\n\n        for t in time_steps\n            sum_of_flows = JuMP.AffExpr()\n            for (mult, inter_area_branches) in direction_branch_map\n                for (type, names) in inter_area_branches\n                    flow_expr = get_expression(container, PTDFBranchFlow(), type)\n                    for name in names\n                        JuMP.add_to_expression!(sum_of_flows, flow_expr[name, t], mult)\n                    end\n                end\n            end\n            con_ub[inter_change_name, t] =\n                JuMP.@constraint(jm, sum_of_flows <= area_ex_var[inter_change_name, t])\n            con_lb[inter_change_name, t] =\n                JuMP.@constraint(jm, sum_of_flows >= area_ex_var[inter_change_name, t])\n        end\n    end\n    return\nend\n"
  },
  {
    "path": "src/devices_models/devices/common/add_auxiliary_variable.jl",
    "content": "\"\"\"\nAdd variables to the OptimizationContainer for any component.\n\"\"\"\nfunction add_variables!(\n    container::OptimizationContainer,\n    ::Type{T},\n    devices::Union{Vector{U}, IS.FlattenIteratorWrapper{U}},\n    formulation::Union{AbstractDeviceFormulation, AbstractServiceFormulation},\n) where {T <: AuxVariableType, U <: PSY.Component}\n    add_variable!(container, T(), devices, formulation)\n    return\nend\n\n@doc raw\"\"\"\nDefault implementation of adding auxiliary variable to the model.\n\"\"\"\nfunction add_variable!(\n    container::OptimizationContainer,\n    var_type::AuxVariableType,\n    devices::U,\n    formulation,\n) where {U <: Union{Vector{D}, IS.FlattenIteratorWrapper{D}}} where {D <: PSY.Component}\n    @assert !isempty(devices)\n    time_steps = get_time_steps(container)\n    add_aux_variable_container!(\n        container,\n        var_type,\n        D,\n        PSY.get_name.(devices),\n        time_steps,\n    )\n    return\nend\n"
  },
  {
    "path": "src/devices_models/devices/common/add_constraint_dual.jl",
    "content": "function add_constraint_dual!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    model::DeviceModel{T, D},\n) where {T <: PSY.Component, D <: AbstractDeviceFormulation}\n    if !isempty(get_duals(model))\n        devices = get_available_components(model, sys)\n        for constraint_type in get_duals(model)\n            assign_dual_variable!(container, constraint_type, devices, D)\n        end\n    end\n    return\nend\n\nfunction add_constraint_dual!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    model::NetworkModel{T},\n) where {T <: PM.AbstractPowerModel}\n    if !isempty(get_duals(model))\n        devices = get_available_components(model, PSY.ACBus, sys)\n        for constraint_type in get_duals(model)\n            assign_dual_variable!(container, constraint_type, devices, model)\n        end\n    end\n    return\nend\n\nfunction add_constraint_dual!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    model::NetworkModel{T},\n) where {T <: Union{CopperPlatePowerModel, AbstractPTDFModel}}\n    if !isempty(get_duals(model))\n        for constraint_type in get_duals(model)\n            assign_dual_variable!(container, constraint_type, sys, model)\n        end\n    end\n    return\nend\n\nfunction add_constraint_dual!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    model::ServiceModel{T, D},\n) where {T <: PSY.Service, D <: AbstractServiceFormulation}\n    if !isempty(get_duals(model))\n        service = get_available_components(model, sys)\n        for constraint_type in get_duals(model)\n            assign_dual_variable!(container, constraint_type, service, D)\n        end\n    end\n    return\nend\n\nfunction assign_dual_variable!(\n    container::OptimizationContainer,\n    constraint_type::Type{<:ConstraintType},\n    service::D,\n    ::Type{<:AbstractServiceFormulation},\n) where {D <: PSY.Service}\n    time_steps = get_time_steps(container)\n    service_name = PSY.get_name(service)\n    add_dual_container!(\n        container,\n        constraint_type,\n        D,\n        [service_name],\n        time_steps;\n        meta = service_name,\n    )\n    return\nend\n\nfunction assign_dual_variable!(\n    container::OptimizationContainer,\n    constraint_type::Type{<:ConstraintType},\n    devices::U,\n    ::Type{<:AbstractDeviceFormulation},\n) where {U <: Union{Vector{D}, IS.FlattenIteratorWrapper{D}}} where {D <: PSY.Device}\n    @assert !isempty(devices)\n    time_steps = get_time_steps(container)\n    metas = _existing_constraint_metas(container, constraint_type, D)\n    if isempty(metas)\n        device_names = PSY.get_name.(devices)\n        add_dual_container!(container, constraint_type, D, device_names, time_steps)\n    else\n        # Reuse the existing constraint container's row axis so the dual axis\n        # matches the constraint exactly. Network reductions (radial /\n        # degree-two) drop branches that pass the device-model filter, so the\n        # constraint axis is a strict subset of PSY.get_name.(devices). Sizing\n        # the dual from the device list would leave the dual broadcast in\n        # process_duals incompatible with the constraint matrix.\n        for meta in metas\n            existing =\n                get_constraint(container, ConstraintKey(constraint_type, D, meta))\n            row_axis = axes(existing)[1]\n            add_dual_container!(\n                container,\n                constraint_type,\n                D,\n                row_axis,\n                time_steps;\n                meta = meta,\n            )\n        end\n    end\n    return\nend\n\nfunction _existing_constraint_metas(\n    container::OptimizationContainer,\n    ::Type{T},\n    ::Type{D},\n) where {T <: ConstraintType, D}\n    metas = String[]\n    for key in get_constraint_keys(container)\n        if IS.Optimization.get_entry_type(key) === T &&\n           IS.Optimization.get_component_type(key) === D\n            push!(metas, key.meta)\n        end\n    end\n    return metas\nend\n\nfunction assign_dual_variable!(\n    container::OptimizationContainer,\n    constraint_type::Type{<:ConstraintType},\n    devices::U,\n    ::NetworkModel{<:PM.AbstractPowerModel},\n) where {U <: Union{Vector{D}, IS.FlattenIteratorWrapper{D}}} where {D <: PSY.ACBus}\n    @assert !isempty(devices)\n    time_steps = get_time_steps(container)\n    add_dual_container!(\n        container,\n        constraint_type,\n        D,\n        PSY.get_name.(devices),\n        time_steps,\n    )\n    return\nend\n\nfunction assign_dual_variable!(\n    container::OptimizationContainer,\n    constraint_type::Type{CopperPlateBalanceConstraint},\n    ::U,\n    network_model::NetworkModel{<:PM.AbstractPowerModel},\n) where {U <: PSY.System}\n    time_steps = get_time_steps(container)\n    ref_buses = get_reference_buses(network_model)\n    add_dual_container!(container, constraint_type, U, ref_buses, time_steps)\n    return\nend\n"
  },
  {
    "path": "src/devices_models/devices/common/add_pwl_methods.jl",
    "content": "\"\"\"\n    _get_breakpoints_for_pwl_function(min_val, max_val, f; num_segments = DEFAULT_INTERPOLATION_LENGTH)\n\nGenerate breakpoints for piecewise linear (PWL) approximation of a nonlinear function.\n\nThis function creates equally-spaced breakpoints over the specified domain [min_val, max_val]\nand evaluates the given function at each breakpoint to construct a piecewise linear approximation.\nThe breakpoints are used in optimization problems to linearize nonlinear constraints or objectives.\n\n# Arguments\n- `min_val::Float64`: Minimum value of the domain for the PWL approximation\n- `max_val::Float64`: Maximum value of the domain for the PWL approximation  \n- `f`: Function to be approximated (must be callable with Float64 input)\n- `num_segments::Int`: Number of linear segments in the PWL approximation (default: DEFAULT_INTERPOLATION_LENGTH)\n\n# Returns\n- `Tuple{Vector{Float64}, Vector{Float64}}`: A tuple containing:\n  - `x_bkpts`: Vector of x-coordinates (breakpoints) in the domain\n  - `y_bkpts`: Vector of y-coordinates (function values at breakpoints)\n\n# Notes\n- The number of breakpoints is `num_segments + 1`\n- Breakpoints are equally spaced across the domain\n- The first breakpoint is always at `min_val` and the last at `max_val`\n\"\"\"\nfunction _get_breakpoints_for_pwl_function(\n    min_val::Float64,\n    max_val::Float64,\n    f;\n    num_segments = DEFAULT_INTERPOLATION_LENGTH,\n)\n    # Calculate total number of breakpoints (one more than segments)\n    # num_segments is the number of linear segments in the PWL approximation\n    # num_bkpts is the total number of breakpoints needed for the segments\n    num_bkpts = num_segments + 1\n\n    # Calculate step size for equally-spaced breakpoints\n    step = (max_val - min_val) / num_segments\n\n    # Pre-allocate vectors for breakpoint coordinates\n    x_bkpts = Vector{Float64}(undef, num_bkpts)  # Domain values (x-coordinates)\n    y_bkpts = Vector{Float64}(undef, num_bkpts)  # Function values (y-coordinates)\n\n    # Set the first breakpoint at the minimum domain value\n    x_bkpts[1] = min_val\n    y_bkpts[1] = f(min_val)\n\n    # Generate remaining breakpoints by stepping through the domain\n    for i in 1:num_segments\n        x_val = min_val + step * i  # Calculate x-coordinate of current breakpoint\n        x_bkpts[i + 1] = x_val\n        y_bkpts[i + 1] = f(x_val)  # Evaluate function at current breakpoint\n    end\n    return x_bkpts, y_bkpts\nend\n\n\"\"\"\n    add_sparse_pwl_interpolation_variables!(container, devices, ::T, model, num_segments = DEFAULT_INTERPOLATION_LENGTH)\n\nAdd piecewise linear interpolation variables to an optimization container.\n\nThis function creates the necessary variables for piecewise linear (PWL) approximation in optimization models.\nIt adds either continuous interpolation variables (δ) or binary interpolation variables (z) depending on the\nvariable type `T`. These variables are used in the incremental method for PWL approximation where:\n\n- **Interpolation variables (δ)**: Continuous variables ∈ [0,1] that represent weights for each segment\n- **Binary interpolation variables (z)**: Binary variables that enforce ordering constraints in incremental method\n\nThe function creates a 3-dimensional variable structure indexed by (device_name, segment_index, time_step).\nFor binary variables, the number of variables is one less than for continuous variables since they control\ntransitions between segments.\n\n# Arguments\n- `container::OptimizationContainer`: The optimization container to add variables to\n- `devices`: Collection of devices for which to create PWL variables\n- `::T`: Type parameter specifying the variable type (InterpolationVariableType or BinaryInterpolationVariableType)\n- `model::DeviceModel{U, V}`: Device model containing formulation information for bounds\n- `num_segments::Int`: Number of linear segments in the PWL approximation (default: DEFAULT_INTERPOLATION_LENGTH)\n\n# Type Parameters\n- `T <: Union{InterpolationVariableType, BinaryInterpolationVariableType}`: Variable type to create\n- `U <: PSY.Component`: Component type for devices\n- `V <: AbstractDeviceFormulation`: Device formulation type for bounds\n\n# Notes\n- Binary variables have `num_segments - 1` variables (control transitions between segments)\n- Continuous variables have `num_segments` variables (one per segment)\n- Variable bounds are set based on the device formulation if available\n- Variables are created for all devices and time steps in the optimization horizon\n\n# See Also\n- `_add_generic_incremental_interpolation_constraint!`: Function that uses these variables in constraints\n\"\"\"\nfunction add_sparse_pwl_interpolation_variables!(\n    container::OptimizationContainer,\n    ::T,\n    devices,\n    model::DeviceModel{U, V},\n    num_segments = DEFAULT_INTERPOLATION_LENGTH,\n) where {\n    T <: Union{InterpolationVariableType, BinaryInterpolationVariableType},\n    U <: PSY.Component,\n    V <: AbstractDeviceFormulation,\n}\n    # TODO: Implement approach for deciding segment length\n    # Extract time steps from the optimization container\n    time_steps = get_time_steps(container)\n\n    # Create variable container using lazy initialization\n    var_container = lazy_container_addition!(container, T(), U)\n    # Determine if this variable type should be binary based on type, component, and formulation\n    binary_flag = get_variable_binary(T(), U, V())\n    # Calculate number of segments based on variable type:\n    # - Binary variables: (num_segments - 1) to control transitions between segments\n    # - Continuous variables: num_segments (one per segment)\n    len_segs = binary_flag ? (num_segments - 1) : num_segments\n\n    # Iterate over all devices to create PWL variables\n    for d in devices\n        name = PSY.get_name(d)\n        # Create variables for each time step\n        for t in time_steps\n            # Pre-allocate array to store variable references for this device and time step\n            pwlvars = Array{JuMP.VariableRef}(undef, len_segs)\n\n            # Create individual PWL variables for each segment\n            for i in 1:len_segs\n                # Create JuMP variable with descriptive name and store in both arrays\n                pwlvars[i] =\n                    var_container[(name, i, t)] = JuMP.@variable(\n                        get_jump_model(container),\n                        base_name = \"$(T)_$(name)_{pwl_$(i), $(t)}\",  # Descriptive variable name\n                        binary = binary_flag  # Set as binary if this is a binary variable type\n                    )\n\n                # Set upper bound if specified by the device formulation\n                ub = get_variable_upper_bound(T(), d, V())\n                ub !== nothing && JuMP.set_upper_bound(var_container[name, i, t], ub)\n\n                # Set lower bound if specified by the device formulation  \n                lb = get_variable_lower_bound(T(), d, V())\n                lb !== nothing && JuMP.set_lower_bound(var_container[name, i, t], lb)\n            end\n        end\n    end\n    return\nend\n\n\"\"\"\n    _add_generic_incremental_interpolation_constraint!(container, ::R, ::S, ::T, ::U, ::V, devices, dic_var_bkpts, dic_function_bkpts; meta)\n\nAdd incremental piecewise linear interpolation constraints to an optimization container.\n\nThis function implements the incremental method for piecewise linear approximation in optimization models.\nIt creates constraints that relate the original variable (x) to its piecewise linear approximation (y = f(x))\nusing interpolation variables (δ) and binary variables (z) to ensure proper ordering.\n\nThe incremental method represents each segment of the PWL function as:\n- x = x₁ + Σᵢ δᵢ(xᵢ₊₁ - xᵢ) where δᵢ ∈ [0,1]\n- y = y₁ + Σᵢ δᵢ(yᵢ₊₁ - yᵢ) where yᵢ = f(xᵢ)\n\nBinary variables z ensure the incremental property: δᵢ₊₁ ≤ zᵢ ≤ δᵢ for adjacent segments.\n\n# Arguments\n- `container::OptimizationContainer`: The optimization container to add constraints to\n- `::R`: Type parameter for the original variable (x)\n- `::S`: Type parameter for the approximated variable (y = f(x))  \n- `::T`: Type parameter for the interpolation variables (δ)\n- `::U`: Type parameter for the binary interpolation variables (z)\n- `::V`: Type parameter for the constraint type\n- `devices::IS.FlattenIteratorWrapper{W}`: Collection of devices to apply constraints to\n- `dic_var_bkpts::Dict{String, Vector{Float64}}`: Breakpoints in the domain (x-coordinates) for each device\n- `dic_function_bkpts::Dict{String, Vector{Float64}}`: Function values at breakpoints (y-coordinates) for each device\n- `meta`: Metadata for constraint naming (default: empty)\n\n# Type Parameters\n- `R <: VariableType`: Original variable type\n- `S <: VariableType`: Approximated variable type  \n- `T <: VariableType`: Interpolation variable type\n- `U <: VariableType`: Binary interpolation variable type\n- `V <: ConstraintType`: Constraint type\n- `W <: PSY.Component`: Component type for devices\n\n# Notes\n- Creates two types of constraints: variable interpolation and function interpolation\n- Adds ordering constraints for binary variables to ensure incremental property\n- All constraints are applied for each device and time step\n\"\"\"\nfunction _add_generic_incremental_interpolation_constraint!(\n    container::OptimizationContainer,\n    ::R, # original var : x\n    ::S, # approximated var : y = f(x)\n    ::T, # interpolation var : δ\n    ::U, # binary interpolation var : z\n    ::V, # constraint\n    devices::IS.FlattenIteratorWrapper{W},\n    dic_var_bkpts::Dict{String, Vector{Float64}},\n    dic_function_bkpts::Dict{String, Vector{Float64}};\n    meta = IS.Optimization.CONTAINER_KEY_EMPTY_META,\n) where {\n    R <: VariableType,\n    S <: VariableType,\n    T <: VariableType,\n    U <: VariableType,\n    V <: ConstraintType,\n    W <: PSY.Component,\n}\n    # Extract time steps and device names for constraint indexing\n    time_steps = get_time_steps(container)\n    names = [PSY.get_name(d) for d in devices]\n    JuMPmodel = get_jump_model(container)\n\n    # Retrieve all required variables from the optimization container\n    # Retrieve original variable for DCVoltage from the Bus\n    x_var = if (R <: DCVoltage)\n        get_variable(container, R(), PSY.DCBus)  # Original variable (domain of function)\n    else\n        get_variable(container, R(), W)  # Original variable (domain of function)\n    end  # Original variable (domain of function)\n    y_var = get_variable(container, S(), W)  # Approximated variable (range of function)\n    δ_var = get_variable(container, T(), W)  # Interpolation variables (weights for segments)\n    z_var = get_variable(container, U(), W)  # Binary variables (ordering constraints)\n\n    # Create containers for the two main constraint types\n    # Container for variable interpolation constraints: x = x₁ + Σᵢ δᵢ(xᵢ₊₁ - xᵢ)\n    const_container_var = add_constraints_container!(\n        container,\n        V(),\n        W,\n        names,\n        time_steps;\n        meta = \"$(meta)pwl_variable\",\n    )\n\n    # Container for function interpolation constraints: y = y₁ + Σᵢ δᵢ(yᵢ₊₁ - yᵢ)\n    const_container_function = add_constraints_container!(\n        container,\n        V(),\n        W,\n        names,\n        time_steps;\n        meta = \"$(meta)pwl_function\",\n    )\n\n    # Iterate over all devices to add constraints for each device and time step\n    for d in devices\n        name = PSY.get_name(d)\n        bus_name = PSY.get_name(PSY.get_dc_bus(d))\n        # Get proper name for x variable (if is DCVoltage or not)\n        x_name = (R <: DCVoltage) ? bus_name : name\n        var_bkpts = dic_var_bkpts[name]        # Breakpoints in domain (x-values)\n        function_bkpts = dic_function_bkpts[name]  # Function values at breakpoints (y-values)\n        num_segments = length(var_bkpts) - 1   # Number of linear segments\n\n        for t in time_steps\n            # Variable interpolation constraint: x = x₁ + Σᵢ δᵢ(xᵢ₊₁ - xᵢ)\n            # This ensures the original variable is expressed as a convex combination\n            # of breakpoint intervals weighted by interpolation variables\n            const_container_var[name, t] = JuMP.@constraint(\n                JuMPmodel,\n                x_var[x_name, t] ==\n                var_bkpts[1] + sum(\n                    δ_var[name, i, t] * (var_bkpts[i + 1] - var_bkpts[i]) for\n                    i in 1:num_segments\n                )\n            )\n\n            # Function interpolation constraint: y = y₁ + Σᵢ δᵢ(yᵢ₊₁ - yᵢ)\n            # This defines the piecewise linear approximation of the function\n            const_container_function[name, t] = JuMP.@constraint(\n                JuMPmodel,\n                y_var[name, t] ==\n                function_bkpts[1] + sum(\n                    δ_var[name, i, t] * (function_bkpts[i + 1] - function_bkpts[i]) for\n                    i in 1:num_segments\n                )\n            )\n\n            # Incremental ordering constraints using binary variables (SOS2)\n            # These ensure that δᵢ₊₁ ≤ zᵢ ≤ δᵢ, which maintains the incremental property:\n            # segments must be filled in order (δ₁ before δ₂, δ₂ before δ₃, etc.)\n            for i in 1:(num_segments - 1)\n                # z[i] must be >= δ[i+1]: can't activate later segment without current one\n                JuMP.@constraint(JuMPmodel, z_var[name, i, t] >= δ_var[name, i + 1, t])\n                # z[i] must be <= δ[i]: can't be more activated than current segment\n                JuMP.@constraint(JuMPmodel, z_var[name, i, t] <= δ_var[name, i, t])\n            end\n        end\n    end\n    return\nend\n"
  },
  {
    "path": "src/devices_models/devices/common/add_to_expression.jl",
    "content": "_system_expression_type(::Type{PTDFPowerModel}) = PSY.System\n_system_expression_type(::Type{CopperPlatePowerModel}) = PSY.System\n_system_expression_type(::Type{AreaPTDFPowerModel}) = PSY.Area\n\nfunction _ref_index(network_model::NetworkModel{<:PM.AbstractPowerModel}, bus::PSY.ACBus)\n    return get_reference_bus(network_model, bus)\nend\n\nfunction _ref_index(::NetworkModel{AreaPTDFPowerModel}, device_bus::PSY.ACBus)\n    return PSY.get_name(PSY.get_area(device_bus))\nend\n\n_get_variable_if_exists(::PSY.MarketBidCost) = nothing\n_get_variable_if_exists(cost::PSY.OperationalCost) = PSY.get_variable(cost)\n\n_is_fuel_curve(::Nothing) = false\n_is_fuel_curve(::PSY.CostCurve) = false\n_is_fuel_curve(::PSY.FuelCurve) = true\n\n_value_curve_is_quadratic(::PSY.LinearCurve) = false\n_value_curve_is_quadratic(::PSY.QuadraticCurve) = true\n_value_curve_is_quadratic(::PSY.PiecewisePointCurve) = false\n_value_curve_is_quadratic(::PSY.IncrementalCurve) = false\n_value_curve_is_quadratic(::PSY.AverageRateCurve) = false\n\nfunction add_expressions!(\n    container::OptimizationContainer,\n    ::Type{T},\n    devices::U,\n    model::DeviceModel{D, W},\n) where {\n    T <: ExpressionType,\n    U <: Union{Vector{D}, IS.FlattenIteratorWrapper{D}},\n    W <: AbstractDeviceFormulation,\n} where {D <: PSY.Component}\n    time_steps = get_time_steps(container)\n    names = PSY.get_name.(devices)\n    add_expression_container!(container, T(), D, names, time_steps)\n    return\nend\n\nfunction add_expressions!(\n    container::OptimizationContainer,\n    ::Type{T},\n    devices::U,\n    model::DeviceModel{D, W},\n) where {\n    T <: FuelConsumptionExpression,\n    U <: Union{Vector{D}, IS.FlattenIteratorWrapper{D}},\n    W <: AbstractDeviceFormulation,\n} where {D <: PSY.Component}\n    time_steps = get_time_steps(container)\n    names = String[]\n    found_quad_fuel_functions = false\n    for d in devices\n        op_cost = PSY.get_operation_cost(d)\n        fuel_curve = _get_variable_if_exists(op_cost)\n        if _is_fuel_curve(fuel_curve)\n            push!(names, PSY.get_name(d))\n            if !found_quad_fuel_functions\n                found_quad_fuel_functions =\n                    _value_curve_is_quadratic(PSY.get_value_curve(fuel_curve))\n            end\n        end\n    end\n\n    if !isempty(names)\n        expr_type = found_quad_fuel_functions ? JuMP.QuadExpr : GAE\n        add_expression_container!(\n            container,\n            T(),\n            D,\n            names,\n            time_steps;\n            expr_type = expr_type,\n        )\n    end\n    return\nend\n\nfunction add_expressions!(\n    container::OptimizationContainer,\n    ::Type{T},\n    devices::U,\n    model::ServiceModel{V, W},\n) where {\n    T <: ExpressionType,\n    U <: Union{Vector{D}, IS.FlattenIteratorWrapper{D}},\n    V <: PSY.Reserve,\n    W <: AbstractReservesFormulation,\n} where {D <: PSY.Component}\n    time_steps = get_time_steps(container)\n    @assert length(devices) == 1\n    add_expression_container!(\n        container,\n        T(),\n        D,\n        PSY.get_name.(devices),\n        time_steps;\n        meta = get_service_name(model),\n    )\n    return\nend\n\n# Note: add_to_jump_expression! are used to control depending on the parameter type used\n# on the simulation.\nfunction _add_to_jump_expression!(\n    expression::T,\n    var::JuMP.VariableRef,\n    multiplier::Float64,\n) where {T <: JuMP.AbstractJuMPScalar}\n    JuMP.add_to_expression!(expression, multiplier, var)\n    return\nend\n\nfunction _add_to_jump_expression!(\n    expression::T,\n    value::Float64,\n) where {T <: JuMP.AbstractJuMPScalar}\n    JuMP.add_to_expression!(expression, value)\n    return\nend\n\nfunction _add_to_jump_expression!(\n    expression::T,\n    var::JuMP.VariableRef,\n    multiplier::Float64,\n    constant::Float64,\n) where {T <: JuMP.AbstractJuMPScalar}\n    _add_to_jump_expression!(expression, constant)\n    _add_to_jump_expression!(expression, var, multiplier)\n    return\nend\n\nfunction _add_to_jump_expression!(\n    expression::T,\n    parameter::Float64,\n    multiplier::Float64,\n) where {T <: JuMP.AbstractJuMPScalar}\n    _add_to_jump_expression!(expression, parameter * multiplier)\n    return\nend\n\n\"\"\"\nDefault implementation to add parameters to SystemBalanceExpressions\n\"\"\"\nfunction add_to_expression!(\n    container::OptimizationContainer,\n    ::Type{T},\n    ::Type{U},\n    devices::IS.FlattenIteratorWrapper{V},\n    model::DeviceModel{V, W},\n    network_model::NetworkModel{X},\n) where {\n    T <: SystemBalanceExpressions,\n    U <: TimeSeriesParameter,\n    V <: PSY.Device,\n    W <: AbstractDeviceFormulation,\n    X <: PM.AbstractPowerModel,\n}\n    param_container = get_parameter(container, U(), V)\n    multiplier = get_multiplier_array(param_container)\n    network_reduction = get_network_reduction(network_model)\n    for d in devices, t in get_time_steps(container)\n        bus_no = PNM.get_mapped_bus_number(network_reduction, PSY.get_bus(d))\n        name = PSY.get_name(d)\n        _add_to_jump_expression!(\n            get_expression(container, T(), PSY.ACBus)[bus_no, t],\n            get_parameter_column_refs(param_container, name)[t],\n            multiplier[name, t],\n        )\n    end\n    return\nend\n\n\"\"\"\nGeneric electric load implementation to add parameters to SystemBalanceExpressions\n\"\"\"\nfunction add_to_expression!(\n    container::OptimizationContainer,\n    ::Type{T},\n    ::Type{U},\n    devices::IS.FlattenIteratorWrapper{V},\n    model::DeviceModel{V, W},\n    network_model::NetworkModel{X},\n) where {\n    T <: SystemBalanceExpressions,\n    U <: TimeSeriesParameter,\n    V <: PSY.ElectricLoad,\n    W <: AbstractLoadFormulation,\n    X <: PM.AbstractPowerModel,\n}\n    param_container = get_parameter(container, U(), V)\n    multiplier = get_multiplier_array(param_container)\n    network_reduction = get_network_reduction(network_model)\n    ts_name = get_time_series_names(model)[U]\n    ts_type = get_default_time_series_type(container)\n    for d in devices\n        bus_no = PNM.get_mapped_bus_number(network_reduction, PSY.get_bus(d))\n        name = PSY.get_name(d)\n        has_ts = PSY.has_time_series(d, ts_type, ts_name)\n        if !has_ts\n            @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.\"\n        end\n        for t in get_time_steps(container)\n            if has_ts\n                param_value = get_parameter_column_refs(param_container, name)[t]\n                mult = multiplier[name, t]\n            else\n                param_value = 1.0\n                mult = get_multiplier_value(U(), d, W())\n            end\n            _add_to_jump_expression!(\n                get_expression(container, T(), PSY.ACBus)[bus_no, t],\n                param_value,\n                mult,\n            )\n        end\n    end\n    return\nend\n\n\"\"\"\nMotor load implementation to add constant power to ActivePowerBalance expression\n\"\"\"\nfunction add_to_expression!(\n    container::OptimizationContainer,\n    ::Type{T},\n    ::Type{U},\n    devices::IS.FlattenIteratorWrapper{V},\n    model::DeviceModel{V, W},\n    network_model::NetworkModel{X},\n) where {\n    T <: ActivePowerBalance,\n    U <: ActivePowerTimeSeriesParameter,\n    V <: PSY.MotorLoad,\n    W <: StaticPowerLoad,\n    X <: PM.AbstractPowerModel,\n}\n    network_reduction = get_network_reduction(network_model)\n    for d in devices\n        bus_no = PNM.get_mapped_bus_number(network_reduction, PSY.get_bus(d))\n        for t in get_time_steps(container)\n            _add_to_jump_expression!(\n                get_expression(container, T(), PSY.ACBus)[bus_no, t],\n                PSY.get_active_power(d),\n                -1.0,\n            )\n        end\n    end\n    return\nend\n\n\"\"\"\nMotor load implementation to add constant power to ActivePowerBalance expression for AreaBalancePowerModel\n\"\"\"\nfunction add_to_expression!(\n    container::OptimizationContainer,\n    ::Type{T},\n    ::Type{U},\n    devices::IS.FlattenIteratorWrapper{V},\n    model::DeviceModel{V, W},\n    network_model::NetworkModel{AreaBalancePowerModel},\n) where {\n    T <: ActivePowerBalance,\n    U <: ActivePowerTimeSeriesParameter,\n    V <: PSY.MotorLoad,\n    W <: StaticPowerLoad,\n}\n    network_reduction = get_network_reduction(network_model)\n    for d in devices\n        bus = PSY.get_bus(d)\n        area_name = PSY.get_name(PSY.get_area(bus))\n        for t in get_time_steps(container)\n            _add_to_jump_expression!(\n                get_expression(container, T(), PSY.Area)[area_name, t],\n                PSY.get_active_power(d),\n                -1.0,\n            )\n        end\n    end\n    return\nend\n\n\"\"\"\nMotor load implementation to add constant power to ActivePowerBalance expression\n\"\"\"\nfunction add_to_expression!(\n    container::OptimizationContainer,\n    ::Type{T},\n    ::Type{U},\n    devices::IS.FlattenIteratorWrapper{V},\n    model::DeviceModel{V, W},\n    network_model::NetworkModel{X},\n) where {\n    T <: ReactivePowerBalance,\n    U <: ReactivePowerTimeSeriesParameter,\n    V <: PSY.MotorLoad,\n    W <: StaticPowerLoad,\n    X <: PM.ACPPowerModel,\n}\n    network_reduction = get_network_reduction(network_model)\n    for d in devices\n        bus_no = PNM.get_mapped_bus_number(network_reduction, PSY.get_bus(d))\n        for t in get_time_steps(container)\n            _add_to_jump_expression!(\n                get_expression(container, T(), PSY.ACBus)[bus_no, t],\n                PSY.get_reactive_power(d),\n                -1.0,\n            )\n        end\n    end\n    return\nend\n\nfunction add_to_expression!(\n    container::OptimizationContainer,\n    ::Type{T},\n    ::Type{U},\n    devices::IS.FlattenIteratorWrapper{V},\n    model::DeviceModel{V, W},\n    network_model::NetworkModel{AreaBalancePowerModel},\n) where {\n    T <: SystemBalanceExpressions,\n    U <: TimeSeriesParameter,\n    V <: PSY.Device,\n    W <: AbstractDeviceFormulation,\n}\n    param_container = get_parameter(container, U(), V)\n    multiplier = get_multiplier_array(param_container)\n    for d in devices, t in get_time_steps(container)\n        bus = PSY.get_bus(d)\n        area_name = PSY.get_name(PSY.get_area(bus))\n        name = PSY.get_name(d)\n        _add_to_jump_expression!(\n            get_expression(container, T(), PSY.Area)[area_name, t],\n            get_parameter_column_refs(param_container, name)[t],\n            multiplier[name, t],\n        )\n    end\n    return\nend\n\nfunction add_to_expression!(\n    container::OptimizationContainer,\n    ::Type{T},\n    ::Type{U},\n    devices::IS.FlattenIteratorWrapper{V},\n    model::DeviceModel{V, W},\n    network_model::NetworkModel{AreaBalancePowerModel},\n) where {\n    T <: SystemBalanceExpressions,\n    U <: TimeSeriesParameter,\n    V <: PSY.ElectricLoad,\n    W <: AbstractLoadFormulation,\n}\n    param_container = get_parameter(container, U(), V)\n    multiplier = get_multiplier_array(param_container)\n    ts_name = get_time_series_names(model)[U]\n    ts_type = get_default_time_series_type(container)\n    for d in devices\n        bus = PSY.get_bus(d)\n        area_name = PSY.get_name(PSY.get_area(bus))\n        name = PSY.get_name(d)\n        has_ts = PSY.has_time_series(d, ts_type, ts_name)\n        if !has_ts\n            @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.\"\n        end\n        for t in get_time_steps(container)\n            if has_ts\n                param_value = get_parameter_column_refs(param_container, name)[t]\n                mult = multiplier[name, t]\n            else\n                param_value = 1.0\n                mult = get_multiplier_value(U(), d, W())\n            end\n            _add_to_jump_expression!(\n                get_expression(container, T(), PSY.Area)[area_name, t],\n                param_value,\n                mult,\n            )\n        end\n    end\n    return\nend\n\nfunction add_to_expression!(\n    container::OptimizationContainer,\n    ::Type{T},\n    ::Type{U},\n    devices::IS.FlattenIteratorWrapper{V},\n    ::DeviceModel{V, W},\n    network_model::NetworkModel{X},\n) where {\n    T <: ActivePowerBalance,\n    U <: OnStatusParameter,\n    V <: PSY.ThermalGen,\n    W <: AbstractDeviceFormulation,\n    X <: PM.AbstractPowerModel,\n}\n    parameter = get_parameter_array(container, U(), V)\n    network_reduction = get_network_reduction(network_model)\n    for d in devices, t in get_time_steps(container)\n        bus_no = PNM.get_mapped_bus_number(network_reduction, PSY.get_bus(d))\n        name = PSY.get_name(d)\n        mult = get_expression_multiplier(U(), T(), d, W())\n        _add_to_jump_expression!(\n            get_expression(container, T(), PSY.ACBus)[bus_no, t],\n            parameter[name, t],\n            mult,\n        )\n    end\n    return\nend\n\n\"\"\"\nDefault implementation to add device variables to SystemBalanceExpressions\n\"\"\"\nfunction add_to_expression!(\n    container::OptimizationContainer,\n    ::Type{T},\n    ::Type{U},\n    devices::IS.FlattenIteratorWrapper{V},\n    ::DeviceModel{V, W},\n    network_model::NetworkModel{X},\n) where {\n    T <: SystemBalanceExpressions,\n    U <: VariableType,\n    V <: PSY.StaticInjection,\n    W <: AbstractDeviceFormulation,\n    X <: PM.AbstractPowerModel,\n}\n    variable = get_variable(container, U(), V)\n    expression = get_expression(container, T(), PSY.ACBus)\n    network_reduction = get_network_reduction(network_model)\n    for d in devices\n        name = PSY.get_name(d)\n        bus_no = PNM.get_mapped_bus_number(network_reduction, PSY.get_bus(d))\n        for t in get_time_steps(container)\n            _add_to_jump_expression!(\n                expression[bus_no, t],\n                variable[name, t],\n                get_variable_multiplier(U(), V, W()),\n            )\n        end\n    end\n    return\nend\n\nfunction add_to_expression!(\n    container::OptimizationContainer,\n    ::Type{T},\n    ::Type{U},\n    devices::IS.FlattenIteratorWrapper{V},\n    ::DeviceModel{V, W},\n    network_model::NetworkModel{AreaBalancePowerModel},\n) where {\n    T <: SystemBalanceExpressions,\n    U <: VariableType,\n    V <: PSY.StaticInjection,\n    W <: AbstractDeviceFormulation,\n}\n    variable = get_variable(container, U(), V)\n    expression = get_expression(container, T(), PSY.Area)\n    for d in devices, t in get_time_steps(container)\n        name = PSY.get_name(d)\n        bus = PSY.get_bus(d)\n        area_name = PSY.get_name(PSY.get_area(bus))\n        _add_to_jump_expression!(\n            expression[area_name, t],\n            variable[name, t],\n            get_variable_multiplier(U(), V, W()),\n        )\n    end\n    return\nend\n\n\"\"\"\nDefault implementation to add branch variables to SystemBalanceExpressions\n\"\"\"\nfunction add_to_expression!(\n    container::OptimizationContainer,\n    ::Type{T},\n    ::Type{U},\n    devices::IS.FlattenIteratorWrapper{V},\n    ::DeviceModel{V, W},\n    network_model::NetworkModel{X},\n) where {\n    T <: ActivePowerBalance,\n    U <: HVDCLosses,\n    V <: PSY.TwoTerminalHVDC,\n    W <: HVDCTwoTerminalDispatch,\n    X <: Union{PTDFPowerModel, CopperPlatePowerModel},\n}\n    variable = get_variable(container, U(), V)\n    expression = get_expression(container, T(), PSY.System)\n    for d in devices\n        name = PSY.get_name(d)\n        device_bus_from = PSY.get_arc(d).from\n        device_bus_to = PSY.get_arc(d).to\n        ref_bus_from = get_reference_bus(network_model, device_bus_from)\n        ref_bus_to = get_reference_bus(network_model, device_bus_to)\n        if ref_bus_from == ref_bus_to\n            for t in get_time_steps(container)\n                _add_to_jump_expression!(\n                    expression[ref_bus_from, t],\n                    variable[name, t],\n                    get_variable_multiplier(U(), d, W()),\n                )\n            end\n        end\n    end\n    return\nend\n\nfunction add_to_expression!(\n    container::OptimizationContainer,\n    ::Type{T},\n    ::Type{U},\n    devices::IS.FlattenIteratorWrapper{V},\n    ::DeviceModel{V, W},\n    network_model::NetworkModel{X},\n) where {\n    T <: ActivePowerBalance,\n    U <: HVDCLosses,\n    V <: PSY.TwoTerminalHVDC,\n    W <: HVDCTwoTerminalDispatch,\n    X <:\n    Union{AreaPTDFPowerModel, AreaBalancePowerModel},\n}\n    variable = get_variable(container, U(), V)\n    expression = get_expression(container, T(), PSY.Area)\n    for d in devices\n        name = PSY.get_name(d)\n        device_bus_from = PSY.get_arc(d).from\n        area_name = PSY.get_name(PSY.get_area(device_bus_from))\n        for t in get_time_steps(container)\n            _add_to_jump_expression!(\n                expression[area_name, t],\n                variable[name, t],\n                get_variable_multiplier(U(), d, W()),\n            )\n        end\n    end\n    return\nend\n\n\"\"\"\nDefault implementation to add branch variables to SystemBalanceExpressions\n\"\"\"\nfunction add_to_expression!(\n    container::OptimizationContainer,\n    ::Type{T},\n    ::Type{U},\n    devices::IS.FlattenIteratorWrapper{V},\n    ::DeviceModel{V, W},\n    network_model::NetworkModel{Union{PTDFPowerModel}},\n) where {\n    T <: ActivePowerBalance,\n    U <: FlowActivePowerToFromVariable,\n    V <: PSY.TwoTerminalHVDC,\n    W <: AbstractTwoTerminalDCLineFormulation,\n}\n    var = get_variable(container, U(), V)\n    nodal_expr = get_expression(container, T(), PSY.ACBus)\n    sys_expr = get_expression(container, T(), PSY.System)\n    network_reduction = get_network_reduction(network_model)\n    for d in devices\n        bus_no_to = PNM.get_mapped_bus_number(network_reduction, PSY.get_arc(d).to)\n        ref_bus_from = get_reference_bus(network_model, PSY.get_arc(d).from)\n        ref_bus_to = get_reference_bus(network_model, PSY.get_arc(d).to)\n        for t in get_time_steps(container)\n            flow_variable = var[PSY.get_name(d), t]\n            _add_to_jump_expression!(nodal_expr[bus_no_to, t], flow_variable, -1.0)\n            if ref_bus_from != ref_bus_to\n                _add_to_jump_expression!(sys_expr[ref_bus_to, t], flow_variable, -1.0)\n            end\n        end\n    end\n    return\nend\n\n\"\"\"\nDefault implementation to add branch variables to SystemBalanceExpressions\n\"\"\"\nfunction add_to_expression!(\n    container::OptimizationContainer,\n    ::Type{T},\n    ::Type{U},\n    devices::IS.FlattenIteratorWrapper{V},\n    ::DeviceModel{V, W},\n    network_model::NetworkModel{X},\n) where {\n    T <: ActivePowerBalance,\n    U <: FlowActivePowerFromToVariable,\n    V <: PSY.TwoTerminalHVDC,\n    W <: AbstractTwoTerminalDCLineFormulation,\n    X <: AbstractPTDFModel,\n}\n    var = get_variable(container, U(), V)\n    nodal_expr = get_expression(container, T(), PSY.ACBus)\n    sys_expr = get_expression(container, T(), _system_expression_type(X))\n    network_reduction = get_network_reduction(network_model)\n    for d in devices\n        bus_no_from =\n            PNM.get_mapped_bus_number(network_reduction, PSY.get_arc(d).from)\n        ref_bus_to = get_reference_bus(network_model, PSY.get_arc(d).to)\n        ref_bus_from = get_reference_bus(network_model, PSY.get_arc(d).from)\n        for t in get_time_steps(container)\n            flow_variable = var[PSY.get_name(d), t]\n            _add_to_jump_expression!(nodal_expr[bus_no_from, t], flow_variable, -1.0)\n            if ref_bus_from != ref_bus_to\n                _add_to_jump_expression!(sys_expr[ref_bus_from, t], flow_variable, -1.0)\n            end\n        end\n    end\n    return\nend\n\n\"\"\"\nPWL implementation to add FromTo branch variables to SystemBalanceExpressions\n\"\"\"\nfunction add_to_expression!(\n    container::OptimizationContainer,\n    ::Type{T},\n    ::Type{U},\n    devices::IS.FlattenIteratorWrapper{V},\n    ::DeviceModel{V, W},\n    network_model::NetworkModel{X},\n) where {\n    T <: ActivePowerBalance,\n    U <: HVDCActivePowerReceivedFromVariable,\n    V <: PSY.TwoTerminalHVDC,\n    W <: HVDCTwoTerminalPiecewiseLoss,\n    X <: AbstractPTDFModel,\n}\n    var = get_variable(container, U(), V)\n    nodal_expr = get_expression(container, T(), PSY.ACBus)\n    sys_expr = get_expression(container, T(), _system_expression_type(X))\n    network_reduction = get_network_reduction(network_model)\n    for d in devices\n        bus_no_from =\n            PNM.get_mapped_bus_number(network_reduction, PSY.get_arc(d).from)\n        ref_bus_to = get_reference_bus(network_model, PSY.get_arc(d).to)\n        ref_bus_from = get_reference_bus(network_model, PSY.get_arc(d).from)\n        for t in get_time_steps(container)\n            flow_variable = var[PSY.get_name(d), t]\n            _add_to_jump_expression!(nodal_expr[bus_no_from, t], flow_variable, 1.0)\n            if ref_bus_from != ref_bus_to\n                _add_to_jump_expression!(sys_expr[ref_bus_from, t], flow_variable, 1.0)\n            end\n        end\n    end\n    return\nend\n\n\"\"\"\nHVDC LCC implementation to add ActivePowerBalance expression for HVDCActivePowerReceivedFromVariable variable\n\"\"\"\nfunction add_to_expression!(\n    container::OptimizationContainer,\n    ::Type{T},\n    ::Type{U},\n    devices::IS.FlattenIteratorWrapper{V},\n    ::DeviceModel{V, W},\n    network_model::NetworkModel{X},\n) where {\n    T <: ActivePowerBalance,                        # expression\n    U <: HVDCActivePowerReceivedFromVariable,       # variable\n    V <: PSY.TwoTerminalHVDC,                      # power system type\n    W <: HVDCTwoTerminalLCC,                        # formulation\n    X <: ACPPowerModel,                             # network model\n}\n    var = get_variable(container, U(), V)\n    nodal_expr = get_expression(container, T(), PSY.ACBus)\n    network_reduction = get_network_reduction(network_model)\n    for d in devices\n        bus_no_from =\n            PNM.get_mapped_bus_number(network_reduction, PSY.get_arc(d).from)\n        for t in get_time_steps(container)\n            flow_variable = var[PSY.get_name(d), t]\n            _add_to_jump_expression!(nodal_expr[bus_no_from, t], flow_variable, -1.0)\n        end\n    end\n    return\nend\n\n\"\"\"\nHVDC LCC implementation to add ActivePowerBalance expression for HVDCActivePowerReceivedToVariable variable\n\"\"\"\nfunction add_to_expression!(\n    container::OptimizationContainer,\n    ::Type{T},\n    ::Type{U},\n    devices::IS.FlattenIteratorWrapper{V},\n    ::DeviceModel{V, W},\n    network_model::NetworkModel{X},\n) where {\n    T <: ActivePowerBalance,\n    U <: HVDCActivePowerReceivedToVariable,\n    V <: PSY.TwoTerminalHVDC,\n    W <: HVDCTwoTerminalLCC,\n    X <: ACPPowerModel,\n}\n    var = get_variable(container, U(), V)\n    nodal_expr = get_expression(container, T(), PSY.ACBus)\n    network_reduction = get_network_reduction(network_model)\n    for d in devices\n        bus_no_to =\n            PNM.get_mapped_bus_number(network_reduction, PSY.get_arc(d).to)\n        for t in get_time_steps(container)\n            flow_variable = var[PSY.get_name(d), t]\n            _add_to_jump_expression!(nodal_expr[bus_no_to, t], flow_variable, 1.0)\n        end\n    end\n    return\nend\n\n\"\"\"\nHVDC LCC implementation to add ReactivePowerBalance expression for HVDCReactivePowerReceivedFromVariable variable\n\"\"\"\nfunction add_to_expression!(\n    container::OptimizationContainer,\n    ::Type{T},\n    ::Type{U},\n    devices::IS.FlattenIteratorWrapper{V},\n    ::DeviceModel{V, W},\n    network_model::NetworkModel{X},\n) where {\n    T <: ReactivePowerBalance,                        # expression\n    U <: HVDCReactivePowerReceivedFromVariable,     # variable\n    V <: PSY.TwoTerminalHVDC,                      # power system type\n    W <: HVDCTwoTerminalLCC,                        # formulation\n    X <: ACPPowerModel,                             # network model\n}\n    var = get_variable(container, U(), V)\n    nodal_expr = get_expression(container, T(), PSY.ACBus)\n    network_reduction = get_network_reduction(network_model)\n    for d in devices\n        bus_no_from =\n            PNM.get_mapped_bus_number(network_reduction, PSY.get_arc(d).from)\n        for t in get_time_steps(container)\n            flow_variable = var[PSY.get_name(d), t]\n            _add_to_jump_expression!(nodal_expr[bus_no_from, t], flow_variable, -1.0)\n        end\n    end\n    return\nend\n\n\"\"\"\nHVDC LCC implementation to add ReactivePowerBalance expression for HVDCReactivePowerReceivedToVariable variable\n\"\"\"\nfunction add_to_expression!(\n    container::OptimizationContainer,\n    ::Type{T},\n    ::Type{U},\n    devices::IS.FlattenIteratorWrapper{V},\n    ::DeviceModel{V, W},\n    network_model::NetworkModel{X},\n) where {\n    T <: ReactivePowerBalance,\n    U <: HVDCReactivePowerReceivedToVariable,\n    V <: PSY.TwoTerminalHVDC,\n    W <: HVDCTwoTerminalLCC,\n    X <: ACPPowerModel,\n}\n    var = get_variable(container, U(), V)\n    nodal_expr = get_expression(container, T(), PSY.ACBus)\n    network_reduction = get_network_reduction(network_model)\n    for d in devices\n        bus_no_to =\n            PNM.get_mapped_bus_number(network_reduction, PSY.get_arc(d).to)\n        for t in get_time_steps(container)\n            flow_variable = var[PSY.get_name(d), t]\n            _add_to_jump_expression!(nodal_expr[bus_no_to, t], flow_variable, -1.0)\n        end\n    end\n    return\nend\n\n\"\"\"\nPWL implementation to add FromTo branch variables to SystemBalanceExpressions\n\"\"\"\nfunction add_to_expression!(\n    container::OptimizationContainer,\n    ::Type{T},\n    ::Type{U},\n    devices::IS.FlattenIteratorWrapper{V},\n    ::DeviceModel{V, W},\n    network_model::NetworkModel{X},\n) where {\n    T <: ActivePowerBalance,\n    U <: HVDCActivePowerReceivedToVariable,\n    V <: PSY.TwoTerminalHVDC,\n    W <: HVDCTwoTerminalPiecewiseLoss,\n    X <: AbstractPTDFModel,\n}\n    var = get_variable(container, U(), V)\n    nodal_expr = get_expression(container, T(), PSY.ACBus)\n    sys_expr = get_expression(container, T(), _system_expression_type(X))\n    network_reduction = get_network_reduction(network_model)\n    for d in devices\n        bus_no_to =\n            PNM.get_mapped_bus_number(network_reduction, PSY.get_arc(d).to)\n        ref_bus_to = get_reference_bus(network_model, PSY.get_arc(d).to)\n        ref_bus_from = get_reference_bus(network_model, PSY.get_arc(d).from)\n        for t in get_time_steps(container)\n            flow_variable = var[PSY.get_name(d), t]\n            _add_to_jump_expression!(nodal_expr[bus_no_to, t], flow_variable, 1.0)\n            if ref_bus_from != ref_bus_to\n                _add_to_jump_expression!(sys_expr[ref_bus_to, t], flow_variable, 1.0)\n            end\n        end\n    end\n    return\nend\n\n\"\"\"\nDefault implementation to add branch variables to SystemBalanceExpressions\n\"\"\"\nfunction add_to_expression!(\n    container::OptimizationContainer,\n    ::Type{T},\n    ::Type{U},\n    devices::IS.FlattenIteratorWrapper{V},\n    ::DeviceModel{V, W},\n    network_model::NetworkModel{X},\n) where {\n    T <: ActivePowerBalance,\n    U <: FlowActivePowerToFromVariable,\n    V <: PSY.TwoTerminalHVDC,\n    W <: AbstractTwoTerminalDCLineFormulation,\n    X <: CopperPlatePowerModel,\n}\n    if has_subnetworks(network_model)\n        var = get_variable(container, U(), V)\n        sys_expr = get_expression(container, T(), PSY.System)\n        for d in devices\n            ref_bus_from = get_reference_bus(network_model, PSY.get_arc(d).from)\n            ref_bus_to = get_reference_bus(network_model, PSY.get_arc(d).to)\n            for t in get_time_steps(container)\n                flow_variable = var[PSY.get_name(d), t]\n                if ref_bus_from != ref_bus_to\n                    _add_to_jump_expression!(sys_expr[ref_bus_to, t], flow_variable, 1.0)\n                end\n            end\n        end\n    end\n    return\nend\n\n\"\"\"\nDefault implementation to add branch variables to SystemBalanceExpressions\n\"\"\"\nfunction add_to_expression!(\n    container::OptimizationContainer,\n    ::Type{T},\n    ::Type{U},\n    devices::IS.FlattenIteratorWrapper{V},\n    ::DeviceModel{V, W},\n    network_model::NetworkModel{X},\n) where {\n    T <: ActivePowerBalance,\n    U <: FlowActivePowerFromToVariable,\n    V <: PSY.TwoTerminalHVDC,\n    W <: AbstractTwoTerminalDCLineFormulation,\n    X <: CopperPlatePowerModel,\n}\n    if has_subnetworks(network_model)\n        var = get_variable(container, U(), V)\n        sys_expr = get_expression(container, T(), PSY.System)\n        for d in devices\n            ref_bus_from = get_reference_bus(network_model, PSY.get_arc(d).from)\n            ref_bus_to = get_reference_bus(network_model, PSY.get_arc(d).to)\n            for t in get_time_steps(container)\n                flow_variable = var[PSY.get_name(d), t]\n                if ref_bus_from != ref_bus_to\n                    _add_to_jump_expression!(sys_expr[ref_bus_to, t], flow_variable, -1.0)\n                end\n            end\n        end\n    end\n    return\nend\n\n\"\"\"\nDefault implementation to add branch variables to SystemBalanceExpressions\n\"\"\"\nfunction add_to_expression!(\n    container::OptimizationContainer,\n    ::Type{T},\n    ::Type{U},\n    devices::IS.FlattenIteratorWrapper{V},\n    ::DeviceModel{V, W},\n    network_model::NetworkModel{X},\n) where {\n    T <: ActivePowerBalance,\n    U <: FlowActivePowerFromToVariable,\n    V <: PSY.Branch,\n    W <: AbstractDeviceFormulation,\n    X <: PM.AbstractPowerModel,\n}\n    variable = get_variable(container, U(), V)\n    expression = get_expression(container, T(), PSY.ACBus)\n    network_reduction = get_network_reduction(network_model)\n    for d in devices\n        name = PSY.get_name(d)\n        bus_no_ = PSY.get_number(PSY.get_arc(d).from)\n        bus_no = PNM.get_mapped_bus_number(network_reduction, bus_no_)\n        for t in get_time_steps(container)\n            _add_to_jump_expression!(\n                expression[bus_no, t],\n                variable[name, t],\n                get_variable_multiplier(U(), V, W()),\n            )\n        end\n    end\n    return\nend\n\n\"\"\"\nDefault implementation to add branch variables to SystemBalanceExpressions\n\"\"\"\nfunction add_to_expression!(\n    container::OptimizationContainer,\n    ::Type{T},\n    ::Type{U},\n    devices::IS.FlattenIteratorWrapper{V},\n    ::DeviceModel{V, W},\n    network_model::NetworkModel{X},\n) where {\n    T <: ActivePowerBalance,\n    U <: FlowActivePowerToFromVariable,\n    V <: PSY.ACBranch,\n    W <: AbstractDeviceFormulation,\n    X <: PM.AbstractPowerModel,\n}\n    variable = get_variable(container, U(), V)\n    expression = get_expression(container, T(), PSY.ACBus)\n    network_reduction = get_network_reduction(network_model)\n    for d in devices\n        name = PSY.get_name(d)\n        bus_no_ = PSY.get_number(PSY.get_arc(d).to)\n        bus_no = PNM.get_mapped_bus_number(network_reduction, bus_no_)\n        for t in get_time_steps(container)\n            _add_to_jump_expression!(\n                expression[bus_no, t],\n                variable[name, t],\n                get_variable_multiplier(U(), V, W()),\n            )\n        end\n    end\n    return\nend\n\nfunction add_to_expression!(\n    container::OptimizationContainer,\n    ::Type{T},\n    ::Type{U},\n    devices::IS.FlattenIteratorWrapper{V},\n    ::DeviceModel{V, HVDCTwoTerminalDispatch},\n    network_model::NetworkModel{AreaBalancePowerModel},\n) where {\n    T <: ActivePowerBalance,\n    U <: FlowActivePowerToFromVariable,\n    V <: PSY.TwoTerminalHVDC,\n}\n    variable = get_variable(container, U(), V)\n    expression = get_expression(container, T(), PSY.Area)\n    for d in devices\n        name = PSY.get_name(d)\n        area_name = PSY.get_name(PSY.get_area(PSY.get_arc(d).to))\n        for t in get_time_steps(container)\n            _add_to_jump_expression!(\n                expression[area_name, t],\n                variable[name, t],\n                get_variable_multiplier(U(), V, HVDCTwoTerminalDispatch()),\n            )\n        end\n    end\n    return\nend\n\nfunction add_to_expression!(\n    container::OptimizationContainer,\n    ::Type{T},\n    ::Type{U},\n    devices::IS.FlattenIteratorWrapper{V},\n    device_model::DeviceModel{V, W},\n    network_model::NetworkModel{AreaBalancePowerModel},\n) where {\n    T <: SystemBalanceExpressions,\n    U <: OnVariable,\n    V <: PSY.ThermalGen,\n    W <: AbstractCompactUnitCommitment,\n}\n    _add_to_expression!(\n        container,\n        T,\n        U,\n        devices,\n        device_model,\n        network_model,\n    )\n    return\nend\n\nfunction add_to_expression!(\n    container::OptimizationContainer,\n    ::Type{T},\n    ::Type{U},\n    devices::IS.FlattenIteratorWrapper{V},\n    device_model::DeviceModel{V, W},\n    network_model::NetworkModel{X},\n) where {\n    T <: SystemBalanceExpressions,\n    U <: OnVariable,\n    V <: PSY.ThermalGen,\n    W <: AbstractCompactUnitCommitment,\n    X <: PM.AbstractPowerModel,\n}\n    _add_to_expression!(\n        container,\n        T,\n        U,\n        devices,\n        device_model,\n        network_model,\n    )\n    return\nend\n\nfunction _add_to_expression!(\n    container::OptimizationContainer,\n    ::Type{T},\n    ::Type{U},\n    devices::IS.FlattenIteratorWrapper{V},\n    ::DeviceModel{V, W},\n    network_model::NetworkModel{X},\n) where {\n    T <: SystemBalanceExpressions,\n    U <: OnVariable,\n    V <: PSY.ThermalGen,\n    W <: AbstractCompactUnitCommitment,\n    X <: PM.AbstractPowerModel,\n}\n    variable = get_variable(container, U(), V)\n    expression = get_expression(container, T(), PSY.ACBus)\n    network_reduction = get_network_reduction(network_model)\n    for d in devices\n        name = PSY.get_name(d)\n        bus_no_ = PSY.get_number(PSY.get_bus(d))\n        bus_no = PNM.get_mapped_bus_number(network_reduction, bus_no_)\n        for t in get_time_steps(container)\n            if PSY.get_must_run(d)\n                _add_to_jump_expression!(\n                    expression[bus_no, t],\n                    get_variable_multiplier(U(), d, W()),\n                )\n            else\n                _add_to_jump_expression!(\n                    expression[bus_no, t],\n                    variable[name, t],\n                    get_variable_multiplier(U(), d, W()),\n                )\n            end\n        end\n    end\n    return\nend\n\nfunction add_to_expression!(\n    container::OptimizationContainer,\n    ::Type{T},\n    ::Type{U},\n    devices::IS.FlattenIteratorWrapper{V},\n    ::DeviceModel{V, W},\n    network_model::NetworkModel{AreaBalancePowerModel},\n) where {\n    T <: SystemBalanceExpressions,\n    U <: OnVariable,\n    V <: PSY.ThermalGen,\n    W <: Union{AbstractCompactUnitCommitment, ThermalCompactDispatch},\n}\n    variable = get_variable(container, U(), V)\n    expression = get_expression(container, T(), PSY.Area)\n    for d in devices\n        name = PSY.get_name(d)\n        bus = PSY.get_bus(d)\n        area_name = PSY.get_name(PSY.get_area(bus))\n        for t in get_time_steps(container)\n            if PSY.get_must_run(d)\n                _add_to_jump_expression!(\n                    expression[area_name, t],\n                    get_variable_multiplier(U(), d, W()),\n                )\n            else\n                _add_to_jump_expression!(\n                    expression[area_name, t],\n                    variable[name, t],\n                    get_variable_multiplier(U(), d, W()),\n                )\n            end\n        end\n    end\n    return\nend\n\n\"\"\"\nDefault implementation to add parameters to Copperplate SystemBalanceExpressions\n\"\"\"\nfunction add_to_expression!(\n    container::OptimizationContainer,\n    ::Type{T},\n    ::Type{U},\n    devices::IS.FlattenIteratorWrapper{V},\n    device_model::DeviceModel{V, W},\n    network_model::NetworkModel{CopperPlatePowerModel},\n) where {\n    T <: SystemBalanceExpressions,\n    U <: TimeSeriesParameter,\n    V <: PSY.StaticInjection,\n    W <: AbstractDeviceFormulation,\n}\n    param_container = get_parameter(container, U(), V)\n    multiplier = get_multiplier_array(param_container)\n    expression = get_expression(container, T(), PSY.System)\n    for d in devices\n        device_bus = PSY.get_bus(d)\n        ref_bus = get_reference_bus(network_model, device_bus)\n        name = PSY.get_name(d)\n        for t in get_time_steps(container)\n            _add_to_jump_expression!(\n                expression[ref_bus, t],\n                get_parameter_column_refs(param_container, name)[t],\n                multiplier[name, t],\n            )\n        end\n    end\nend\n\n\"\"\"\nElectric Load implementation to add parameters to Copperplate SystemBalanceExpressions\n\"\"\"\nfunction add_to_expression!(\n    container::OptimizationContainer,\n    ::Type{T},\n    ::Type{U},\n    devices::IS.FlattenIteratorWrapper{V},\n    device_model::DeviceModel{V, W},\n    network_model::NetworkModel{CopperPlatePowerModel},\n) where {\n    T <: SystemBalanceExpressions,\n    U <: TimeSeriesParameter,\n    V <: PSY.ElectricLoad,\n    W <: AbstractLoadFormulation,\n}\n    param_container = get_parameter(container, U(), V)\n    multiplier = get_multiplier_array(param_container)\n    expression = get_expression(container, T(), PSY.System)\n    ts_name = get_time_series_names(device_model)[U]\n    ts_type = get_default_time_series_type(container)\n    for d in devices\n        device_bus = PSY.get_bus(d)\n        ref_bus = get_reference_bus(network_model, device_bus)\n        name = PSY.get_name(d)\n        has_ts = PSY.has_time_series(d, ts_type, ts_name)\n        if !has_ts\n            @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.\"\n        end\n        for t in get_time_steps(container)\n            if has_ts\n                param_value = get_parameter_column_refs(param_container, name)[t]\n                mult = multiplier[name, t]\n            else\n                param_value = 1.0\n                mult = get_multiplier_value(U(), d, W())\n            end\n            _add_to_jump_expression!(\n                expression[ref_bus, t],\n                param_value,\n                mult,\n            )\n        end\n    end\n    return\nend\n\n\"\"\"\nMotor load implementation to add parameters to SystemBalanceExpressions CopperPlate\n\"\"\"\nfunction add_to_expression!(\n    container::OptimizationContainer,\n    ::Type{T},\n    ::Type{U},\n    devices::IS.FlattenIteratorWrapper{V},\n    device_model::DeviceModel{V, W},\n    network_model::NetworkModel{CopperPlatePowerModel},\n) where {\n    T <: ActivePowerBalance,\n    U <: ActivePowerTimeSeriesParameter,\n    V <: PSY.MotorLoad,\n    W <: StaticPowerLoad,\n}\n    expression = get_expression(container, T(), PSY.System)\n    for d in devices\n        device_bus = PSY.get_bus(d)\n        ref_bus = get_reference_bus(network_model, device_bus)\n        for t in get_time_steps(container)\n            _add_to_jump_expression!(\n                expression[ref_bus, t],\n                PSY.get_active_power(d),\n                -1.0,\n            )\n        end\n    end\n    return\nend\n\nfunction add_to_expression!(\n    container::OptimizationContainer,\n    ::Type{T},\n    ::Type{U},\n    devices::IS.FlattenIteratorWrapper{V},\n    ::DeviceModel{V, W},\n    network_model::NetworkModel{CopperPlatePowerModel},\n) where {\n    T <: ActivePowerBalance,\n    U <: OnStatusParameter,\n    V <: PSY.ThermalGen,\n    W <: AbstractDeviceFormulation,\n}\n    parameter = get_parameter_array(container, U(), V)\n    expression = get_expression(container, T(), PSY.System)\n    for d in devices\n        name = PSY.get_name(d)\n        device_bus = PSY.get_bus(d)\n        ref_bus = get_reference_bus(network_model, device_bus)\n        for t in get_time_steps(container)\n            mult = get_expression_multiplier(U(), T(), d, W())\n            _add_to_jump_expression!(expression[ref_bus, t], parameter[name, t], mult)\n        end\n    end\n    return\nend\n\n\"\"\"\nDefault implementation to add variables to SystemBalanceExpressions\n\"\"\"\nfunction add_to_expression!(\n    container::OptimizationContainer,\n    ::Type{T},\n    ::Type{U},\n    devices::IS.FlattenIteratorWrapper{V},\n    device_model::DeviceModel{V, W},\n    network_model::NetworkModel{CopperPlatePowerModel},\n) where {\n    T <: ActivePowerBalance,\n    U <: VariableType,\n    V <: PSY.StaticInjection,\n    W <: AbstractDeviceFormulation,\n}\n    variable = get_variable(container, U(), V)\n    expression = get_expression(container, T(), PSY.System)\n    for d in devices\n        device_bus = PSY.get_bus(d)\n        ref_bus = get_reference_bus(network_model, device_bus)\n        name = PSY.get_name(d)\n        for t in get_time_steps(container)\n            _add_to_jump_expression!(\n                expression[ref_bus, t],\n                variable[name, t],\n                get_variable_multiplier(U(), V, W()),\n            )\n        end\n    end\n    return\nend\n\nfunction add_to_expression!(\n    container::OptimizationContainer,\n    ::Type{T},\n    ::Type{U},\n    devices::IS.FlattenIteratorWrapper{V},\n    device_model::DeviceModel{V, W},\n    network_model::NetworkModel{CopperPlatePowerModel},\n) where {\n    T <: ActivePowerBalance,\n    U <: OnVariable,\n    V <: PSY.ThermalGen,\n    W <: AbstractCompactUnitCommitment,\n}\n    variable = get_variable(container, U(), V)\n    expression = get_expression(container, T(), PSY.System)\n    for d in devices\n        name = PSY.get_name(d)\n        device_bus = PSY.get_bus(d)\n        ref_bus = get_reference_bus(network_model, device_bus)\n        for t in get_time_steps(container)\n            _add_to_jump_expression!(\n                expression[ref_bus, t],\n                variable[name, t],\n                get_variable_multiplier(U(), d, W()),\n            )\n        end\n    end\n    return\nend\n\n\"\"\"\nDefault implementation to add parameters to PTDF SystemBalanceExpressions\n\"\"\"\nfunction add_to_expression!(\n    container::OptimizationContainer,\n    ::Type{T},\n    ::Type{U},\n    devices::IS.FlattenIteratorWrapper{V},\n    device_model::DeviceModel{V, W},\n    network_model::NetworkModel{X},\n) where {\n    T <: SystemBalanceExpressions,\n    U <: TimeSeriesParameter,\n    V <: PSY.StaticInjection,\n    W <: AbstractDeviceFormulation,\n    X <: AbstractPTDFModel,\n}\n    param_container = get_parameter(container, U(), V)\n    multiplier = get_multiplier_array(param_container)\n    sys_expr = get_expression(container, T(), _system_expression_type(X))\n    nodal_expr = get_expression(container, T(), PSY.ACBus)\n    network_reduction = get_network_reduction(network_model)\n    for d in devices\n        name = PSY.get_name(d)\n        device_bus = PSY.get_bus(d)\n        bus_no = PNM.get_mapped_bus_number(network_reduction, PSY.get_number(device_bus))\n        ref_index = _ref_index(network_model, device_bus)\n        param = get_parameter_column_refs(param_container, name)\n        for t in get_time_steps(container)\n            _add_to_jump_expression!(sys_expr[ref_index, t], param[t], multiplier[name, t])\n            _add_to_jump_expression!(nodal_expr[bus_no, t], param[t], multiplier[name, t])\n        end\n    end\n    return\nend\n\n\"\"\"\nElectric Load implementation to add parameters to PTDF SystemBalanceExpressions\n\"\"\"\nfunction add_to_expression!(\n    container::OptimizationContainer,\n    ::Type{T},\n    ::Type{U},\n    devices::IS.FlattenIteratorWrapper{V},\n    device_model::DeviceModel{V, W},\n    network_model::NetworkModel{X},\n) where {\n    T <: SystemBalanceExpressions,\n    U <: TimeSeriesParameter,\n    V <: PSY.ElectricLoad,\n    W <: AbstractLoadFormulation,\n    X <: AbstractPTDFModel,\n}\n    param_container = get_parameter(container, U(), V)\n    multiplier = get_multiplier_array(param_container)\n    sys_expr = get_expression(container, T(), _system_expression_type(X))\n    nodal_expr = get_expression(container, T(), PSY.ACBus)\n    network_reduction = get_network_reduction(network_model)\n    ts_name = get_time_series_names(device_model)[U]\n    ts_type = get_default_time_series_type(container)\n    for d in devices\n        name = PSY.get_name(d)\n        has_ts = PSY.has_time_series(d, ts_type, ts_name)\n        if !has_ts\n            @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.\"\n        end\n        device_bus = PSY.get_bus(d)\n        bus_no_ = PSY.get_number(device_bus)\n        bus_no = PNM.get_mapped_bus_number(network_reduction, bus_no_)\n        ref_index = _ref_index(network_model, device_bus)\n        for t in get_time_steps(container)\n            if has_ts\n                param = get_parameter_column_refs(param_container, name)[t]\n                mult = multiplier[name, t]\n            else\n                param = 1.0\n                mult = get_multiplier_value(U(), d, W())\n            end\n            _add_to_jump_expression!(sys_expr[ref_index, t], param, mult)\n            _add_to_jump_expression!(nodal_expr[bus_no, t], param, mult)\n        end\n    end\n    return\nend\n\nfunction add_to_expression!(\n    container::OptimizationContainer,\n    ::Type{T},\n    ::Type{U},\n    devices::IS.FlattenIteratorWrapper{V},\n    ::DeviceModel{V, W},\n    network_model::NetworkModel{X},\n) where {\n    T <: ActivePowerBalance,\n    U <: OnStatusParameter,\n    V <: PSY.ThermalGen,\n    W <: AbstractDeviceFormulation,\n    X <: AbstractPTDFModel,\n}\n    parameter = get_parameter_array(container, U(), V)\n    sys_expr = get_expression(container, T(), PSY.System)\n    nodal_expr = get_expression(container, T(), PSY.ACBus)\n    network_reduction = get_network_reduction(network_model)\n    for d in devices\n        name = PSY.get_name(d)\n        bus_no_ = PSY.get_number(PSY.get_bus(d))\n        bus_no = PNM.get_mapped_bus_number(network_reduction, bus_no_)\n        mult = get_expression_multiplier(U(), T(), d, W())\n        device_bus = PSY.get_bus(d)\n        ref_index = _ref_index(network_model, device_bus)\n        for t in get_time_steps(container)\n            _add_to_jump_expression!(sys_expr[ref_index, t], parameter[name, t], mult)\n            _add_to_jump_expression!(nodal_expr[bus_no, t], parameter[name, t], mult)\n        end\n    end\n    return\nend\n\n\"\"\"\nDefault implementation to add variables to SystemBalanceExpressions\n\"\"\"\nfunction add_to_expression!(\n    container::OptimizationContainer,\n    ::Type{T},\n    ::Type{U},\n    devices::IS.FlattenIteratorWrapper{V},\n    device_model::DeviceModel{V, W},\n    network_model::NetworkModel{X},\n) where {\n    T <: ActivePowerBalance,\n    U <: VariableType,\n    V <: PSY.StaticInjection,\n    W <: AbstractDeviceFormulation,\n    X <: PTDFPowerModel,\n}\n    variable = get_variable(container, U(), V)\n    sys_expr = get_expression(container, T(), PSY.System)\n    nodal_expr = get_expression(container, T(), PSY.ACBus)\n    network_reduction = get_network_reduction(network_model)\n    for d in devices\n        name = PSY.get_name(d)\n        device_bus = PSY.get_bus(d)\n        bus_no = PNM.get_mapped_bus_number(network_reduction, device_bus)\n        ref_index = _ref_index(network_model, device_bus)\n        for t in get_time_steps(container)\n            _add_to_jump_expression!(\n                sys_expr[ref_index, t],\n                variable[name, t],\n                get_variable_multiplier(U(), V, W()),\n            )\n            _add_to_jump_expression!(\n                nodal_expr[bus_no, t],\n                variable[name, t],\n                get_variable_multiplier(U(), V, W()),\n            )\n        end\n    end\n    return\nend\n\n\"\"\"\nMotor Load implementation to add constant motor power to PTDF SystemBalanceExpressions\n\"\"\"\nfunction add_to_expression!(\n    container::OptimizationContainer,\n    ::Type{T},\n    ::Type{U},\n    devices::IS.FlattenIteratorWrapper{V},\n    device_model::DeviceModel{V, W},\n    network_model::NetworkModel{X},\n) where {\n    T <: ActivePowerBalance,\n    U <: ActivePowerTimeSeriesParameter,\n    V <: PSY.MotorLoad,\n    W <: StaticPowerLoad,\n    X <: AbstractPTDFModel,\n}\n    sys_expr = get_expression(container, T(), PSY.System)\n    nodal_expr = get_expression(container, T(), PSY.ACBus)\n    network_reduction = get_network_reduction(network_model)\n    for d in devices\n        device_bus = PSY.get_bus(d)\n        bus_no = PNM.get_mapped_bus_number(network_reduction, device_bus)\n        ref_index = _ref_index(network_model, device_bus)\n        for t in get_time_steps(container)\n            _add_to_jump_expression!(\n                sys_expr[ref_index, t],\n                PSY.get_active_power(d),\n                -1.0,\n            )\n            _add_to_jump_expression!(\n                nodal_expr[bus_no, t],\n                PSY.get_active_power(d),\n                -1.0,\n            )\n        end\n    end\n    return\nend\n\nfunction add_to_expression!(\n    container::OptimizationContainer,\n    ::Type{T},\n    ::Type{U},\n    devices::IS.FlattenIteratorWrapper{V},\n    device_model::DeviceModel{V, W},\n    network_model::NetworkModel{X},\n) where {\n    T <: ActivePowerBalance,\n    U <: ActivePowerVariable,\n    V <: PSY.StaticInjection,\n    W <: AbstractDeviceFormulation,\n    X <: AreaPTDFPowerModel,\n}\n    variable = get_variable(container, U(), V)\n    area_expr = get_expression(container, T(), PSY.Area)\n    nodal_expr = get_expression(container, T(), PSY.ACBus)\n    network_reduction = get_network_reduction(network_model)\n    for d in devices\n        name = PSY.get_name(d)\n        device_bus = PSY.get_bus(d)\n        area_name = PSY.get_name(PSY.get_area(device_bus))\n        bus_no = PNM.get_mapped_bus_number(network_reduction, device_bus)\n        for t in get_time_steps(container)\n            _add_to_jump_expression!(\n                area_expr[area_name, t],\n                variable[name, t],\n                get_variable_multiplier(U(), V, W()),\n            )\n            _add_to_jump_expression!(\n                nodal_expr[bus_no, t],\n                variable[name, t],\n                get_variable_multiplier(U(), V, W()),\n            )\n        end\n    end\n    return\nend\n\n# The on variables are included in the system balance expressions becuase they\n# are multiplied by the Pmin and the active power is not the total active power\n# but the power above minimum.\nfunction add_to_expression!(\n    container::OptimizationContainer,\n    ::Type{T},\n    ::Type{U},\n    devices::IS.FlattenIteratorWrapper{V},\n    device_model::DeviceModel{V, W},\n    network_model::NetworkModel{X},\n) where {\n    T <: ActivePowerBalance,\n    U <: OnVariable,\n    V <: PSY.ThermalGen,\n    W <: AbstractCompactUnitCommitment,\n    X <: PTDFPowerModel,\n}\n    variable = get_variable(container, U(), V)\n    sys_expr = get_expression(container, T(), _system_expression_type(PTDFPowerModel))\n    nodal_expr = get_expression(container, T(), PSY.ACBus)\n    network_reduction = get_network_reduction(network_model)\n    for d in devices\n        name = PSY.get_name(d)\n        bus_no = PNM.get_mapped_bus_number(network_reduction, PSY.get_bus(d))\n        ref_index = _ref_index(network_model, PSY.get_bus(d))\n        for t in get_time_steps(container)\n            _add_to_jump_expression!(\n                sys_expr[ref_index, t],\n                variable[name, t],\n                get_variable_multiplier(U(), d, W()),\n            )\n            _add_to_jump_expression!(\n                nodal_expr[bus_no, t],\n                variable[name, t],\n                get_variable_multiplier(U(), d, W()),\n            )\n        end\n    end\n    return\nend\n\n\"\"\"\nImplementation of add_to_expression! for lossless branch/network models\n\"\"\"\nfunction add_to_expression!(\n    container::OptimizationContainer,\n    ::Type{T},\n    ::Type{U},\n    devices::IS.FlattenIteratorWrapper{V},\n    ::DeviceModel{V, W},\n    network_model::NetworkModel{X},\n) where {\n    T <: ActivePowerBalance,\n    U <: FlowActivePowerVariable,\n    V <: PSY.ACBranch,\n    W <: AbstractBranchFormulation,\n    X <: PM.AbstractActivePowerModel,\n}\n    var = get_variable(container, U(), V)\n    expression = get_expression(container, T(), PSY.ACBus)\n    network_reduction = get_network_reduction(network_model)\n    for d in devices\n        bus_no_from =\n            PNM.get_mapped_bus_number(network_reduction, PSY.get_arc(d).from)\n        bus_no_to = PNM.get_mapped_bus_number(network_reduction, PSY.get_arc(d).to)\n        for t in get_time_steps(container)\n            flow_variable = var[PSY.get_name(d), t]\n            _add_to_jump_expression!(\n                expression[bus_no_from, t],\n                flow_variable,\n                -1.0,\n            )\n            _add_to_jump_expression!(\n                expression[bus_no_to, t],\n                flow_variable,\n                1.0,\n            )\n        end\n    end\n    return\nend\n\nfunction add_to_expression!(\n    container::OptimizationContainer,\n    ::Type{ActivePowerBalance},\n    ::Type{FlowActivePowerVariable},\n    devices::IS.FlattenIteratorWrapper{PSY.AreaInterchange},\n    ::DeviceModel{PSY.AreaInterchange, W},\n    network_model::NetworkModel{U},\n) where {\n    W <: AbstractBranchFormulation,\n    U <: Union{AreaBalancePowerModel, AreaPTDFPowerModel},\n}\n    flow_variable = get_variable(container, FlowActivePowerVariable(), PSY.AreaInterchange)\n    expression = get_expression(container, ActivePowerBalance(), PSY.Area)\n    for d in devices\n        area_from_name = PSY.get_name(PSY.get_from_area(d))\n        area_to_name = PSY.get_name(PSY.get_to_area(d))\n        for t in get_time_steps(container)\n            _add_to_jump_expression!(\n                expression[area_from_name, t],\n                flow_variable[PSY.get_name(d), t],\n                -1.0,\n            )\n            _add_to_jump_expression!(\n                expression[area_to_name, t],\n                flow_variable[PSY.get_name(d), t],\n                1.0,\n            )\n        end\n    end\n    return\nend\n\nfunction add_to_expression!(\n    container::OptimizationContainer,\n    ::Type{ActivePowerBalance},\n    ::Type{FlowActivePowerVariable},\n    devices::IS.FlattenIteratorWrapper{PSY.AreaInterchange},\n    ::DeviceModel{PSY.AreaInterchange, W},\n    network_model::NetworkModel{U},\n) where {\n    W <: AbstractBranchFormulation,\n    U <: PM.AbstractActivePowerModel,\n}\n    @debug \"AreaInterchanges do not contribute to ActivePowerBalance expressions in non-area models.\"\n    return\nend\n\n\"\"\"\nImplementation of add_to_expression! for lossless branch/network models\n\"\"\"\nfunction add_to_expression!(\n    container::OptimizationContainer,\n    ::Type{T},\n    ::Type{U},\n    devices::IS.FlattenIteratorWrapper{V},\n    ::DeviceModel{V, W},\n    network_model::NetworkModel{X},\n) where {\n    T <: ActivePowerBalance,\n    U <: FlowActivePowerVariable,\n    V <: PSY.TwoTerminalHVDC,\n    W <: AbstractBranchFormulation,\n    X <: PTDFPowerModel,\n}\n    var = get_variable(container, U(), V)\n    nodal_expr = get_expression(container, T(), PSY.ACBus)\n    sys_expr = get_expression(container, T(), PSY.System)\n    network_reduction = get_network_reduction(network_model)\n    for d in devices\n        bus_no_from =\n            PNM.get_mapped_bus_number(network_reduction, PSY.get_arc(d).from)\n        bus_no_to = PNM.get_mapped_bus_number(network_reduction, PSY.get_arc(d).to)\n        ref_bus_from = get_reference_bus(network_model, PSY.get_arc(d).from)\n        ref_bus_to = get_reference_bus(network_model, PSY.get_arc(d).to)\n        for t in get_time_steps(container)\n            flow_variable = var[PSY.get_name(d), t]\n            _add_to_jump_expression!(nodal_expr[bus_no_from, t], flow_variable, -1.0)\n            _add_to_jump_expression!(nodal_expr[bus_no_to, t], flow_variable, 1.0)\n            if ref_bus_from != ref_bus_to\n                _add_to_jump_expression!(sys_expr[ref_bus_from, t], flow_variable, -1.0)\n                _add_to_jump_expression!(sys_expr[ref_bus_to, t], flow_variable, 1.0)\n            end\n        end\n    end\n    return\nend\n\n\"\"\"\nImplementation of add_to_expression! for lossless branch/network models\n\"\"\"\nfunction add_to_expression!(\n    container::OptimizationContainer,\n    ::Type{T},\n    ::Type{U},\n    devices::IS.FlattenIteratorWrapper{V},\n    ::DeviceModel{V, W},\n    network_model::NetworkModel{CopperPlatePowerModel},\n) where {\n    T <: ActivePowerBalance,\n    U <: FlowActivePowerVariable,\n    V <: PSY.ACBranch,\n    W <: AbstractBranchFormulation,\n}\n    inter_network_branches = V[]\n    for d in devices\n        ref_bus_from = get_reference_bus(network_model, PSY.get_arc(d).from)\n        ref_bus_to = get_reference_bus(network_model, PSY.get_arc(d).to)\n        if ref_bus_from != ref_bus_to\n            push!(inter_network_branches, d)\n        end\n    end\n    if !isempty(inter_network_branches)\n        var = get_variable(container, U(), V)\n        sys_expr = get_expression(container, T(), PSY.System)\n        for d in devices\n            ref_bus_from = get_reference_bus(network_model, PSY.get_arc(d).from)\n            ref_bus_to = get_reference_bus(network_model, PSY.get_arc(d).to)\n            if ref_bus_from == ref_bus_to\n                continue\n            end\n            for t in get_time_steps(container)\n                flow_variable = var[PSY.get_name(d), t]\n                _add_to_jump_expression!(sys_expr[ref_bus_from, t], flow_variable, -1.0)\n                _add_to_jump_expression!(sys_expr[ref_bus_to, t], flow_variable, 1.0)\n            end\n        end\n    end\n    return\nend\n\n\"\"\"\nImplementation of add_to_expression! for lossless branch/network models\n\"\"\"\nfunction add_to_expression!(\n    container::OptimizationContainer,\n    ::Type{T},\n    ::Type{U},\n    devices::IS.FlattenIteratorWrapper{PSY.PhaseShiftingTransformer},\n    ::DeviceModel{PSY.PhaseShiftingTransformer, V},\n    network_model::NetworkModel{<:AbstractPTDFModel},\n) where {T <: ActivePowerBalance, U <: PhaseShifterAngle, V <: PhaseAngleControl}\n    var = get_variable(container, U(), PSY.PhaseShiftingTransformer)\n    expression = get_expression(container, T(), PSY.ACBus)\n    network_reduction = get_network_reduction(network_model)\n    for d in devices\n        bus_no_from =\n            PNM.get_mapped_bus_number(network_reduction, PSY.get_arc(d).from)\n        bus_no_to = PNM.get_mapped_bus_number(network_reduction, PSY.get_arc(d).to)\n        for t in get_time_steps(container)\n            flow_variable = var[PSY.get_name(d), t]\n            _add_to_jump_expression!(\n                expression[bus_no_from, t],\n                flow_variable,\n                -get_variable_multiplier(U(), d, V()),\n            )\n            _add_to_jump_expression!(\n                expression[bus_no_to, t],\n                flow_variable,\n                get_variable_multiplier(U(), d, V()),\n            )\n        end\n    end\n    return\nend\n\nfunction add_to_expression!(\n    container::OptimizationContainer,\n    ::Type{T},\n    ::Type{U},\n    devices::IS.FlattenIteratorWrapper{V},\n    model::DeviceModel{V, W},\n    network_model::NetworkModel{X},\n) where {\n    T <: Union{ActivePowerRangeExpressionUB, ActivePowerRangeExpressionLB},\n    U <: VariableType,\n    V <: PSY.Device,\n    W <: AbstractDeviceFormulation,\n    X <: PM.AbstractPowerModel,\n}\n    variable = get_variable(container, U(), V)\n    if !has_container_key(container, T, V)\n        add_expressions!(container, T, devices, model)\n    end\n    expression = get_expression(container, T(), V)\n    for d in devices, t in get_time_steps(container)\n        name = PSY.get_name(d)\n        _add_to_jump_expression!(expression[name, t], variable[name, t], 1.0)\n    end\n    return\nend\n\nfunction add_to_expression!(\n    container::OptimizationContainer,\n    ::Type{T},\n    ::Type{U},\n    devices::Union{Vector{V}, IS.FlattenIteratorWrapper{V}},\n    model::ServiceModel{X, W},\n) where {\n    T <: ActivePowerRangeExpressionUB,\n    U <: VariableType,\n    V <: PSY.Component,\n    X <: PSY.Reserve{PSY.ReserveUp},\n    W <: AbstractReservesFormulation,\n}\n    service_name = get_service_name(model)\n    variable = get_variable(container, U(), X, service_name)\n    if !has_container_key(container, T, V)\n        add_expressions!(container, T, devices, model)\n    end\n    expression = get_expression(container, T(), V)\n    for d in devices, t in get_time_steps(container)\n        name = PSY.get_name(d)\n        _add_to_jump_expression!(expression[name, t], variable[name, t], 1.0)\n    end\n    return\nend\n\nfunction add_to_expression!(\n    container::OptimizationContainer,\n    ::Type{InterfaceTotalFlow},\n    ::Type{T},\n    service::PSY.TransmissionInterface,\n    model::ServiceModel{PSY.TransmissionInterface, U},\n) where {\n    T <: Union{InterfaceFlowSlackUp, InterfaceFlowSlackDown},\n    U <: Union{ConstantMaxInterfaceFlow, VariableMaxInterfaceFlow},\n}\n    expression = get_expression(container, InterfaceTotalFlow(), PSY.TransmissionInterface)\n    service_name = PSY.get_name(service)\n    variable = get_variable(container, T(), PSY.TransmissionInterface, service_name)\n    for t in get_time_steps(container)\n        _add_to_jump_expression!(\n            expression[service_name, t],\n            variable[t],\n            get_variable_multiplier(T(), service, U()),\n        )\n    end\n    return\nend\n\nfunction _handle_nodal_or_zonal_interfaces(\n    br_type::Type{V},\n    net_reduction_data::PNM.NetworkReductionData,\n    direction_map::Dict{String, Int},\n    contributing_devices::Vector{V},\n    variable::JuMPVariableArray,\n    expression::DenseAxisArray, # There is no good type for a DenseAxisArray slice\n) where {V <: PSY.ACTransmission}\n    all_branch_maps_by_type = net_reduction_data.all_branch_maps_by_type\n    for (name, (arc, reduction)) in\n        PNM.get_name_to_arc_map(net_reduction_data, br_type)\n        reduction_entry = all_branch_maps_by_type[reduction][br_type][arc]\n        if _reduced_entry_in_interface(reduction_entry, contributing_devices)\n            if isempty(direction_map)\n                direction = 1.0\n            else\n                direction = _get_direction(\n                    arc,\n                    reduction_entry,\n                    direction_map,\n                    net_reduction_data,\n                )\n            end\n            for t in axes(variable, 2)\n                _add_to_jump_expression!(\n                    expression[t],\n                    variable[name, t],\n                    Float64(direction),\n                )\n            end\n        end\n    end\n    return\nend\n\nfunction _handle_nodal_or_zonal_interfaces(\n    ::Type{PSY.AreaInterchange},\n    net_reduction_data::PNM.NetworkReductionData,\n    direction_map::Dict{String, Int},\n    contributing_devices::Vector{PSY.AreaInterchange},\n    variable::JuMPVariableArray,\n    expression::DenseAxisArray, # There is no good type for a DenseAxisArray slice\n)\n    for device in contributing_devices\n        name = PSY.get_name(device)\n        if isempty(direction_map)\n            direction = 1.0\n        else\n            direction = direction_map[name]\n        end\n        for t in axes(variable, 2)\n            _add_to_jump_expression!(\n                expression[t],\n                variable[name, t],\n                Float64(direction),\n            )\n        end\n    end\n    return\nend\n\nfunction add_to_expression!(\n    container::OptimizationContainer,\n    ::Type{InterfaceTotalFlow},\n    ::Type{FlowActivePowerVariable},\n    service::PSY.TransmissionInterface,\n    model::ServiceModel{PSY.TransmissionInterface, V},\n    network_model::NetworkModel{<:PM.AbstractActivePowerModel},\n) where {V <: Union{ConstantMaxInterfaceFlow, VariableMaxInterfaceFlow}}\n    net_reduction_data = get_network_reduction(network_model)\n    expression = get_expression(container, InterfaceTotalFlow(), PSY.TransmissionInterface)\n    service_name = get_service_name(model)\n    direction_map = PSY.get_direction_mapping(service)\n    contributing_devices_map = get_contributing_devices_map(model)\n    for (br_type, contributing_devices) in contributing_devices_map\n        variable = get_variable(container, FlowActivePowerVariable(), br_type)\n        _handle_nodal_or_zonal_interfaces(\n            br_type,\n            net_reduction_data,\n            direction_map,\n            contributing_devices,\n            variable,\n            expression[service_name, :],\n        )\n    end\n    return\nend\n\nfunction _is_interchanges_interfaces(\n    contributing_devices_map::Dict{Type{<:PSY.Component}, Vector{<:PSY.Component}},\n)\n    if PSY.AreaInterchange ∈ keys(contributing_devices_map)\n        @assert length(keys(contributing_devices_map)) == 1\n        return true\n    end\n    return false\nend\n\nfunction add_to_expression!(\n    container::OptimizationContainer,\n    ::Type{InterfaceTotalFlow},\n    ::Type{FlowActivePowerVariable},\n    service::PSY.TransmissionInterface,\n    model::ServiceModel{PSY.TransmissionInterface, V},\n    network_model::NetworkModel{AreaPTDFPowerModel},\n) where {V <: Union{ConstantMaxInterfaceFlow, VariableMaxInterfaceFlow}}\n    net_reduction_data = get_network_reduction(network_model)\n    expression = get_expression(container, InterfaceTotalFlow(), PSY.TransmissionInterface)\n    service_name = get_service_name(model)\n    direction_map = PSY.get_direction_mapping(service)\n    contributing_devices_map = get_contributing_devices_map(model)\n    # Ignore interfaces over lines for AreaPTDFModel\n    if !_is_interchanges_interfaces(contributing_devices_map)\n        return\n    end\n    variable = get_variable(container, FlowActivePowerVariable(), PSY.AreaInterchange)\n    _handle_nodal_or_zonal_interfaces(\n        PSY.AreaInterchange,\n        net_reduction_data,\n        direction_map,\n        contributing_devices_map[PSY.AreaInterchange],\n        variable,\n        expression[service_name, :],\n    )\n    return\nend\n\nfunction add_to_expression!(\n    container::OptimizationContainer,\n    ::Type{InterfaceTotalFlow},\n    ::Type{PTDFBranchFlow},\n    service::PSY.TransmissionInterface,\n    model::ServiceModel{PSY.TransmissionInterface, V},\n    network_model::NetworkModel{<:AbstractPTDFModel},\n) where {V <: Union{ConstantMaxInterfaceFlow, VariableMaxInterfaceFlow}}\n    net_reduction_data = get_network_reduction(network_model)\n    expression = get_expression(container, InterfaceTotalFlow(), PSY.TransmissionInterface)\n    service_name = get_service_name(model)\n    direction_map = PSY.get_direction_mapping(service)\n    contributing_devices_map = get_contributing_devices_map(model)\n    # Interfaces over interchanges\n    if _is_interchanges_interfaces(contributing_devices_map)\n        return\n    end\n\n    for (br_type, contributing_devices) in contributing_devices_map\n        flow_expression = get_expression(container, PTDFBranchFlow(), br_type)\n        all_branch_maps_by_type = net_reduction_data.all_branch_maps_by_type\n        for (name, (arc, reduction)) in PNM.get_name_to_arc_map(net_reduction_data, br_type)\n            reduction_entry = all_branch_maps_by_type[reduction][br_type][arc]\n            if _reduced_entry_in_interface(reduction_entry, contributing_devices)\n                if isempty(direction_map)\n                    direction = 1.0\n                else\n                    direction = _get_direction(\n                        arc,\n                        reduction_entry,\n                        direction_map,\n                        net_reduction_data,\n                    )\n                end\n                for t in axes(flow_expression, 2)\n                    JuMP.add_to_expression!(\n                        expression[service_name, t],\n                        flow_expression[name, t],\n                        Float64(direction),\n                    )\n                end\n            end\n        end\n    end\n    return\nend\n\nfunction _get_direction(\n    ::Tuple{Int, Int},\n    reduction_entry::PSY.ACTransmission,\n    direction_map::Dict{String, Int},\n    ::PNM.NetworkReductionData,\n)\n    name = PSY.get_name(reduction_entry)\n    if !haskey(direction_map, name)\n        @warn \"Direction not found for $(summary(reduction_entry)). Will use the default from -> to direction\"\n        return 1.0\n    else\n        return direction_map[name]\n    end\nend\n\nfunction _get_direction(\n    arc_tuple::Tuple{Int, Int},\n    reduction_entry::PNM.BranchesParallel,\n    direction_map::Dict{String, Int},\n    net_reduction_data::PNM.NetworkReductionData,\n)\n    # Loops through parallel branches twice, but there are relatively few parallel branches per reduction entry:\n    directions = [\n        _get_direction(arc_tuple, x, direction_map, net_reduction_data) for\n        x in reduction_entry\n    ]\n    if allequal(directions)\n        return first(directions)\n    end\n    throw(\n        ArgumentError(\n            \"The interface direction mapping contains a double circuit with opposite directions. Modify the data to have consistent directions for double circuits.\",\n        ),\n    )\nend\n\nfunction _get_direction(\n    arc_tuple::Tuple{Int, Int},\n    reduction_entry::PNM.BranchesSeries,\n    direction_map::Dict{String, Int},\n    net_reduction_data::PNM.NetworkReductionData,\n)\n    # direction of segments from the user provided mapping:\n    mapping_directions = [\n        _get_direction(arc_tuple, x, direction_map, net_reduction_data) for\n        x in reduction_entry\n    ]\n    # direction of segments relative to the reduced degree two chain:\n    _, segment_orientations =\n        PNM._get_chain_data(arc_tuple, reduction_entry, net_reduction_data)\n    segment_directions = [x == :FromTo ? 1.0 : -1.0 for x in segment_orientations]\n    net_directions = mapping_directions .* segment_directions\n    if allequal(net_directions)\n        return first(net_directions)\n    else\n        throw(\n            ArgumentError(\n                \"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.\",\n            ),\n        )\n    end\nend\n\n# These checks can be moved to happen at the service template check level\nfunction _reduced_entry_in_interface(\n    reduction_entry::PSY.ACTransmission,\n    contributing_devices::Vector{<:PSY.ACTransmission},\n)\n    reduction_entry_name = PSY.get_name(reduction_entry)\n    # This is compared by name given that the reduction data uses copies of the devices\n    # so, simple comparisons will not work\n    for device in contributing_devices\n        device_name = PSY.get_name(device)\n        if reduction_entry_name == device_name\n            return true\n        end\n    end\n    return false\nend\n\nfunction _reduced_entry_in_interface(\n    reduction_entry::PNM.BranchesParallel,\n    contributing_devices::Vector{<:PSY.ACTransmission},\n)\n    in_interface = [\n        _reduced_entry_in_interface(x, contributing_devices) for\n        x in reduction_entry\n    ]\n\n    if !allequal(in_interface)\n        branch_names = PSY.get_name.(reduction_entry)\n        throw(\n            ArgumentError(\n                \"An interface is specified with only part of a double-circuit that has been reduced. \n                Branches: $(branch_names[in_interface]) are in the interface and branches: $(branch_names[.!in_interface]) are not.\n                Modify the data to include all of or none of the parallel segements.\",\n            ),\n        )\n    end\n    return first(in_interface)\nend\n\nfunction _reduced_entry_in_interface(\n    reduction_entry::PNM.BranchesSeries,\n    contributing_devices::Vector{<:PSY.ACTransmission},\n)\n    in_interface = [\n        _reduced_entry_in_interface(x, contributing_devices) for\n        x in reduction_entry\n    ]\n\n    if !allequal(in_interface)\n        throw(\n            ArgumentError(\n                \"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\",\n            ),\n        )\n    end\n    return first(in_interface)\nend\n\nfunction add_to_expression!(\n    container::OptimizationContainer,\n    ::Type{T},\n    ::Type{U},\n    devices::Union{Vector{V}, IS.FlattenIteratorWrapper{V}},\n    model::ServiceModel{X, W},\n) where {\n    T <: ActivePowerRangeExpressionLB,\n    U <: VariableType,\n    V <: PSY.Component,\n    X <: PSY.Reserve{PSY.ReserveDown},\n    W <: AbstractReservesFormulation,\n}\n    service_name = get_service_name(model)\n    variable = get_variable(container, U(), X, service_name)\n    if !has_container_key(container, T, V)\n        add_expressions!(container, T, devices, model)\n    end\n    expression = get_expression(container, T(), V)\n    for d in devices, t in get_time_steps(container)\n        name = PSY.get_name(d)\n        _add_to_jump_expression!(expression[name, t], variable[name, t], -1.0)\n    end\n    return\nend\n\nfunction add_to_expression!(\n    container::OptimizationContainer,\n    ::Type{T},\n    ::U,\n    devices::IS.FlattenIteratorWrapper{V},\n    model::DeviceModel{V, W},\n) where {\n    T <: Union{ActivePowerRangeExpressionUB, ActivePowerRangeExpressionLB},\n    U <: OnStatusParameter,\n    V <: PSY.Device,\n    W <: AbstractDeviceFormulation,\n}\n    parameter_array = get_parameter_array(container, U(), V)\n    if !has_container_key(container, T, V)\n        add_expressions!(container, T, devices, model)\n    end\n    expression = get_expression(container, T(), V)\n    for d in devices\n        mult = get_expression_multiplier(U(), T(), d, W())\n        for t in get_time_steps(container)\n            name = PSY.get_name(d)\n            _add_to_jump_expression!(\n                expression[name, t],\n                parameter_array[name, t],\n                -mult,\n                mult,\n            )\n        end\n    end\n    return\nend\n\nfunction add_to_expression!(\n    container::OptimizationContainer,\n    ::Type{T},\n    ::U,\n    devices::IS.FlattenIteratorWrapper{V},\n    model::DeviceModel{V, W},\n) where {\n    T <: Union{ActivePowerRangeExpressionUB, ActivePowerRangeExpressionLB},\n    U <: OnStatusParameter,\n    V <: PSY.ThermalGen,\n    W <: AbstractThermalDispatchFormulation,\n}\n    parameter_array = get_parameter_array(container, U(), V)\n    if !has_container_key(container, T, V)\n        add_expressions!(container, T, devices, model)\n    end\n    expression = get_expression(container, T(), V)\n    for d in devices\n        if PSY.get_must_run(d)\n            continue\n        end\n        mult = get_expression_multiplier(U(), T(), d, W())\n        for t in get_time_steps(container)\n            name = PSY.get_name(d)\n            _add_to_jump_expression!(expression[name, t], parameter_array[name, t], -mult)\n        end\n    end\n    return\nend\n\nfunction add_to_expression!(\n    container::OptimizationContainer,\n    ::Type{U},\n    model::ServiceModel{V, W},\n    devices_template::Dict{Symbol, DeviceModel},\n) where {U <: VariableType, V <: PSY.Reserve, W <: AbstractReservesFormulation}\n    contributing_devices_map = get_contributing_devices_map(model)\n    for (device_type, devices) in contributing_devices_map\n        device_model = get(devices_template, Symbol(device_type), nothing)\n        device_model === nothing && continue\n        expression_type = get_expression_type_for_reserve(U(), device_type, V)\n        add_to_expression!(container, expression_type, U, devices, model)\n    end\n    return\nend\n\nfunction add_to_expression!(\n    container::OptimizationContainer,\n    ::Type{T},\n    ::Type{U},\n    ::PSY.System,\n    network_model::NetworkModel{W},\n) where {\n    T <: ActivePowerBalance,\n    U <: Union{SystemBalanceSlackUp, SystemBalanceSlackDown},\n    W <: Union{CopperPlatePowerModel, PTDFPowerModel},\n}\n    variable = get_variable(container, U(), PSY.System)\n    expression = get_expression(container, T(), _system_expression_type(W))\n    reference_buses = get_reference_buses(network_model)\n    for t in get_time_steps(container), n in reference_buses\n        _add_to_jump_expression!(\n            expression[n, t],\n            variable[n, t],\n            get_variable_multiplier(U(), PSY.System, W()),\n        )\n    end\n    return\nend\n\nfunction add_to_expression!(\n    container::OptimizationContainer,\n    ::Type{T},\n    ::Type{U},\n    sys::PSY.System,\n    network_model::NetworkModel{V},\n) where {\n    T <: ActivePowerBalance,\n    U <: Union{SystemBalanceSlackUp, SystemBalanceSlackDown},\n    V <: AreaPTDFPowerModel,\n}\n    variable =\n        get_variable(container, U(), _system_expression_type(AreaPTDFPowerModel))\n    expression =\n        get_expression(container, T(), _system_expression_type(AreaPTDFPowerModel))\n    areas = get_available_components(network_model, PSY.Area, sys)\n    for t in get_time_steps(container), n in PSY.get_name.(areas)\n        _add_to_jump_expression!(\n            expression[n, t],\n            variable[n, t],\n            get_variable_multiplier(U(), PSY.Area, AreaPTDFPowerModel()),\n        )\n    end\n    return\nend\n\nfunction add_to_expression!(\n    container::OptimizationContainer,\n    ::Type{T},\n    ::Type{U},\n    sys::PSY.System,\n    ::NetworkModel{AreaBalancePowerModel},\n) where {\n    T <: ActivePowerBalance,\n    U <: Union{SystemBalanceSlackUp, SystemBalanceSlackDown},\n}\n    variable = get_variable(container, U(), PSY.Area)\n    expression = get_expression(container, T(), PSY.Area)\n    @assert_op length(axes(variable, 1)) == length(axes(expression, 1))\n    for t in get_time_steps(container), n in axes(expression, 1)\n        _add_to_jump_expression!(\n            expression[n, t],\n            variable[n, t],\n            get_variable_multiplier(U(), PSY.Area, AreaBalancePowerModel),\n        )\n    end\n    return\nend\n\nfunction add_to_expression!(\n    container::OptimizationContainer,\n    ::Type{T},\n    ::Type{U},\n    sys::PSY.System,\n    ::NetworkModel{W},\n) where {\n    T <: ActivePowerBalance,\n    U <: Union{SystemBalanceSlackUp, SystemBalanceSlackDown},\n    W <: PM.AbstractActivePowerModel,\n}\n    variable = get_variable(container, U(), PSY.ACBus)\n    expression = get_expression(container, T(), PSY.ACBus)\n    @assert_op length(axes(variable, 1)) == length(axes(expression, 1))\n    # We uses axis here to avoid double addition of the slacks to the aggregated buses\n    for t in get_time_steps(container), n in axes(expression, 1)\n        _add_to_jump_expression!(\n            expression[n, t],\n            variable[n, t],\n            get_variable_multiplier(U(), PSY.ACBus, W),\n        )\n    end\n    return\nend\n\nfunction add_to_expression!(\n    container::OptimizationContainer,\n    ::Type{T},\n    ::Type{U},\n    sys::PSY.System,\n    ::NetworkModel{W},\n) where {\n    T <: ActivePowerBalance,\n    U <: Union{SystemBalanceSlackUp, SystemBalanceSlackDown},\n    W <: PM.AbstractPowerModel,\n}\n    variable = get_variable(container, U(), PSY.ACBus, \"P\")\n    expression = get_expression(container, T(), PSY.ACBus)\n    # We uses axis here to avoid double addition of the slacks to the aggregated buses\n    for t in get_time_steps(container), n in axes(expression, 1)\n        _add_to_jump_expression!(\n            expression[n, t],\n            variable[n, t],\n            get_variable_multiplier(U(), PSY.ACBus, W),\n        )\n    end\n    return\nend\n\nfunction add_to_expression!(\n    container::OptimizationContainer,\n    ::Type{T},\n    ::Type{U},\n    sys::PSY.System,\n    ::NetworkModel{W},\n) where {\n    T <: ReactivePowerBalance,\n    U <: Union{SystemBalanceSlackUp, SystemBalanceSlackDown},\n    W <: PM.AbstractPowerModel,\n}\n    variable = get_variable(container, U(), PSY.ACBus, \"Q\")\n    expression = get_expression(container, T(), PSY.ACBus)\n    # We uses axis here to avoid double addition of the slacks to the aggregated buses\n    for t in get_time_steps(container), n in axes(expression, 1)\n        _add_to_jump_expression!(\n            expression[n, t],\n            variable[n, t],\n            get_variable_multiplier(U(), PSY.ACBus, W),\n        )\n    end\n    return\nend\n\nfunction add_to_expression!(\n    container::OptimizationContainer,\n    ::Type{S},\n    cost_expression::JuMPOrFloat,\n    component::T,\n    time_period::Int,\n) where {S <: Union{CostExpressions, FuelConsumptionExpression}, T <: PSY.Component}\n    if has_container_key(container, S, T)\n        device_cost_expression = get_expression(container, S(), T)\n        component_name = PSY.get_name(component)\n        JuMP.add_to_expression!(\n            device_cost_expression[component_name, time_period],\n            cost_expression,\n        )\n    end\n    return\nend\n\n\"\"\"\nSpecialized `add_to_expression!` for `ConstituentCostExpression` subtypes. In addition to\nadding to the constituent expression, this method automatically propagates the cost to\n`ProductionCostExpression`, so callers do not need to add to both.\n\"\"\"\nfunction add_to_expression!(\n    container::OptimizationContainer,\n    ::Type{S},\n    cost_expression::JuMPOrFloat,\n    component::T,\n    time_period::Int,\n) where {S <: ConstituentCostExpression, T <: PSY.Component}\n    if has_container_key(container, S, T)\n        device_cost_expression = get_expression(container, S(), T)\n        component_name = PSY.get_name(component)\n        JuMP.add_to_expression!(\n            device_cost_expression[component_name, time_period],\n            cost_expression,\n        )\n    end\n    if has_container_key(container, ProductionCostExpression, T)\n        prod_cost_expression = get_expression(container, ProductionCostExpression(), T)\n        JuMP.add_to_expression!(\n            prod_cost_expression[PSY.get_name(component), time_period],\n            cost_expression,\n        )\n    end\n    return\nend\n\nfunction add_to_expression!(\n    container::OptimizationContainer,\n    ::Type{S},\n    cost_expression::JuMP.AbstractJuMPScalar,\n    component::T,\n    time_period::Int,\n) where {S <: CostExpressions, T <: PSY.ReserveDemandCurve}\n    if has_container_key(container, S, T, PSY.get_name(component))\n        device_cost_expression = get_expression(container, S(), T, PSY.get_name(component))\n        component_name = PSY.get_name(component)\n        JuMP.add_to_expression!(\n            device_cost_expression[component_name, time_period],\n            cost_expression,\n        )\n    end\n    return\nend\n\n# Disambiguate: ConstituentCostExpression ∩ ReserveDemandCurve — no ProductionCostExpression\n# propagation since reserves don't have a ProductionCostExpression container.\nfunction add_to_expression!(\n    container::OptimizationContainer,\n    ::Type{S},\n    cost_expression::JuMP.AbstractJuMPScalar,\n    component::T,\n    time_period::Int,\n) where {S <: ConstituentCostExpression, T <: PSY.ReserveDemandCurve}\n    if has_container_key(container, S, T, PSY.get_name(component))\n        device_cost_expression = get_expression(container, S(), T, PSY.get_name(component))\n        component_name = PSY.get_name(component)\n        JuMP.add_to_expression!(\n            device_cost_expression[component_name, time_period],\n            cost_expression,\n        )\n    end\n    return\nend\n\n# Method-level dispatch on the value-curve type carried by the FuelCurve. One method per\n# concrete ValueCurve subtype defined by PowerSystems; non-Linear/Quadratic curves are\n# explicit no-ops, mirroring the silent skip of the original `if/elseif` chain.\nfunction _add_fuel_consumption_term!(\n    expression,\n    variable,\n    name::String,\n    var_cost::PSY.FuelCurve,\n    value_curve::PSY.LinearCurve,\n    base_power::Float64,\n    device_base_power::Float64,\n    dt::Float64,\n    time_steps,\n)\n    power_units = PSY.get_power_units(var_cost)\n    proportional_term = PSY.get_proportional_term(value_curve)\n    prop_term_per_unit = get_proportional_cost_per_system_unit(\n        proportional_term,\n        power_units,\n        base_power,\n        device_base_power,\n    )\n    for t in time_steps\n        JuMP.add_to_expression!(\n            expression[name, t],\n            prop_term_per_unit * dt,\n            variable[name, t],\n        )\n    end\n    return\nend\n\nfunction _add_fuel_consumption_term!(\n    expression,\n    variable,\n    name::String,\n    var_cost::PSY.FuelCurve,\n    value_curve::PSY.QuadraticCurve,\n    base_power::Float64,\n    device_base_power::Float64,\n    dt::Float64,\n    time_steps,\n)\n    power_units = PSY.get_power_units(var_cost)\n    proportional_term = PSY.get_proportional_term(value_curve)\n    quadratic_term = PSY.get_quadratic_term(value_curve)\n    prop_term_per_unit = get_proportional_cost_per_system_unit(\n        proportional_term,\n        power_units,\n        base_power,\n        device_base_power,\n    )\n    quad_term_per_unit = get_quadratic_cost_per_system_unit(\n        quadratic_term,\n        power_units,\n        base_power,\n        device_base_power,\n    )\n    for t in time_steps\n        fuel_expr =\n            (\n                variable[name, t] .^ 2 * quad_term_per_unit +\n                variable[name, t] * prop_term_per_unit\n            ) * dt\n        JuMP.add_to_expression!(\n            expression[name, t],\n            fuel_expr,\n        )\n    end\n    return\nend\n\n_add_fuel_consumption_term!(_, _, _::String, _::PSY.FuelCurve, ::PSY.PiecewisePointCurve,\n    _::Float64, _::Float64, _::Float64, _) = nothing\n_add_fuel_consumption_term!(_, _, _::String, _::PSY.FuelCurve, ::PSY.IncrementalCurve,\n    _::Float64, _::Float64, _::Float64, _) = nothing\n_add_fuel_consumption_term!(_, _, _::String, _::PSY.FuelCurve, ::PSY.AverageRateCurve,\n    _::Float64, _::Float64, _::Float64, _) = nothing\n\nfunction add_to_expression!(\n    container::OptimizationContainer,\n    ::Type{T},\n    ::Type{U},\n    devices::IS.FlattenIteratorWrapper{V},\n    model::DeviceModel{V, W},\n) where {\n    T <: FuelConsumptionExpression,\n    U <: ActivePowerVariable,\n    V <: PSY.ThermalGen,\n    W <: AbstractDeviceFormulation,\n}\n    variable = get_variable(container, U(), V)\n    time_steps = get_time_steps(container)\n    base_power = get_base_power(container)\n    resolution = get_resolution(container)\n    dt = Dates.value(resolution) / MILLISECONDS_IN_HOUR\n    for d in devices\n        op_cost = PSY.get_operation_cost(d)\n        var_cost = _get_variable_if_exists(op_cost)\n        _is_fuel_curve(var_cost) || continue\n        expression = get_expression(container, T(), V)\n        name = PSY.get_name(d)\n        device_base_power = PSY.get_base_power(d)\n        value_curve = PSY.get_value_curve(var_cost)\n        _add_fuel_consumption_term!(\n            expression,\n            variable,\n            name,\n            var_cost,\n            value_curve,\n            base_power,\n            device_base_power,\n            dt,\n            time_steps,\n        )\n    end\n    return\nend\n\nfunction _add_compact_fuel_consumption_term!(\n    container::OptimizationContainer,\n    ::Type{W},\n    expression,\n    variable,\n    d::V,\n    var_cost::PSY.FuelCurve,\n    value_curve::PSY.LinearCurve,\n    base_power::Float64,\n    device_base_power::Float64,\n    dt::Float64,\n    time_steps,\n) where {V <: PSY.ThermalGen, W <: AbstractDeviceFormulation}\n    name = PSY.get_name(d)\n    P_min = PSY.get_active_power_limits(d).min\n    power_units = PSY.get_power_units(var_cost)\n    proportional_term = PSY.get_proportional_term(value_curve)\n    prop_term_per_unit = get_proportional_cost_per_system_unit(\n        proportional_term,\n        power_units,\n        base_power,\n        device_base_power,\n    )\n    for t in time_steps\n        sos_status = _get_sos_value(container, W, d)\n        if sos_status == SOSStatusVariable.NO_VARIABLE\n            JuMP.add_to_expression!(\n                expression[name, t],\n                P_min * prop_term_per_unit * dt,\n            )\n        elseif sos_status == SOSStatusVariable.PARAMETER\n            param = get_default_on_parameter(d)\n            bin = get_parameter(container, param, V).parameter_array[name, t]\n            JuMP.add_to_expression!(\n                expression[name, t],\n                P_min * prop_term_per_unit * dt,\n                bin,\n            )\n        elseif sos_status == SOSStatusVariable.VARIABLE\n            var = get_default_on_variable(d)\n            bin = get_variable(container, var, V)[name, t]\n            JuMP.add_to_expression!(\n                expression[name, t],\n                P_min * prop_term_per_unit * dt,\n                bin,\n            )\n        else\n            @assert false\n        end\n        JuMP.add_to_expression!(\n            expression[name, t],\n            prop_term_per_unit * dt,\n            variable[name, t],\n        )\n    end\n    return\nend\n\nfunction _add_compact_fuel_consumption_term!(\n    ::OptimizationContainer,\n    ::Type{W},\n    _,\n    _,\n    ::PSY.ThermalGen,\n    ::PSY.FuelCurve,\n    ::PSY.QuadraticCurve,\n    ::Float64,\n    ::Float64,\n    ::Float64,\n    _,\n) where {W <: AbstractDeviceFormulation}\n    error(\"Quadratic Curves are not accepted with Compact Formulation: $W\")\nend\n\n_add_compact_fuel_consumption_term!(::OptimizationContainer,\n    ::Type{<:AbstractDeviceFormulation},\n    _, _, ::PSY.ThermalGen, ::PSY.FuelCurve, ::PSY.PiecewisePointCurve,\n    ::Float64, ::Float64, ::Float64, _) = nothing\n_add_compact_fuel_consumption_term!(::OptimizationContainer,\n    ::Type{<:AbstractDeviceFormulation},\n    _, _, ::PSY.ThermalGen, ::PSY.FuelCurve, ::PSY.IncrementalCurve,\n    ::Float64, ::Float64, ::Float64, _) = nothing\n_add_compact_fuel_consumption_term!(::OptimizationContainer,\n    ::Type{<:AbstractDeviceFormulation},\n    _, _, ::PSY.ThermalGen, ::PSY.FuelCurve, ::PSY.AverageRateCurve,\n    ::Float64, ::Float64, ::Float64, _) = nothing\n\nfunction add_to_expression!(\n    container::OptimizationContainer,\n    ::Type{T},\n    ::Type{U},\n    devices::IS.FlattenIteratorWrapper{V},\n    model::DeviceModel{V, W},\n) where {\n    T <: FuelConsumptionExpression,\n    U <: PowerAboveMinimumVariable,\n    V <: PSY.ThermalGen,\n    W <: AbstractDeviceFormulation,\n}\n    variable = get_variable(container, U(), V)\n    time_steps = get_time_steps(container)\n    base_power = get_base_power(container)\n    resolution = get_resolution(container)\n    dt = Dates.value(resolution) / MILLISECONDS_IN_HOUR\n    for d in devices\n        op_cost = PSY.get_operation_cost(d)\n        var_cost = _get_variable_if_exists(op_cost)\n        _is_fuel_curve(var_cost) || continue\n        expression = get_expression(container, T(), V)\n        name = PSY.get_name(d)\n        device_base_power = PSY.get_base_power(d)\n        value_curve = PSY.get_value_curve(var_cost)\n        _add_compact_fuel_consumption_term!(\n            container,\n            W,\n            expression,\n            variable,\n            d,\n            var_cost,\n            value_curve,\n            base_power,\n            device_base_power,\n            dt,\n            time_steps,\n        )\n    end\n    return\nend\n\nfunction add_to_expression!(\n    container::OptimizationContainer,\n    ::Type{T},\n    ::U,\n    devices::IS.FlattenIteratorWrapper{V},\n    model::DeviceModel{V, W},\n) where {\n    T <: NetActivePower,\n    U <: Union{ActivePowerInVariable, ActivePowerOutVariable},\n    V <: PSY.Source,\n    W <: AbstractSourceFormulation,\n}\n    expression = get_expression(container, T(), V)\n    variable = get_variable(container, U(), V)\n    mult = get_variable_multiplier(U(), V, W())\n    for d in devices\n        name = PSY.get_name(d)\n        for t in get_time_steps(container)\n            JuMP.add_to_expression!(\n                expression[name, t],\n                variable[name, t] * mult,\n            )\n        end\n    end\n    return\nend\n\n##################################\n##### Cost Expression Setup ######\n##################################\n\n\"\"\"\nAdds all cost expression containers appropriate for the given device type and formulation\nin a single pass through the device list.\n\nThe default method adds only `ProductionCostExpression`. The `PSY.ThermalGen` overload\nadds its full set of `ConstituentCostExpression` subtypes (Fuel, StartUp, ShutDown,\nFixed, VOM), which automatically propagate into `ProductionCostExpression` via the\n`ConstituentCostExpression` dispatch on `add_to_expression!`. The `PSY.RenewableGen`\noverload adds `FixedCostExpression` and `VOMCostExpression` (which propagate the same\nway) plus `CurtailmentCostExpression` (a direct `CostExpressions` subtype, recorded\nas a per-device reporting expression and not propagated to `ProductionCostExpression`).\n\"\"\"\nfunction add_cost_expressions!(\n    container::OptimizationContainer,\n    devices::U,\n    model::DeviceModel{D, W},\n) where {D <: PSY.Component, U, W <: AbstractDeviceFormulation}\n    time_steps = get_time_steps(container)\n    names = PSY.get_name.(devices)\n    add_expression_container!(container, ProductionCostExpression(), D, names, time_steps)\n    return\nend\n\nfunction add_cost_expressions!(\n    container::OptimizationContainer,\n    devices::U,\n    model::DeviceModel{D, W},\n) where {D <: PSY.ThermalGen, U, W <: AbstractThermalFormulation}\n    time_steps = get_time_steps(container)\n    n_devices = length(devices)\n    all_names = Vector{String}(undef, n_devices)\n    fuel_names = sizehint!(String[], n_devices)\n    has_quad_fuel = false\n    for (i, d) in enumerate(devices)\n        name = PSY.get_name(d)\n        all_names[i] = name\n        fuel_curve = _get_variable_if_exists(PSY.get_operation_cost(d))\n        if _is_fuel_curve(fuel_curve)\n            push!(fuel_names, name)\n            if !has_quad_fuel\n                has_quad_fuel = _value_curve_is_quadratic(PSY.get_value_curve(fuel_curve))\n            end\n        end\n    end\n    if !isempty(fuel_names)\n        fuel_expr_type = has_quad_fuel ? JuMP.QuadExpr : GAE\n        add_expression_container!(\n            container, FuelConsumptionExpression(), D, fuel_names, time_steps;\n            expr_type = fuel_expr_type,\n        )\n    end\n    add_expression_container!(\n        container,\n        ProductionCostExpression(),\n        D,\n        all_names,\n        time_steps,\n    )\n    add_expression_container!(container, FuelCostExpression(), D, all_names, time_steps)\n    add_expression_container!(container, StartUpCostExpression(), D, all_names, time_steps)\n    add_expression_container!(container, ShutDownCostExpression(), D, all_names, time_steps)\n    add_expression_container!(container, FixedCostExpression(), D, all_names, time_steps)\n    add_expression_container!(container, VOMCostExpression(), D, all_names, time_steps)\n    return\nend\n\nfunction add_cost_expressions!(\n    container::OptimizationContainer,\n    devices::U,\n    model::DeviceModel{D, W},\n) where {D <: PSY.RenewableGen, U, W <: AbstractRenewableDispatchFormulation}\n    time_steps = get_time_steps(container)\n    names = PSY.get_name.(devices)\n    add_expression_container!(container, ProductionCostExpression(), D, names, time_steps)\n    add_expression_container!(container, FixedCostExpression(), D, names, time_steps)\n    add_expression_container!(container, CurtailmentCostExpression(), D, names, time_steps)\n    add_expression_container!(container, VOMCostExpression(), D, names, time_steps)\n    return\nend\n\n#=\nfunction add_to_expression!(\n    container::OptimizationContainer,\n    ::Type{T},\n    ::Type{U},\n    areas::IS.FlattenIteratorWrapper{V},\n    model::ServiceModel{PSY.AGC, W},\n) where {\n    T <: Union{EmergencyUp, EmergencyDown},\n    U <:\n    Union{AdditionalDeltaActivePowerUpVariable, AdditionalDeltaActivePowerDownVariable},\n    V <: PSY.Area,\n    W <: AbstractServiceFormulation,\n}\n    names = PSY.get_name.(areas)\n    time_steps = get_time_steps(container)\n    if !has_container_key(container, T, V)\n        expression = add_expression_container!(container, T(), V, names, time_steps)\n    end\n    expression = get_expression(container, T(), V)\n    variable = get_variable(container, U(), V)\n    for n in names, t in time_steps\n        _add_to_jump_expression!(expression[n, t], variable[n, t], 1.0)\n    end\n    return\nend\n\nfunction add_to_expression!(\n    container::OptimizationContainer,\n    ::Type{T},\n    ::Type{U},\n    services::IS.FlattenIteratorWrapper{V},\n    model::ServiceModel{V, W},\n) where {\n    T <: RawACE,\n    U <: SteadyStateFrequencyDeviation,\n    V <: PSY.AGC,\n    W <: AbstractServiceFormulation,\n}\n    names = PSY.get_name.(services)\n    time_steps = get_time_steps(container)\n    if !has_container_key(container, T, V)\n        expression = add_expression_container!(container, T(), PSY.AGC, names, time_steps)\n    end\n    expression = get_expression(container, T(), PSY.AGC)\n    variable = get_variable(container, U(), PSY.AGC)\n    for s in services, t in time_steps\n        name = PSY.get_name(s)\n        _add_to_jump_expression!(\n            expression[name, t],\n            variable[t],\n            get_variable_multiplier(U(), s, W()),\n        )\n    end\n    return\nend\n=#\n"
  },
  {
    "path": "src/devices_models/devices/common/add_variable.jl",
    "content": "\"\"\"\nAdd variables to the OptimizationContainer for any component.\n\"\"\"\nfunction add_variables!(\n    container::OptimizationContainer,\n    ::Type{T},\n    devices::Union{Vector{U}, IS.FlattenIteratorWrapper{U}},\n    formulation::Union{AbstractServiceFormulation, AbstractDeviceFormulation},\n) where {T <: VariableType, U <: PSY.Component}\n    add_variable!(container, T(), devices, formulation)\n    return\nend\n\n#=\n# TODO: Contingency. Is this needed?\n\"\"\"\nAdd Contingency-related variables to the OptimizationContainer for any component.\n\"\"\"\nfunction add_variables!(\n    container::OptimizationContainer,\n    ::Type{T},\n    devices::Union{Vector{U}, IS.FlattenIteratorWrapper{U}},\n    devices_outages::Union{Vector{U}, IS.FlattenIteratorWrapper{U}},\n    formulation::Union{AbstractServiceFormulation, AbstractDeviceFormulation},\n) where {T <: VariableType, U <: PSY.Component}\n    add_variable!(container, T(), devices, devices_outages, formulation)\n    return\nend\n=#\n\n\"\"\"\nAdd variables to the OptimizationContainer for a service.\n\"\"\"\nfunction add_variables!(\n    container::OptimizationContainer,\n    ::Type{T},\n    service::U,\n    contributing_devices::Union{Vector{V}, IS.FlattenIteratorWrapper{V}},\n    formulation::AbstractReservesFormulation,\n) where {T <: VariableType, U <: PSY.AbstractReserve, V <: PSY.Component}\n    # PERF: compilation hotspot. Switch to TSC.\n    add_service_variable!(container, T(), service, contributing_devices, formulation)\n    return\nend\n\n@doc raw\"\"\"\nAdds a variable to the optimization model and to the affine expressions contained\nin the optimization_container model according to the specified sign. Based on the inputs, the variable can\nbe specified as binary.\n\n# Bounds\n\n``` lb_value_function <= varstart[name, t] <= ub_value_function ```\n\nIf binary = true:\n\n``` varstart[name, t] in {0,1} ```\n\n# LaTeX\n\n``  lb \\ge x^{device}_t \\le ub \\forall t ``\n\n``  x^{device}_t \\in {0,1} \\forall t iff \\text{binary = true}``\n\n# Arguments\n* container::OptimizationContainer : the optimization_container model built in PowerSimulations\n* devices : Vector or Iterator with the devices\n* var_key::VariableKey : Base Name for the variable\n* binary::Bool : Select if the variable is binary\n* expression_name::Symbol : Expression_name name stored in container.expressions to add the variable\n* sign::Float64 : sign of the addition of the variable to the expression_name. Default Value is 1.0\n\n# Accepted Keyword Arguments\n* ub_value : Provides the function over device to obtain the value for a upper_bound\n* 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\n* initial_value : Provides the function over device to obtain the warm start value\n\n\"\"\"\nfunction add_variable!(\n    container::OptimizationContainer,\n    variable_type::T,\n    devices::U,\n    formulation,\n) where {\n    T <: VariableType,\n    U <: Union{Vector{D}, IS.FlattenIteratorWrapper{D}},\n} where {D <: PSY.Component}\n    @assert !isempty(devices)\n    time_steps = get_time_steps(container)\n    settings = get_settings(container)\n    binary = get_variable_binary(variable_type, D, formulation)\n\n    variable = add_variable_container!(\n        container,\n        variable_type,\n        D,\n        PSY.get_name.(devices),\n        time_steps,\n    )\n\n    for t in time_steps, d in devices\n        name = PSY.get_name(d)\n        variable[name, t] = JuMP.@variable(\n            get_jump_model(container),\n            base_name = \"$(T)_$(D)_{$(name), $(t)}\",\n            binary = binary\n        )\n        ub = get_variable_upper_bound(variable_type, d, formulation)\n        ub !== nothing && JuMP.set_upper_bound(variable[name, t], ub)\n\n        lb = get_variable_lower_bound(variable_type, d, formulation)\n        lb !== nothing && JuMP.set_lower_bound(variable[name, t], lb)\n\n        if get_warm_start(settings)\n            init = get_variable_warm_start_value(variable_type, d, formulation)\n            init !== nothing && JuMP.set_start_value(variable[name, t], init)\n        end\n    end\n\n    return\nend\n\nfunction add_service_variable!(\n    container::OptimizationContainer,\n    variable_type::T,\n    service::U,\n    contributing_devices::V,\n    formulation::AbstractServiceFormulation,\n) where {\n    T <: VariableType,\n    U <: PSY.Service,\n    V <: Union{Vector{D}, IS.FlattenIteratorWrapper{D}},\n} where {D <: PSY.Component}\n    @assert !isempty(contributing_devices)\n    time_steps = get_time_steps(container)\n\n    binary = get_variable_binary(variable_type, U, formulation)\n\n    variable = add_variable_container!(\n        container,\n        variable_type,\n        U,\n        PSY.get_name(service),\n        [PSY.get_name(d) for d in contributing_devices],\n        time_steps,\n    )\n\n    for t in time_steps, d in contributing_devices\n        name = PSY.get_name(d)\n        variable[name, t] = JuMP.@variable(\n            get_jump_model(container),\n            base_name = \"$(T)_$(U)_$(PSY.get_name(service))_{$(name), $(t)}\",\n            binary = binary\n        )\n\n        ub = get_variable_upper_bound(variable_type, service, d, formulation)\n        ub !== nothing && JuMP.set_upper_bound(variable[name, t], ub)\n\n        lb = get_variable_lower_bound(variable_type, service, d, formulation)\n        lb !== nothing && !binary && JuMP.set_lower_bound(variable[name, t], lb)\n\n        init = get_variable_warm_start_value(variable_type, d, formulation)\n        init !== nothing && JuMP.set_start_value(variable[name, t], init)\n    end\n\n    return\nend\n"
  },
  {
    "path": "src/devices_models/devices/common/duration_constraints.jl",
    "content": "@doc raw\"\"\"\nThis formulation of the duration constraints adds over the start times looking backwards.\n\n# LaTeX\n\n* Minimum up-time constraint:\n\nIf ``t \\leq d_{min}^{up} - d_{init}^{up}`` and ``d_{init}^{up} > 0``\n\n`` 1 + \\sum_{i=t-d_{min}^{up} + 1}^t x_i^{start} - x_t^{on} \\leq 0 ``\n\nfor i in the set of time steps. Otherwise:\n\n`` \\sum_{i=t-d_{min}^{up} + 1}^t x_i^{start} - x_t^{on} \\leq 0 ``\n\nfor i in the set of time steps.\n\n* Minimum down-time constraint:\n\nIf ``t \\leq d_{min}^{down} - d_{init}^{down}`` and ``d_{init}^{down} > 0``\n\n`` 1 + \\sum_{i=t-d_{min}^{down} + 1}^t x_i^{stop} + x_t^{on} \\leq 1 ``\n\nfor i in the set of time steps. Otherwise:\n\n`` \\sum_{i=t-d_{min}^{down} + 1}^t x_i^{stop} + x_t^{on} \\leq 1 ``\n\nfor i in the set of time steps.\n\n\n# Arguments\n* container::OptimizationContainer : the optimization_container model built in PowerSimulations\n* duration_data::Vector{UpDown} : gives how many time steps variable needs to be up or down\n* initial_duration::Matrix{InitialCondition} : gives initial conditions for up (column 1) and down (column 2)\n* cons_name::Symbol : name of the constraint\n* var_keys::Tuple{VariableKey, VariableKey, VariableKey}) : names of the variables\n- : var_keys[1] : varon\n- : var_keys[2] : varstart\n- : var_keys[3] : varstop\n\"\"\"\nfunction device_duration_retrospective!(\n    container::OptimizationContainer,\n    duration_data::Vector{UpDown},\n    initial_duration::Matrix{InitialCondition},\n    cons_type::ConstraintType,\n    var_types::Tuple{VariableType, VariableType, VariableType},\n    ::Type{T},\n) where {T <: PSY.Component}\n    time_steps = get_time_steps(container)\n\n    varon = get_variable(container, var_types[1], T)\n    varstart = get_variable(container, var_types[2], T)\n    varstop = get_variable(container, var_types[3], T)\n\n    device_name_sets = [\n        get_component_name(ic) for\n        ic in initial_duration[:, 1] if !isnothing(get_value(ic))\n    ]\n    con_up = add_constraints_container!(\n        container,\n        cons_type,\n        T,\n        device_name_sets,\n        time_steps;\n        meta = \"up\",\n    )\n    con_down = add_constraints_container!(\n        container,\n        cons_type,\n        T,\n        device_name_sets,\n        time_steps;\n        meta = \"dn\",\n    )\n\n    for t in time_steps\n        for (ix, ic) in enumerate(initial_duration[:, 1])\n            isnothing(get_value(ic)) && continue\n            name = get_component_name(ic)\n            # Minimum Up-time Constraint\n            lhs_on = JuMP.GenericAffExpr{Float64, JuMP.VariableRef}(0)\n            for i in UnitRange{Int}(Int(t - duration_data[ix].up + 1), t)\n                if i in time_steps\n                    JuMP.add_to_expression!(lhs_on, varstart[name, i])\n                end\n            end\n            if t <= max(0, duration_data[ix].up - get_value(ic)) && get_value(ic) > 0\n                JuMP.add_to_expression!(lhs_on, 1)\n            end\n            con_up[name, t] =\n                JuMP.@constraint(get_jump_model(container), lhs_on - varon[name, t] <= 0.0)\n        end\n\n        for (ix, ic) in enumerate(initial_duration[:, 2])\n            isnothing(get_value(ic)) && continue\n            name = get_component_name(ic)\n            # Minimum Down-time Constraint\n            lhs_off = JuMP.GenericAffExpr{Float64, JuMP.VariableRef}(0)\n            for i in UnitRange{Int}(Int(t - duration_data[ix].down + 1), t)\n                if i in time_steps\n                    JuMP.add_to_expression!(lhs_off, varstop[name, i])\n                end\n            end\n            if t <= max(0, duration_data[ix].down - get_value(ic)) && get_value(ic) > 0\n                JuMP.add_to_expression!(lhs_off, 1)\n            end\n            con_down[name, t] =\n                JuMP.@constraint(get_jump_model(container), lhs_off + varon[name, t] <= 1.0)\n        end\n    end\n    return\nend\n\n@doc raw\"\"\"\nThis formulation of the duration constraints looks ahead in the time frame of the model.\n\n# LaTeX\n\n* Minimum up-time constraint:\n\nIf ``t \\leq d_{min}^{up}``\n\n`` d_{min}^{down}x_t^{stop} - \\sum_{i=t-d_{min}^{up} + 1}^t x_i^{on} - x_{init}^{up} \\leq 0 ``\n\nfor i in the set of time steps. Otherwise:\n\n`` d_{min}^{down}x_t^{stop} - \\sum_{i=t-d_{min}^{up} + 1}^t x_i^{on} \\leq 0 ``\n\nfor i in the set of time steps.\n\n* Minimum down-time constraint:\n\nIf ``t \\leq d_{min}^{down}``\n\n`` d_{min}^{up}x_t^{start} - \\sum_{i=t-d_{min^{down} + 1}^t (1 - x_i^{on}) - x_{init}^{down} \\leq 0 ``\n\nfor i in the set of time steps. Otherwise:\n\n`` d_{min}^{up}x_t^{start} - \\sum_{i=t-d_{min^{down} + 1}^t (1 - x_i^{on}) \\leq 0 ``\n\nfor i in the set of time steps.\n\n\n# Arguments\n* container::OptimizationContainer : the optimization_container model built in PowerSimulations\n* duration_data::Vector{UpDown} : gives how many time steps variable needs to be up or down\n* initial_duration::Matrix{InitialCondition} : gives initial conditions for up (column 1) and down (column 2)\n* cons_name::Symbol : name of the constraint\n* var_keys::Tuple{VariableKey, VariableKey, VariableKey}) : names of the variables\n- : var_keys[1] : varon\n- : var_keys[2] : varstart\n- : var_keys[3] : varstop\n\"\"\"\nfunction device_duration_look_ahead!(\n    container::OptimizationContainer,\n    duration_data::Vector{UpDown},\n    initial_duration::Matrix{InitialCondition},\n    cons_type_up::ConstraintType,\n    cons_type_down::ConstraintType,\n    var_types::Tuple{VariableType, VariableType, VariableType},\n    ::Type{T},\n) where {T <: PSY.Component}\n    time_steps = get_time_steps(container)\n    varon = get_variable(container, var_types[1], T)\n    varstart = get_variable(container, var_types[2], T)\n    varstop = get_variable(container, var_types[3], T)\n\n    device_name_sets = [get_component_name(ic) for ic in initial_duration[:, 1]]\n    con_up =\n        add_constraints_container!(container, cons_type_up, device_name_sets, time_steps)\n    con_down =\n        add_constraints_container!(container, cons_type_down, device_name_sets, time_steps)\n\n    for t in time_steps\n        for (ix, ic) in enumerate(initial_duration[:, 1])\n            isnothing(get_value(ic)) && continue\n            name = get_component_name(ic)\n            # Minimum Up-time Constraint\n            lhs_on = JuMP.GenericAffExpr{Float64, JuMP.VariableRef}(0)\n            for i in UnitRange{Int}(Int(t - duration_data[ix].up + 1), t)\n                if i in time_steps\n                    JuMP.add_to_expression!(lhs_on, varon[name, i])\n                end\n            end\n            if t <= duration_data[ix].up\n                JuMP.add_to_expression!(lhs_on, get_value(ic))\n            end\n            con_up[name, t] = JuMP.@constraint(\n                get_jump_model(container),\n                varstop[name, t] * duration_data[ix].up - lhs_on <= 0.0\n            )\n        end\n\n        for (ix, ic) in enumerate(initial_duration[:, 2])\n            isnothing(get_value(ic)) && continue\n            name = get_component_name(ic)\n            # Minimum Down-time Constraint\n            lhs_off = JuMP.GenericAffExpr{Float64, JuMP.VariableRef}(0)\n            for i in UnitRange{Int}(Int(t - duration_data[ix].down + 1), t)\n                if i in time_steps\n                    JuMP.add_to_expression!(lhs_off, 1)\n                    JuMP.add_to_expression!(lhs_off, -1, varon[name, i])\n                end\n            end\n            if t <= duration_data[ix].down\n                JuMP.add_to_expression!(lhs_off, get_value(ic))\n            end\n            con_down[name, t] = JuMP.@constraint(\n                get_jump_model(container),\n                varstart[name, t] * duration_data[ix].down - lhs_off <= 0.0\n            )\n        end\n    end\n\n    return\nend\n\n@doc raw\"\"\"\nThis formulation of the duration constraints considers parameters.\n\n# LaTeX\n\n* Minimum up-time constraint:\n\nIf ``t \\leq d_{min}^{up}``\n\n`` d_{min}^{down}x_t^{stop} - \\sum_{i=t-d_{min}^{up} + 1}^t x_i^{on} - x_{init}^{up} \\leq 0 ``\n\nfor i in the set of time steps. Otherwise:\n\n`` \\sum_{i=t-d_{min}^{up} + 1}^t x_i^{start} - x_t^{on} \\leq 0 ``\n\nfor i in the set of time steps.\n\n* Minimum down-time constraint:\n\nIf ``t \\leq d_{min}^{down}``\n\n`` d_{min}^{up}x_t^{start} - \\sum_{i=t-d_{min^{down} + 1}^t (1 - x_i^{on}) - x_{init}^{down} \\leq 0 ``\n\nfor i in the set of time steps. Otherwise:\n\n`` \\sum_{i=t-d_{min}^{down} + 1}^t x_i^{stop} + x_t^{on} \\leq 1 ``\n\nfor i in the set of time steps.\n\n# Arguments\n* container::OptimizationContainer : the optimization_container model built in PowerSimulations\n* duration_data::Vector{UpDown} : gives how many time steps variable needs to be up or down\n* initial_duration_on::Vector{InitialCondition} : gives initial number of time steps variable is up\n* initial_duration_off::Vector{InitialCondition} : gives initial number of time steps variable is down\n* cons_name::Symbol : name of the constraint\n* var_keys::Tuple{VariableKey, VariableKey, VariableKey}) : names of the variables\n- : var_keys[1] : varon\n- : var_keys[2] : varstart\n- : var_keys[3] : varstop\n\"\"\"\nfunction device_duration_parameters!(\n    container::OptimizationContainer,\n    duration_data::Vector{UpDown},\n    initial_duration::Matrix{InitialCondition},\n    cons_type::ConstraintType,\n    var_types::Tuple{VariableType, VariableType, VariableType},\n    ::Type{T},\n) where {T <: PSY.Component}\n    time_steps = get_time_steps(container)\n\n    varon = get_variable(container, var_types[1], T)\n    varstart = get_variable(container, var_types[2], T)\n    varstop = get_variable(container, var_types[3], T)\n\n    device_name_sets = [get_component_name(ic) for ic in initial_duration[:, 1]]\n    con_up = add_constraints_container!(\n        container,\n        cons_type,\n        T,\n        device_name_sets,\n        time_steps;\n        meta = \"up\",\n    )\n    con_down = add_constraints_container!(\n        container,\n        cons_type,\n        T,\n        device_name_sets,\n        time_steps;\n        meta = \"dn\",\n    )\n\n    for t in time_steps\n        for (ix, ic) in enumerate(initial_duration[:, 1])\n            isnothing(get_value(ic)) && continue\n            name = get_component_name(ic)\n            # Minimum Up-time Constraint\n            lhs_on = JuMP.GenericAffExpr{Float64, JuMP.VariableRef}(0)\n            for i in UnitRange{Int}(Int(t - duration_data[ix].up + 1), t)\n                if t <= duration_data[ix].up\n                    if in(i, time_steps)\n                        JuMP.add_to_expression!(lhs_on, varon[name, i])\n                    end\n                else\n                    JuMP.add_to_expression!(lhs_on, varstart[name, i])\n                end\n            end\n            if t <= duration_data[ix].up\n                JuMP.add_to_expression!(lhs_on, get_value(ic))\n                con_up[name, t] = JuMP.@constraint(\n                    get_jump_model(container),\n                    varstop[name, t] * duration_data[ix].up - lhs_on <= 0.0\n                )\n            else\n                con_up[name, t] =\n                    JuMP.@constraint(\n                        get_jump_model(container),\n                        lhs_on - varon[name, t] <= 0.0\n                    )\n            end\n        end\n\n        for (ix, ic) in enumerate(initial_duration[:, 2])\n            isnothing(get_value(ic)) && continue\n            name = get_component_name(ic)\n            # Minimum Down-time Constraint\n            lhs_off = JuMP.GenericAffExpr{Float64, JuMP.VariableRef}(0)\n            for i in UnitRange{Int}(Int(t - duration_data[ix].down + 1), t)\n                if t <= duration_data[ix].down\n                    if in(i, time_steps)\n                        JuMP.add_to_expression!(lhs_off, 1)\n                        JuMP.add_to_expression!(lhs_off, -1, varon[name, i])\n                    end\n                else\n                    JuMP.add_to_expression!(lhs_off, varstop[name, i])\n                end\n            end\n            if t <= duration_data[ix].down\n                JuMP.add_to_expression!(lhs_off, get_value(ic))\n                con_down[name, t] = JuMP.@constraint(\n                    get_jump_model(container),\n                    varstart[name, t] * duration_data[ix].down - lhs_off <= 0.0\n                )\n            else\n                con_down[name, t] =\n                    JuMP.@constraint(\n                        get_jump_model(container),\n                        lhs_off + varon[name, t] <= 1.0\n                    )\n            end\n        end\n    end\n    return\nend\n\n@doc raw\"\"\"\nThis formulation of the duration constraints adds over the start times looking backwards.\n\n# LaTeX\n\n* Minimum up-time constraint:\n\n`` \\sum_{i=t-min(d_{min}^{up}, T)+ 1}^t x_i^{start} - x_t^{on} \\leq 0 ``\n\nfor i in the set of time steps.\n\n* Minimum down-time constraint:\n\n`` \\sum_{i=t-min(d_{min}^{down}, T) + 1}^t x_i^{stop} + x_t^{on} \\leq 1 ``\n\nfor i in the set of time steps.\n\n\n# Arguments\n* container::OptimizationContainer : the optimization_container model built in PowerSimulations\n* duration_data::Vector{UpDown} : gives how many time steps variable needs to be up or down\n* initial_duration::Matrix{InitialCondition} : gives initial conditions for up (column 1) and down (column 2)\n* cons_name::Symbol : name of the constraint\n* var_keys::Tuple{VariableKey, VariableKey, VariableKey}) : names of the variables\n- : var_keys[1] : varon\n- : var_keys[2] : varstart\n- : var_keys[3] : varstop\n\"\"\"\nfunction device_duration_compact_retrospective!(\n    container::OptimizationContainer,\n    duration_data::Vector{UpDown},\n    initial_duration::Matrix{InitialCondition},\n    cons_type::ConstraintType,\n    var_types::Tuple{VariableType, VariableType, VariableType},\n    ::Type{T},\n) where {T <: PSY.Component}\n    time_steps = get_time_steps(container)\n\n    varon = get_variable(container, var_types[1], T)\n    varstart = get_variable(container, var_types[2], T)\n    varstop = get_variable(container, var_types[3], T)\n\n    device_name_sets = [get_component_name(ic) for ic in initial_duration[:, 1]]\n    con_up = add_constraints_container!(\n        container,\n        cons_type,\n        T,\n        device_name_sets,\n        time_steps;\n        meta = \"up\",\n        sparse = true,\n    )\n    con_down = add_constraints_container!(\n        container,\n        cons_type,\n        T,\n        device_name_sets,\n        time_steps;\n        meta = \"dn\",\n        sparse = true,\n    )\n    total_time_steps = length(time_steps)\n    for t in time_steps\n        for (ix, ic) in enumerate(initial_duration[:, 1])\n            isnothing(get_value(ic)) && continue\n            name = get_component_name(ic)\n            # Minimum Up-time Constraint\n            lhs_on = JuMP.GenericAffExpr{Float64, JuMP.VariableRef}(0)\n            if t in UnitRange{Int}(\n                Int(min(duration_data[ix].up, total_time_steps)),\n                total_time_steps,\n            )\n                for i in UnitRange{Int}(Int(t - duration_data[ix].up + 1), t)\n                    if i in time_steps\n                        JuMP.add_to_expression!(lhs_on, varstart[name, i])\n                    end\n                end\n            elseif t <= max(0, duration_data[ix].up - get_value(ic)) && get_value(ic) > 0\n                JuMP.add_to_expression!(lhs_on, 1)\n            else\n                continue\n            end\n            con_up[name, t] =\n                JuMP.@constraint(get_jump_model(container), lhs_on - varon[name, t] <= 0.0)\n        end\n\n        for (ix, ic) in enumerate(initial_duration[:, 2])\n            isnothing(get_value(ic)) && continue\n            name = get_component_name(ic)\n            # Minimum Down-time Constraint\n            lhs_off = JuMP.GenericAffExpr{Float64, JuMP.VariableRef}(0)\n            if t in UnitRange{Int}(\n                Int(min(duration_data[ix].down, total_time_steps)),\n                total_time_steps,\n            )\n                for i in UnitRange{Int}(Int(t - duration_data[ix].down + 1), t)\n                    if i in time_steps\n                        JuMP.add_to_expression!(lhs_off, varstop[name, i])\n                    end\n                end\n            elseif t <= max(0, duration_data[ix].down - get_value(ic)) && get_value(ic) > 0\n                JuMP.add_to_expression!(lhs_off, 1)\n            else\n                continue\n            end\n            con_down[name, t] =\n                JuMP.@constraint(get_jump_model(container), lhs_off + varon[name, t] <= 1.0)\n        end\n    end\n    for c in [con_up, con_down]\n        # Workaround to remove invalid key combinations\n        filter!(x -> x.second !== nothing, c.data)\n    end\n    return\nend\n"
  },
  {
    "path": "src/devices_models/devices/common/get_time_series.jl",
    "content": "function _get_time_series(\n    container::OptimizationContainer,\n    component::PSY.Component,\n    attributes::TimeSeriesAttributes{T};\n    interval::Dates.Millisecond = UNSET_INTERVAL,\n) where {T <: PSY.TimeSeriesData}\n    return get_time_series_initial_values!(\n        container,\n        T,\n        component,\n        get_time_series_name(attributes);\n        interval = interval,\n    )\nend\n\nfunction get_time_series(\n    container::OptimizationContainer,\n    component::T,\n    parameter::TimeSeriesParameter,\n    meta = ISOPT.CONTAINER_KEY_EMPTY_META;\n    interval::Dates.Millisecond = UNSET_INTERVAL,\n) where {T <: PSY.Component}\n    parameter_container = get_parameter(container, parameter, T, meta)\n    return _get_time_series(\n        container,\n        component,\n        parameter_container.attributes;\n        interval = interval,\n    )\nend\n\n# This is just for temporary compatibility with current code. Needs to be eliminated once the time series\n# refactor is done.\nfunction get_time_series(\n    container::OptimizationContainer,\n    component::PSY.Component,\n    forecast_name::String;\n    interval::Dates.Millisecond = UNSET_INTERVAL,\n)\n    ts_type = get_default_time_series_type(container)\n    return _get_time_series(\n        container,\n        component,\n        TimeSeriesAttributes(ts_type, forecast_name);\n        interval = interval,\n    )\nend\n"
  },
  {
    "path": "src/devices_models/devices/common/objective_function/common.jl",
    "content": "##################################\n######## Helper Functions ########\n##################################\n\nget_output_offer_curves(cost::PSY.ImportExportCost, args...; kwargs...) =\n    PSY.get_import_offer_curves(cost, args...; kwargs...)\nget_output_offer_curves(cost::PSY.MarketBidCost, args...; kwargs...) =\n    PSY.get_incremental_offer_curves(cost, args...; kwargs...)\nget_input_offer_curves(cost::PSY.ImportExportCost, args...; kwargs...) =\n    PSY.get_export_offer_curves(cost, args...; kwargs...)\nget_input_offer_curves(cost::PSY.MarketBidCost, args...; kwargs...) =\n    PSY.get_decremental_offer_curves(cost, args...; kwargs...)\n\n# TODO deduplicate, these signatures are getting out of hand\nget_output_offer_curves(\n    component::PSY.Component,\n    cost::PSY.ImportExportCost,\n    args...;\n    kwargs...,\n) =\n    PSY.get_import_offer_curves(component, cost, args...; kwargs...)\nget_output_offer_curves(\n    component::PSY.Component,\n    cost::PSY.MarketBidCost,\n    args...;\n    kwargs...,\n) =\n    PSY.get_incremental_offer_curves(component, cost, args...; kwargs...)\nget_input_offer_curves(\n    component::PSY.Component,\n    cost::PSY.ImportExportCost,\n    args...;\n    kwargs...,\n) =\n    PSY.get_export_offer_curves(component, cost, args...; kwargs...)\nget_input_offer_curves(\n    component::PSY.Component,\n    cost::PSY.MarketBidCost,\n    args...;\n    kwargs...,\n) =\n    PSY.get_decremental_offer_curves(component, cost, args...; kwargs...)\n\n\"\"\"\nEither looks up a value in the component using `getter_func` or fetches the value from the\nparameter `U()`, depending on whether we are in the time-variant case or not\n\"\"\"\nfunction _lookup_maybe_time_variant_param(\n    ::OptimizationContainer,\n    component::T,\n    ::Int,\n    ::Val{false},  # not time variant\n    getter_func::F,\n    ::U,\n) where {T <: PSY.Component, F <: Function, U <: ParameterType}\n    return getter_func(component)\nend\n\nfunction _lookup_maybe_time_variant_param(\n    container::OptimizationContainer,\n    component::T,\n    time_period::Int,\n    ::Val{true},  # yes time variant\n    ::F,\n    ::U,\n) where {T <: PSY.Component, F <: Function, U <: ParameterType}\n    # PERF this is modeled on the old get_fuel_cost_value function, but is it really\n    # performant to be fetching the whole array and multiplier array anew for every time step?\n    parameter_array = get_parameter_array(container, U(), T)\n    parameter_multiplier =\n        get_parameter_multiplier_array(container, U(), T)\n    name = PSY.get_name(component)\n    return parameter_array[name, time_period] .* parameter_multiplier[name, time_period]\nend\n\n##################################\n#### ActivePowerVariable Cost ####\n##################################\n\nfunction add_variable_cost!(\n    container::OptimizationContainer,\n    ::U,\n    devices::IS.FlattenIteratorWrapper{T},\n    ::V,\n) where {T <: PSY.Component, U <: VariableType, V <: AbstractDeviceFormulation}\n    for d in devices\n        op_cost_data = PSY.get_operation_cost(d)\n        _add_variable_cost_to_objective!(container, U(), d, op_cost_data, V())\n        _add_vom_cost_to_objective!(container, U(), d, op_cost_data, V())\n    end\n    return\nend\n\n##################################\n#### Curtailment Cost ############\n##################################\nfunction add_curtailment_cost!(\n    container::OptimizationContainer,\n    ::U,\n    devices::IS.FlattenIteratorWrapper{T},\n    ::V,\n) where {T <: PSY.RenewableGen, U <: VariableType, V <: AbstractDeviceFormulation}\n    for d in devices\n        op_cost_data = PSY.get_operation_cost(d)\n        !hasproperty(op_cost_data, :curtailment_cost) && continue\n        cost_function = PSY.get_curtailment_cost(op_cost_data)\n        isnothing(cost_function) && continue\n        _add_curtailment_cost!(container, U(), d, cost_function, V())\n    end\n    return\nend\n\n##################################\n#### Start/Stop Variable Cost ####\n##################################\n\nget_shutdown_cost_value(\n    container::OptimizationContainer,\n    component::PSY.Component,\n    time_period::Int,\n    is_time_variant_::Bool,\n) = _lookup_maybe_time_variant_param(\n    container,\n    component,\n    time_period,\n    Val(is_time_variant_),\n    PSY.get_shut_down ∘ PSY.get_operation_cost,\n    ShutdownCostParameter(),\n)\n\nfunction add_shut_down_cost!(\n    container::OptimizationContainer,\n    ::U,\n    devices::IS.FlattenIteratorWrapper{T},\n    ::V,\n) where {T <: PSY.Component, U <: VariableType, V <: AbstractDeviceFormulation}\n    multiplier = objective_function_multiplier(U(), V())\n    for d in devices\n        PSY.get_must_run(d) && continue\n\n        add_as_time_variant = is_time_variant(PSY.get_shut_down(PSY.get_operation_cost(d)))\n        for t in get_time_steps(container)\n            my_cost_term = get_shutdown_cost_value(\n                container,\n                d,\n                t,\n                add_as_time_variant,\n            )\n            iszero(my_cost_term) && continue\n            exp = _add_proportional_term_maybe_variant!(\n                Val(add_as_time_variant), container, U(), d, my_cost_term * multiplier,\n                t)\n            add_to_expression!(container, ShutDownCostExpression, exp, d, t)\n        end\n    end\n    return\nend\n\n##################################\n####### Proportional Cost ########\n##################################\nfunction add_proportional_cost!(\n    container::OptimizationContainer,\n    ::U,\n    devices::IS.FlattenIteratorWrapper{T},\n    ::V,\n) where {T <: PSY.Component, U <: VariableType, V <: AbstractDeviceFormulation}\n    # NOTE: anything time-varying should implement its own method.\n    multiplier = objective_function_multiplier(U(), V())\n    for d in devices\n        op_cost_data = PSY.get_operation_cost(d)\n        cost_term = proportional_cost(op_cost_data, U(), d, V())\n        iszero(cost_term) && continue\n        for t in get_time_steps(container)\n            exp = _add_proportional_term!(container, U(), d, cost_term * multiplier, t)\n            add_to_expression!(container, FixedCostExpression, exp, d, t)\n        end\n    end\n    return\nend\n\n##################################\n########## VOM Cost ##############\n##################################\n\nfunction _add_vom_cost_to_objective!(\n    container::OptimizationContainer,\n    ::T,\n    component::PSY.Component,\n    op_cost::PSY.OperationalCost,\n    ::U,\n) where {T <: VariableType, U <: AbstractDeviceFormulation}\n    variable_cost_data = variable_cost(op_cost, T(), component, U())\n    power_units = PSY.get_power_units(variable_cost_data)\n    vom_cost = PSY.get_vom_cost(variable_cost_data)\n    multiplier = 1.0 # VOM Cost is always positive\n    cost_term = PSY.get_proportional_term(vom_cost)\n    iszero(cost_term) && return\n    base_power = get_base_power(container)\n    device_base_power = PSY.get_base_power(component)\n    cost_term_normalized = get_proportional_cost_per_system_unit(\n        cost_term,\n        power_units,\n        base_power,\n        device_base_power,\n    )\n    resolution = get_resolution(container)\n    dt = Dates.value(resolution) / MILLISECONDS_IN_HOUR\n    for t in get_time_steps(container)\n        exp =\n            _add_proportional_term!(\n                container,\n                T(),\n                component,\n                cost_term_normalized * multiplier * dt,\n                t,\n            )\n        add_to_expression!(container, VOMCostExpression, exp, component, t)\n    end\n    return\nend\n\n##################################\n######## OnVariable Cost #########\n##################################\n\nfunction add_proportional_cost!(\n    container::OptimizationContainer,\n    ::U,\n    devices::IS.FlattenIteratorWrapper{T},\n    ::V,\n) where {T <: PSY.ThermalGen, U <: OnVariable, V <: AbstractThermalUnitCommitment}\n    multiplier = objective_function_multiplier(U(), V())\n    for d in devices\n        op_cost_data = PSY.get_operation_cost(d)\n        for t in get_time_steps(container)\n            cost_term = proportional_cost(container, op_cost_data, U(), d, V(), t)\n            add_as_time_variant =\n                is_time_variant_term(container, op_cost_data, U(), d, V(), t)\n            iszero(cost_term) && continue\n            cost_term *= multiplier\n            exp = if PSY.get_must_run(d)\n                cost_term  # note we do not add this to the objective function\n            else\n                _add_proportional_term_maybe_variant!(\n                    Val(add_as_time_variant), container, U(), d, cost_term, t)\n            end\n            add_to_expression!(container, FixedCostExpression, exp, d, t)\n        end\n    end\n    return\nend\n\n# code repetition: same as above, just change types and remove must_run check.\nfunction add_proportional_cost!(\n    container::OptimizationContainer,\n    ::U,\n    devices::IS.FlattenIteratorWrapper{T},\n    ::PowerLoadInterruption,\n) where {T <: PSY.ControllableLoad, U <: OnVariable}\n    multiplier = objective_function_multiplier(U(), PowerLoadInterruption())\n    for d in devices\n        op_cost_data = PSY.get_operation_cost(d)\n        for t in get_time_steps(container)\n            cost_term = proportional_cost(\n                container,\n                op_cost_data,\n                U(),\n                d,\n                PowerLoadInterruption(),\n                t,\n            )\n            add_as_time_variant =\n                is_time_variant_term(\n                    container,\n                    op_cost_data,\n                    U(),\n                    d,\n                    PowerLoadInterruption(),\n                    t,\n                )\n            iszero(cost_term) && continue\n            cost_term *= multiplier\n            exp = _add_proportional_term_maybe_variant!(\n                Val(add_as_time_variant), container, U(), d, cost_term, t)\n            add_to_expression!(container, FixedCostExpression, exp, d, t)\n        end\n    end\n    return\nend\n\nfunction _add_variable_cost_to_objective!(\n    container::OptimizationContainer,\n    ::T,\n    component::PSY.Component,\n    op_cost::PSY.OperationalCost,\n    ::U,\n) where {T <: VariableType, U <: AbstractDeviceFormulation}\n    variable_cost_data = variable_cost(op_cost, T(), component, U())\n    _add_variable_cost_to_objective!(container, T(), component, variable_cost_data, U())\n    return\nend\n\nfunction add_start_up_cost!(\n    container::OptimizationContainer,\n    ::U,\n    devices::IS.FlattenIteratorWrapper{T},\n    ::V,\n) where {T <: PSY.Component, U <: VariableType, V <: AbstractDeviceFormulation}\n    for d in devices\n        op_cost_data = PSY.get_operation_cost(d)\n        _add_start_up_cost_to_objective!(container, U(), d, op_cost_data, V())\n    end\n    return\nend\n\nfunction get_startup_cost_value(\n    container::OptimizationContainer,\n    ::T,\n    component::V,\n    ::U,\n    time_period::Int,\n    is_time_variant_::Bool,\n) where {T <: VariableType, V <: PSY.Component, U <: AbstractDeviceFormulation}\n    raw_startup_cost = _lookup_maybe_time_variant_param(\n        container,\n        component,\n        time_period,\n        Val(is_time_variant_),\n        PSY.get_start_up ∘ PSY.get_operation_cost,\n        StartupCostParameter(),\n    )\n    return start_up_cost(raw_startup_cost, component, T(), U())\nend\n\nfunction _add_start_up_cost_to_objective!(\n    container::OptimizationContainer,\n    ::T,\n    component::PSY.ThermalGen,\n    op_cost::Union{PSY.ThermalGenerationCost, PSY.MarketBidCost},\n    ::U,\n) where {T <: VariableType, U <: AbstractDeviceFormulation}\n    multiplier = objective_function_multiplier(T(), U())\n    PSY.get_must_run(component) && return\n    add_as_time_variant = is_time_variant(PSY.get_start_up(op_cost))\n    for t in get_time_steps(container)\n        my_cost_term = get_startup_cost_value(\n            container,\n            T(),\n            component,\n            U(),\n            t,\n            add_as_time_variant,\n        )\n        iszero(my_cost_term) && continue\n        exp = _add_proportional_term_maybe_variant!(\n            Val(add_as_time_variant), container, T(), component,\n            my_cost_term * multiplier, t)\n        add_to_expression!(container, StartUpCostExpression, exp, component, t)\n    end\n    return\nend\n\nfunction _get_cost_function_parameter_container(\n    container::OptimizationContainer,\n    ::S,\n    component::T,\n    ::U,\n    ::V,\n    cost_type::Type{W},\n) where {\n    S <: ObjectiveFunctionParameter,\n    T <: PSY.Component,\n    U <: VariableType,\n    V <: Union{AbstractDeviceFormulation, AbstractServiceFormulation},\n    W,\n}\n    if has_container_key(container, S, T)\n        return get_parameter(container, S(), T)\n    else\n        container_axes = axes(get_variable(container, U(), T))\n        if has_container_key(container, OnStatusParameter, T)\n            sos_val = SOSStatusVariable.PARAMETER\n        else\n            sos_val = sos_status(component, V())\n        end\n        return add_param_container!(\n            container,\n            S(),\n            T,\n            U,\n            sos_val,\n            uses_compact_power(component, V()),\n            W,\n            container_axes...,\n        )\n    end\nend\n\nfunction _add_proportional_term_helper(\n    container::OptimizationContainer,\n    ::T,\n    component::U,\n    linear_term::Float64,\n    time_period::Int,\n) where {T <: VariableType, U <: PSY.Component}\n    component_name = PSY.get_name(component)\n    @debug \"Linear Variable Cost\" _group = LOG_GROUP_COST_FUNCTIONS component_name\n    variable = get_variable(container, T(), U)[component_name, time_period]\n    lin_cost = variable * linear_term\n    return lin_cost\nend\n\n# Invariant\nfunction _add_proportional_term!(\n    container::OptimizationContainer,\n    ::T,\n    component::U,\n    linear_term::Float64,\n    time_period::Int,\n) where {T <: VariableType, U <: PSY.Component}\n    lin_cost = _add_proportional_term_helper(\n        container, T(), component, linear_term, time_period)\n    add_to_objective_invariant_expression!(container, lin_cost)\n    return lin_cost\nend\n\n# Variant\nfunction _add_proportional_term_variant!(\n    container::OptimizationContainer,\n    ::T,\n    component::U,\n    linear_term::Float64,\n    time_period::Int,\n) where {T <: VariableType, U <: PSY.Component}\n    lin_cost = _add_proportional_term_helper(\n        container, T(), component, linear_term, time_period)\n    add_to_objective_variant_expression!(container, lin_cost)\n    return lin_cost\nend\n\n# Maybe variant\n_add_proportional_term_maybe_variant!(\n    ::Val{false},\n    container::OptimizationContainer,\n    ::T,\n    component::U,\n    linear_term::Float64,\n    time_period::Int,\n) where {T <: VariableType, U <: PSY.Component} =\n    _add_proportional_term!(container, T(), component, linear_term, time_period)\n_add_proportional_term_maybe_variant!(\n    ::Val{true},\n    container::OptimizationContainer,\n    ::T,\n    component::U,\n    linear_term::Float64,\n    time_period::Int,\n) where {T <: VariableType, U <: PSY.Component} =\n    _add_proportional_term_variant!(container, T(), component, linear_term, time_period)\n\nfunction _add_quadratic_term!(\n    container::OptimizationContainer,\n    ::T,\n    component::U,\n    q_terms::NTuple{2, Float64},\n    expression_multiplier::Float64,\n    time_period::Int,\n) where {T <: VariableType, U <: PSY.Component}\n    component_name = PSY.get_name(component)\n    @debug \"$component_name Quadratic Variable Cost\" _group = LOG_GROUP_COST_FUNCTIONS component_name\n    var = get_variable(container, T(), U)[component_name, time_period]\n    q_cost_ = var .^ 2 * q_terms[1] + var * q_terms[2]\n    q_cost = q_cost_ * expression_multiplier\n    add_to_objective_invariant_expression!(container, q_cost)\n    return q_cost\nend\n\n##################################################\n################## Fuel Cost #####################\n##################################################\n\nget_fuel_cost_value(\n    container::OptimizationContainer,\n    component::PSY.Component,\n    time_period::Int,\n    is_time_variant_::Bool,\n) = _lookup_maybe_time_variant_param(\n    container,\n    component,\n    time_period,\n    Val(is_time_variant_),\n    PSY.get_fuel_cost,\n    FuelCostParameter(),\n)\n\nfunction _add_time_varying_fuel_variable_cost!(\n    container::OptimizationContainer,\n    ::T,\n    component::V,\n    fuel_cost::IS.TimeSeriesKey,\n) where {T <: VariableType, V <: PSY.Component}\n    parameter = get_parameter_array(container, FuelCostParameter(), V)\n    multiplier = get_parameter_multiplier_array(container, FuelCostParameter(), V)\n    expression = get_expression(container, FuelConsumptionExpression(), V)\n    name = PSY.get_name(component)\n    for t in get_time_steps(container)\n        cost_expr = expression[name, t] * parameter[name, t] * multiplier[name, t]\n        add_to_expression!(\n            container,\n            FuelCostExpression,\n            cost_expr,\n            component,\n            t,\n        )\n        add_to_objective_variant_expression!(container, cost_expr)\n    end\n    return\nend\n\n# Used for dispatch (on/off decision) for devices where operation_cost::Union{MarketBidCost, FooCost}\n# currently: ThermalGen, ControllableLoad subtypes.\nfunction _onvar_cost(::PSY.CostCurve{PSY.PiecewisePointCurve})\n    # OnVariableCost is included in the Point itself for PiecewisePointCurve\n    return 0.0\nend\n\nfunction _onvar_cost(\n    cost_function::Union{PSY.CostCurve{PSY.LinearCurve}, PSY.CostCurve{PSY.QuadraticCurve}},\n)\n    value_curve = PSY.get_value_curve(cost_function)\n    cost_component = PSY.get_function_data(value_curve)\n    # Always in \\$/h\n    constant_term = PSY.get_constant_term(cost_component)\n    return constant_term\nend\n\nfunction _onvar_cost(::PSY.CostCurve{PSY.PiecewiseIncrementalCurve})\n    # Input at min is used to transform to InputOutputCurve\n    return 0.0\nend\n\nfunction _onvar_cost(::PSY.CostCurve{PSY.PiecewiseAverageCurve})\n    # Input at min is used to transform to InputOutputCurve\n    return 0.0\nend\n\nfunction _onvar_cost(\n    ::OptimizationContainer,\n    cost_function::PSY.CostCurve{T},\n    ::PSY.Component,\n    ::Int,\n) where {T <: IS.ValueCurve}\n    return _onvar_cost(cost_function)\nend\n"
  },
  {
    "path": "src/devices_models/devices/common/objective_function/import_export.jl",
    "content": "_include_min_gen_power_in_constraint(\n    ::PSY.Source,\n    ::ActivePowerOutVariable,\n    ::AbstractDeviceFormulation,\n) = false\n_include_min_gen_power_in_constraint(\n    ::PSY.Source,\n    ::ActivePowerInVariable,\n    ::AbstractDeviceFormulation,\n) = false\n\nfunction _add_variable_cost_to_objective!(\n    container::OptimizationContainer,\n    ::T,\n    component::PSY.Source,\n    cost_function::PSY.ImportExportCost,\n    ::U,\n) where {\n    T <: ActivePowerOutVariable,\n    U <: AbstractSourceFormulation,\n}\n    component_name = PSY.get_name(component)\n    @debug \"Import Export Cost\" _group = PSI.LOG_GROUP_COST_FUNCTIONS component_name\n    import_cost_curves = PSY.get_import_offer_curves(cost_function)\n    if !isnothing(import_cost_curves)\n        add_pwl_term!(\n            false,\n            container,\n            component,\n            cost_function,\n            T(),\n            U(),\n        )\n    end\n    return\nend\n\nfunction _add_variable_cost_to_objective!(\n    container::OptimizationContainer,\n    ::T,\n    component::PSY.Source,\n    cost_function::PSY.ImportExportCost,\n    ::U,\n) where {\n    T <: ActivePowerInVariable,\n    U <: AbstractSourceFormulation,\n}\n    component_name = PSY.get_name(component)\n    @debug \"Import Export Cost\" _group = PSI.LOG_GROUP_COST_FUNCTIONS component_name\n    export_cost_curves = PSY.get_export_offer_curves(cost_function)\n    if !isnothing(export_cost_curves)\n        add_pwl_term!(\n            true,\n            container,\n            component,\n            cost_function,\n            T(),\n            U(),\n        )\n    end\n    return\nend\n\n# _process_occ_parameters_helper and the helpers it depends on (even purely IEC ones) are in objective_function/market_bid.jl\nfunction process_import_export_parameters!(\n    container::OptimizationContainer,\n    devices_in,\n    model::DeviceModel,\n)\n    devices = filter(_has_import_export_cost, collect(devices_in))\n\n    for param in (\n        IncrementalPiecewiseLinearSlopeParameter(),\n        IncrementalPiecewiseLinearBreakpointParameter(),\n        DecrementalPiecewiseLinearSlopeParameter(),\n        DecrementalPiecewiseLinearBreakpointParameter(),\n    )\n        # Validate and add the parameters\n        _process_occ_parameters_helper(param, container, model, devices)\n    end\nend\n"
  },
  {
    "path": "src/devices_models/devices/common/objective_function/linear_curve.jl",
    "content": "# Add proportional terms to objective function and expression\nfunction _add_linearcurve_variable_term_to_model!(\n    container::OptimizationContainer,\n    ::T,\n    component::PSY.Component,\n    proportional_term_per_unit::Float64,\n    time_period::Int,\n) where {T <: VariableType}\n    resolution = get_resolution(container)\n    dt = Dates.value(resolution) / MILLISECONDS_IN_HOUR\n    linear_cost = _add_proportional_term!(\n        container,\n        T(),\n        component,\n        proportional_term_per_unit * dt,\n        time_period,\n    )\n    add_to_expression!(\n        container,\n        FuelCostExpression,\n        linear_cost,\n        component,\n        time_period,\n    )\n    return\nend\n\n# Dispatch for vector of proportional terms\nfunction _add_linearcurve_variable_cost!(\n    container::OptimizationContainer,\n    ::T,\n    component::PSY.Component,\n    proportional_terms_per_unit::Vector{Float64},\n) where {T <: VariableType}\n    for t in get_time_steps(container)\n        _add_linearcurve_variable_term_to_model!(\n            container,\n            T(),\n            component,\n            proportional_terms_per_unit[t],\n            t,\n        )\n    end\n    return\nend\n\n# Dispatch for scalar proportional terms\nfunction _add_linearcurve_variable_cost!(\n    container::OptimizationContainer,\n    ::T,\n    component::PSY.Component,\n    proportional_term_per_unit::Float64,\n) where {T <: VariableType}\n    for t in get_time_steps(container)\n        _add_linearcurve_variable_term_to_model!(\n            container,\n            T(),\n            component,\n            proportional_term_per_unit,\n            t,\n        )\n    end\n    return\nend\n\n\"\"\"\nAdds to the cost function cost terms for sum of variables with common factor to be used for cost expression for optimization_container model.\n\n# Arguments\n\n  - container::OptimizationContainer : the optimization_container model built in PowerSimulations\n  - var_key::VariableKey: The variable name\n  - component_name::String: The component_name of the variable container\n  - cost_component::PSY.CostCurve{PSY.LinearCurve} : container for cost to be associated with variable\n\"\"\"\nfunction _add_variable_cost_to_objective!(\n    container::OptimizationContainer,\n    ::T,\n    component::PSY.Component,\n    cost_function::PSY.CostCurve{PSY.LinearCurve},\n    ::U,\n) where {T <: VariableType, U <: AbstractDeviceFormulation}\n    base_power = get_base_power(container)\n    device_base_power = PSY.get_base_power(component)\n    value_curve = PSY.get_value_curve(cost_function)\n    power_units = PSY.get_power_units(cost_function)\n    cost_component = PSY.get_function_data(value_curve)\n    proportional_term = PSY.get_proportional_term(cost_component)\n    proportional_term_per_unit = get_proportional_cost_per_system_unit(\n        proportional_term,\n        power_units,\n        base_power,\n        device_base_power,\n    )\n    multiplier = objective_function_multiplier(T(), U())\n    _add_linearcurve_variable_cost!(\n        container,\n        T(),\n        component,\n        multiplier * proportional_term_per_unit,\n    )\n    return\nend\n\nfunction _add_fuel_linear_variable_cost!(\n    container::OptimizationContainer,\n    ::T,\n    component::PSY.Component,\n    fuel_curve::Float64,\n    fuel_cost::Float64,\n) where {T <: VariableType}\n    _add_linearcurve_variable_cost!(container, T(), component, fuel_curve * fuel_cost)\nend\n\nfunction _add_fuel_linear_variable_cost!(\n    container::OptimizationContainer,\n    ::T,\n    component::V,\n    ::Float64, # already normalized in MMBTU/p.u.\n    fuel_cost::IS.TimeSeriesKey,\n) where {T <: VariableType, V <: PSY.Component}\n    _add_time_varying_fuel_variable_cost!(container, T(), component, fuel_cost)\n    return\nend\n\n\"\"\"\nAdds to the cost function cost terms for sum of variables with common factor to be used for cost expression for optimization_container model.\n\n# Arguments\n\n  - container::OptimizationContainer : the optimization_container model built in PowerSimulations\n  - var_key::VariableKey: The variable name\n  - component_name::String: The component_name of the variable container\n  - cost_component::PSY.FuelCurve{PSY.LinearCurve} : container for cost to be associated with variable\n\"\"\"\nfunction _add_variable_cost_to_objective!(\n    container::OptimizationContainer,\n    ::T,\n    component::PSY.Component,\n    cost_function::PSY.FuelCurve{PSY.LinearCurve},\n    ::U,\n) where {T <: VariableType, U <: AbstractDeviceFormulation}\n    base_power = get_base_power(container)\n    device_base_power = PSY.get_base_power(component)\n    value_curve = PSY.get_value_curve(cost_function)\n    power_units = PSY.get_power_units(cost_function)\n    cost_component = PSY.get_function_data(value_curve)\n    proportional_term = PSY.get_proportional_term(cost_component)\n    fuel_curve_per_unit = get_proportional_cost_per_system_unit(\n        proportional_term,\n        power_units,\n        base_power,\n        device_base_power,\n    )\n    fuel_cost = PSY.get_fuel_cost(cost_function)\n    # Multiplier is not necessary here. There is no negative cost for fuel curves.\n    _add_fuel_linear_variable_cost!(\n        container,\n        T(),\n        component,\n        fuel_curve_per_unit,\n        fuel_cost,\n    )\n    return\nend\n\nfunction _add_curtailment_cost!(\n    container::OptimizationContainer,\n    ::T,\n    component::PSY.RenewableDispatch,\n    cost_function::PSY.CostCurve{PSY.LinearCurve},\n    ::U,\n) where {T <: VariableType, U <: AbstractDeviceFormulation}\n    base_power = get_base_power(container)\n    device_base_power = PSY.get_base_power(component)\n\n    value_curve = PSY.get_value_curve(cost_function)\n    power_units = PSY.get_power_units(cost_function)\n    cost_component = PSY.get_function_data(value_curve)\n    proportional_term = PSY.get_proportional_term(cost_component)\n    iszero(proportional_term) && return\n\n    proportional_term_per_unit = get_proportional_cost_per_system_unit(\n        proportional_term,\n        power_units,\n        base_power,\n        device_base_power,\n    )\n\n    resolution = get_resolution(container)\n    dt = Dates.value(resolution) / MILLISECONDS_IN_HOUR\n\n    name = PSY.get_name(component)\n    dispatch_vars = get_variable(container, T(), PSY.RenewableDispatch)\n\n    for t in get_time_steps(container)\n        breakpoints, _ = _get_pwl_data(false, container, component, t)\n        offer_max = breakpoints[end]\n\n        dispatch = dispatch_vars[name, t]\n        curtailment_cost = proportional_term_per_unit * dt * (offer_max - dispatch)\n\n        add_to_expression!(\n            container,\n            CurtailmentCostExpression,\n            curtailment_cost,\n            component,\n            t,\n        )\n    end\n\n    return\nend\n\nfunction _add_curtailment_cost!(\n    container::OptimizationContainer,\n    ::T,\n    component::PSY.RenewableGen,\n    cost_function::PSY.CostCurve{PSY.LinearCurve},\n    ::U,\n) where {T <: VariableType, U <: AbstractDeviceFormulation}\n    base_power = get_base_power(container)\n    device_base_power = PSY.get_base_power(component)\n\n    value_curve = PSY.get_value_curve(cost_function)\n    power_units = PSY.get_power_units(cost_function)\n    cost_component = PSY.get_function_data(value_curve)\n    proportional_term = PSY.get_proportional_term(cost_component)\n    iszero(proportional_term) && return\n\n    proportional_term_per_unit = get_proportional_cost_per_system_unit(\n        proportional_term,\n        power_units,\n        base_power,\n        device_base_power,\n    )\n\n    resolution = get_resolution(container)\n    dt = Dates.value(resolution) / MILLISECONDS_IN_HOUR\n\n    name = PSY.get_name(component)\n    dispatch_vars = get_variable(container, T(), PSY.RenewableGen)\n\n    for t in get_time_steps(container)\n        breakpoints, _ = _get_pwl_data(false, container, component, t)\n        offer_max = breakpoints[end]\n\n        dispatch = dispatch_vars[name, t]\n        curtailment_cost = proportional_term_per_unit * dt * (offer_max - dispatch)\n\n        add_to_expression!(\n            container,\n            CurtailmentCostExpression,\n            curtailment_cost,\n            component,\n            t,\n        )\n    end\n\n    return\nend\n"
  },
  {
    "path": "src/devices_models/devices/common/objective_function/market_bid.jl",
    "content": "##################################################\n################ PWL Parameters  #################\n##################################################\n\n# Helper functions to manage incremental (false) vs. decremental (true) cases\nget_initial_input_maybe_decremental(::Val{true}, device::PSY.StaticInjection) =\n    PSY.get_decremental_initial_input(PSY.get_operation_cost(device))\nget_initial_input_maybe_decremental(::Val{false}, device::PSY.StaticInjection) =\n    PSY.get_incremental_initial_input(PSY.get_operation_cost(device))\n\nget_offer_curves_maybe_decremental(::Val{true}, device::PSY.StaticInjection) =\n    get_input_offer_curves(PSY.get_operation_cost(device))\nget_offer_curves_maybe_decremental(::Val{false}, device::PSY.StaticInjection) =\n    get_output_offer_curves(PSY.get_operation_cost(device))\n\n# Dictionaries to handle more incremental (false) vs. decremental (true) cases\nconst _SLOPE_PARAMS::Dict{Bool, Type{<:AbstractPiecewiseLinearSlopeParameter}} = Dict(\n    false => IncrementalPiecewiseLinearSlopeParameter,\n    true => DecrementalPiecewiseLinearSlopeParameter)\nconst _BREAKPOINT_PARAMS::Dict{Bool, Type{<:AbstractPiecewiseLinearBreakpointParameter}} =\n    Dict(\n        false => IncrementalPiecewiseLinearBreakpointParameter,\n        true => DecrementalPiecewiseLinearBreakpointParameter)\nconst _PIECEWISE_BLOCK_VARS::Dict{Bool, Type{<:AbstractPiecewiseLinearBlockOffer}} = Dict(\n    false => PiecewiseLinearBlockIncrementalOffer,\n    true => PiecewiseLinearBlockDecrementalOffer)\nconst _PIECEWISE_BLOCK_CONSTRAINTS::Dict{\n    Bool,\n    Type{<:AbstractPiecewiseLinearBlockOfferConstraint},\n} = Dict(\n    false => PiecewiseLinearBlockIncrementalOfferConstraint,\n    true => PiecewiseLinearBlockDecrementalOfferConstraint)\n\n# Determines whether we care about various types of costs, given the formulation\n# NOTE: currently works based on what has already been added to the container;\n# alternatively we could dispatch on the formulation directly\n\n_consider_parameter(\n    ::StartupCostParameter,\n    container::OptimizationContainer,\n    ::DeviceModel{T, D},\n) where {T, D} =\n    any(has_container_key.([container], [StartVariable, MULTI_START_VARIABLES...], [T]))\n\n_consider_parameter(\n    ::ShutdownCostParameter,\n    container::OptimizationContainer,\n    ::DeviceModel{T, D},\n) where {T, D} = has_container_key(container, StopVariable, T)\n\n# FIXME storage doesn't currently have an OnVariable. should it have one?\n_consider_parameter(\n    ::AbstractCostAtMinParameter,\n    container::OptimizationContainer,\n    ::DeviceModel{T, D},\n) where {T, D} = has_container_key(container, OnVariable, T)\n\n# For slopes and breakpoints, the relevant variables won't have been created yet, so we'll\n# just check all components for the presence of the relevant time series\n_consider_parameter(\n    ::AbstractPiecewiseLinearSlopeParameter,\n    ::OptimizationContainer,\n    ::DeviceModel{T, D},\n) where {T, D} = true\n\n_consider_parameter(\n    ::AbstractPiecewiseLinearBreakpointParameter,\n    ::OptimizationContainer,\n    ::DeviceModel{T, D},\n) where {T, D} = true\n\n_has_market_bid_cost(device::PSY.StaticInjection) =\n    PSY.get_operation_cost(device) isa PSY.MarketBidCost\n\n_has_market_bid_cost(::PSY.RenewableNonDispatch) = false\n\n_has_market_bid_cost(::PSY.PowerLoad) = false # PowerLoads don't even have operation cost.\n_has_market_bid_cost(device::PSY.ControllableLoad) =\n    PSY.get_operation_cost(device) isa PSY.MarketBidCost\n\n_has_import_export_cost(device::PSY.Source) =\n    PSY.get_operation_cost(device) isa PSY.ImportExportCost\n\n_has_import_export_cost(::PSY.StaticInjection) = false\n\n_has_offer_curve_cost(device::PSY.Component) =\n    _has_market_bid_cost(device) || _has_import_export_cost(device)\n\n_has_parameter_time_series(::StartupCostParameter, device::PSY.StaticInjection) =\n    is_time_variant(PSY.get_start_up(PSY.get_operation_cost(device)))\n\n_has_parameter_time_series(::ShutdownCostParameter, device::PSY.StaticInjection) =\n    is_time_variant(PSY.get_shut_down(PSY.get_operation_cost(device)))\n\n_has_parameter_time_series(\n    ::T,\n    device::PSY.StaticInjection,\n) where {T <: AbstractCostAtMinParameter} =\n    _has_offer_curve_cost(device) &&\n    is_time_variant(_get_parameter_field(T(), PSY.get_operation_cost(device)))\n\n_has_parameter_time_series(\n    ::T,\n    device::PSY.StaticInjection,\n) where {T <: AbstractPiecewiseLinearSlopeParameter} =\n    _has_offer_curve_cost(device) &&\n    is_time_variant(_get_parameter_field(T(), PSY.get_operation_cost(device)))\n\n_has_parameter_time_series(\n    ::T,\n    device::PSY.StaticInjection,\n) where {T <: AbstractPiecewiseLinearBreakpointParameter} =\n    _has_offer_curve_cost(device) &&\n    is_time_variant(_get_parameter_field(T(), PSY.get_operation_cost(device)))\n\nfunction validate_initial_input_time_series(device::PSY.StaticInjection, decremental::Bool)\n    initial_input = get_initial_input_maybe_decremental(Val(decremental), device)\n    initial_is_ts = is_time_variant(initial_input)\n    variable_is_ts = is_time_variant(\n        get_offer_curves_maybe_decremental(Val(decremental), device))\n    label = decremental ? \"decremental\" : \"incremental\"\n\n    (initial_is_ts && !variable_is_ts) &&\n        @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\"\n    (variable_is_ts && !initial_is_ts) &&\n        throw(\n            ArgumentError(\n                \"In `MarketBidCost` for $(get_name(device)), if providing time series for `$(label)_offer_curves`, must also provide time series for `$(label)_initial_input`\",\n            ),\n        )\n\n    if !variable_is_ts && !initial_is_ts\n        _validate_eltype(\n            Union{Float64, Nothing}, device, initial_input, \" initial_input\",\n        )\n    else\n        _validate_eltype(\n            Float64, device, initial_input, \" initial_input\",\n        )\n    end\nend\n\nfunction validate_occ_breakpoints_slopes(device::PSY.StaticInjection, decremental::Bool)\n    offer_curves = get_offer_curves_maybe_decremental(Val(decremental), device)\n    device_name = get_name(device)\n    is_ts = is_time_variant(offer_curves)\n    expected_type = if is_ts\n        IS.PiecewiseStepData\n    else\n        PSY.CostCurve{PSY.PiecewiseIncrementalCurve}\n    end\n    p1 = nothing\n    apply_maybe_across_time_series(device, offer_curves) do x\n        curve_type = decremental ? \"decremental\" : \"incremental\"\n        _validate_eltype(expected_type, device, x, \" $curve_type offer curves\")\n        if decremental\n            PSY.is_concave(x) ||\n                throw(\n                    ArgumentError(\n                        \"Decremental $(nameof(typeof(PSY.get_operation_cost(device)))) for component $(device_name) is non-concave\",\n                    ),\n                )\n        else\n            PSY.is_convex(x) ||\n                throw(\n                    ArgumentError(\n                        \"Incremental $(nameof(typeof(PSY.get_operation_cost(device)))) for component $(device_name) is non-convex\",\n                    ),\n                )\n        end\n\n        # Different specific validations for MBC versus IEC\n        p1 = _validate_occ_subtype(\n            PSY.get_operation_cost(device),\n            decremental,\n            is_ts,\n            x,\n            device_name,\n            p1,\n        )\n    end\nend\n\nfunction _validate_occ_subtype(\n    ::PSY.MarketBidCost,\n    decremental,\n    is_ts,\n    curve::PSY.PiecewiseStepData,\n    device_name::String,\n    p1::Union{Nothing, Float64},\n)\n    @assert is_ts\n    my_p1 = first(PSY.get_x_coords(curve))\n    if isnothing(p1)\n        p1 = my_p1\n    elseif !isapprox(p1, my_p1)\n        throw(\n            ArgumentError(\n                \"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.\",\n            ),\n        )\n    end\n    return p1\nend\n\n_validate_occ_subtype(\n    ::PSY.MarketBidCost,\n    decremental,\n    is_ts,\n    ::PSY.CostCurve,\n    args...,\n) =\n    @assert !is_ts\n\nfunction _validate_occ_subtype(\n    cost::PSY.ImportExportCost,\n    decremental,\n    is_ts,\n    curve::PSY.CostCurve,\n    args...,\n)\n    # In the non-time-variable case, a VOM cost and initial input are represented; these must be zero\n    @assert !is_ts\n    !iszero(PSY.get_vom_cost(curve)) && throw(\n        ArgumentError(\n            \"For ImportExportCost, VOM cost must be zero.\",\n        ),\n    )\n    vc = PSY.get_value_curve(curve)\n    !iszero(PSY.get_initial_input(curve)) && throw(\n        ArgumentError(\n            \"For ImportExportCost, initial input must be zero.\",\n        ),\n    )\n    _validate_occ_subtype(cost, decremental, true, PSY.get_function_data(vc))  # also do the FunctionData validations\nend\n\nfunction _validate_occ_subtype(\n    ::PSY.ImportExportCost,\n    decremental,\n    is_ts,\n    curve::PSY.PiecewiseStepData,\n    args...,\n)\n    # In the time-variable case, VOM cost and initial input cannot be represented, so they cannot be nonzero\n    @assert is_ts\n    if !iszero(first(PSY.get_x_coords(curve)))\n        throw(\n            ArgumentError(\n                \"For ImportExportCost, the first breakpoint must be zero.\",\n            ),\n        )\n    end\nend\n\n# Warn if hot/warm/cold startup costs are given for non-`ThermalMultiStart`\nfunction validate_occ_component(\n    ::StartupCostParameter,\n    device::PSY.ThermalMultiStart,\n)\n    startup = PSY.get_start_up(PSY.get_operation_cost(device))\n    _validate_eltype(\n        Union{Float64, NTuple{3, Float64}, StartUpStages},\n        device,\n        startup,\n        \" startup cost\",\n    )\nend\n\nfunction validate_occ_component(::StartupCostParameter, device::PSY.StaticInjection)\n    startup = PSY.get_start_up(PSY.get_operation_cost(device))\n    contains_multistart = false\n    apply_maybe_across_time_series(device, startup) do x\n        if x isa Float64\n            return\n        elseif x isa Union{NTuple{3, Float64}, StartUpStages}\n            contains_multistart = true\n        else\n            location =\n                is_time_variant(startup) ? \" in time series $(get_name(startup))\" : \"\"\n            throw(\n                ArgumentError(\n                    \"Expected Float64 or NTuple{3, Float64} or StartUpStages startup cost but got $(typeof(x))$location for $(get_name(device))\",\n                ),\n            )\n        end\n    end\n    if contains_multistart\n        location = is_time_variant(startup) ? \" in time series $(get_name(startup))\" : \"\"\n        @warn \"Multi-start costs detected$location for non-multi-start unit $(get_name(device)), will take the maximum\"\n    end\n    return\nend\n\n# Validate eltype of shutdown costs\nfunction validate_occ_component(::ShutdownCostParameter, device::PSY.StaticInjection)\n    shutdown = PSY.get_shut_down(PSY.get_operation_cost(device))\n    _validate_eltype(Float64, device, shutdown, \" for shutdown cost\")\nend\n\n# Renewable-specific validations that warn when costs are nonzero.\n# There warnings are captured by the with_logger, though, so we don't actually see them.\nfunction validate_occ_component(\n    ::StartupCostParameter,\n    device::Union{PSY.RenewableDispatch, PSY.Storage},\n)\n    startup = PSY.get_start_up(PSY.get_operation_cost(device))\n    apply_maybe_across_time_series(device, startup) do x\n        if x != PSY.single_start_up_to_stages(0.0)\n            #println(\n            @warn \"Nonzero startup cost detected for renewable generation or storage device $(get_name(device)).\"\n            # )\n        end\n    end\nend\n\nfunction validate_occ_component(\n    ::ShutdownCostParameter,\n    device::Union{PSY.RenewableDispatch, PSY.Storage},\n)\n    shutdown = PSY.get_shut_down(PSY.get_operation_cost(device))\n    apply_maybe_across_time_series(device, shutdown) do x\n        if x != 0.0\n            #println(\n            @warn \"Nonzero shutdown cost detected for renewable generation or storage device $(get_name(device)).\"\n            #)\n        end\n    end\nend\n\nfunction validate_occ_component(\n    ::IncrementalCostAtMinParameter,\n    device::Union{PSY.RenewableDispatch, PSY.Storage},\n)\n    no_load_cost = PSY.get_no_load_cost(PSY.get_operation_cost(device))\n    if !isnothing(no_load_cost)\n        apply_maybe_across_time_series(device, no_load_cost) do x\n            if x != 0.0\n                #println(\n                @warn \"Nonzero no-load cost detected for renewable generation or storage device $(get_name(device)).\"\n                #)\n            end\n        end\n    end\nend\n\nfunction validate_occ_component(\n    ::DecrementalCostAtMinParameter,\n    device::PSY.Storage,\n)\n    no_load_cost = PSY.get_no_load_cost(PSY.get_operation_cost(device))\n    if !isnothing(no_load_cost)\n        apply_maybe_across_time_series(device, no_load_cost) do x\n            if x != 0.0\n                #println(\n                @warn \"Nonzero no-load cost detected for storage device $(get_name(device)).\"\n                #)\n            end\n        end\n    end\nend\n\n# Validate that initial input ts always appears if variable ts appears, warn if initial input ts appears without variable ts\nvalidate_occ_component(\n    ::IncrementalCostAtMinParameter,\n    device::PSY.StaticInjection,\n) =\n    validate_initial_input_time_series(device, false)\nvalidate_occ_component(\n    ::DecrementalCostAtMinParameter,\n    device::PSY.StaticInjection,\n) =\n    validate_initial_input_time_series(device, true)\n\n# Validate convexity/concavity of cost curves as appropriate, verify P1 = min gen power\nvalidate_occ_component(\n    ::IncrementalPiecewiseLinearBreakpointParameter,\n    device::PSY.StaticInjection,\n) =\n    validate_occ_breakpoints_slopes(device, false)\nvalidate_occ_component(\n    ::DecrementalPiecewiseLinearBreakpointParameter,\n    device::PSY.StaticInjection,\n) =\n    validate_occ_breakpoints_slopes(device, true)\n\n# Slope and breakpoint validations are done together, nothing to do here\nvalidate_occ_component(\n    ::AbstractPiecewiseLinearSlopeParameter,\n    device::PSY.StaticInjection,\n) = nothing\n\n# Validates and adds parameters for a given OfferCurveCost-related ParameterType\n# PERF: could switch to a TSC here.\nfunction _process_occ_parameters_helper(\n    ::P,\n    container::OptimizationContainer,\n    model,\n    devices,\n) where {P <: ParameterType}\n    param_instance = P()\n    for device in devices\n        validate_occ_component(param_instance, device)\n    end\n    if _consider_parameter(param_instance, container, model)\n        ts_devices =\n            filter(device -> _has_parameter_time_series(param_instance, device), devices)\n        (length(ts_devices) > 0) && add_parameters!(container, P, ts_devices, model)\n    end\nend\n\n\"Validate MarketBidCosts and add the appropriate parameters\"\nfunction process_market_bid_parameters!(\n    container::OptimizationContainer,\n    devices_in,\n    model::DeviceModel,\n    incremental::Bool = true,\n    decremental::Bool = false,\n)\n    devices = filter(_has_market_bid_cost, collect(devices_in))  # https://github.com/Sienna-Platform/InfrastructureSystems.jl/issues/460\n    isempty(devices) && return\n\n    # Validate and add the parameters:\n    for param in (\n        StartupCostParameter(),\n        ShutdownCostParameter(),\n    )\n        _process_occ_parameters_helper(param, container, model, devices)\n    end\n    if incremental\n        for param in (\n            IncrementalCostAtMinParameter(),\n            IncrementalPiecewiseLinearSlopeParameter(),\n            IncrementalPiecewiseLinearBreakpointParameter(),\n        )\n            _process_occ_parameters_helper(param, container, model, devices)\n        end\n    end\n    if decremental\n        for param in (\n            DecrementalCostAtMinParameter(),\n            DecrementalPiecewiseLinearSlopeParameter(),\n            DecrementalPiecewiseLinearBreakpointParameter(),\n        )\n            _process_occ_parameters_helper(param, container, model, devices)\n        end\n    end\nend\n\n##################################################\n################# PWL Variables ##################\n##################################################\n\n# For Market Bid\nfunction _add_pwl_variables!(\n    container::OptimizationContainer,\n    ::Type{T},\n    component_name::String,\n    time_period::Int,\n    n_tranches::Int,\n    ::Type{U},\n) where {\n    T <: PSY.Component,\n    U <: AbstractPiecewiseLinearBlockOffer,\n}\n    var_container = lazy_container_addition!(container, U(), T)\n    # length(PiecewiseStepData) gets number of segments, here we want number of points\n    pwlvars = Array{JuMP.VariableRef}(undef, n_tranches)\n    for i in 1:n_tranches\n        pwlvars[i] =\n            var_container[(component_name, i, time_period)] = JuMP.@variable(\n                get_jump_model(container),\n                base_name = \"$(nameof(U))_$(component_name)_{pwl_$(i), $time_period}\",\n                lower_bound = 0.0,\n            )\n    end\n    return pwlvars\nend\n\n##################################################\n################# PWL Constraints ################\n##################################################\n\n# without this, you get \"variable OnVariable__RenewableDispatch is not stored\"\n# TODO: really this falls along the divide of\n# commitment (OnVariable + ActivePower) vs dispatch (ActivePower only)\n_include_min_gen_power_in_constraint(\n    ::PSY.RenewableDispatch,\n    ::ActivePowerVariable,\n    ::AbstractDeviceFormulation,\n) = false\n_include_min_gen_power_in_constraint(\n    ::PSY.Generator,\n    ::ActivePowerVariable,\n    ::AbstractDeviceFormulation,\n) = true\n_include_min_gen_power_in_constraint(\n    ::PSY.ControllableLoad,\n    ::ActivePowerVariable,\n    ::PowerLoadInterruption,\n) = true\n_include_min_gen_power_in_constraint(\n    ::PSY.ControllableLoad,\n    ::ActivePowerVariable,\n    ::PowerLoadDispatch,\n) = false\n_include_min_gen_power_in_constraint(\n    ::Any,\n    ::PowerAboveMinimumVariable,\n    ::AbstractDeviceFormulation,\n) = false\n\n# add the minimum generation power to the PWL constraint, as a constant. Returns true for\n# formulations where there's nonzero minimum power (first breakpoint), but no OnVariable.\n# TODO: cleaner way? e.g. can we just do this whenever there's no OnVariable?\n_include_constant_min_gen_power_in_constraint(\n    ::PSY.ControllableLoad,\n    ::ActivePowerVariable,\n    ::PowerLoadDispatch,\n) = true\n_include_constant_min_gen_power_in_constraint(\n    ::PSY.ControllableLoad,\n    ::ActivePowerVariable,\n    ::PowerLoadInterruption,\n) = false\n_include_constant_min_gen_power_in_constraint(\n    ::PSY.RenewableGen,\n    ::ActivePowerVariable,\n    ::AbstractRenewableDispatchFormulation,\n) = true\n_include_constant_min_gen_power_in_constraint(\n    ::Any,\n    ::VariableType,\n    ::AbstractDeviceFormulation,\n) = false\n\n\"\"\"\nImplement the constraints for PWL Block Offer variables. That is:\n\n```math\n\\\\sum_{k\\\\in\\\\mathcal{K}} \\\\delta_{k,t} = p_t \\\\\\\\\n\\\\sum_{k\\\\in\\\\mathcal{K}} \\\\delta_{k,t} <= P_{k+1,t}^{max} - P_{k,t}^{max}\n```\n\"\"\"\nfunction _add_pwl_constraint!(\n    container::OptimizationContainer,\n    component::T,\n    ::U,\n    ::D,\n    break_points::Vector{<:JuMPOrFloat},\n    period::Int,\n    ::Type{V},\n    ::Type{W},\n) where {T <: PSY.Component, U <: VariableType,\n    D <: AbstractDeviceFormulation,\n    V <: AbstractPiecewiseLinearBlockOffer,\n    W <: AbstractPiecewiseLinearBlockOfferConstraint}\n    variables = get_variable(container, U(), T)\n    const_container = lazy_container_addition!(\n        container,\n        W(),\n        T,\n        axes(variables)...,\n    )\n    len_cost_data = length(break_points) - 1\n    jump_model = get_jump_model(container)\n    pwl_vars = get_variable(container, V(), T)\n    name = PSY.get_name(component)\n    sum_pwl_vars = sum(pwl_vars[name, ix, period] for ix in 1:len_cost_data)\n\n    # As detailed in https://github.com/Sienna-Platform/PowerSimulations.jl/issues/1318,\n    # time-variable P1 is problematic, so for now we require P1 to be constant. Thus we can\n    # just look up what it is currently fixed to and use that here without worrying about\n    # updating.\n    if _include_constant_min_gen_power_in_constraint(component, U(), D())\n        # TODO this seems kind of redundant with the\n        sum_pwl_vars += jump_fixed_value(first(break_points))::Float64\n    elseif _include_min_gen_power_in_constraint(component, U(), D())\n        on_vars = get_variable(container, OnVariable(), T)\n        p1::Float64 = jump_fixed_value(first(break_points))\n        sum_pwl_vars += p1 * on_vars[name, period]\n    end\n\n    const_container[name, period] = JuMP.@constraint(\n        jump_model,\n        variables[name, period] == sum_pwl_vars\n    )\n\n    for ix in 1:len_cost_data\n        JuMP.@constraint(\n            jump_model,\n            pwl_vars[name, ix, period] <= break_points[ix + 1] - break_points[ix]\n        )\n    end\n    return\nend\n\n\"\"\"\nImplement the constraints for PWL Block Offer variables for ORDC. That is:\n\n```math\n\\\\sum_{k\\\\in\\\\mathcal{K}} \\\\delta_{k,t} = p_t \\\\\\\\\n\\\\sum_{k\\\\in\\\\mathcal{K}} \\\\delta_{k,t} <= P_{k+1,t}^{max} - P_{k,t}^{max}\n```\n\"\"\"\nfunction _add_pwl_constraint!(\n    container::OptimizationContainer,\n    component::T,\n    ::U,\n    break_points::Vector{Float64},\n    sos_status::SOSStatusVariable,\n    period::Int,\n) where {T <: PSY.ReserveDemandCurve, U <: ServiceRequirementVariable}\n    name = PSY.get_name(component)\n    variables = get_variable(container, U(), T, name)\n    const_container = lazy_container_addition!(\n        container,\n        PiecewiseLinearBlockIncrementalOfferConstraint(),\n        T,\n        axes(variables)...;\n        meta = name,\n    )\n    len_cost_data = length(break_points) - 1\n    jump_model = get_jump_model(container)\n    pwl_vars = get_variable(container, PiecewiseLinearBlockIncrementalOffer(), T)\n    const_container[name, period] = JuMP.@constraint(\n        jump_model,\n        variables[name, period] ==\n        sum(pwl_vars[name, ix, period] for ix in 1:len_cost_data)\n    )\n\n    for ix in 1:len_cost_data\n        JuMP.@constraint(\n            jump_model,\n            pwl_vars[name, ix, period] <= break_points[ix + 1] - break_points[ix]\n        )\n    end\n    return\nend\n\n##################################################\n################ PWL Expressions #################\n##################################################\n\nget_offer_curves_for_var(var, comp::PSY.Component) =\n    get_offer_curves_for_var(var, get_operation_cost(comp))\nget_offer_curves_for_var(::PiecewiseLinearBlockIncrementalOffer, cost::PSY.MarketBidCost) =\n    get_offer_curves_maybe_decremental(Val(false), cost)\nget_offer_curves_for_var(::PiecewiseLinearBlockDecrementalOffer, cost::PSY.MarketBidCost) =\n    get_offer_curves_maybe_decremental(Val(true), cost)\n\nget_multiplier_for_var(::PiecewiseLinearBlockIncrementalOffer) = OBJECTIVE_FUNCTION_POSITIVE\nget_multiplier_for_var(::PiecewiseLinearBlockDecrementalOffer) = OBJECTIVE_FUNCTION_NEGATIVE\n\nfunction _get_pwl_cost_expression(\n    container::OptimizationContainer,\n    component::T,\n    time_period::Int,\n    slopes_normalized::Vector{Float64},\n    ::U,\n    ::V,\n    ::W,\n) where {\n    T <: PSY.Component,\n    U <: VariableType,\n    V <: AbstractDeviceFormulation,\n    W <: AbstractPiecewiseLinearBlockOffer,\n}\n    resolution = get_resolution(container)\n    dt = Dates.value(resolution) / MILLISECONDS_IN_HOUR\n    multiplier = get_multiplier_for_var(W()) * dt\n\n    name = PSY.get_name(component)\n    pwl_var_container = get_variable(container, W(), T)\n    gen_cost = JuMP.AffExpr(0.0)\n    for (i, cost) in enumerate(slopes_normalized)\n        JuMP.add_to_expression!(\n            gen_cost,\n            (cost * multiplier),\n            pwl_var_container[(name, i, time_period)],\n        )\n    end\n    return gen_cost\nend\n\n\"\"\"\nGet cost expression for StepwiseCostReserve\n\"\"\"\nfunction _get_pwl_cost_expression(\n    container::OptimizationContainer,\n    component::T,\n    time_period::Int,\n    slopes_normalized::Vector{Float64},\n    multiplier::Float64,\n) where {T <: PSY.ReserveDemandCurve}\n    name = PSY.get_name(component)\n    pwl_var_container = get_variable(container, PiecewiseLinearBlockIncrementalOffer(), T)\n    ordc_cost = JuMP.AffExpr(0.0)\n    for (i, slope) in enumerate(slopes_normalized)\n        JuMP.add_to_expression!(\n            ordc_cost,\n            slope * multiplier,\n            pwl_var_container[(name, i, time_period)],\n        )\n    end\n    return ordc_cost\nend\n\n###############################################\n######## MarketBidCost: Fixed Curves ##########\n###############################################\n\n# Serves a similar role as _lookup_maybe_time_variant_param, but needs extra logic\nfunction _get_pwl_data(\n    is_decremental::Bool,\n    container::OptimizationContainer,\n    component::T,\n    time::Int,\n) where {T <: PSY.Component}\n    cost_data = get_offer_curves_maybe_decremental(Val(is_decremental), component)\n\n    if is_time_variant(cost_data)\n        name = PSY.get_name(component)\n\n        SlopeParam = _SLOPE_PARAMS[is_decremental]\n        slope_param_arr = get_parameter_array(container, SlopeParam(), T)\n        slope_param_mult = get_parameter_multiplier_array(container, SlopeParam(), T)\n        @assert size(slope_param_arr) == size(slope_param_mult)  # multiplier arrays should be 3D too\n        slope_cost_component =\n            slope_param_arr[name, :, time] .* slope_param_mult[name, :, time]\n        slope_cost_component = slope_cost_component.data\n\n        BreakpointParam = _BREAKPOINT_PARAMS[is_decremental]\n        breakpoint_param_container = get_parameter(container, BreakpointParam(), T)\n        breakpoint_param_arr = get_parameter_column_refs(breakpoint_param_container, name)  # performs component -> time series many-to-one mapping\n        breakpoint_param_mult = get_multiplier_array(breakpoint_param_container)\n        @assert size(breakpoint_param_arr) == size(breakpoint_param_mult[name, :, :])\n        breakpoint_cost_component =\n            breakpoint_param_arr[:, time] .* breakpoint_param_mult[name, :, time]\n        breakpoint_cost_component = breakpoint_cost_component.data\n\n        @assert_op length(slope_cost_component) == length(breakpoint_cost_component) - 1\n        # PSY's cost_function_timeseries.jl says this will always be natural units\n        unit_system = PSY.UnitSystem.NATURAL_UNITS\n    else\n        cost_component = PSY.get_function_data(PSY.get_value_curve(cost_data))\n        breakpoint_cost_component = PSY.get_x_coords(cost_component)\n        slope_cost_component = PSY.get_y_coords(cost_component)\n        unit_system = PSY.get_power_units(cost_data)\n    end\n\n    breakpoints, slopes = get_piecewise_curve_per_system_unit(\n        breakpoint_cost_component,\n        slope_cost_component,\n        unit_system,\n        get_base_power(container),\n        PSY.get_base_power(component),\n    )\n\n    return breakpoints, slopes\nend\n\n\"\"\"\nAdd PWL cost terms for data coming from the MarketBidCost\nwith a fixed incremental offer curve\n\"\"\"\nfunction add_pwl_term!(\n    is_decremental::Bool,\n    container::OptimizationContainer,\n    component::T,\n    ::PSY.OfferCurveCost,\n    ::U,\n    ::V,\n) where {T <: PSY.Component, U <: VariableType, V <: AbstractDeviceFormulation}\n    name = PSY.get_name(component)\n    W = _PIECEWISE_BLOCK_VARS[is_decremental]\n    X = _PIECEWISE_BLOCK_CONSTRAINTS[is_decremental]\n\n    name = PSY.get_name(component)\n    time_steps = get_time_steps(container)\n    for t in time_steps\n        breakpoints, slopes = _get_pwl_data(is_decremental, container, component, t)\n        _add_pwl_variables!(container, T, name, t, length(slopes), W)\n        _add_pwl_constraint!(\n            container,\n            component,\n            U(),\n            V(),\n            breakpoints,\n            t,\n            W,\n            X,\n        )\n        pwl_cost = _get_pwl_cost_expression(\n            container,\n            component,\n            t,\n            slopes,\n            U(),\n            V(),\n            W(),\n        )\n\n        add_to_expression!(\n            container,\n            ProductionCostExpression,\n            pwl_cost,\n            component,\n            t,\n        )\n\n        if is_time_variant(\n            get_offer_curves_maybe_decremental(Val(is_decremental), component),\n        )\n            add_to_objective_variant_expression!(container, pwl_cost)\n        else\n            add_to_objective_invariant_expression!(container, pwl_cost)\n        end\n    end\nend\n\n##################################################\n########## PWL for StepwiseCostReserve  ##########\n##################################################\n\n# Not touching this in PR #1303, TODO figure it out later -GKS\nfunction _add_pwl_term!(\n    container::OptimizationContainer,\n    component::T,\n    cost_data::PSY.CostCurve{PSY.PiecewiseIncrementalCurve},\n    ::U,\n    ::V,\n) where {T <: PSY.Component, U <: VariableType, V <: AbstractServiceFormulation}\n    multiplier = objective_function_multiplier(U(), V())\n    resolution = get_resolution(container)\n    dt = Dates.value(Dates.Second(resolution)) / SECONDS_IN_HOUR\n    base_power = get_base_power(container)\n    value_curve = PSY.get_value_curve(cost_data)\n    power_units = PSY.get_power_units(cost_data)\n    cost_component = PSY.get_function_data(value_curve)\n    device_base_power = PSY.get_base_power(component)\n    data = get_piecewise_curve_per_system_unit(\n        cost_component,\n        power_units,\n        base_power,\n        device_base_power,\n    )\n    name = PSY.get_name(component)\n    time_steps = get_time_steps(container)\n    pwl_cost_expressions = Vector{JuMP.AffExpr}(undef, time_steps[end])\n    sos_val = _get_sos_value(container, V, component)\n    for t in time_steps\n        break_points = PSY.get_x_coords(data)\n        _add_pwl_variables!(\n            container,\n            T,\n            name,\n            t,\n            length(IS.get_y_coords(data)),\n            PiecewiseLinearBlockIncrementalOffer,\n        )\n        _add_pwl_constraint!(container, component, U(), break_points, sos_val, t)\n        pwl_cost = _get_pwl_cost_expression(\n            container,\n            component,\n            t,\n            IS.get_y_coords(data),\n            multiplier * dt,\n        )\n        pwl_cost_expressions[t] = pwl_cost\n    end\n    return pwl_cost_expressions\nend\n\n############################################################\n######## MarketBidCost: PiecewiseIncrementalCurve ##########\n############################################################\n\n\"\"\"\nCreates piecewise linear market bid function using a sum of variables and expression for market participants.\nDecremental offers are not accepted for most components, except Storage systems and loads.\n\n# Arguments\n\n  - container::OptimizationContainer : the optimization_container model built in PowerSimulations\n  - var_key::VariableKey: The variable name\n  - component_name::String: The component_name of the variable container\n  - cost_function::MarketBidCost : container for market bid cost\n\"\"\"\nfunction _add_variable_cost_to_objective!(\n    container::OptimizationContainer,\n    ::T,\n    component::PSY.Component,\n    cost_function::PSY.OfferCurveCost,\n    ::U,\n) where {T <: VariableType, U <: AbstractDeviceFormulation}\n    component_name = PSY.get_name(component)\n    @debug \"Market Bid\" _group = LOG_GROUP_COST_FUNCTIONS component_name\n    if !isnothing(get_input_offer_curves(cost_function))\n        error(\"Component $(component_name) is not allowed to participate as a demand.\")\n    end\n    add_pwl_term!(\n        false, # I suspect this is a problem. could very well be decremental, storage\n        container,\n        component,\n        cost_function,\n        T(),\n        U(),\n    )\n    return\nend\n\nfunction _add_variable_cost_to_objective!(\n    container::OptimizationContainer,\n    ::T,\n    component::PSY.Component,\n    cost_function::PSY.OfferCurveCost,\n    ::U,\n) where {T <: VariableType,\n    U <: AbstractControllablePowerLoadFormulation}\n    component_name = PSY.get_name(component)\n    @debug \"Market Bid\" _group = LOG_GROUP_COST_FUNCTIONS component_name\n    if !(isnothing(get_output_offer_curves(cost_function)))\n        error(\"Component $(component_name) is not allowed to participate as a supply.\")\n    end\n    add_pwl_term!(\n        true,\n        container,\n        component,\n        cost_function,\n        T(),\n        U(),\n    )\n    return\nend\n\nfunction _add_service_bid_cost!(\n    container::OptimizationContainer,\n    component::PSY.Component,\n    service::T,\n) where {T <: PSY.Reserve{<:PSY.ReserveDirection}}\n    time_steps = get_time_steps(container)\n    initial_time = get_initial_time(container)\n    base_power = get_base_power(container)\n    forecast_data = PSY.get_services_bid(\n        component,\n        PSY.get_operation_cost(component),\n        service;\n        start_time = initial_time,\n        len = length(time_steps),\n    )\n    forecast_data_values = PSY.get_cost.(TimeSeries.values(forecast_data))\n    # Single Price Bid\n    if eltype(forecast_data_values) == Float64\n        data_values = forecast_data_values\n        # Single Price/Quantity Bid\n    elseif eltype(forecast_data_values) == Vector{NTuple{2, Float64}}\n        data_values = [v[1][1] for v in forecast_data_values]\n    else\n        error(\"$(eltype(forecast_data_values)) not supported for MarketBidCost\")\n    end\n\n    reserve_variable =\n        get_variable(container, ActivePowerReserveVariable(), T, PSY.get_name(service))\n    component_name = PSY.get_name(component)\n    for t in time_steps\n        add_to_objective_invariant_expression!(\n            container,\n            data_values[t] * base_power * reserve_variable[component_name, t],\n        )\n    end\n    return\nend\n\nfunction _add_service_bid_cost!(::OptimizationContainer, ::PSY.Component, ::PSY.Service) end\n\n# \"copy-paste and change incremental to decremental\" here. Refactor?\nfunction _add_vom_cost_to_objective!(\n    container::OptimizationContainer,\n    ::T,\n    component::PSY.Component,\n    op_cost::PSY.OfferCurveCost,\n    ::U,\n) where {T <: VariableType, U <: AbstractDeviceFormulation}\n    incremental_cost_curves = get_output_offer_curves(op_cost)\n    if is_time_variant(incremental_cost_curves)\n        # TODO this might imply a change to the MBC struct?\n        @warn \"Incremental curves are time variant, there is no VOM cost source. Skipping VOM cost.\"\n        return\n    end\n    _add_vom_cost_to_objective_helper!(\n        container,\n        T(),\n        component,\n        op_cost,\n        incremental_cost_curves,\n        U(),\n    )\n    return\nend\n\nfunction _add_vom_cost_to_objective!(\n    container::OptimizationContainer,\n    ::T,\n    component::PSY.Component,\n    op_cost::PSY.OfferCurveCost,\n    ::U,\n) where {T <: VariableType,\n    U <: AbstractControllablePowerLoadFormulation}\n    decremental_cost_curves = get_input_offer_curves(op_cost)\n    if is_time_variant(decremental_cost_curves)\n        # TODO this might imply a change to the MBC struct?\n        @warn \"Decremental curves are time variant, there is no VOM cost source. Skipping VOM cost.\"\n        return\n    end\n    _add_vom_cost_to_objective_helper!(\n        container,\n        T(),\n        component,\n        op_cost,\n        decremental_cost_curves,\n        U(),\n    )\n    return\nend\n\nfunction _add_vom_cost_to_objective_helper!(\n    container::OptimizationContainer,\n    ::T,\n    component::PSY.Component,\n    ::PSY.OfferCurveCost,\n    cost_data::PSY.CostCurve{PSY.PiecewiseIncrementalCurve},\n    ::U,\n) where {T <: VariableType,\n    U <: AbstractDeviceFormulation}\n    power_units = PSY.get_power_units(cost_data)\n    vom_cost = PSY.get_vom_cost(cost_data)\n    multiplier = 1.0 # VOM Cost is always positive\n    cost_term = PSY.get_proportional_term(vom_cost)\n    iszero(cost_term) && return\n    base_power = get_base_power(container)\n    device_base_power = PSY.get_base_power(component)\n    cost_term_normalized = get_proportional_cost_per_system_unit(cost_term,\n        power_units,\n        base_power,\n        device_base_power)\n    resolution = get_resolution(container)\n    dt = Dates.value(resolution) / MILLISECONDS_IN_HOUR\n    for t in get_time_steps(container)\n        exp = _add_proportional_term!(\n            container, T(), component, cost_term_normalized * multiplier * dt, t)\n        add_to_expression!(container, VOMCostExpression, exp, component, t)\n    end\n    return\nend\n"
  },
  {
    "path": "src/devices_models/devices/common/objective_function/piecewise_linear.jl",
    "content": "##################################################\n################# SOS Methods ####################\n##################################################\n\nfunction _get_sos_value(\n    container::OptimizationContainer,\n    ::Type{V},\n    component::T,\n) where {T <: PSY.Component, V <: AbstractDeviceFormulation}\n    if has_container_key(container, OnStatusParameter, T)\n        sos_val = SOSStatusVariable.PARAMETER\n    else\n        sos_val = sos_status(component, V())\n    end\n    return sos_val\nend\n\nfunction _get_sos_value(\n    container::OptimizationContainer,\n    ::Type{V},\n    component::T,\n) where {T <: PSY.Component, V <: AbstractServiceFormulation}\n    return SOSStatusVariable.NO_VARIABLE\nend\n\n##################################################\n################# PWL Variables ##################\n##################################################\n\n# This cases bounds the data by 1 - 0\nfunction _add_pwl_variables!(\n    container::OptimizationContainer,\n    ::Type{T},\n    component_name::String,\n    time_period::Int,\n    cost_data::PSY.PiecewiseLinearData,\n) where {T <: PSY.Component}\n    var_container = lazy_container_addition!(container, PiecewiseLinearCostVariable(), T)\n    # length(PiecewiseStepData) gets number of segments, here we want number of points\n    pwlvars = Array{JuMP.VariableRef}(undef, length(cost_data) + 1)\n    for i in 1:(length(cost_data) + 1)\n        pwlvars[i] =\n            var_container[(component_name, i, time_period)] = JuMP.@variable(\n                get_jump_model(container),\n                base_name = \"PiecewiseLinearCostVariable_$(component_name)_{pwl_$(i), $time_period}\",\n                lower_bound = 0.0,\n                upper_bound = 1.0\n            )\n    end\n    return pwlvars\nend\n\n##################################################\n################# PWL Constraints ################\n##################################################\n\nfunction _determine_bin_lhs(\n    container::OptimizationContainer,\n    sos_status::SOSStatusVariable,\n    component::T,\n    period::Int) where {T <: PSY.Component}\n    name = PSY.get_name(component)\n    if sos_status == SOSStatusVariable.NO_VARIABLE\n        return 1.0\n        @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 =\n            LOG_GROUP_COST_FUNCTIONS\n\n    elseif sos_status == SOSStatusVariable.PARAMETER\n        param = get_default_on_parameter(component)\n        return get_parameter(container, param, T).parameter_array[name, period]\n        @debug \"Using Piecewise Linear cost function with parameter OnStatusParameter, $T\" _group =\n            LOG_GROUP_COST_FUNCTIONS\n    elseif sos_status == SOSStatusVariable.VARIABLE\n        var = get_default_on_variable(component)\n        return get_variable(container, var, T)[name, period]\n        @debug \"Using Piecewise Linear cost function with variable OnVariable $T\" _group =\n            LOG_GROUP_COST_FUNCTIONS\n    else\n        @assert false\n    end\nend\n\nfunction _get_bin_lhs(\n    container::OptimizationContainer,\n    sos_status::SOSStatusVariable,\n    component::T,\n    period::Int) where {T <: PSY.Component}\n    return _determine_bin_lhs(container, sos_status, component, period)\nend\n\nfunction _get_bin_lhs(\n    container::OptimizationContainer,\n    sos_status::SOSStatusVariable,\n    component::PSY.ThermalGen,\n    period::Int)\n    if PSY.get_must_run(component)\n        return 1.0\n    else\n        return _determine_bin_lhs(container, sos_status, component, period)\n    end\nend\n\n\"\"\"\nImplement the constraints for PWL variables. That is:\n\n```math\n\\\\sum_{k\\\\in\\\\mathcal{K}} P_k^{max} \\\\delta_{k,t} = p_t \\\\\\\\\n\\\\sum_{k\\\\in\\\\mathcal{K}} \\\\delta_{k,t} = on_t\n```\n\"\"\"\nfunction _add_pwl_constraint!(\n    container::OptimizationContainer,\n    component::T,\n    ::U,\n    break_points::Vector{Float64},\n    sos_status::SOSStatusVariable,\n    period::Int,\n) where {T <: PSY.Component, U <: VariableType}\n    variables = get_variable(container, U(), T)\n    const_container = lazy_container_addition!(\n        container,\n        PiecewiseLinearCostConstraint(),\n        T,\n        axes(variables)...,\n    )\n    len_cost_data = length(break_points)\n    jump_model = get_jump_model(container)\n    pwl_vars = get_variable(container, PiecewiseLinearCostVariable(), T)\n    name = PSY.get_name(component)\n    const_container[name, period] = JuMP.@constraint(\n        jump_model,\n        variables[name, period] ==\n        sum(pwl_vars[name, ix, period] * break_points[ix] for ix in 1:len_cost_data)\n    )\n    bin = _get_bin_lhs(container, sos_status, component, period)\n    const_normalization_container = lazy_container_addition!(\n        container,\n        PiecewiseLinearCostConstraint(),\n        T,\n        axes(variables)...;\n        meta = \"normalization\",\n    )\n\n    const_normalization_container[name, period] = JuMP.@constraint(\n        jump_model,\n        sum(pwl_vars[name, i, period] for i in 1:len_cost_data) == bin\n    )\n    return\nend\n\n\"\"\"\nImplement the constraints for PWL variables for Compact form. That is:\n\n```math\n\\\\sum_{k\\\\in\\\\mathcal{K}} P_k^{max} \\\\delta_{k,t} = p_t + P_min * u_t \\\\\\\\\n\\\\sum_{k\\\\in\\\\mathcal{K}} \\\\delta_{k,t} = on_t\n```\n\"\"\"\nfunction _add_pwl_constraint!(\n    container::OptimizationContainer,\n    component::T,\n    ::U,\n    break_points::Vector{Float64},\n    sos_status::SOSStatusVariable,\n    period::Int,\n) where {T <: PSY.Component, U <: PowerAboveMinimumVariable}\n    variables = get_variable(container, U(), T)\n    const_container = lazy_container_addition!(\n        container,\n        PiecewiseLinearCostConstraint(),\n        T,\n        axes(variables)...,\n    )\n    len_cost_data = length(break_points)\n    jump_model = get_jump_model(container)\n    pwl_vars = get_variable(container, PiecewiseLinearCostVariable(), T)\n    name = PSY.get_name(component)\n\n    if sos_status == SOSStatusVariable.NO_VARIABLE\n        bin = 1.0\n        @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 =\n            LOG_GROUP_COST_FUNCTIONS\n\n    elseif sos_status == SOSStatusVariable.PARAMETER\n        param = get_default_on_parameter(component)\n        bin = get_parameter(container, param, T).parameter_array[name, period]\n        @debug \"Using Piecewise Linear cost function with parameter OnStatusParameter, $T\" _group =\n            LOG_GROUP_COST_FUNCTIONS\n    elseif sos_status == SOSStatusVariable.VARIABLE\n        var = get_default_on_variable(component)\n        bin = get_variable(container, var, T)[name, period]\n        @debug \"Using Piecewise Linear cost function with variable OnVariable $T\" _group =\n            LOG_GROUP_COST_FUNCTIONS\n    else\n        @assert false\n    end\n    P_min = PSY.get_active_power_limits(component).min\n\n    const_container[name, period] = JuMP.@constraint(\n        jump_model,\n        bin * P_min + variables[name, period] ==\n        sum(pwl_vars[name, ix, period] * break_points[ix] for ix in 1:len_cost_data)\n    )\n\n    const_normalization_container = lazy_container_addition!(\n        container,\n        PiecewiseLinearCostConstraint(),\n        T,\n        axes(variables)...;\n        meta = \"normalization\",\n    )\n\n    const_normalization_container[name, period] = JuMP.@constraint(\n        jump_model,\n        sum(pwl_vars[name, i, period] for i in 1:len_cost_data) == bin\n    )\n    return\nend\n\n\"\"\"\nImplement the SOS for PWL variables. That is:\n\n```math\n\\\\{\\\\delta_{i,t}, ..., \\\\delta_{k,t}\\\\} \\\\in \\\\text{SOS}_2\n```\n\"\"\"\nfunction _add_pwl_sos_constraint!(\n    container::OptimizationContainer,\n    component::T,\n    ::U,\n    break_points::Vector{Float64},\n    sos_status::SOSStatusVariable,\n    period::Int,\n) where {T <: PSY.Component, U <: VariableType}\n    name = PSY.get_name(component)\n    @warn(\n        \"The cost function provided for $(name) is not compatible with a linear PWL cost function.\n  An SOS-2 formulation will be added to the model. This will result in additional binary variables.\"\n    )\n\n    jump_model = get_jump_model(container)\n    pwl_vars = get_variable(container, PiecewiseLinearCostVariable(), T)\n    bp_count = length(break_points)\n    pwl_vars_subset = [pwl_vars[name, i, period] for i in 1:bp_count]\n    JuMP.@constraint(jump_model, pwl_vars_subset in MOI.SOS2(collect(1:bp_count)))\n    return\nend\n\n##################################################\n################ PWL Expressions #################\n##################################################\n\nfunction _get_pwl_cost_expression(\n    container::OptimizationContainer,\n    component::T,\n    time_period::Int,\n    cost_data::PSY.PiecewiseLinearData,\n    multiplier::Float64,\n) where {T <: PSY.Component}\n    name = PSY.get_name(component)\n    pwl_var_container = get_variable(container, PiecewiseLinearCostVariable(), T)\n    gen_cost = JuMP.AffExpr(0.0)\n    y_coords_cost_data = PSY.get_y_coords(cost_data)\n    for (i, cost) in enumerate(y_coords_cost_data)\n        JuMP.add_to_expression!(\n            gen_cost,\n            (cost * multiplier),\n            pwl_var_container[(name, i, time_period)],\n        )\n    end\n    return gen_cost\nend\n\nfunction _get_pwl_cost_expression(\n    container::OptimizationContainer,\n    component::T,\n    time_period::Int,\n    cost_function::PSY.CostCurve{PSY.PiecewisePointCurve},\n    ::U,\n    ::V,\n) where {T <: PSY.Component, U <: VariableType, V <: AbstractDeviceFormulation}\n    value_curve = PSY.get_value_curve(cost_function)\n    power_units = PSY.get_power_units(cost_function)\n    cost_component = PSY.get_function_data(value_curve)\n    base_power = get_base_power(container)\n    device_base_power = PSY.get_base_power(component)\n    cost_data_normalized = get_piecewise_pointcurve_per_system_unit(\n        cost_component,\n        power_units,\n        base_power,\n        device_base_power,\n    )\n    multiplier = objective_function_multiplier(U(), V())\n    resolution = get_resolution(container)\n    dt = Dates.value(resolution) / MILLISECONDS_IN_HOUR\n    return _get_pwl_cost_expression(\n        container,\n        component,\n        time_period,\n        cost_data_normalized,\n        multiplier * dt,\n    )\nend\n\nfunction _get_pwl_cost_expression(\n    container::OptimizationContainer,\n    component::T,\n    time_period::Int,\n    cost_function::PSY.FuelCurve{PSY.PiecewisePointCurve},\n    ::U,\n    ::V,\n) where {T <: PSY.Component, U <: VariableType, V <: AbstractDeviceFormulation}\n    value_curve = PSY.get_value_curve(cost_function)\n    power_units = PSY.get_power_units(cost_function)\n    cost_component = PSY.get_function_data(value_curve)\n    base_power = get_base_power(container)\n    device_base_power = PSY.get_base_power(component)\n    cost_data_normalized = get_piecewise_pointcurve_per_system_unit(\n        cost_component,\n        power_units,\n        base_power,\n        device_base_power,\n    )\n    # Multiplier is not necessary here. There is no negative cost for fuel curves.\n    resolution = get_resolution(container)\n    dt = Dates.value(resolution) / MILLISECONDS_IN_HOUR\n    fuel_consumption_expression = _get_pwl_cost_expression(\n        container,\n        component,\n        time_period,\n        cost_data_normalized,\n        dt,\n    )\n    return fuel_consumption_expression\nend\n\n##################################################\n######## CostCurve: PiecewisePointCurve ##########\n##################################################\n\n\"\"\"\nAdd PWL cost terms for data coming from a PiecewisePointCurve\n\"\"\"\nfunction _add_pwl_term!(\n    container::OptimizationContainer,\n    component::T,\n    cost_function::Union{\n        PSY.CostCurve{PSY.PiecewisePointCurve},\n        PSY.FuelCurve{PSY.PiecewisePointCurve},\n    },\n    ::U,\n    ::V,\n) where {T <: PSY.Component, U <: VariableType, V <: AbstractDeviceFormulation}\n    # multiplier = objective_function_multiplier(U(), V())\n    name = PSY.get_name(component)\n    value_curve = PSY.get_value_curve(cost_function)\n    cost_component = PSY.get_function_data(value_curve)\n    base_power = get_base_power(container)\n    device_base_power = PSY.get_base_power(component)\n    power_units = PSY.get_power_units(cost_function)\n\n    # Normalize data\n    data = get_piecewise_pointcurve_per_system_unit(\n        cost_component,\n        power_units,\n        base_power,\n        device_base_power,\n    )\n\n    if all(iszero.((point -> point.y).(PSY.get_points(data))))  # TODO I think this should have been first. before?\n        @debug \"All cost terms for component $(name) are 0.0\" _group =\n            LOG_GROUP_COST_FUNCTIONS\n        return\n    end\n\n    # Compact PWL data does not exists anymore\n\n    cost_is_convex = PSY.is_convex(data)\n    break_points = PSY.get_x_coords(data)\n    time_steps = get_time_steps(container)\n    pwl_cost_expressions = Vector{JuMP.AffExpr}(undef, time_steps[end])\n    sos_val = _get_sos_value(container, V, component)\n    for t in time_steps\n        _add_pwl_variables!(container, T, name, t, data)\n        _add_pwl_constraint!(container, component, U(), break_points, sos_val, t)\n        if !cost_is_convex\n            _add_pwl_sos_constraint!(container, component, U(), break_points, sos_val, t)\n        end\n        pwl_cost =\n            _get_pwl_cost_expression(container, component, t, cost_function, U(), V())\n        pwl_cost_expressions[t] = pwl_cost\n    end\n    return pwl_cost_expressions\nend\n\n\"\"\"\nAdd PWL cost terms for data coming from a PiecewisePointCurve for ThermalDispatchNoMin formulation\n\"\"\"\nfunction _add_pwl_term!(\n    container::OptimizationContainer,\n    component::T,\n    cost_function::Union{\n        PSY.CostCurve{PSY.PiecewisePointCurve},\n        PSY.FuelCurve{PSY.PiecewisePointCurve},\n    },\n    ::U,\n    ::V,\n) where {T <: PSY.ThermalGen, U <: VariableType, V <: ThermalDispatchNoMin}\n    name = PSY.get_name(component)\n    value_curve = PSY.get_value_curve(cost_function)\n    cost_component = PSY.get_function_data(value_curve)\n    base_power = get_base_power(container)\n    device_base_power = PSY.get_base_power(component)\n    power_units = PSY.get_power_units(cost_function)\n\n    # Normalize data\n    data = get_piecewise_pointcurve_per_system_unit(\n        cost_component,\n        power_units,\n        base_power,\n        device_base_power,\n    )\n    @debug \"PWL cost function detected for device $(name) using $V\"\n    slopes = PSY.get_slopes(data)\n    if any(slopes .< 0) || !PSY.is_convex(data)\n        throw(\n            IS.InvalidValue(\n                \"The PWL cost data provided for generator $(name) is not compatible with $U.\",\n            ),\n        )\n    end\n\n    # Compact PWL data does not exists anymore\n    x_coords = PSY.get_x_coords(data)\n    if x_coords[1] != 0.0\n        y_coords = PSY.get_y_coords(data)\n        x_first = round(x_coords[1]; digits = 3)\n        y_first = round(y_coords[1]; digits = 3)\n        slope_first = round(slopes[1]; digits = 3)\n        guess_y_zero = y_coords[1] - slopes[1] * x_coords[1]\n        @warn(\n            \"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)\"\n        )\n        if guess_y_zero < 0.0\n            error(\n                \"Added zero intercept has negative cost for generator $(name). Consider using other formulation or improve data.\",\n            )\n        end\n        # 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)\n        intercept_point = (x = 0.0, y = guess_y_zero + COST_EPSILON)\n        data = PSY.PiecewiseLinearData(vcat(intercept_point, PSY.get_points(data)))\n        @assert PSY.is_convex(data)\n    end\n\n    time_steps = get_time_steps(container)\n    pwl_cost_expressions = Vector{JuMP.AffExpr}(undef, time_steps[end])\n    break_points = PSY.get_x_coords(data)\n    sos_val = _get_sos_value(container, V, component)\n    temp_cost_function =\n        create_temporary_cost_function_in_system_per_unit(cost_function, data)\n    for t in time_steps\n        _add_pwl_variables!(container, T, name, t, data)\n        _add_pwl_constraint!(container, component, U(), break_points, sos_val, t)\n        pwl_cost =\n            _get_pwl_cost_expression(container, component, t, temp_cost_function, U(), V())\n        pwl_cost_expressions[t] = pwl_cost\n    end\n    return pwl_cost_expressions\nend\n\n\"\"\"\nCreates piecewise linear cost function using a sum of variables and expression with sign and time step included.\n\n# Arguments\n\n  - container::OptimizationContainer : the optimization_container model built in PowerSimulations\n  - var_key::VariableKey: The variable name\n  - component_name::String: The component_name of the variable container\n  - cost_function::PSY.CostCurve{PSY.PiecewisePointCurve}: container for piecewise linear cost\n\"\"\"\nfunction _add_variable_cost_to_objective!(\n    container::OptimizationContainer,\n    ::T,\n    component::PSY.Component,\n    cost_function::PSY.CostCurve{PSY.PiecewisePointCurve},\n    ::U,\n) where {T <: VariableType, U <: AbstractDeviceFormulation}\n    component_name = PSY.get_name(component)\n    @debug \"PWL Variable Cost\" _group = LOG_GROUP_COST_FUNCTIONS component_name\n    # If array is full of tuples with zeros return 0.0\n    value_curve = PSY.get_value_curve(cost_function)\n    cost_component = PSY.get_function_data(value_curve)\n    if all(iszero.((point -> point.y).(PSY.get_points(cost_component))))  # TODO I think this should have been first. before?\n        @debug \"All cost terms for component $(component_name) are 0.0\" _group =\n            LOG_GROUP_COST_FUNCTIONS\n        return\n    end\n    pwl_cost_expressions =\n        _add_pwl_term!(container, component, cost_function, T(), U())\n    for t in get_time_steps(container)\n        add_to_expression!(\n            container,\n            FuelCostExpression,\n            pwl_cost_expressions[t],\n            component,\n            t,\n        )\n        add_to_objective_invariant_expression!(container, pwl_cost_expressions[t])\n    end\n    return\nend\n\n\"\"\"\nCreates piecewise linear cost function using a sum of variables and expression with sign and time step included.\n# Arguments\n  - container::OptimizationContainer : the optimization_container model built in PowerSimulations\n  - var_key::VariableKey: The variable name\n  - component_name::String: The component_name of the variable container\n  - cost_function::PSY.CostCurve{PSY.PiecewisePointCurve}: container for piecewise linear cost\n\"\"\"\nfunction _add_variable_cost_to_objective!(\n    container::OptimizationContainer,\n    ::T,\n    component::PSY.Component,\n    cost_function::PSY.FuelCurve{PSY.PiecewisePointCurve},\n    ::U,\n) where {T <: VariableType, U <: AbstractDeviceFormulation}\n    component_name = PSY.get_name(component)\n    @debug \"PWL Variable Cost\" _group = LOG_GROUP_COST_FUNCTIONS component_name\n    # If array is full of tuples with zeros return 0.0\n    value_curve = PSY.get_value_curve(cost_function)\n    cost_component = PSY.get_function_data(value_curve)\n    if all(iszero.((point -> point.y).(PSY.get_points(cost_component))))  # TODO I think this should have been first. before?\n        @debug \"All cost terms for component $(component_name) are 0.0\" _group =\n            LOG_GROUP_COST_FUNCTIONS\n        return\n    end\n    pwl_fuel_consumption_expressions =\n        _add_pwl_term!(container, component, cost_function, T(), U())\n\n    is_time_variant_ = is_time_variant(PSY.get_fuel_cost(cost_function))\n    for t in get_time_steps(container)\n        fuel_cost_value = get_fuel_cost_value(\n            container,\n            component,\n            t,\n            is_time_variant_,\n        )\n        pwl_cost_expression = pwl_fuel_consumption_expressions[t] * fuel_cost_value\n        add_to_expression!(\n            container,\n            FuelCostExpression,\n            pwl_cost_expression,\n            component,\n            t,\n        )\n        add_to_expression!(\n            container,\n            FuelConsumptionExpression,\n            pwl_fuel_consumption_expressions[t],\n            component,\n            t,\n        )\n        if is_time_variant_\n            add_to_objective_variant_expression!(container, pwl_cost_expression)\n        else\n            add_to_objective_invariant_expression!(container, pwl_cost_expression)\n        end\n    end\n    return\nend\n\n##################################################\n###### CostCurve: PiecewiseIncrementalCurve ######\n######### and PiecewiseAverageCurve ##############\n##################################################\n\n\"\"\"\nCreates piecewise linear cost function using a sum of variables and expression with sign and time step included.\n\n# Arguments\n\n  - container::OptimizationContainer : the optimization_container model built in PowerSimulations\n  - var_key::VariableKey: The variable name\n  - component_name::String: The component_name of the variable container\n  - cost_function::PSY.Union{PSY.CostCurve{PSY.PiecewiseIncrementalCurve}, PSY.CostCurve{PSY.PiecewiseAverageCurve}}: container for piecewise linear cost\n\"\"\"\nfunction _add_variable_cost_to_objective!(\n    container::OptimizationContainer,\n    ::T,\n    component::PSY.Component,\n    cost_function::V,\n    ::U,\n) where {\n    T <: VariableType,\n    V <: Union{\n        PSY.CostCurve{PSY.PiecewiseIncrementalCurve},\n        PSY.CostCurve{PSY.PiecewiseAverageCurve},\n    },\n    U <: AbstractDeviceFormulation,\n}\n    # Create new PiecewisePointCurve\n    value_curve = PSY.get_value_curve(cost_function)\n    power_units = PSY.get_power_units(cost_function)\n    pointbased_value_curve = PSY.InputOutputCurve(value_curve)\n    pointbased_cost_function =\n        PSY.CostCurve(; value_curve = pointbased_value_curve, power_units = power_units)\n    # Call method for PiecewisePointCurve\n    _add_variable_cost_to_objective!(\n        container,\n        T(),\n        component,\n        pointbased_cost_function,\n        U(),\n    )\n    return\nend\n\n##################################################\n###### FuelCurve: PiecewiseIncrementalCurve ######\n######### and PiecewiseAverageCurve ##############\n##################################################\n\n\"\"\"\nCreates piecewise linear fuel cost function using a sum of variables and expression with sign and time step included.\n\n# Arguments\n\n  - container::OptimizationContainer : the optimization_container model built in PowerSimulations\n  - var_key::VariableKey: The variable name\n  - component_name::String: The component_name of the variable container\n  - cost_function::PSY.Union{PSY.FuelCurve{PSY.PiecewiseIncrementalCurve}, PSY.FuelCurve{PSY.PiecewiseAverageCurve}}: container for piecewise linear cost\n\"\"\"\nfunction _add_variable_cost_to_objective!(\n    container::OptimizationContainer,\n    ::T,\n    component::PSY.Component,\n    cost_function::V,\n    ::U,\n) where {\n    T <: VariableType,\n    V <: Union{\n        PSY.FuelCurve{PSY.PiecewiseIncrementalCurve},\n        PSY.FuelCurve{PSY.PiecewiseAverageCurve},\n    },\n    U <: AbstractDeviceFormulation,\n}\n    # Create new PiecewisePointCurve\n    value_curve = PSY.get_value_curve(cost_function)\n    power_units = PSY.get_power_units(cost_function)\n    fuel_cost = PSY.get_fuel_cost(cost_function)\n    pointbased_value_curve = PSY.InputOutputCurve(value_curve)\n    pointbased_cost_function =\n        PSY.FuelCurve(;\n            value_curve = pointbased_value_curve,\n            power_units = power_units,\n            fuel_cost = fuel_cost,\n        )\n    # Call method for PiecewisePointCurve\n    _add_variable_cost_to_objective!(\n        container,\n        T(),\n        component,\n        pointbased_cost_function,\n        U(),\n    )\n    return\nend\n"
  },
  {
    "path": "src/devices_models/devices/common/objective_function/quadratic_curve.jl",
    "content": "# Add proportional terms to objective function and expression\nfunction _add_quadraticcurve_variable_term_to_model!(\n    container::OptimizationContainer,\n    ::T,\n    component::PSY.Component,\n    proportional_term_per_unit::Float64,\n    quadratic_term_per_unit::Float64,\n    time_period::Int,\n) where {T <: VariableType}\n    resolution = get_resolution(container)\n    dt = Dates.value(resolution) / MILLISECONDS_IN_HOUR\n    if quadratic_term_per_unit >= eps()\n        cost_term = _add_quadratic_term!(\n            container,\n            T(),\n            component,\n            (quadratic_term_per_unit, proportional_term_per_unit),\n            dt,\n            time_period,\n        )\n    else\n        cost_term = _add_proportional_term!(\n            container,\n            T(),\n            component,\n            proportional_term_per_unit * dt,\n            time_period,\n        )\n    end\n    add_to_expression!(\n        container,\n        FuelCostExpression,\n        cost_term,\n        component,\n        time_period,\n    )\n    return\nend\n\n# Dispatch for vector proportional/quadratic terms\nfunction _add_quadraticcurve_variable_cost!(\n    container::OptimizationContainer,\n    ::T,\n    ::U,\n    component::PSY.Component,\n    proportional_term_per_unit::Vector{Float64},\n    quadratic_term_per_unit::Vector{Float64},\n) where {T <: VariableType, U <: AbstractDeviceFormulation}\n    lb, ub = get_min_max_limits(component, ActivePowerVariableLimitsConstraint, U)\n    for t in get_time_steps(container)\n        _check_quadratic_monotonicity(\n            PSY.get_name(component),\n            quadratic_term_per_unit[t],\n            proportional_term_per_unit[t],\n            lb,\n            ub,\n        )\n        _add_quadraticcurve_variable_term_to_model!(\n            container,\n            T(),\n            component,\n            proportional_term_per_unit[t],\n            quadratic_term_per_unit[t],\n            t,\n        )\n    end\n    return\nend\n\n# Dispatch for scalar proportional/quadratic terms\nfunction _add_quadraticcurve_variable_cost!(\n    container::OptimizationContainer,\n    ::T,\n    ::U,\n    component::PSY.Component,\n    proportional_term_per_unit::Float64,\n    quadratic_term_per_unit::Float64,\n) where {T <: VariableType, U <: AbstractDeviceFormulation}\n    lb, ub = get_min_max_limits(component, ActivePowerVariableLimitsConstraint, U)\n    _check_quadratic_monotonicity(PSY.get_name(component),\n        quadratic_term_per_unit,\n        proportional_term_per_unit,\n        lb,\n        ub,\n    )\n    for t in get_time_steps(container)\n        _add_quadraticcurve_variable_term_to_model!(\n            container,\n            T(),\n            component,\n            proportional_term_per_unit,\n            quadratic_term_per_unit,\n            t,\n        )\n    end\n    return\nend\n\nfunction _check_quadratic_monotonicity(\n    name::String,\n    quad_term::Float64,\n    linear_term::Float64,\n    lb::Float64,\n    ub::Float64,\n)\n    fp_lb = 2 * quad_term * lb + linear_term\n    fp_ub = 2 * quad_term * ub + linear_term\n\n    if fp_lb < 0 || fp_ub < 0\n        @warn \"Cost function for component $name is not monotonically increasing in the range [$lb, $ub]. \\\n               This can lead to unexpected results\"\n    end\n    return\nend\n\n@doc raw\"\"\"\nAdds to the cost function cost terms for sum of variables with common factor to be used for cost expression for optimization_container model.\n\n# Equation\n\n``` gen_cost = dt*sign*(sum(variable.^2)*cost_data[1] + sum(variable)*cost_data[2]) ```\n\n# LaTeX\n\n`` cost = dt\\times sign (sum_{i\\in I} c_1 v_i^2 + sum_{i\\in I} c_2 v_i ) ``\n\nfor quadratic factor large enough. If the first term of the quadratic objective is 0.0, adds a\nlinear cost term `sum(variable)*cost_data[2]`\n\n# Arguments\n\n* container::OptimizationContainer : the optimization_container model built in PowerSimulations\n* var_key::VariableKey: The variable name\n* component_name::String: The component_name of the variable container\n* cost_component::PSY.CostCurve{PSY.QuadraticCurve} : container for quadratic factors\n\"\"\"\nfunction _add_variable_cost_to_objective!(\n    container::OptimizationContainer,\n    ::T,\n    component::PSY.Component,\n    cost_function::PSY.CostCurve{PSY.QuadraticCurve},\n    ::U,\n) where {T <: VariableType, U <: AbstractDeviceFormulation}\n    multiplier = objective_function_multiplier(T(), U())\n    base_power = get_base_power(container)\n    device_base_power = PSY.get_base_power(component)\n    value_curve = PSY.get_value_curve(cost_function)\n    power_units = PSY.get_power_units(cost_function)\n    cost_component = PSY.get_function_data(value_curve)\n    quadratic_term = PSY.get_quadratic_term(cost_component)\n    proportional_term = PSY.get_proportional_term(cost_component)\n    proportional_term_per_unit = get_proportional_cost_per_system_unit(\n        proportional_term,\n        power_units,\n        base_power,\n        device_base_power,\n    )\n    quadratic_term_per_unit = get_quadratic_cost_per_system_unit(\n        quadratic_term,\n        power_units,\n        base_power,\n        device_base_power,\n    )\n    _add_quadraticcurve_variable_cost!(\n        container,\n        T(),\n        U(),\n        component,\n        multiplier * proportional_term_per_unit,\n        multiplier * quadratic_term_per_unit,\n    )\n    return\nend\n\nfunction _add_variable_cost_to_objective!(\n    ::OptimizationContainer,\n    ::T,\n    component::PSY.Component,\n    cost_function::PSY.CostCurve{PSY.QuadraticCurve},\n    ::U,\n) where {\n    T <: PowerAboveMinimumVariable,\n    U <: Union{AbstractCompactUnitCommitment, ThermalCompactDispatch},\n}\n    throw(\n        IS.ConflictingInputsError(\n            \"Quadratic Cost Curves are not compatible with Compact formulations\",\n        ),\n    )\n    return\nend\n\nfunction _add_fuel_quadratic_variable_cost!(\n    container::OptimizationContainer,\n    ::T,\n    ::U,\n    component::PSY.Component,\n    proportional_fuel_curve::Float64,\n    quadratic_fuel_curve::Float64,\n    fuel_cost::Float64,\n) where {T <: VariableType, U <: AbstractDeviceFormulation}\n    _add_quadraticcurve_variable_cost!(\n        container,\n        T(),\n        U(),\n        component,\n        proportional_fuel_curve * fuel_cost,\n        quadratic_fuel_curve * fuel_cost,\n    )\nend\n\nfunction _add_fuel_quadratic_variable_cost!(\n    container::OptimizationContainer,\n    ::T,\n    ::AbstractDeviceFormulation,\n    component::PSY.Component,\n    proportional_fuel_curve::Float64,\n    quadratic_fuel_curve::Float64,\n    fuel_cost::IS.TimeSeriesKey,\n) where {T <: VariableType}\n    _add_time_varying_fuel_variable_cost!(container, T(), component, fuel_cost)\nend\n\n@doc raw\"\"\"\nAdds to the cost function cost terms for sum of variables with common factor to be used for cost expression for optimization_container model.\n\n# Equation\n\n``` gen_cost = dt*(sum(variable.^2)*cost_data[1]*fuel_cost + sum(variable)*cost_data[2]*fuel_cost) ```\n\n# LaTeX\n\n`` cost = dt\\times  (sum_{i\\in I} c_f c_1 v_i^2 + sum_{i\\in I} c_f c_2 v_i ) ``\n\nfor quadratic factor large enough. If the first term of the quadratic objective is 0.0, adds a\nlinear cost term `sum(variable)*cost_data[2]`\n\n# Arguments\n\n* container::OptimizationContainer : the optimization_container model built in PowerSimulations\n* var_key::VariableKey: The variable name\n* component_name::String: The component_name of the variable container\n* cost_component::PSY.FuelCurve{PSY.QuadraticCurve} : container for quadratic factors\n\"\"\"\nfunction _add_variable_cost_to_objective!(\n    container::OptimizationContainer,\n    ::T,\n    component::PSY.Component,\n    cost_function::PSY.FuelCurve{PSY.QuadraticCurve},\n    ::U,\n) where {T <: VariableType, U <: AbstractDeviceFormulation}\n    multiplier = objective_function_multiplier(T(), U())\n    base_power = get_base_power(container)\n    device_base_power = PSY.get_base_power(component)\n    value_curve = PSY.get_value_curve(cost_function)\n    power_units = PSY.get_power_units(cost_function)\n    cost_component = PSY.get_function_data(value_curve)\n    quadratic_term = PSY.get_quadratic_term(cost_component)\n    proportional_term = PSY.get_proportional_term(cost_component)\n    proportional_term_per_unit = get_proportional_cost_per_system_unit(\n        proportional_term,\n        power_units,\n        base_power,\n        device_base_power,\n    )\n    quadratic_term_per_unit = get_quadratic_cost_per_system_unit(\n        quadratic_term,\n        power_units,\n        base_power,\n        device_base_power,\n    )\n    fuel_cost = PSY.get_fuel_cost(cost_function)\n    # Multiplier is not necessary here. There is no negative cost for fuel curves.\n    _add_fuel_quadratic_variable_cost!(\n        container,\n        T(),\n        U(),\n        component,\n        multiplier * proportional_term_per_unit,\n        multiplier * quadratic_term_per_unit,\n        fuel_cost,\n    )\n    return\nend\n"
  },
  {
    "path": "src/devices_models/devices/common/range_constraint.jl",
    "content": "######## CONSTRAINTS ############\n\n_add_lb(::RangeConstraintLBExpressions) = true\n_add_ub(::RangeConstraintLBExpressions) = false\n\n_add_lb(::RangeConstraintUBExpressions) = false\n_add_ub(::RangeConstraintUBExpressions) = true\n\n_add_lb(::ExpressionType) = true\n_add_ub(::ExpressionType) = false\n\n# Generic fallback functions\nfunction get_startup_shutdown(\n    device,\n    ::Type{<:VariableType},\n    ::Type{<:AbstractDeviceFormulation},\n) #  -> Union{Nothing, NamedTuple{(:startup, :shutdown), Tuple{Float64, Float64}}}\n    nothing\nend\n\n@doc raw\"\"\"\nConstructs min/max range constraint from device variable.\n\n\nIf min and max within an epsilon width:\n\n``` variable[name, t] == limits.max ```\n\nOtherwise:\n\n``` limits.min <= variable[name, t] <= limits.max ```\n\nwhere limits in constraint_infos.\n\n# LaTeX\n\n`` x = limits^{max}, \\text{ for } |limits^{max} - limits^{min}| < \\varepsilon ``\n\n`` limits^{min} \\leq x \\leq limits^{max}, \\text{ otherwise } ``\n\"\"\"\nfunction add_range_constraints!(\n    container::OptimizationContainer,\n    ::Type{T},\n    ::Type{U},\n    devices::IS.FlattenIteratorWrapper{V},\n    model::DeviceModel{V, W},\n    ::Type{X},\n) where {\n    T <: ConstraintType,\n    U <: VariableType,\n    V <: PSY.Component,\n    W <: AbstractDeviceFormulation,\n    X <: PM.AbstractPowerModel,\n}\n    array = get_variable(container, U(), V)\n    _add_lower_bound_range_constraints_impl!(container, T, array, devices, model)\n    _add_upper_bound_range_constraints_impl!(container, T, array, devices, model)\n    return\nend\n\nfunction add_range_constraints!(\n    container::OptimizationContainer,\n    ::Type{T},\n    ::Type{U},\n    devices::IS.FlattenIteratorWrapper{V},\n    model::DeviceModel{V, W},\n    ::Type{X},\n) where {\n    T <: ConstraintType,\n    U <: RangeConstraintLBExpressions,\n    V <: PSY.Component,\n    W <: AbstractDeviceFormulation,\n    X <: PM.AbstractPowerModel,\n}\n    array = get_expression(container, U(), V)\n    _add_lower_bound_range_constraints_impl!(container, T, array, devices, model)\n    return\nend\n\nfunction add_range_constraints!(\n    container::OptimizationContainer,\n    ::Type{T},\n    ::Type{U},\n    devices::IS.FlattenIteratorWrapper{V},\n    model::DeviceModel{V, W},\n    ::Type{X},\n) where {\n    T <: ConstraintType,\n    U <: RangeConstraintUBExpressions,\n    V <: PSY.Component,\n    W <: AbstractDeviceFormulation,\n    X <: PM.AbstractPowerModel,\n}\n    array = get_expression(container, U(), V)\n    _add_upper_bound_range_constraints_impl!(container, T, array, devices, model)\n    return\nend\n\nfunction _add_lower_bound_range_constraints_impl!(\n    container::OptimizationContainer,\n    ::Type{T},\n    array,\n    devices::IS.FlattenIteratorWrapper{V},\n    model::DeviceModel{V, W},\n) where {T <: ConstraintType, V <: PSY.Component, W <: AbstractDeviceFormulation}\n    time_steps = get_time_steps(container)\n    device_names = PSY.get_name.(devices)\n\n    con_lb =\n        add_constraints_container!(container, T(), V, device_names, time_steps; meta = \"lb\")\n\n    for device in devices, t in time_steps\n        ci_name = PSY.get_name(device)\n        limits = get_min_max_limits(device, T, W) # depends on constraint type and formulation type\n        con_lb[ci_name, t] =\n            JuMP.@constraint(get_jump_model(container), array[ci_name, t] >= limits.min)\n    end\n    return\nend\n\nfunction _add_upper_bound_range_constraints_impl!(\n    container::OptimizationContainer,\n    ::Type{T},\n    array,\n    devices::IS.FlattenIteratorWrapper{V},\n    model::DeviceModel{V, W},\n) where {T <: ConstraintType, V <: PSY.Component, W <: AbstractDeviceFormulation}\n    time_steps = get_time_steps(container)\n    device_names = PSY.get_name.(devices)\n\n    con_ub =\n        add_constraints_container!(container, T(), V, device_names, time_steps; meta = \"ub\")\n\n    for device in devices, t in time_steps\n        ci_name = PSY.get_name(device)\n        limits = get_min_max_limits(device, T, W) # depends on constraint type and formulation type\n        con_ub[ci_name, t] =\n            JuMP.@constraint(get_jump_model(container), array[ci_name, t] <= limits.max)\n    end\n    return\nend\n\n@doc raw\"\"\"\nConstructs min/max range constraint from device variable and on/off decision variable.\n\n\nIf device min = 0:\n\n``` varcts[name, t] <= limits.max*varbin[name, t]) ```\n\n``` varcts[name, t] >= 0.0 ```\n\nOtherwise:\n\n``` varcts[name, t] <= limits.max*varbin[name, t] ```\n\n``` varcts[name, t] >= limits.min*varbin[name, t] ```\n\nwhere limits in constraint_infos.\n\n# LaTeX\n\n`` 0 \\leq x^{cts} \\leq limits^{max} x^{bin}, \\text{ for } limits^{min} = 0 ``\n\n`` limits^{min} x^{bin} \\leq x^{cts} \\leq limits^{max} x^{bin}, \\text{ otherwise } ``\n\"\"\"\nfunction add_semicontinuous_range_constraints!(\n    container::OptimizationContainer,\n    ::Type{T},\n    ::Type{U},\n    devices::IS.FlattenIteratorWrapper{V},\n    model::DeviceModel{V, W},\n    ::Type{X},\n) where {\n    T <: ConstraintType,\n    U <: VariableType,\n    V <: PSY.Component,\n    W <: AbstractDeviceFormulation,\n    X <: PM.AbstractPowerModel,\n}\n    array = get_variable(container, U(), V)\n    _add_semicontinuous_lower_bound_range_constraints_impl!(\n        container,\n        T,\n        array,\n        devices,\n        model,\n    )\n    _add_semicontinuous_upper_bound_range_constraints_impl!(\n        container,\n        T,\n        array,\n        devices,\n        model,\n    )\n    return\nend\n\nfunction add_semicontinuous_range_constraints!(\n    container::OptimizationContainer,\n    ::Type{T},\n    ::Type{U},\n    devices::IS.FlattenIteratorWrapper{V},\n    model::DeviceModel{V, W},\n    ::Type{X},\n) where {\n    T <: ConstraintType,\n    U <: RangeConstraintLBExpressions,\n    V <: PSY.Component,\n    W <: AbstractDeviceFormulation,\n    X <: PM.AbstractPowerModel,\n}\n    array = get_expression(container, U(), V)\n    _add_semicontinuous_lower_bound_range_constraints_impl!(\n        container,\n        T,\n        array,\n        devices,\n        model,\n    )\n    return\nend\n\nfunction add_semicontinuous_range_constraints!(\n    container::OptimizationContainer,\n    ::Type{T},\n    ::Type{U},\n    devices::IS.FlattenIteratorWrapper{V},\n    model::DeviceModel{V, W},\n    ::Type{X},\n) where {\n    T <: ConstraintType,\n    U <: RangeConstraintUBExpressions,\n    V <: PSY.Component,\n    W <: AbstractDeviceFormulation,\n    X <: PM.AbstractPowerModel,\n}\n    array = get_expression(container, U(), V)\n    _add_semicontinuous_upper_bound_range_constraints_impl!(\n        container,\n        T,\n        array,\n        devices,\n        model,\n    )\n    return\nend\n\nfunction _add_semicontinuous_lower_bound_range_constraints_impl!(\n    container::OptimizationContainer,\n    ::Type{T},\n    array,\n    devices::IS.FlattenIteratorWrapper{V},\n    ::DeviceModel{V, W},\n) where {T <: ConstraintType, V <: PSY.Component, W <: AbstractDeviceFormulation}\n    time_steps = get_time_steps(container)\n    names = PSY.get_name.(devices)\n    con_lb = add_constraints_container!(container, T(), V, names, time_steps; meta = \"lb\")\n    varbin = get_variable(container, OnVariable(), V)\n\n    for device in devices\n        ci_name = PSY.get_name(device)\n        limits = get_min_max_limits(device, T, W) # depends on constraint type and formulation type\n        for t in time_steps\n            con_lb[ci_name, t] = JuMP.@constraint(\n                get_jump_model(container),\n                array[ci_name, t] >= limits.min * varbin[ci_name, t]\n            )\n        end\n    end\n    return\nend\n\nfunction _add_semicontinuous_lower_bound_range_constraints_impl!(\n    container::OptimizationContainer,\n    ::Type{T},\n    array,\n    devices::IS.FlattenIteratorWrapper{V},\n    ::DeviceModel{V, W},\n) where {T <: ConstraintType, V <: PSY.ThermalGen, W <: AbstractDeviceFormulation}\n    time_steps = get_time_steps(container)\n    names = PSY.get_name.(devices)\n    con_lb = add_constraints_container!(container, T(), V, names, time_steps; meta = \"lb\")\n    varbin = get_variable(container, OnVariable(), V)\n\n    for device in devices\n        ci_name = PSY.get_name(device)\n        limits = get_min_max_limits(device, T, W) # depends on constraint type and formulation type\n        if PSY.get_must_run(device)\n            for t in time_steps\n                con_lb[ci_name, t] = JuMP.@constraint(\n                    get_jump_model(container),\n                    array[ci_name, t] >= limits.min\n                )\n            end\n        else\n            for t in time_steps\n                con_lb[ci_name, t] = JuMP.@constraint(\n                    get_jump_model(container),\n                    array[ci_name, t] >= limits.min * varbin[ci_name, t]\n                )\n            end\n        end\n    end\n    return\nend\n\nfunction _add_semicontinuous_upper_bound_range_constraints_impl!(\n    container::OptimizationContainer,\n    ::Type{T},\n    array,\n    devices::IS.FlattenIteratorWrapper{V},\n    model::DeviceModel{V, W},\n) where {T <: ConstraintType, V <: PSY.ThermalGen, W <: AbstractDeviceFormulation}\n    time_steps = get_time_steps(container)\n    names = PSY.get_name.(devices)\n    con_ub = add_constraints_container!(container, T(), V, names, time_steps; meta = \"ub\")\n    varbin = get_variable(container, OnVariable(), V)\n\n    for device in devices\n        ci_name = PSY.get_name(device)\n        limits = get_min_max_limits(device, T, W) # depends on constraint type and formulation type\n        if PSY.get_must_run(device)\n            for t in time_steps\n                con_ub[ci_name, t] = JuMP.@constraint(\n                    get_jump_model(container),\n                    array[ci_name, t] <= limits.max\n                )\n            end\n        else\n            for t in time_steps\n                con_ub[ci_name, t] = JuMP.@constraint(\n                    get_jump_model(container),\n                    array[ci_name, t] <= limits.max * varbin[ci_name, t]\n                )\n            end\n        end\n    end\n    return\nend\n\nfunction _add_semicontinuous_upper_bound_range_constraints_impl!(\n    container::OptimizationContainer,\n    ::Type{T},\n    array,\n    devices::IS.FlattenIteratorWrapper{V},\n    model::DeviceModel{V, W},\n) where {T <: ConstraintType, V <: PSY.Component, W <: AbstractDeviceFormulation}\n    time_steps = get_time_steps(container)\n    names = PSY.get_name.(devices)\n    con_ub = add_constraints_container!(container, T(), V, names, time_steps; meta = \"ub\")\n    varbin = get_variable(container, OnVariable(), V)\n\n    for device in devices, t in time_steps\n        ci_name = PSY.get_name(device)\n        limits = get_min_max_limits(device, T, W) # depends on constraint type and formulation type\n        con_ub[ci_name, t] = JuMP.@constraint(\n            get_jump_model(container),\n            array[ci_name, t] <= limits.max * varbin[ci_name, t]\n        )\n    end\n    return\nend\n\n@doc raw\"\"\"\nConstructs min/max range constraint from device variable and reservation decision variable.\n\n\n\n``` varcts[name, t] <= limits.max * (1 - varbin[name, t]) ```\n\n``` varcts[name, t] >= limits.min * (1 - varbin[name, t]) ```\n\nwhere limits in constraint_infos.\n\n# LaTeX\n\n`` 0 \\leq x^{cts} \\leq limits^{max} (1 - x^{bin}), \\text{ for } limits^{min} = 0 ``\n\n`` limits^{min} (1 - x^{bin}) \\leq x^{cts} \\leq limits^{max} (1 - x^{bin}), \\text{ otherwise } ``\n\"\"\"\nfunction add_reserve_range_constraints!(\n    container::OptimizationContainer,\n    ::Type{T},\n    ::Type{U},\n    devices::IS.FlattenIteratorWrapper{V},\n    model::DeviceModel{V, W},\n    ::Type{X},\n) where {\n    T <: InputActivePowerVariableLimitsConstraint,\n    U <: VariableType,\n    V <: PSY.Component,\n    W <: AbstractDeviceFormulation,\n    X <: PM.AbstractPowerModel,\n}\n    array = get_variable(container, U(), V)\n    _add_reserve_upper_bound_range_constraints!(container, T, array, devices, model)\n    _add_reserve_lower_bound_range_constraints!(container, T, array, devices, model)\n    return\nend\n\nfunction add_reserve_range_constraints!(\n    container::OptimizationContainer,\n    ::Type{T},\n    ::Type{U},\n    devices::IS.FlattenIteratorWrapper{V},\n    model::DeviceModel{V, W},\n    ::Type{X},\n) where {\n    T <: InputActivePowerVariableLimitsConstraint,\n    U <: ExpressionType,\n    V <: PSY.Component,\n    W <: AbstractDeviceFormulation,\n    X <: PM.AbstractPowerModel,\n}\n    array = get_expression(container, U(), W)\n    _add_ub(U()) &&\n        _add_reserve_upper_bound_range_constraints!(container, T, array, devices, model)\n    _add_lb(U()) &&\n        _add_reserve_lower_bound_range_constraints!(container, T, array, devices, model)\n    return\nend\n\nfunction _add_reserve_lower_bound_range_constraints!(\n    container::OptimizationContainer,\n    ::Type{T},\n    array,\n    devices::IS.FlattenIteratorWrapper{V},\n    ::DeviceModel{V, W},\n) where {\n    T <: InputActivePowerVariableLimitsConstraint,\n    V <: PSY.Component,\n    W <: AbstractDeviceFormulation,\n}\n    time_steps = get_time_steps(container)\n    names = PSY.get_name.(devices)\n    binary_variables = [ReservationVariable()]\n\n    IS.@assert_op length(binary_variables) == 1\n    varbin = get_variable(container, only(binary_variables), V)\n\n    names = [PSY.get_name(x) for x in devices]\n    # MOI has a semicontinous set, but after some tests is not clear most MILP solvers support it.\n    # In the future this can be updated\n    con_lb = add_constraints_container!(container, T(), V, names, time_steps; meta = \"lb\")\n\n    for device in devices, t in time_steps\n        ci_name = PSY.get_name(device)\n        limits = get_min_max_limits(device, T, W)\n        con_lb[ci_name, t] = JuMP.@constraint(\n            get_jump_model(container),\n            array[ci_name, t] >= limits.min * (1 - varbin[ci_name, t])\n        )\n    end\n    return\nend\n\nfunction _add_reserve_upper_bound_range_constraints!(\n    container::OptimizationContainer,\n    ::Type{T},\n    array,\n    devices::IS.FlattenIteratorWrapper{V},\n    model::DeviceModel{V, W},\n) where {\n    T <: InputActivePowerVariableLimitsConstraint,\n    V <: PSY.Component,\n    W <: AbstractDeviceFormulation,\n}\n    time_steps = get_time_steps(container)\n    names = PSY.get_name.(devices)\n    binary_variables = [ReservationVariable()]\n\n    IS.@assert_op length(binary_variables) == 1\n    varbin = get_variable(container, only(binary_variables), V)\n\n    names = [PSY.get_name(x) for x in devices]\n    # MOI has a semicontinous set, but after some tests is not clear most MILP solvers support it.\n    # In the future this can be updated\n    con_ub = add_constraints_container!(container, T(), V, names, time_steps; meta = \"ub\")\n\n    for device in devices, t in time_steps\n        ci_name = PSY.get_name(device)\n        limits = get_min_max_limits(device, T, W)\n        con_ub[ci_name, t] = JuMP.@constraint(\n            get_jump_model(container),\n            array[ci_name, t] <= limits.max * (1 - varbin[ci_name, t])\n        )\n    end\n    return\nend\n\n@doc raw\"\"\"\nConstructs min/max range constraint from device variable and reservation decision variable.\n\n\n\n``` varcts[name, t] <= limits.max * varbin[name, t] ```\n\n``` varcts[name, t] >= limits.min * varbin[name, t] ```\n\nwhere limits in constraint_infos.\n\n# LaTeX\n\n`` limits^{min} x^{bin} \\leq x^{cts} \\leq limits^{max} x^{bin},``\n\"\"\"\nfunction add_reserve_range_constraints!(\n    container::OptimizationContainer,\n    ::Type{T},\n    ::Type{U},\n    devices::IS.FlattenIteratorWrapper{W},\n    model::DeviceModel{W, X},\n    ::Type{Y},\n) where {\n    T <:\n    Union{\n        ReactivePowerVariableLimitsConstraint,\n        ActivePowerVariableLimitsConstraint,\n        OutputActivePowerVariableLimitsConstraint,\n    },\n    U <: VariableType,\n    W <: PSY.Component,\n    X <: AbstractDeviceFormulation,\n    Y <: PM.AbstractPowerModel,\n}\n    array = get_variable(container, U(), W)\n    _add_reserve_upper_bound_range_constraints!(container, T, array, devices, model)\n    _add_reserve_lower_bound_range_constraints!(container, T, array, devices, model)\n    return\nend\n\n@doc raw\"\"\"\nConstructs min/max range constraint from device variable and reservation decision variable.\n\n\n\n``` varcts[name, t] <= limits.max * varbin[name, t] ```\n\n``` varcts[name, t] >= limits.min * varbin[name, t] ```\n\nwhere limits in constraint_infos.\n\n# LaTeX\n\n`` limits^{min} x^{bin} \\leq x^{cts} \\leq limits^{max} x^{bin},``\n\"\"\"\nfunction add_reserve_range_constraints!(\n    container::OptimizationContainer,\n    ::Type{T},\n    ::Type{U},\n    devices::IS.FlattenIteratorWrapper{W},\n    model::DeviceModel{W, X},\n    ::Type{Y},\n) where {\n    T <:\n    Union{\n        ReactivePowerVariableLimitsConstraint,\n        ActivePowerVariableLimitsConstraint,\n        OutputActivePowerVariableLimitsConstraint,\n    },\n    U <: ExpressionType,\n    W <: PSY.Component,\n    X <: AbstractDeviceFormulation,\n    Y <: PM.AbstractPowerModel,\n}\n    array = get_expression(container, U(), W)\n    _add_ub(U()) &&\n        _add_reserve_upper_bound_range_constraints!(container, T, array, devices, model)\n    _add_lb(U()) &&\n        _add_reserve_lower_bound_range_constraints!(container, T, array, devices, model)\n    return\nend\n\nfunction _add_reserve_lower_bound_range_constraints!(\n    container::OptimizationContainer,\n    ::Type{T},\n    array,\n    devices::IS.FlattenIteratorWrapper{W},\n    ::DeviceModel{W, X},\n) where {\n    T <:\n    Union{\n        ReactivePowerVariableLimitsConstraint,\n        ActivePowerVariableLimitsConstraint,\n        OutputActivePowerVariableLimitsConstraint,\n    },\n    W <: PSY.Component,\n    X <: AbstractDeviceFormulation,\n}\n    time_steps = get_time_steps(container)\n    names = PSY.get_name.(devices)\n    binary_variables = [ReservationVariable()]\n\n    con_lb = add_constraints_container!(container, T(), W, names, time_steps; meta = \"lb\")\n\n    @assert length(binary_variables) == 1 \"Expected $(binary_variables) for $U $V $T $W to be length 1\"\n    varbin = get_variable(container, only(binary_variables), W)\n\n    for device in devices, t in time_steps\n        ci_name = PSY.get_name(device)\n        limits = get_min_max_limits(device, T, X) # depends on constraint type and formulation type\n        con_lb[ci_name, t] = JuMP.@constraint(\n            get_jump_model(container),\n            array[ci_name, t] >= limits.min * varbin[ci_name, t]\n        )\n    end\n    return\nend\n\nfunction _add_reserve_upper_bound_range_constraints!(\n    container::OptimizationContainer,\n    ::Type{T},\n    array,\n    devices::IS.FlattenIteratorWrapper{W},\n    ::DeviceModel{W, X},\n) where {\n    T <:\n    Union{\n        ReactivePowerVariableLimitsConstraint,\n        ActivePowerVariableLimitsConstraint,\n        OutputActivePowerVariableLimitsConstraint,\n    },\n    W <: PSY.Component,\n    X <: AbstractDeviceFormulation,\n}\n    time_steps = get_time_steps(container)\n    names = PSY.get_name.(devices)\n    binary_variables = [ReservationVariable()]\n\n    con_ub = add_constraints_container!(container, T(), W, names, time_steps; meta = \"ub\")\n\n    @assert length(binary_variables) == 1 \"Expected $(binary_variables) for $U $V $T $W to be length 1\"\n    varbin = get_variable(container, only(binary_variables), W)\n\n    for device in devices, t in time_steps\n        ci_name = PSY.get_name(device)\n        limits = get_min_max_limits(device, T, X) # depends on constraint type and formulation type\n        con_ub[ci_name, t] = JuMP.@constraint(\n            get_jump_model(container),\n            array[ci_name, t] <= limits.max * varbin[ci_name, t]\n        )\n    end\n    return\nend\n\nfunction add_parameterized_lower_bound_range_constraints(\n    container::OptimizationContainer,\n    ::Type{T},\n    ::Type{U},\n    ::Type{P},\n    devices::IS.FlattenIteratorWrapper{V},\n    model::DeviceModel{V, W},\n    ::Type{X},\n) where {\n    T <: ConstraintType,\n    U <: ExpressionType,\n    P <: ParameterType,\n    V <: PSY.Component,\n    W <: AbstractDeviceFormulation,\n    X <: PM.AbstractPowerModel,\n}\n    array = get_expression(container, U(), V)\n    _add_parameterized_lower_bound_range_constraints_impl!(\n        container,\n        T,\n        array,\n        P,\n        devices,\n        model,\n    )\n    return\nend\n\nfunction add_parameterized_lower_bound_range_constraints(\n    container::OptimizationContainer,\n    ::Type{T},\n    ::Type{U},\n    ::Type{P},\n    devices::IS.FlattenIteratorWrapper{V},\n    model::DeviceModel{V, W},\n    ::Type{X},\n) where {\n    T <: ConstraintType,\n    U <: VariableType,\n    P <: ParameterType,\n    V <: PSY.Component,\n    W <: AbstractDeviceFormulation,\n    X <: PM.AbstractPowerModel,\n}\n    array = get_variable(container, U(), V)\n    _add_parameterized_lower_bound_range_constraints_impl!(\n        container,\n        T,\n        array,\n        P,\n        devices,\n        model,\n    )\n    return\nend\n\n# This function is re-used in SemiContinuousFeedforward\nfunction lower_bound_range_with_parameter!(\n    container::OptimizationContainer,\n    constraint_container::JuMPConstraintArray,\n    lhs_array,\n    ::Type{P},\n    devices::IS.FlattenIteratorWrapper{V},\n    model::DeviceModel{V, W},\n) where {P <: ParameterType, V <: PSY.Component, W <: AbstractDeviceFormulation}\n    param_array = get_parameter_array(container, P(), V)\n    param_multiplier = get_parameter_multiplier_array(container, P(), V)\n    jump_model = get_jump_model(container)\n    time_steps = axes(constraint_container)[2]\n    for device in devices, t in time_steps\n        name = PSY.get_name(device)\n        constraint_container[name, t] = JuMP.@constraint(\n            jump_model,\n            lhs_array[name, t] >= param_multiplier[name, t] * param_array[name, t]\n        )\n    end\n    return\nend\n\nfunction lower_bound_range_with_parameter!(\n    container::OptimizationContainer,\n    constraint_container::JuMPConstraintArray,\n    lhs_array,\n    ::Type{P},\n    devices::IS.FlattenIteratorWrapper{V},\n    model::DeviceModel{V, W},\n) where {P <: TimeSeriesParameter, V <: PSY.Component, W <: AbstractDeviceFormulation}\n    param_container = get_parameter(container, U(), V)\n    mult = get_multiplier_array(param_container)\n    jump_model = get_jump_model(container)\n    time_steps = axes(constraint_container)[2]\n    ts_name = get_time_series_names(model)[P]\n    ts_type = get_default_time_series_type(container)\n    for device in devices\n        if !(PSY.has_time_series(device, ts_type, ts_name))\n            continue\n        end\n        name = PSY.get_name(device)\n        param = get_parameter_column_refs(param_container, name)\n        for t in time_steps\n            constraint_container[name, t] =\n                JuMP.@constraint(jump_model, lhs_array[name, t] >= mult[name, t] * param[t])\n        end\n    end\n    return\nend\n\nfunction _add_parameterized_lower_bound_range_constraints_impl!(\n    container::OptimizationContainer,\n    ::Type{T},\n    array,\n    ::Type{U},\n    devices::IS.FlattenIteratorWrapper{V},\n    model::DeviceModel{V, W},\n) where {\n    T <: ConstraintType,\n    U <: ParameterType,\n    V <: PSY.Component,\n    W <: AbstractDeviceFormulation,\n}\n    time_steps = get_time_steps(container)\n    ts_name = get_time_series_names(model)[U]\n    ts_type = get_default_time_series_type(container)\n    names = [PSY.get_name(d) for d in devices if PSY.has_time_series(d, ts_type, ts_name)]\n    if isempty(names)\n        @debug \"There are no $V devices with time series data\"\n        return\n    end\n    constraint =\n        add_constraints_container!(container, T(), V, names, time_steps; meta = \"lb\")\n\n    parameter = get_parameter_array(container, U(), V)\n    multiplier = get_parameter_multiplier_array(container, U(), V)\n    jump_model = get_jump_model(container)\n    lower_bound_range_with_parameter!(\n        jump_model,\n        constraint,\n        array,\n        multiplier,\n        parameter,\n        devices,\n    )\n    return\nend\n\nfunction add_parameterized_upper_bound_range_constraints(\n    container::OptimizationContainer,\n    ::Type{T},\n    ::Type{U},\n    ::Type{P},\n    devices::Union{Vector{V}, IS.FlattenIteratorWrapper{V}},\n    model::DeviceModel{V, W},\n    ::Type{X},\n) where {\n    T <: ConstraintType,\n    U <: ExpressionType,\n    P <: ParameterType,\n    V <: PSY.Component,\n    W <: AbstractDeviceFormulation,\n    X <: PM.AbstractPowerModel,\n}\n    array = get_expression(container, U(), V)\n    _add_parameterized_upper_bound_range_constraints_impl!(\n        container,\n        T,\n        array,\n        P(),\n        devices,\n        model,\n    )\n    return\nend\n\nfunction add_parameterized_upper_bound_range_constraints(\n    container::OptimizationContainer,\n    ::Type{T},\n    ::Type{U},\n    ::Type{P},\n    devices::Union{Vector{V}, IS.FlattenIteratorWrapper{V}},\n    model::DeviceModel{V, W},\n    ::Type{X},\n) where {\n    T <: ConstraintType,\n    U <: VariableType,\n    P <: ParameterType,\n    V <: PSY.Component,\n    W <: AbstractDeviceFormulation,\n    X <: PM.AbstractPowerModel,\n}\n    array = get_variable(container, U(), V)\n    _add_parameterized_upper_bound_range_constraints_impl!(\n        container,\n        T,\n        array,\n        P(),\n        devices,\n        model,\n    )\n    return\nend\n\n# This function is re-used in SemiContinuousFeedforward\nfunction upper_bound_range_with_parameter!(\n    container::OptimizationContainer,\n    constraint_container::JuMPConstraintArray,\n    lhs_array,\n    param::P,\n    devices::Union{Vector{V}, IS.FlattenIteratorWrapper{V}},\n    ::DeviceModel{V, W},\n) where {P <: AvailableStatusParameter, V <: PSY.Component, W <: AbstractDeviceFormulation}\n    param_array = get_parameter_array(container, param, V)\n    param_multiplier = get_parameter_multiplier_array(container, P(), V)\n    jump_model = get_jump_model(container)\n    time_steps = axes(constraint_container)[2]\n    for device in devices, t in time_steps\n        ub = PSY.get_max_active_power(device)\n        name = PSY.get_name(device)\n        constraint_container[name, t] = JuMP.@constraint(\n            jump_model,\n            lhs_array[name, t] <= ub * param_array[name, t]\n        )\n    end\n    return\nend\n\n# This function is re-used in SemiContinuousFeedforward\nfunction upper_bound_range_with_parameter!(\n    container::OptimizationContainer,\n    constraint_container::JuMPConstraintArray,\n    lhs_array,\n    param::P,\n    devices::Union{Vector{V}, IS.FlattenIteratorWrapper{V}},\n    ::DeviceModel{V, W},\n) where {P <: ParameterType, V <: PSY.Component, W <: AbstractDeviceFormulation}\n    param_array = get_parameter_array(container, param, V)\n    param_multiplier = get_parameter_multiplier_array(container, P(), V)\n    jump_model = get_jump_model(container)\n    time_steps = axes(constraint_container)[2]\n    for device in devices, t in time_steps\n        name = PSY.get_name(device)\n        constraint_container[name, t] = JuMP.@constraint(\n            jump_model,\n            lhs_array[name, t] <= param_multiplier[name, t] * param_array[name, t]\n        )\n    end\n    return\nend\n\nfunction upper_bound_range_with_parameter!(\n    container::OptimizationContainer,\n    constraint_container::JuMPConstraintArray,\n    lhs_array,\n    param::P,\n    devices::IS.FlattenIteratorWrapper{V},\n    model::DeviceModel{V, W},\n) where {P <: TimeSeriesParameter, V <: PSY.Component, W <: AbstractDeviceFormulation}\n    param_container = get_parameter(container, param, V)\n    mult = get_multiplier_array(param_container)\n    jump_model = get_jump_model(container)\n    time_steps = axes(constraint_container)[2]\n    ts_name = get_time_series_names(model)[P]\n    ts_type = get_default_time_series_type(container)\n    for device in devices\n        name = PSY.get_name(device)\n        if !(PSY.has_time_series(device, ts_type, ts_name))\n            continue\n        end\n        param = get_parameter_column_refs(param_container, name)\n        for t in time_steps\n            constraint_container[name, t] =\n                JuMP.@constraint(jump_model, lhs_array[name, t] <= mult[name, t] * param[t])\n        end\n    end\n    return\nend\n\nfunction _add_parameterized_upper_bound_range_constraints_impl!(\n    container::OptimizationContainer,\n    ::Type{T},\n    array,\n    param::P,\n    devices::Union{Vector{V}, IS.FlattenIteratorWrapper{V}},\n    model::DeviceModel{V, W},\n) where {\n    T <: ConstraintType,\n    P <: TimeSeriesParameter,\n    V <: PSY.Component,\n    W <: AbstractDeviceFormulation,\n}\n    time_steps = get_time_steps(container)\n    ts_name = get_time_series_names(model)[P]\n    ts_type = get_default_time_series_type(container)\n    # PERF: compilation hotspot. Switch to TSC.\n    names = [PSY.get_name(d) for d in devices if PSY.has_time_series(d, ts_type, ts_name)]\n    if isempty(names)\n        @debug \"There are no $V devices with time series data $ts_type, $ts_name\"\n        return\n    end\n\n    constraint =\n        add_constraints_container!(container, T(), V, names, time_steps; meta = \"ub\")\n\n    upper_bound_range_with_parameter!(container, constraint, array, param, devices, model)\n    return\nend\n\nfunction _add_parameterized_upper_bound_range_constraints_impl!(\n    container::OptimizationContainer,\n    ::Type{T},\n    array,\n    param::P,\n    devices::Union{Vector{V}, IS.FlattenIteratorWrapper{V}},\n    model::DeviceModel{V, W},\n) where {\n    T <: ConstraintType,\n    P <: ParameterType,\n    V <: PSY.Component,\n    W <: AbstractDeviceFormulation,\n}\n    time_steps = get_time_steps(container)\n    names = PSY.get_name.(devices)\n    constraint =\n        add_constraints_container!(container, T(), V, names, time_steps; meta = \"ub\")\n\n    upper_bound_range_with_parameter!(container, constraint, array, param, devices, model)\n    return\nend\n"
  },
  {
    "path": "src/devices_models/devices/common/rateofchange_constraints.jl",
    "content": "function _get_minutes_per_period(container::OptimizationContainer)\n    resolution = get_resolution(container)\n    if resolution > Dates.Minute(1)\n        minutes_per_period = Dates.value(Dates.Minute(resolution))\n    else\n        @warn(\"Not all formulations support under 1-minute resolutions. Exercise caution.\")\n        minutes_per_period = Dates.value(Dates.Second(resolution)) / 60\n    end\n    return minutes_per_period\nend\n\nfunction _get_ramp_constraint_devices(\n    container::OptimizationContainer,\n    devices::Union{Vector{U}, IS.FlattenIteratorWrapper{U}},\n) where {U <: PSY.Component}\n    minutes_per_period = _get_minutes_per_period(container)\n    filtered_device = Vector{U}()\n    for d in devices\n        ramp_limits = PSY.get_ramp_limits(d)\n        if ramp_limits !== nothing\n            p_lims = PSY.get_active_power_limits(d)\n            max_rate = abs(p_lims.min - p_lims.max) / minutes_per_period\n            if (ramp_limits.up >= max_rate) & (ramp_limits.down >= max_rate)\n                @debug \"Generator has a nonbinding ramp limits. Constraints Skipped\" PSY.get_name(\n                    d,\n                )\n                continue\n            else\n                push!(filtered_device, d)\n            end\n        end\n    end\n    return filtered_device\nend\n\nfunction _get_ramp_slack_vars(\n    container::OptimizationContainer,\n    model::DeviceModel{V, W},\n    name::String,\n    t::Int,\n) where {V <: PSY.Component, W <: AbstractDeviceFormulation}\n    if get_use_slacks(model)\n        slack_ub = get_variable(container, RateofChangeConstraintSlackUp(), V)\n        slack_lb = get_variable(container, RateofChangeConstraintSlackDown(), V)\n        sl_ub = slack_ub[name, t]\n        sl_lb = slack_lb[name, t]\n    else\n        sl_ub = 0.0\n        sl_lb = 0.0\n    end\n    return sl_ub, sl_lb\nend\n\n@doc raw\"\"\"\nConstructs allowed rate-of-change constraints from variables, initial condtions, and rate data.\n\n\nIf t = 1:\n\n``` variable[name, 1] - initial_conditions[ix].value <= rate_data[1][ix].up ```\n\n``` initial_conditions[ix].value - variable[name, 1] <= rate_data[1][ix].down ```\n\nIf t > 1:\n\n``` variable[name, t] - variable[name, t-1] <= rate_data[1][ix].up ```\n\n``` variable[name, t-1] - variable[name, t] <= rate_data[1][ix].down ```\n\n# LaTeX\n\n`` r^{down} \\leq x_1 - x_{init} \\leq r^{up}, \\text{ for } t = 1 ``\n\n`` r^{down} \\leq x_t - x_{t-1} \\leq r^{up}, \\forall t \\geq 2 ``\n\n\"\"\"\nfunction add_linear_ramp_constraints!(\n    container::OptimizationContainer,\n    T::Type{<:ConstraintType},\n    U::Type{S},\n    devices::IS.FlattenIteratorWrapper{V},\n    model::DeviceModel{V, W},\n    ::Type{<:PM.AbstractPowerModel},\n) where {\n    S <: Union{PowerAboveMinimumVariable, ActivePowerVariable},\n    V <: PSY.Component,\n    W <: AbstractDeviceFormulation,\n}\n    time_steps = get_time_steps(container)\n    variable = get_variable(container, U(), V)\n    ramp_devices = _get_ramp_constraint_devices(container, devices)\n    minutes_per_period = _get_minutes_per_period(container)\n    IC = _get_initial_condition_type(T, V, W)\n    initial_conditions_power = get_initial_condition(container, IC(), V)\n    expr_dn = get_expression(container, ActivePowerRangeExpressionLB(), V)\n    expr_up = get_expression(container, ActivePowerRangeExpressionUB(), V)\n\n    device_name_set = PSY.get_name.(ramp_devices)\n    con_up =\n        add_constraints_container!(\n            container,\n            T(),\n            V,\n            device_name_set,\n            time_steps;\n            meta = \"up\",\n        )\n    con_down =\n        add_constraints_container!(\n            container,\n            T(),\n            V,\n            device_name_set,\n            time_steps;\n            meta = \"dn\",\n        )\n\n    for ic in initial_conditions_power\n        name = get_component_name(ic)\n        # This is to filter out devices that dont need a ramping constraint\n        name ∉ device_name_set && continue\n        ramp_limits = PSY.get_ramp_limits(get_component(ic))\n        ic_power = get_value(ic)\n        @debug \"add rate_of_change_constraint\" name ic_power\n        sl_ub, sl_lb = _get_ramp_slack_vars(container, model, name, 1)\n        con_up[name, 1] = JuMP.@constraint(\n            get_jump_model(container),\n            expr_up[name, 1] - ic_power - sl_ub <= ramp_limits.up * minutes_per_period\n        )\n        con_down[name, 1] = JuMP.@constraint(\n            get_jump_model(container),\n            ic_power - expr_dn[name, 1] + sl_lb >=\n            -1 * ramp_limits.down * minutes_per_period\n        )\n        for t in time_steps[2:end]\n            sl_ub, sl_lb = _get_ramp_slack_vars(container, model, name, t)\n            con_up[name, t] = JuMP.@constraint(\n                get_jump_model(container),\n                expr_up[name, t] - variable[name, t - 1] - sl_ub <=\n                ramp_limits.up * minutes_per_period\n            )\n            con_down[name, t] = JuMP.@constraint(\n                get_jump_model(container),\n                variable[name, t - 1] - expr_dn[name, t] + sl_lb >=\n                -1 * ramp_limits.down * minutes_per_period\n            )\n        end\n    end\n    return\nend\n\n# Helper function containing the shared ramp constraint logic\nfunction _add_linear_ramp_constraints_impl!(\n    container::OptimizationContainer,\n    T::Type{<:ConstraintType},\n    U::Type{<:VariableType},\n    devices::IS.FlattenIteratorWrapper{V},\n    model::DeviceModel{V, W},\n) where {V <: PSY.Component, W <: AbstractDeviceFormulation}\n    parameters = built_for_recurrent_solves(container)\n    time_steps = get_time_steps(container)\n    variable = get_variable(container, U(), V)\n    ramp_devices = _get_ramp_constraint_devices(container, devices)\n    minutes_per_period = _get_minutes_per_period(container)\n    IC = _get_initial_condition_type(T, V, W)\n    initial_conditions_power = get_initial_condition(container, IC(), V)\n\n    device_name_set = PSY.get_name.(ramp_devices)\n    con_up =\n        add_constraints_container!(\n            container,\n            T(),\n            V,\n            device_name_set,\n            time_steps;\n            meta = \"up\",\n        )\n    con_down =\n        add_constraints_container!(\n            container,\n            T(),\n            V,\n            device_name_set,\n            time_steps;\n            meta = \"dn\",\n        )\n\n    for ic in initial_conditions_power\n        name = get_component_name(ic)\n        # This is to filter out devices that dont need a ramping constraint\n        name ∉ device_name_set && continue\n        ramp_limits = PSY.get_ramp_limits(get_component(ic))\n        ic_power = get_value(ic)\n        @debug \"add rate_of_change_constraint\" name ic_power\n        @assert (parameters && isa(ic_power, JuMP.VariableRef)) || !parameters\n        sl_ub, sl_lb = _get_ramp_slack_vars(container, model, name, 1)\n        con_up[name, 1] = JuMP.@constraint(\n            get_jump_model(container),\n            variable[name, 1] - ic_power - sl_ub <= ramp_limits.up * minutes_per_period\n        )\n        con_down[name, 1] = JuMP.@constraint(\n            get_jump_model(container),\n            ic_power - variable[name, 1] - sl_lb <= ramp_limits.down * minutes_per_period\n        )\n        for t in time_steps[2:end]\n            sl_ub, sl_lb = _get_ramp_slack_vars(container, model, name, t)\n            con_up[name, t] = JuMP.@constraint(\n                get_jump_model(container),\n                variable[name, t] - variable[name, t - 1] - sl_ub <=\n                ramp_limits.up * minutes_per_period\n            )\n            con_down[name, t] = JuMP.@constraint(\n                get_jump_model(container),\n                variable[name, t - 1] - variable[name, t] - sl_lb <=\n                ramp_limits.down * minutes_per_period\n            )\n        end\n    end\n    return\nend\n\nfunction add_linear_ramp_constraints!(\n    container::OptimizationContainer,\n    T::Type{<:ConstraintType},\n    U::Type{<:VariableType},\n    devices::IS.FlattenIteratorWrapper{V},\n    model::DeviceModel{V, W},\n    X::Type{<:PM.AbstractPowerModel},\n) where {V <: PSY.Component, W <: AbstractDeviceFormulation}\n    return _add_linear_ramp_constraints_impl!(container, T, U, devices, model)\nend\n\nfunction add_linear_ramp_constraints!(\n    container::OptimizationContainer,\n    T::Type{<:ConstraintType},\n    U::Type{ActivePowerVariable},\n    devices::IS.FlattenIteratorWrapper{V},\n    model::DeviceModel{V, W},\n    X::Type{<:PM.AbstractPowerModel},\n) where {V <: PSY.ThermalGen, W <: AbstractThermalDispatchFormulation}\n\n    # Fallback to generic implementation if OnStatusParameter is not present\n    if !has_container_key(container, OnStatusParameter, V)\n        return _add_linear_ramp_constraints_impl!(container, T, U, devices, model)\n    end\n\n    time_steps = get_time_steps(container)\n    variable = get_variable(container, U(), V)\n    ramp_devices = _get_ramp_constraint_devices(container, devices)\n    minutes_per_period = _get_minutes_per_period(container)\n    IC = _get_initial_condition_type(T, V, W)\n    initial_conditions_power = get_initial_condition(container, IC(), V)\n\n    # Commitment path from UC as a PARAMETER (fixed 0/1)\n    on_param = get_parameter(container, OnStatusParameter(), V)\n    on_status = on_param.parameter_array  # on_status[name, t] ∈ {0,1} (fixed)\n\n    set_name = [PSY.get_name(r) for r in ramp_devices]\n    con_up =\n        add_constraints_container!(container, T(), V, set_name, time_steps; meta = \"up\")\n    con_down =\n        add_constraints_container!(container, T(), V, set_name, time_steps; meta = \"dn\")\n\n    jump_model = get_jump_model(container)\n\n    for dev in ramp_devices\n        name = PSY.get_name(dev)\n        ramp_limits = PSY.get_ramp_limits(dev)\n        power_limits = PSY.get_active_power_limits(dev)\n\n        # --- t = 1: Use ic_power to determine starting ramp condition\n        ic_idx = findfirst(ic -> get_component_name(ic) == name, initial_conditions_power)\n        ic_power = get_value(initial_conditions_power[ic_idx])\n        ycur = on_status[name, 1]\n        sl_ub, sl_lb = _get_ramp_slack_vars(container, model, name, 1)\n\n        # Ramp UP from IC\n        con_up[name, 1] = JuMP.@constraint(jump_model,\n            variable[name, 1] - ic_power - sl_ub <=\n            ramp_limits.up * minutes_per_period + power_limits.max * (1 - ycur)\n        )\n\n        # Ramp DOWN from IC  \n        con_down[name, 1] = JuMP.@constraint(jump_model,\n            ic_power - variable[name, 1] - sl_lb <=\n            ramp_limits.down * minutes_per_period + power_limits.max * (1 - ycur)\n        )\n\n        # --- t ≥ 2: gate by previous status y_{t-1}\n        for t in time_steps[2:end]\n            yprev = on_status[name, t - 1]   # 0/1 fixed from UC\n            ycur = on_status[name, t]       # 0/1 fixed from UC\n            sl_ub, sl_lb = _get_ramp_slack_vars(container, model, name, t)\n\n            # Ramp UP when already ON previously\n            con_up[name, t] = JuMP.@constraint(jump_model,\n                variable[name, t] - variable[name, t - 1] - sl_ub <=\n                ramp_limits.up * minutes_per_period + power_limits.max * (2 - yprev - ycur)\n            )\n\n            # Ramp DOWN when already ON previously\n            con_down[name, t] = JuMP.@constraint(jump_model,\n                variable[name, t - 1] - variable[name, t] - sl_lb <=\n                ramp_limits.down * minutes_per_period +\n                power_limits.max * (2 - yprev - ycur)\n            )\n        end\n    end\n\n    return\nend\n\n@doc raw\"\"\"\nConstructs allowed rate-of-change constraints from variables, initial condtions, start/stop status, and rate data\n\n# Equations\nIf t = 1:\n\n``` variable[name, 1] - initial_conditions[ix].value <= rate_data[1][ix].up + rate_data[2][ix].max*varstart[name, 1] ```\n\n``` initial_conditions[ix].value - variable[name, 1] <= rate_data[1][ix].down + rate_data[2][ix].min*varstop[name, 1] ```\n\nIf t > 1:\n\n``` variable[name, t] - variable[name, t-1] <= rate_data[1][ix].up + rate_data[2][ix].max*varstart[name, t] ```\n\n``` variable[name, t-1] - variable[name, t] <= rate_data[1][ix].down + rate_data[2][ix].min*varstop[name, t] ```\n\n# LaTeX\n\n`` r^{down} + r^{min} x^{stop}_1 \\leq x_1 - x_{init} \\leq r^{up} + r^{max} x^{start}_1, \\text{ for } t = 1 ``\n\n`` 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 ``\n\"\"\"\nfunction add_semicontinuous_ramp_constraints!(\n    container::OptimizationContainer,\n    T::Type{<:ConstraintType},\n    U::Type{S},\n    devices::IS.FlattenIteratorWrapper{V},\n    model::DeviceModel{V, W},\n    ::Type{<:PM.AbstractPowerModel},\n) where {\n    S <: Union{PowerAboveMinimumVariable, ActivePowerVariable},\n    V <: PSY.Component,\n    W <: AbstractDeviceFormulation,\n}\n    time_steps = get_time_steps(container)\n    variable = get_variable(container, U(), V)\n    varstart = get_variable(container, StartVariable(), V)\n    varstop = get_variable(container, StopVariable(), V)\n\n    ramp_devices = _get_ramp_constraint_devices(container, devices)\n    minutes_per_period = _get_minutes_per_period(container)\n    IC = _get_initial_condition_type(T, V, W)\n    initial_conditions_power = get_initial_condition(container, IC(), V)\n    expr_dn = get_expression(container, ActivePowerRangeExpressionLB(), V)\n    expr_up = get_expression(container, ActivePowerRangeExpressionUB(), V)\n\n    device_name_set = PSY.get_name.(ramp_devices)\n    con_up =\n        add_constraints_container!(\n            container,\n            T(),\n            V,\n            device_name_set,\n            time_steps;\n            meta = \"up\",\n        )\n    con_down =\n        add_constraints_container!(\n            container,\n            T(),\n            V,\n            device_name_set,\n            time_steps;\n            meta = \"dn\",\n        )\n\n    for ic in initial_conditions_power\n        component = get_component(ic)\n        name = get_component_name(ic)\n        # This is to filter out devices that dont need a ramping constraint\n        name ∉ device_name_set && continue\n        device = get_component(ic)\n        ramp_limits = PSY.get_ramp_limits(device)\n        power_limits = PSY.get_active_power_limits(device)\n        ic_power = get_value(ic)\n        @debug \"add rate_of_change_constraint\" name ic_power\n\n        if hasmethod(PSY.get_must_run, Tuple{V})\n            must_run = PSY.get_must_run(component)\n        else\n            must_run = false\n        end\n\n        if must_run\n            rhs_up = ramp_limits.up * minutes_per_period\n            rhd_dn = ramp_limits.down * minutes_per_period\n        else\n            rhs_up =\n                ramp_limits.up * minutes_per_period + power_limits.min * varstart[name, 1]\n            rhd_dn =\n                ramp_limits.down * minutes_per_period + power_limits.min * varstop[name, 1]\n        end\n        sl_ub, sl_lb = _get_ramp_slack_vars(container, model, name, 1)\n        con_up[name, 1] = JuMP.@constraint(\n            get_jump_model(container),\n            expr_up[name, 1] - ic_power - sl_ub <=\n            if must_run\n                ramp_limits.up * minutes_per_period\n            else\n                ramp_limits.up * minutes_per_period + power_limits.min * varstart[name, 1]\n            end\n        )\n        con_down[name, 1] = JuMP.@constraint(\n            get_jump_model(container),\n            ic_power - expr_dn[name, 1] - sl_lb <=\n            if must_run\n                ramp_limits.down * minutes_per_period\n            else\n                ramp_limits.down * minutes_per_period + power_limits.min * varstop[name, 1]\n            end\n        )\n        for t in time_steps[2:end]\n            sl_ub, sl_lb = _get_ramp_slack_vars(container, model, name, t)\n            con_up[name, t] = JuMP.@constraint(\n                get_jump_model(container),\n                expr_up[name, t] - variable[name, t - 1] - sl_ub <=\n                if must_run\n                    ramp_limits.up * minutes_per_period\n                else\n                    ramp_limits.up * minutes_per_period + power_limits.min * varstart[name, t]\n                end\n            )\n            con_down[name, t] = JuMP.@constraint(\n                get_jump_model(container),\n                variable[name, t - 1] - expr_dn[name, t] - sl_lb <=\n                if must_run\n                    ramp_limits.down * minutes_per_period\n                else\n                    ramp_limits.down * minutes_per_period + power_limits.min * varstop[name, t]\n                end\n            )\n        end\n    end\n    return\nend\n"
  },
  {
    "path": "src/devices_models/devices/common/set_expression.jl",
    "content": "\"\"\"\nReplaces an expression value in the expression container if the key exists\n\"\"\"\nfunction set_expression!(\n    container::OptimizationContainer,\n    ::Type{S},\n    cost_expression::JuMP.AbstractJuMPScalar,\n    component::T,\n    time_period::Int,\n) where {S <: CostExpressions, T <: PSY.Component}\n    if has_container_key(container, S, T)\n        device_cost_expression = get_expression(container, S(), T)\n        component_name = PSY.get_name(component)\n        device_cost_expression[component_name, time_period] = cost_expression\n    end\n    return\nend\n"
  },
  {
    "path": "src/devices_models/devices/default_interface_methods.jl",
    "content": "########################### Interfaces ########################################################\nget_variable_key(variabletype, d) = error(\"Not Implemented\")\n\nget_variable_binary(pv, t::Type{<:PSY.Component}, _) =\n    error(\"`get_variable_binary` must be implemented for $pv and $t\")\n\nget_variable_warm_start_value(_, ::PSY.Component, __) = nothing\n\nget_variable_lower_bound(_, ::PSY.Component, __) = nothing\n\nget_variable_upper_bound(_, ::PSY.Component, __) = nothing\n\n#! format: off\nget_multiplier_value(::StartupCostParameter, d::PSY.Device, ::AbstractDeviceFormulation) = 1.0\nget_multiplier_value(::ShutdownCostParameter, d::PSY.Device, ::AbstractDeviceFormulation) = 1.0\nget_multiplier_value(::AbstractCostAtMinParameter, d::PSY.Device, ::AbstractDeviceFormulation) = 1.0\nget_multiplier_value(::AbstractPiecewiseLinearSlopeParameter, d::PSY.Device, ::AbstractDeviceFormulation) = 1.0\nget_multiplier_value(::AbstractPiecewiseLinearBreakpointParameter, d::PSY.Device, ::AbstractDeviceFormulation) = 1.0\n#! format: on\n\nget_multiplier_value(x, y::PSY.Component, z) =\n    error(\n        \"Unable to get multiplier for parameter $x, device $(IS.summary(y)), formulation $z\",\n    )\n\nget_expression_type_for_reserve(_, y::Type{<:PSY.Component}, z) =\n    error(\"`get_expression_type_for_reserve` must be implemented for $y and $z\")\n\nget_initial_conditions_device_model(\n    ::OperationModel,\n    ::DeviceModel{T, D},\n) where {T <: PSY.Device, D <: AbstractDeviceFormulation} =\n    error(\"`get_initial_conditions_device_model` must be implemented for $T and $D\")\n\nrequires_initialization(::AbstractDeviceFormulation) = false\n\ndoes_subcomponent_exist(T::PSY.Component, S::Type{<:PSY.Component}) =\n    error(\"`does_subcomponent_exist` must be implemented for $T and subcomponent type $S\")\n\n_get_initial_condition_type(\n    X::Type{<:ConstraintType},\n    Y::Type{<:PSY.Component},\n    Z::Type{<:AbstractDeviceFormulation},\n) = error(\"`_get_initial_condition_type` must be implemented for $X , $Y and $Z\")\n\nget_initial_conditions_device_model(\n    ::OperationModel,\n    model::DeviceModel{T, FixedOutput},\n) where {T <: PSY.Device} = model\n\nget_default_on_variable(component::T) where {T <: PSY.Component} = OnVariable()\nget_default_on_parameter(component::T) where {T <: PSY.Component} = OnStatusParameter()\n"
  },
  {
    "path": "src/devices_models/devices/electric_loads.jl",
    "content": "#! format: off\n########################### ElectricLoad ####################################\n\nget_variable_multiplier(_, ::Type{<:PSY.ElectricLoad}, ::AbstractLoadFormulation) = -1.0\n\n########################### ActivePowerVariable, ElectricLoad ####################################\n\nget_variable_binary(::ActivePowerVariable, ::Type{<:PSY.ElectricLoad}, ::AbstractLoadFormulation) = false\nget_variable_lower_bound(::ActivePowerVariable, d::PSY.ElectricLoad, ::AbstractLoadFormulation) = 0.0\nget_variable_upper_bound(::ActivePowerVariable, d::PSY.ElectricLoad, ::AbstractLoadFormulation) = PSY.get_max_active_power(d)\n\n########################### ReactivePowerVariable, ElectricLoad ####################################\n\nget_variable_binary(::ReactivePowerVariable, ::Type{<:PSY.ElectricLoad}, ::AbstractLoadFormulation) = false\n\nget_variable_lower_bound(::ReactivePowerVariable, d::PSY.ElectricLoad, ::AbstractLoadFormulation) = 0.0\nget_variable_upper_bound(::ReactivePowerVariable, d::PSY.ElectricLoad, ::AbstractLoadFormulation) = PSY.get_max_reactive_power(d)\n\n########################### ReactivePowerVariable, ElectricLoad ####################################\n\nget_variable_binary(::OnVariable, ::Type{<:PSY.ElectricLoad}, ::AbstractLoadFormulation) = true\n\nget_multiplier_value(::TimeSeriesParameter, d::PSY.ElectricLoad, ::StaticPowerLoad) = -1*PSY.get_max_active_power(d)\nget_multiplier_value(::ReactivePowerTimeSeriesParameter, d::PSY.ElectricLoad, ::StaticPowerLoad) = -1*PSY.get_max_reactive_power(d)\nget_multiplier_value(::TimeSeriesParameter, d::PSY.ElectricLoad, ::AbstractControllablePowerLoadFormulation) = PSY.get_max_active_power(d)\n\n########################### ShiftablePowerLoad #####################################\n\nget_variable_binary(::ShiftUpActivePowerVariable, ::Type{<:PSY.ElectricLoad}, ::PowerLoadShift) = false\nget_variable_lower_bound(::ShiftUpActivePowerVariable, d::PSY.ElectricLoad, ::PowerLoadShift) = 0.0\nget_variable_upper_bound(::ShiftUpActivePowerVariable, d::PSY.ElectricLoad, ::PowerLoadShift) = nothing # Unbounded above by default, but can be limited by time series parameters\n\nget_variable_binary(::ShiftDownActivePowerVariable, ::Type{<:PSY.ElectricLoad}, ::PowerLoadShift) = false\nget_variable_lower_bound(::ShiftDownActivePowerVariable, d::PSY.ElectricLoad, ::PowerLoadShift) = 0.0\nget_variable_upper_bound(::ShiftDownActivePowerVariable, d::PSY.ElectricLoad, ::PowerLoadShift) = nothing # Unbounded above by default, but can be limited by time series parameters\n\n######################################################\n\n# To avoid ambiguity with default_interface_methods.jl:\nget_multiplier_value(::AbstractPiecewiseLinearBreakpointParameter, ::PSY.ElectricLoad, ::StaticPowerLoad) = 1.0\nget_multiplier_value(::AbstractPiecewiseLinearBreakpointParameter, ::PSY.ElectricLoad, ::AbstractControllablePowerLoadFormulation) = 1.0\n\n\n########################Objective Function##################################################\nproportional_cost(cost::Nothing, ::OnVariable, ::PSY.ElectricLoad, ::AbstractControllablePowerLoadFormulation)=1.0\nproportional_cost(cost::PSY.OperationalCost, ::OnVariable, ::PSY.ElectricLoad, ::AbstractControllablePowerLoadFormulation)=PSY.get_fixed(cost)\n\nobjective_function_multiplier(::VariableType, ::AbstractControllablePowerLoadFormulation)=OBJECTIVE_FUNCTION_NEGATIVE\nobjective_function_multiplier(::ShiftUpActivePowerVariable, ::AbstractControllablePowerLoadFormulation)=OBJECTIVE_FUNCTION_NEGATIVE\nobjective_function_multiplier(::ShiftDownActivePowerVariable, ::PowerLoadShift)=OBJECTIVE_FUNCTION_POSITIVE\n\nvariable_cost(::Nothing, ::PSY.ElectricLoad, ::ActivePowerVariable, ::AbstractControllablePowerLoadFormulation)=1.0\nvariable_cost(cost::PSY.OperationalCost, ::ActivePowerVariable, ::PSY.ElectricLoad, ::AbstractControllablePowerLoadFormulation)=PSY.get_variable(cost)\nvariable_cost(cost::PSY.OperationalCost, ::ShiftUpActivePowerVariable, ::PSY.ElectricLoad, ::AbstractControllablePowerLoadFormulation)=PSY.get_variable(cost)\nvariable_cost(cost::PSY.OperationalCost, ::ShiftDownActivePowerVariable, ::PSY.ElectricLoad, ::AbstractControllablePowerLoadFormulation)=PSY.get_variable(cost)\n\n#! format: on\n\nfunction get_default_time_series_names(\n    ::Type{<:PSY.ElectricLoad},\n    ::Type{<:Union{FixedOutput, AbstractLoadFormulation}},\n)\n    return Dict{Type{<:TimeSeriesParameter}, String}(\n        ActivePowerTimeSeriesParameter => \"max_active_power\",\n        ReactivePowerTimeSeriesParameter => \"max_active_power\",\n    )\nend\n\nfunction get_default_attributes(\n    ::Type{U},\n    ::Type{V},\n) where {U <: PSY.ElectricLoad, V <: Union{FixedOutput, AbstractLoadFormulation}}\n    return Dict{String, Any}()\nend\n\nget_initial_conditions_device_model(\n    ::OperationModel,\n    ::DeviceModel{T, <:AbstractLoadFormulation},\n) where {T <: PSY.ElectricLoad} = DeviceModel(T, StaticPowerLoad)\n\nfunction get_default_time_series_names(\n    ::Type{<:PSY.MotorLoad},\n    ::Type{<:Union{FixedOutput, AbstractLoadFormulation}},\n)\n    return Dict{Type{<:TimeSeriesParameter}, String}()\nend\n\nfunction get_default_time_series_names(\n    ::Type{<:PSY.ShiftablePowerLoad},\n    ::Type{PowerLoadShift},\n)\n    return Dict{Type{<:TimeSeriesParameter}, String}(\n        ActivePowerTimeSeriesParameter => \"max_active_power\",\n        ReactivePowerTimeSeriesParameter => \"max_active_power\",\n        ShiftUpActivePowerTimeSeriesParameter => \"shift_up_max_active_power\",\n        ShiftDownActivePowerTimeSeriesParameter => \"shift_down_max_active_power\",\n    )\nend\n\n####################### Expressions #########################\n\nfunction add_expressions!(\n    container::OptimizationContainer,\n    ::Type{T},\n    devices::U,\n    model::DeviceModel{D, W},\n) where {\n    T <: RealizedShiftedLoad,\n    U <: Union{Vector{D}, IS.FlattenIteratorWrapper{D}},\n    W <: PowerLoadShift,\n} where {D <: PSY.ShiftablePowerLoad}\n    time_steps = get_time_steps(container)\n    names = PSY.get_name.(devices)\n    expression = add_expression_container!(container, T(), D, names, time_steps)\n    shift_up = get_variable(container, ShiftUpActivePowerVariable(), D)\n    shift_down = get_variable(container, ShiftDownActivePowerVariable(), D)\n    param_container = get_parameter(container, ActivePowerTimeSeriesParameter(), D)\n    multiplier = get_multiplier_array(param_container)\n    for t in time_steps, d in devices\n        name = PSY.get_name(d)\n        expression[name, t] =\n            get_parameter_column_refs(param_container, name)[t] * multiplier[name, t] +\n            shift_up[name, t] - shift_down[name, t]\n    end\n    return\nend\n\nfunction add_to_expression!(\n    container::OptimizationContainer,\n    ::Type{T},\n    ::Type{U},\n    devices::IS.FlattenIteratorWrapper{V},\n    device_model::DeviceModel{V, W},\n    network_model::NetworkModel{CopperPlatePowerModel},\n) where {\n    T <: ActivePowerBalance,\n    U <: RealizedShiftedLoad,\n    V <: PSY.StaticInjection,\n    W <: AbstractDeviceFormulation,\n}\n    realized_load = get_expression(container, U(), V)\n    expression = get_expression(container, T(), PSY.System)\n    for d in devices\n        device_bus = PSY.get_bus(d)\n        ref_bus = get_reference_bus(network_model, device_bus)\n        name = PSY.get_name(d)\n        for t in get_time_steps(container)\n            JuMP.add_to_expression!(\n                expression[ref_bus, t],\n                -1.0, # Realized load enter negative to the balance\n                realized_load[name, t],\n            )\n        end\n    end\n    return\nend\n\n\"\"\"\nElectric Load implementation to add parameters to PTDF ActivePowerBalance expressions\n\"\"\"\nfunction add_to_expression!(\n    container::OptimizationContainer,\n    ::Type{T},\n    ::Type{U},\n    devices::IS.FlattenIteratorWrapper{V},\n    device_model::DeviceModel{V, W},\n    network_model::NetworkModel{X},\n) where {\n    T <: ActivePowerBalance,\n    U <: RealizedShiftedLoad,\n    V <: PSY.ShiftablePowerLoad,\n    W <: PowerLoadShift,\n    X <: AbstractPTDFModel,\n}\n    realized_load = get_expression(container, U(), V)\n    sys_expr = get_expression(container, T(), _system_expression_type(X))\n    nodal_expr = get_expression(container, T(), PSY.ACBus)\n    network_reduction = get_network_reduction(network_model)\n    for d in devices\n        name = PSY.get_name(d)\n        device_bus = PSY.get_bus(d)\n        bus_no_ = PSY.get_number(device_bus)\n        bus_no = PNM.get_mapped_bus_number(network_reduction, bus_no_)\n        ref_index = _ref_index(network_model, device_bus)\n        for t in get_time_steps(container)\n            JuMP.add_to_expression!(\n                sys_expr[ref_index, t],\n                -1.0, # Realized load enter negative to the balance\n                realized_load[name, t],\n            )\n            JuMP.add_to_expression!(\n                nodal_expr[bus_no, t],\n                -1.0, # Realized load enter negative to the balance\n                realized_load[name, t],\n            )\n        end\n    end\n    return\nend\n\n####################################### Reactive Power Constraints #########################\n\"\"\"\nReactive Power Constraints on Controllable Loads Assume Constant power_factor\n\"\"\"\nfunction add_constraints!(\n    container::OptimizationContainer,\n    T::Type{<:ReactivePowerVariableLimitsConstraint},\n    U::Type{<:ReactivePowerVariable},\n    devices::IS.FlattenIteratorWrapper{V},\n    ::DeviceModel{V, W},\n    network_model::NetworkModel{X},\n) where {\n    V <: PSY.ElectricLoad,\n    W <: AbstractControllablePowerLoadFormulation,\n    X <: PM.AbstractPowerModel,\n}\n    time_steps = get_time_steps(container)\n    constraint = add_constraints_container!(\n        container,\n        T(),\n        V,\n        PSY.get_name.(devices),\n        time_steps,\n    )\n    jump_model = get_jump_model(container)\n    for t in time_steps, d in devices\n        name = PSY.get_name(d)\n        pf = sin(atan((PSY.get_max_reactive_power(d) / PSY.get_max_active_power(d))))\n        reactive = get_variable(container, U(), V)[name, t]\n        real = get_variable(container, ActivePowerVariable(), V)[name, t]\n        constraint[name, t] = JuMP.@constraint(jump_model, reactive == real * pf)\n    end\nend\n\nfunction add_constraints!(\n    container::OptimizationContainer,\n    ::Type{ActivePowerVariableLimitsConstraint},\n    U::Type{<:VariableType},\n    devices::IS.FlattenIteratorWrapper{V},\n    model::DeviceModel{V, W},\n    ::NetworkModel{X},\n) where {V <: PSY.ControllableLoad, W <: PowerLoadDispatch, X <: PM.AbstractPowerModel}\n    add_parameterized_upper_bound_range_constraints(\n        container,\n        ActivePowerVariableTimeSeriesLimitsConstraint,\n        U,\n        ActivePowerTimeSeriesParameter,\n        devices,\n        model,\n        X,\n    )\n    return\nend\n\nfunction add_constraints!(\n    container::OptimizationContainer,\n    T::Type{ActivePowerVariableLimitsConstraint},\n    U::Type{<:VariableType},\n    devices::IS.FlattenIteratorWrapper{V},\n    model::DeviceModel{V, W},\n    ::NetworkModel{X},\n) where {V <: PSY.ControllableLoad, W <: PowerLoadInterruption, X <: PM.AbstractPowerModel}\n    add_parameterized_upper_bound_range_constraints(\n        container,\n        ActivePowerVariableTimeSeriesLimitsConstraint,\n        U,\n        ActivePowerTimeSeriesParameter,\n        devices,\n        model,\n        X,\n    )\n    return\nend\n\nfunction add_constraints!(\n    container::OptimizationContainer,\n    T::Type{ActivePowerVariableLimitsConstraint},\n    U::Type{OnVariable},\n    devices::IS.FlattenIteratorWrapper{V},\n    model::DeviceModel{V, W},\n    ::NetworkModel{X},\n) where {V <: PSY.ControllableLoad, W <: PowerLoadInterruption, X <: PM.AbstractPowerModel}\n    time_steps = get_time_steps(container)\n    constraint = add_constraints_container!(\n        container,\n        T(),\n        V,\n        PSY.get_name.(devices),\n        time_steps;\n        meta = \"binary\",\n    )\n    on_variable = get_variable(container, U(), V)\n    power = get_variable(container, ActivePowerVariable(), V)\n    jump_model = get_jump_model(container)\n    for t in time_steps, d in devices\n        name = PSY.get_name(d)\n        pmax = PSY.get_max_active_power(d)\n        constraint[name, t] =\n            JuMP.@constraint(jump_model, power[name, t] <= on_variable[name, t] * pmax)\n    end\n    return\nend\n\nfunction add_constraints!(\n    container::OptimizationContainer,\n    T::Type{ShiftedActivePowerBalanceConstraint},\n    devices::IS.FlattenIteratorWrapper{V},\n    model::DeviceModel{V, W},\n    ::NetworkModel{X},\n) where {V <: PSY.ShiftablePowerLoad, W <: PowerLoadShift, X <: PM.AbstractPowerModel}\n    time_steps = get_time_steps(container)\n    constraint = add_constraints_container!(\n        container,\n        T(),\n        V,\n        PSY.get_name.(devices),\n    )\n    up_variable = get_variable(container, ShiftUpActivePowerVariable(), V)\n    down_variable = get_variable(container, ShiftDownActivePowerVariable(), V)\n    jump_model = get_jump_model(container)\n    for d in devices\n        name = PSY.get_name(d)\n        constraint[name] =\n            JuMP.@constraint(\n                jump_model,\n                sum(up_variable[name, t] - down_variable[name, t] for t in time_steps) ==\n                0.0\n            )\n    end\n    additional_balance_interval = PSI.get_attribute(model, \"additional_balance_interval\")\n    if !isnothing(additional_balance_interval)\n        constraint_aux = add_constraints_container!(\n            container,\n            T(),\n            V,\n            PSY.get_name.(devices);\n            meta = \"additional\",\n        )\n        resolution = PSI.get_resolution(container)\n        interval_length =\n            Dates.Millisecond(additional_balance_interval).value ÷\n            Dates.Millisecond(resolution).value\n        for d in devices\n            name = PSY.get_name(d)\n            constraint_aux[name] = JuMP.@constraint(\n                container.JuMPmodel,\n                sum(\n                    up_variable[name, t] - down_variable[name, t] for\n                    t in 1:interval_length\n                ) == 0.0\n            )\n        end\n    end\n    return\nend\n\nfunction add_constraints!(\n    container::OptimizationContainer,\n    T::Type{RealizedShiftedLoadMinimumBoundConstraint},\n    U::Type{<:ExpressionType},\n    devices::IS.FlattenIteratorWrapper{V},\n    ::DeviceModel{V, W},\n    ::NetworkModel{X},\n) where {V <: PSY.ShiftablePowerLoad, W <: PowerLoadShift, X <: PM.AbstractPowerModel}\n    time_steps = get_time_steps(container)\n    constraint = add_constraints_container!(\n        container,\n        T(),\n        V,\n        PSY.get_name.(devices),\n        time_steps,\n    )\n    realized_load = get_expression(container, U(), V)\n    jump_model = get_jump_model(container)\n    for d in devices, t in time_steps\n        name = PSY.get_name(d)\n        constraint[name, t] = JuMP.@constraint(jump_model, realized_load[name, t] >= 0.0)\n    end\n    return\nend\n\nfunction add_constraints!(\n    container::OptimizationContainer,\n    T::Type{NonAnticipativityConstraint},\n    devices::IS.FlattenIteratorWrapper{V},\n    ::DeviceModel{V, W},\n    ::NetworkModel{X},\n) where {V <: PSY.ShiftablePowerLoad, W <: PowerLoadShift, X <: PM.AbstractPowerModel}\n    time_steps = get_time_steps(container)\n    constraint = add_constraints_container!(\n        container,\n        T(),\n        V,\n        PSY.get_name.(devices),\n        time_steps,\n    )\n    up_variable = get_variable(container, ShiftUpActivePowerVariable(), V)\n    down_variable = get_variable(container, ShiftDownActivePowerVariable(), V)\n    jump_model = get_jump_model(container)\n    for d in devices\n        name = PSY.get_name(d)\n        for t in time_steps\n            constraint[name, t] = JuMP.@constraint(\n                jump_model,\n                sum(down_variable[name, τ] - up_variable[name, τ] for τ in 1:t) >= 0.0\n            )\n        end\n    end\n    return\nend\n\nfunction add_constraints!(\n    container::OptimizationContainer,\n    ::Type{ShiftUpActivePowerVariableLimitsConstraint},\n    U::Type{<:VariableType},\n    devices::IS.FlattenIteratorWrapper{V},\n    model::DeviceModel{V, W},\n    ::NetworkModel{X},\n) where {V <: PSY.ShiftablePowerLoad, W <: PowerLoadShift, X <: PM.AbstractPowerModel}\n    add_parameterized_upper_bound_range_constraints(\n        container,\n        ShiftUpActivePowerVariableLimitsConstraint,\n        U,\n        ShiftUpActivePowerTimeSeriesParameter,\n        devices,\n        model,\n        X,\n    )\n    return\nend\n\nfunction add_constraints!(\n    container::OptimizationContainer,\n    ::Type{ShiftDownActivePowerVariableLimitsConstraint},\n    U::Type{<:VariableType},\n    devices::IS.FlattenIteratorWrapper{V},\n    model::DeviceModel{V, W},\n    ::NetworkModel{X},\n) where {V <: PSY.ShiftablePowerLoad, W <: PowerLoadShift, X <: PM.AbstractPowerModel}\n    add_parameterized_upper_bound_range_constraints(\n        container,\n        ShiftDownActivePowerVariableLimitsConstraint,\n        U,\n        ShiftDownActivePowerTimeSeriesParameter,\n        devices,\n        model,\n        X,\n    )\n    return\nend\n\n############################## FormulationControllable Load Cost ###########################\nfunction objective_function!(\n    container::OptimizationContainer,\n    devices::IS.FlattenIteratorWrapper{T},\n    ::DeviceModel{T, U},\n    ::Type{<:PM.AbstractPowerModel},\n) where {T <: PSY.ControllableLoad, U <: PowerLoadDispatch}\n    add_variable_cost!(container, ActivePowerVariable(), devices, U())\n    return\nend\n\nfunction objective_function!(\n    container::OptimizationContainer,\n    devices::IS.FlattenIteratorWrapper{T},\n    ::DeviceModel{T, U},\n    ::Type{<:PM.AbstractPowerModel},\n) where {T <: PSY.ControllableLoad, U <: PowerLoadInterruption}\n    add_variable_cost!(container, ActivePowerVariable(), devices, U())\n    add_proportional_cost!(container, OnVariable(), devices, U())\n    return\nend\n\n# code repetition: basically copy-paste from thermal_generation.jl, just change types\n# and incremental to decremental.\nfunction proportional_cost(\n    container::OptimizationContainer,\n    cost::PSY.LoadCost,\n    S::OnVariable,\n    T::PSY.ControllableLoad,\n    U::PowerLoadInterruption,\n    t::Int,\n)\n    return onvar_cost(container, cost, S, T, U, t) +\n           PSY.get_constant_term(PSY.get_vom_cost(PSY.get_variable(cost))) +\n           PSY.get_fixed(cost)\nend\n\nfunction onvar_cost(\n    container::OptimizationContainer,\n    cost::PSY.LoadCost,\n    ::OnVariable,\n    d::PSY.ControllableLoad,\n    ::PowerLoadInterruption,\n    t::Int,\n)\n    return _onvar_cost(container, PSY.get_variable(cost), d, t)\nend\n\nis_time_variant_term(\n    ::OptimizationContainer,\n    ::PSY.LoadCost,\n    ::OnVariable,\n    ::PSY.ControllableLoad,\n    ::AbstractLoadFormulation,\n    ::Int,\n) = false\n\nis_time_variant_term(\n    ::OptimizationContainer,\n    cost::PSY.MarketBidCost,\n    ::OnVariable,\n    ::PSY.ControllableLoad,\n    ::PowerLoadInterruption,\n    ::Int,\n) =\n    is_time_variant(PSY.get_decremental_initial_input(cost))\n\nproportional_cost(\n    container::OptimizationContainer,\n    cost::PSY.MarketBidCost,\n    ::OnVariable,\n    comp::PSY.ControllableLoad,\n    ::PowerLoadInterruption,\n    t::Int,\n) =\n    _lookup_maybe_time_variant_param(container, comp, t,\n        Val(is_time_variant(PSY.get_decremental_initial_input(cost))),\n        PSY.get_initial_input ∘ PSY.get_decremental_offer_curves ∘ PSY.get_operation_cost,\n        DecrementalCostAtMinParameter())\n\n########## PowerLoadShift Formulation Costs ###############\n\nfunction objective_function!(\n    container::OptimizationContainer,\n    devices::IS.FlattenIteratorWrapper{T},\n    ::DeviceModel{T, U},\n    ::Type{<:PM.AbstractPowerModel},\n) where {T <: PSY.ShiftablePowerLoad, U <: PowerLoadShift}\n    add_variable_cost!(container, ShiftUpActivePowerVariable(), devices, U())\n    add_variable_cost!(container, ShiftDownActivePowerVariable(), devices, U())\n    return\nend\n\n### Special Method to skip VOM cost on ShiftUpActivePowerVariable ###\nfunction add_variable_cost!(\n    container::OptimizationContainer,\n    ::U,\n    devices::IS.FlattenIteratorWrapper{T},\n    ::V,\n) where {T <: PSY.ShiftablePowerLoad, U <: ShiftUpActivePowerVariable, V <: PowerLoadShift}\n    for d in devices\n        op_cost_data = PSY.get_operation_cost(d)\n        _add_variable_cost_to_objective!(container, U(), d, op_cost_data, V())\n    end\n    return\nend\n"
  },
  {
    "path": "src/devices_models/devices/reactivepower_device.jl",
    "content": "#! format: off\n\nrequires_initialization(::AbstractReactivePowerDeviceFormulation) = false\nget_variable_multiplier(_, ::Type{<:PSY.SynchronousCondenser}, ::AbstractReactivePowerDeviceFormulation) = 1.0\n\n############## ReactivePowerVariable, SynchronousCondensers ####################\nget_variable_binary(::ReactivePowerVariable, ::Type{PSY.SynchronousCondenser}, ::AbstractReactivePowerDeviceFormulation) = false\nget_variable_warm_start_value(::ReactivePowerVariable, d::PSY.SynchronousCondenser, ::AbstractReactivePowerDeviceFormulation) = PSY.get_reactive_power(d)\nget_variable_lower_bound(::ReactivePowerVariable, d::PSY.SynchronousCondenser, ::AbstractReactivePowerDeviceFormulation) = isnothing(PSY.get_reactive_power_limits(d)) ? nothing : PSY.get_reactive_power_limits(d).min\nget_variable_upper_bound(::ReactivePowerVariable, d::PSY.SynchronousCondenser, ::AbstractReactivePowerDeviceFormulation) = isnothing(PSY.get_reactive_power_limits(d)) ? nothing : PSY.get_reactive_power_limits(d).max\n\n#! format: on\nfunction get_initial_conditions_device_model(\n    model::OperationModel,\n    ::DeviceModel{T, D},\n) where {T <: PSY.SynchronousCondenser, D <: AbstractReactivePowerDeviceFormulation}\n    return DeviceModel(T, SynchronousCondenserBasicDispatch)\nend\n\nfunction get_default_attributes(\n    ::Type{U},\n    ::Type{V},\n) where {U <: PSY.SynchronousCondenser, V <: AbstractReactivePowerDeviceFormulation}\n    return Dict{String, Any}()\nend\n\nfunction get_default_time_series_names(\n    ::Type{<:PSY.SynchronousCondenser},\n    ::Type{<:AbstractReactivePowerDeviceFormulation},\n)\n    return Dict{Type{<:TimeSeriesParameter}, String}()\nend\n"
  },
  {
    "path": "src/devices_models/devices/regulation_device.jl",
    "content": "#! format: off\nget_variable_multiplier(_, ::Type{PSY.RegulationDevice{PSY.ThermalStandard}}, ::DeviceLimitedRegulation) = NaN\n############################ DeltaActivePowerUpVariable, RegulationDevice ###########################\n\nget_variable_binary(::DeltaActivePowerUpVariable, ::Type{<:PSY.RegulationDevice}, ::AbstractRegulationFormulation) = false\nget_variable_lower_bound(::DeltaActivePowerUpVariable, ::PSY.RegulationDevice, ::AbstractRegulationFormulation) = 0.0\n\n############################ DeltaActivePowerDownVariable, RegulationDevice ###########################\n\nget_variable_binary(::DeltaActivePowerDownVariable, ::Type{<:PSY.RegulationDevice}, ::AbstractRegulationFormulation) = false\nget_variable_lower_bound(::DeltaActivePowerDownVariable, ::PSY.RegulationDevice, ::AbstractRegulationFormulation) = 0.0\n\n############################ AdditionalDeltaActivePowerUpVariable, RegulationDevice ###########################\n\nget_variable_binary(::AdditionalDeltaActivePowerUpVariable, ::Type{<:PSY.RegulationDevice}, ::AbstractRegulationFormulation) = false\nget_variable_lower_bound(::AdditionalDeltaActivePowerUpVariable, ::PSY.RegulationDevice, ::AbstractRegulationFormulation) = 0.0\n\n############################ AdditionalDeltaActivePowerDownVariable, RegulationDevice ###########################\n\nget_variable_binary(::AdditionalDeltaActivePowerDownVariable, ::Type{<:PSY.RegulationDevice}, ::AbstractRegulationFormulation) = false\nget_variable_lower_bound(::AdditionalDeltaActivePowerDownVariable, ::PSY.RegulationDevice, ::AbstractRegulationFormulation) = 0.0\n\nget_multiplier_value(::ActivePowerTimeSeriesParameter, d::PSY.RegulationDevice, _)  = PSY.get_max_active_power(d)\n\n########################Objective Function##################################################\nproportional_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\nproportional_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\n\nobjective_function_multiplier(::VariableType, ::AbstractRegulationFormulation)=OBJECTIVE_FUNCTION_POSITIVE\n\n#! format: on\n\nfunction get_default_time_series_names(\n    ::Type{<:PSY.RegulationDevice{T}},\n    ::Type{<:AbstractRegulationFormulation},\n) where {T <: PSY.StaticInjection}\n    return Dict{Type{<:TimeSeriesParameter}, String}(\n        ActivePowerTimeSeriesParameter => \"max_active_power\",\n    )\nend\n\nfunction get_default_attributes(\n    ::Type{<:PSY.RegulationDevice{T}},\n    ::Type{<:AbstractRegulationFormulation},\n) where {T <: PSY.StaticInjection}\n    return Dict{String, Any}()\nend\n\nfunction add_constraints!(\n    container::OptimizationContainer,\n    ::Type{S},\n    ::Type{DeltaActivePowerUpVariable},\n    devices::IS.FlattenIteratorWrapper{T},\n    ::DeviceModel{T, DeviceLimitedRegulation},\n    ::NetworkModel{AreaBalancePowerModel},\n) where {\n    S <: RegulationLimitsConstraint,\n    T <: PSY.RegulationDevice{U},\n} where {U <: PSY.StaticInjection}\n    var_up = get_variable(container, DeltaActivePowerUpVariable(), T)\n    var_dn = get_variable(container, DeltaActivePowerDownVariable(), T)\n    base_points_param = get_parameter(container, ActivePowerTimeSeriesParameter(), T)\n    multiplier = get_multiplier_array(base_points_param)\n\n    names = [PSY.get_name(g) for g in devices]\n    time_steps = get_time_steps(container)\n\n    container_up =\n        add_constraints_container!(container, S(), U, names, time_steps; meta = \"up\")\n    container_dn =\n        add_constraints_container!(container, S(), U, names, time_steps; meta = \"dn\")\n\n    for d in devices\n        name = PSY.get_name(d)\n        limits = PSY.get_active_power_limits(d)\n        param = get_parameter_column_refs(base_points_param, name)\n        for t in time_steps\n            container_up[name, t] = JuMP.@constraint(\n                container.JuMPmodel,\n                var_up[name, t] <= limits.max - param[t] * multiplier[name, t]\n            )\n            container_dn[name, t] = JuMP.@constraint(\n                container.JuMPmodel,\n                var_dn[name, t] <= param[t] * multiplier[name, t] - limits.min\n            )\n        end\n    end\n    return\nend\n\nfunction add_constraints!(\n    container::OptimizationContainer,\n    ::Type{S},\n    ::Type{DeltaActivePowerUpVariable},\n    devices::IS.FlattenIteratorWrapper{T},\n    ::DeviceModel{T, ReserveLimitedRegulation},\n    ::NetworkModel{AreaBalancePowerModel},\n) where {\n    S <: RegulationLimitsConstraint,\n    T <: PSY.RegulationDevice{U},\n} where {U <: PSY.StaticInjection}\n    var_up = get_variable(container, DeltaActivePowerUpVariable(), T)\n    var_dn = get_variable(container, DeltaActivePowerDownVariable(), T)\n\n    names = [PSY.get_name(g) for g in devices]\n    time_steps = get_time_steps(container)\n\n    container_up =\n        add_constraints_container!(container, S(), U, names, time_steps; meta = \"up\")\n    container_dn =\n        add_constraints_container!(container, S(), U, names, time_steps; meta = \"dn\")\n\n    for d in devices\n        name = PSY.get_name(d)\n        limit_up = PSY.get_reserve_limit_up(d)\n        limit_dn = PSY.get_reserve_limit_dn(d)\n        for t in time_steps\n            container_up[name, t] =\n                JuMP.@constraint(container.JuMPmodel, var_up[name, t] <= limit_up)\n            container_dn[name, t] =\n                JuMP.@constraint(container.JuMPmodel, var_dn[name, t] <= limit_dn)\n        end\n    end\n    return\nend\n\nfunction add_constraints!(\n    container::OptimizationContainer,\n    ::Type{S},\n    devices::IS.FlattenIteratorWrapper{T},\n    ::DeviceModel{T, DeviceLimitedRegulation},\n    ::NetworkModel{AreaBalancePowerModel},\n) where {\n    S <: RampLimitConstraint,\n    T <: PSY.RegulationDevice{U},\n} where {U <: PSY.StaticInjection}\n    R_up = get_variable(container, DeltaActivePowerUpVariable(), T)\n    R_dn = get_variable(container, DeltaActivePowerDownVariable(), T)\n\n    resolution = Dates.value(Dates.Minute(get_resolution(container)))\n    names = [PSY.get_name(g) for g in devices]\n    time_steps = get_time_steps(container)\n\n    container_up =\n        add_constraints_container!(container, S(), U, names, time_steps; meta = \"up\")\n    container_dn =\n        add_constraints_container!(container, S(), U, names, time_steps; meta = \"dn\")\n\n    for d in devices\n        ramp_limits = PSY.get_ramp_limits(d)\n        ramp_limits === nothing && continue\n        scaling_factor = resolution * SECONDS_IN_MINUTE\n        name = PSY.get_name(d)\n        for t in time_steps\n            container_up[name, t] = JuMP.@constraint(\n                container.JuMPmodel,\n                R_up[name, t] <= ramp_limits.up * scaling_factor\n            )\n            container_dn[name, t] = JuMP.@constraint(\n                container.JuMPmodel,\n                R_dn[name, t] <= ramp_limits.down * scaling_factor\n            )\n        end\n    end\n    return\nend\n\nfunction add_constraints!(\n    container::OptimizationContainer,\n    ::Type{S},\n    devices::IS.FlattenIteratorWrapper{T},\n    ::DeviceModel{T, <:AbstractRegulationFormulation},\n    ::NetworkModel{AreaBalancePowerModel},\n) where {\n    S <: ParticipationAssignmentConstraint,\n    T <: PSY.RegulationDevice{U},\n} where {U <: PSY.StaticInjection}\n    time_steps = get_time_steps(container)\n    R_up = get_variable(container, DeltaActivePowerUpVariable(), T)\n    R_dn = get_variable(container, DeltaActivePowerDownVariable(), T)\n    R_up_emergency = get_variable(container, AdditionalDeltaActivePowerUpVariable(), T)\n    R_dn_emergency = get_variable(container, AdditionalDeltaActivePowerUpVariable(), T)\n    area_reserve_up = get_variable(container, DeltaActivePowerUpVariable(), PSY.AGC)\n    area_reserve_dn = get_variable(container, DeltaActivePowerDownVariable(), PSY.AGC)\n\n    component_names = PSY.get_name.(devices)\n    participation_assignment_up = add_constraints_container!(\n        container,\n        S(),\n        T,\n        component_names,\n        time_steps;\n        meta = \"up\",\n    )\n    participation_assignment_dn = add_constraints_container!(\n        container,\n        S(),\n        T,\n        component_names,\n        time_steps;\n        meta = \"dn\",\n    )\n\n    expr_up = get_expression(container, EmergencyUp(), PSY.Area)\n    expr_dn = get_expression(container, EmergencyDown(), PSY.Area)\n    for d in devices\n        name = PSY.get_name(d)\n        services = PSY.get_services(d)\n        if length(services) > 1\n            device_agc = (a for a in PSY.get_services(d) if isa(a, PSY.AGC))\n            agc_name = PSY.get_name.(device_agc)[1]\n            area_name = PSY.get_name.(PSY.get_area.(device_agc))[1]\n        else\n            device_agc = first(services)\n            agc_name = PSY.get_name(device_agc)\n            area_name = PSY.get_name(PSY.get_area(device_agc))\n        end\n        p_factor = PSY.get_participation_factor(d)\n        for t in time_steps\n            participation_assignment_up[name, t] = JuMP.@constraint(\n                container.JuMPmodel,\n                R_up[name, t] ==\n                (p_factor.up * area_reserve_up[agc_name, t]) + R_up_emergency[name, t]\n            )\n            participation_assignment_dn[name, t] = JuMP.@constraint(\n                container.JuMPmodel,\n                R_dn[name, t] ==\n                (p_factor.dn * area_reserve_dn[agc_name, t]) + R_dn_emergency[name, t]\n            )\n            JuMP.add_to_expression!(expr_up[area_name, t], -1.0, R_up_emergency[name, t])\n            JuMP.add_to_expression!(expr_dn[area_name, t], -1.0, R_dn_emergency[name, t])\n        end\n    end\n\n    return\nend\n\nfunction objective_function!(\n    container::OptimizationContainer,\n    devs::IS.FlattenIteratorWrapper{T},\n    ::DeviceModel{T, U},\n) where {\n    T <: PSY.RegulationDevice{V},\n    U <: AbstractRegulationFormulation,\n} where {V <: PSY.StaticInjection}\n    add_proportional_cost!(container, AdditionalDeltaActivePowerUpVariable(), devs, U())\n    add_proportional_cost!(container, AdditionalDeltaActivePowerDownVariable(), devs, U())\n    return\nend\n"
  },
  {
    "path": "src/devices_models/devices/renewable_generation.jl",
    "content": "#! format: off\nget_variable_multiplier(_, ::Type{<:PSY.RenewableGen}, ::AbstractRenewableFormulation) = 1.0\nget_expression_type_for_reserve(::ActivePowerReserveVariable, ::Type{<:PSY.RenewableGen}, ::Type{<:PSY.Reserve{PSY.ReserveUp}}) = ActivePowerRangeExpressionUB\nget_expression_type_for_reserve(::ActivePowerReserveVariable, ::Type{<:PSY.RenewableGen}, ::Type{<:PSY.Reserve{PSY.ReserveDown}}) = ActivePowerRangeExpressionLB\n########################### Parameter related set functions ################################\nget_parameter_multiplier(::VariableValueParameter, d::PSY.RenewableGen, ::AbstractRenewableFormulation) = 1.0\n\n########################### ActivePowerVariable, RenewableGen #################################\n\nget_variable_binary(::ActivePowerVariable, ::Type{<:PSY.RenewableGen}, ::AbstractRenewableFormulation) = false\nget_min_max_limits(d::PSY.RenewableGen, ::Type{ActivePowerVariableLimitsConstraint}, ::Type{<:AbstractRenewableFormulation}) = (min = 0.0, max = PSY.get_max_active_power(d))\nget_variable_lower_bound(::ActivePowerVariable, d::PSY.RenewableGen, ::AbstractRenewableFormulation) = 0.0\nget_variable_upper_bound(::ActivePowerVariable, d::PSY.RenewableGen, ::AbstractRenewableFormulation) = PSY.get_max_active_power(d)\n\n########################### ReactivePowerVariable, RenewableGen #################################\n\nget_variable_binary(::ReactivePowerVariable, ::Type{<:PSY.RenewableGen}, ::AbstractRenewableFormulation) = false\n\nget_multiplier_value(::TimeSeriesParameter, d::PSY.RenewableGen, ::FixedOutput) = PSY.get_max_active_power(d)\nget_multiplier_value(::TimeSeriesParameter, d::PSY.RenewableGen, ::AbstractRenewableFormulation) = PSY.get_max_active_power(d)\n\n# To avoid ambiguity with default_interface_methods.jl:\nget_multiplier_value(::AbstractPiecewiseLinearBreakpointParameter, ::PSY.RenewableGen, ::FixedOutput) = 1.0\nget_multiplier_value(::AbstractPiecewiseLinearBreakpointParameter, ::PSY.RenewableGen, ::AbstractRenewableFormulation) = 1.0\n\n########################Objective Function##################################################\nobjective_function_multiplier(::ActivePowerVariable, ::AbstractRenewableDispatchFormulation)=OBJECTIVE_FUNCTION_NEGATIVE\n\nvariable_cost(::Nothing, ::ActivePowerVariable, ::PSY.RenewableDispatch, ::AbstractRenewableDispatchFormulation)=0.0\nvariable_cost(cost::PSY.OperationalCost, ::ActivePowerVariable, ::PSY.RenewableDispatch, ::AbstractRenewableDispatchFormulation)=PSY.get_variable(cost)\n\n#! format: on\n\nget_initial_conditions_device_model(\n    ::OperationModel,\n    ::DeviceModel{T, <:AbstractRenewableFormulation},\n) where {T <: PSY.RenewableGen} = DeviceModel(T, RenewableFullDispatch)\n\nget_initial_conditions_device_model(\n    ::OperationModel,\n    ::DeviceModel{T, FixedOutput},\n) where {T <: PSY.RenewableGen} = DeviceModel(T, FixedOutput)\n\nfunction get_min_max_limits(\n    device,\n    ::Type{ReactivePowerVariableLimitsConstraint},\n    ::Type{<:AbstractRenewableFormulation},\n)\n    return PSY.get_reactive_power_limits(device)\nend\n\nfunction get_default_time_series_names(\n    ::Type{<:PSY.RenewableGen},\n    ::Type{<:Union{FixedOutput, AbstractRenewableFormulation}},\n)\n    return Dict{Type{<:TimeSeriesParameter}, String}(\n        ActivePowerTimeSeriesParameter => \"max_active_power\",\n        ReactivePowerTimeSeriesParameter => \"max_active_power\",\n    )\nend\n\nfunction get_default_attributes(\n    ::Type{<:PSY.RenewableGen},\n    ::Type{<:Union{FixedOutput, AbstractRenewableFormulation}},\n)\n    return Dict{String, Any}()\nend\n\n####################################### Reactive Power constraint_infos #########################\n\nfunction add_constraints!(\n    container::OptimizationContainer,\n    T::Type{<:ReactivePowerVariableLimitsConstraint},\n    U::Type{<:ReactivePowerVariable},\n    devices::IS.FlattenIteratorWrapper{V},\n    model::DeviceModel{V, W},\n    ::NetworkModel{X},\n) where {V <: PSY.RenewableGen, W <: AbstractDeviceFormulation, X <: PM.AbstractPowerModel}\n    add_range_constraints!(container, T, U, devices, model, X)\n    return\nend\n\n\"\"\"\nReactive Power Constraints on Renewable Gen Constant power_factor\n\"\"\"\nfunction add_constraints!(\n    container::OptimizationContainer,\n    ::Type{<:ReactivePowerVariableLimitsConstraint},\n    ::Type{<:ReactivePowerVariable},\n    devices::IS.FlattenIteratorWrapper{V},\n    ::DeviceModel{V, W},\n    ::NetworkModel{X},\n) where {\n    V <: PSY.RenewableGen,\n    W <: RenewableConstantPowerFactor,\n    X <: PM.AbstractPowerModel,\n}\n    names = PSY.get_name.(devices)\n    time_steps = get_time_steps(container)\n    p_var = get_variable(container, ActivePowerVariable(), V)\n    q_var = get_variable(container, ReactivePowerVariable(), V)\n    jump_model = get_jump_model(container)\n    constraint =\n        add_constraints_container!(container, EqualityConstraint(), V, names, time_steps)\n    for t in time_steps, d in devices\n        name = PSY.get_name(d)\n        pf = sin(acos(PSY.get_power_factor(d)))\n        constraint[name, t] =\n            JuMP.@constraint(jump_model, q_var[name, t] == p_var[name, t] * pf)\n    end\nend\n\nfunction add_constraints!(\n    container::OptimizationContainer,\n    ::Type{ActivePowerVariableLimitsConstraint},\n    U::Type{<:Union{VariableType, ActivePowerRangeExpressionUB}},\n    devices::IS.FlattenIteratorWrapper{V},\n    model::DeviceModel{V, W},\n    ::NetworkModel{X},\n) where {\n    V <: PSY.RenewableGen,\n    W <: AbstractRenewableDispatchFormulation,\n    X <: PM.AbstractPowerModel,\n}\n    add_parameterized_upper_bound_range_constraints(\n        container,\n        ActivePowerVariableTimeSeriesLimitsConstraint,\n        U,\n        ActivePowerTimeSeriesParameter,\n        devices,\n        model,\n        X,\n    )\n    return\nend\n\nfunction add_constraints!(\n    container::OptimizationContainer,\n    T::Type{ActivePowerVariableLimitsConstraint},\n    U::Type{ActivePowerRangeExpressionLB},\n    devices::IS.FlattenIteratorWrapper{V},\n    model::DeviceModel{V, W},\n    ::NetworkModel{X},\n) where {\n    V <: PSY.RenewableGen,\n    W <: AbstractRenewableDispatchFormulation,\n    X <: PM.AbstractPowerModel,\n}\n    add_range_constraints!(\n        container,\n        T,\n        U,\n        devices,\n        model,\n        X,\n    )\n    return\nend\n\n##################################### renewable generation cost ############################\nfunction objective_function!(\n    container::OptimizationContainer,\n    devices::IS.FlattenIteratorWrapper{T},\n    ::DeviceModel{T, U},\n    ::Type{<:PM.AbstractPowerModel},\n) where {T <: PSY.RenewableGen, U <: AbstractRenewableDispatchFormulation}\n    add_variable_cost!(container, ActivePowerVariable(), devices, U())\n    add_curtailment_cost!(container, ActivePowerVariable(), devices, U())\n    return\nend\n"
  },
  {
    "path": "src/devices_models/devices/source.jl",
    "content": "#! format: off\n\nrequires_initialization(::ImportExportSourceModel) = false\n\n\nget_variable_multiplier(::ActivePowerOutVariable, ::Type{<:PSY.Source}, ::AbstractSourceFormulation) = 1.0\nget_variable_multiplier(::ActivePowerInVariable, ::Type{<:PSY.Source}, ::AbstractSourceFormulation) = -1.0\nget_variable_multiplier(::ReactivePowerVariable, ::Type{<:PSY.Source}, ::AbstractSourceFormulation) = 1.0\n############## ActivePowerVariables, Source ####################\nget_variable_binary(::ActivePowerInVariable, ::Type{<:PSY.Source}, ::AbstractSourceFormulation) = false\nget_variable_binary(::ActivePowerOutVariable, ::Type{<:PSY.Source}, ::AbstractSourceFormulation) = false\nget_variable_lower_bound(::ActivePowerInVariable, d::PSY.Source, ::AbstractSourceFormulation) = 0.0\nget_variable_lower_bound(::ActivePowerOutVariable, d::PSY.Source, ::AbstractSourceFormulation) = 0.0\nget_variable_upper_bound(::ActivePowerInVariable, d::PSY.Source, ::AbstractSourceFormulation) = -PSY.get_active_power_limits(d).min\nget_variable_upper_bound(::ActivePowerOutVariable, d::PSY.Source, ::AbstractSourceFormulation) = PSY.get_active_power_limits(d).max\n\n############## ReactivePowerVariable, Source ####################\nget_variable_binary(::ReactivePowerVariable, ::Type{<:PSY.Source}, ::AbstractSourceFormulation) = false\nget_variable_lower_bound(::ReactivePowerVariable, d::PSY.Source, ::AbstractSourceFormulation) = PSY.get_reactive_power_limits(d).min\nget_variable_upper_bound(::ReactivePowerVariable, d::PSY.Source, ::AbstractSourceFormulation) = PSY.get_reactive_power_limits(d).max\n\nget_multiplier_value(::ActivePowerTimeSeriesParameter, d::PSY.Source, ::AbstractSourceFormulation) = PSY.get_active_power_limits(d).max\nget_multiplier_value(::ActivePowerOutTimeSeriesParameter, d::PSY.Source, ::AbstractSourceFormulation) = PSY.get_active_power_limits(d).max\nget_multiplier_value(::ActivePowerInTimeSeriesParameter, d::PSY.Source, ::AbstractSourceFormulation) = PSY.get_active_power_limits(d).max\nget_multiplier_value(::ActivePowerTimeSeriesParameter, d::PSY.Source, ::FixedOutput) = PSY.get_active_power_limits(d).max\nget_multiplier_value(::ActivePowerOutTimeSeriesParameter, d::PSY.Source, ::FixedOutput) = PSY.get_active_power_limits(d).max\nget_multiplier_value(::ActivePowerInTimeSeriesParameter, d::PSY.Source, ::FixedOutput) = PSY.get_active_power_limits(d).min\n# This additional method definition is used to avoid ambiguity with the method defined in default_interface_methods.jl\nget_multiplier_value(::AbstractPiecewiseLinearBreakpointParameter, d::PSY.Source, ::AbstractSourceFormulation) = 1.0\n\n############## ReservationVariable, Source ####################\nget_variable_binary(::ReservationVariable, ::Type{<:PSY.Source}, ::ImportExportSourceModel) = true\n\n\n#! format: on\nfunction get_default_time_series_names(\n    ::Type{U},\n    ::Type{V},\n) where {U <: PSY.Source, V <: Union{FixedOutput, AbstractSourceFormulation}}\n    return Dict{Type{<:TimeSeriesParameter}, String}(\n        ActivePowerOutTimeSeriesParameter => \"max_active_power_out\",\n        ActivePowerInTimeSeriesParameter => \"max_active_power_in\",\n    )\nend\n\nfunction get_default_attributes(\n    ::Type{U},\n    ::Type{V},\n) where {U <: PSY.Source, V <: Union{FixedOutput, AbstractSourceFormulation}}\n    return Dict{String, Any}(\n        \"reservation\" => true,\n    )\nend\n\nfunction get_min_max_limits(\n    device,\n    ::Type{ActivePowerVariableLimitsConstraint},\n    ::Type{<:AbstractSourceFormulation},\n)\n    return PSY.get_active_power_limits(device)\nend\n\nfunction get_min_max_limits(\n    device,\n    ::Type{InputActivePowerVariableLimitsConstraint},\n    ::Type{<:AbstractSourceFormulation},\n)\n    return PSY.get_active_power_limits(device)  # TODO do we need a new field in PSY for this -- input active power limits?\nend\n\nfunction get_min_max_limits(\n    device,\n    ::Type{ReactivePowerVariableLimitsConstraint},\n    ::Type{<:AbstractSourceFormulation},\n)\n    return PSY.get_reactive_power_limits(device)\nend\n\n##### Constraints ######\n\nfunction add_constraints!(\n    container::OptimizationContainer,\n    T::Type{<:PowerVariableLimitsConstraint},\n    U::Type{<:Union{VariableType, ExpressionType}},\n    devices::IS.FlattenIteratorWrapper{V},\n    model::DeviceModel{V, W},\n    ::NetworkModel{X},\n) where {\n    V <: PSY.Source,\n    W <: AbstractSourceFormulation,\n    X <: PM.AbstractPowerModel,\n}\n    if get_attribute(model, \"reservation\")\n        add_reserve_range_constraints!(container, T, U, devices, model, X)\n    else\n        add_range_constraints!(container, T, U, devices, model, X)\n    end\n    return\nend\n\nfunction add_constraints!(\n    container::OptimizationContainer,\n    T::Type{ImportExportBudgetConstraint},\n    devices::IS.FlattenIteratorWrapper{U},\n    model::DeviceModel{U, V},\n    network_model::NetworkModel{X},\n) where {\n    U <: PSY.Source,\n    V <: AbstractSourceFormulation,\n    X <: PM.AbstractPowerModel,\n}\n    time_steps = get_time_steps(container)\n    resolution = get_resolution(container)\n    resolution_in_hours = Dates.Hour(resolution).value\n    hours_in_horizon = length(time_steps) * resolution_in_hours\n    p_out = get_variable(container, ActivePowerOutVariable(), U)\n    p_in = get_variable(container, ActivePowerInVariable(), U)\n    names = PSY.get_name.(devices)\n    constraint_export =\n        add_constraints_container!(\n            container,\n            ImportExportBudgetConstraint(),\n            U,\n            names;\n            meta = \"export\",\n        )\n    constraint_import = add_constraints_container!(\n        container,\n        ImportExportBudgetConstraint(),\n        U,\n        names;\n        meta = \"import\",\n    )\n\n    for d in devices\n        name = PSY.get_name(d)\n        op_cost = PSY.get_operation_cost(d)\n        week_import_limit = PSY.get_energy_import_weekly_limit(op_cost)\n        week_export_limit = PSY.get_energy_export_weekly_limit(op_cost)\n        constraint_import[name] = JuMP.@constraint(\n            get_jump_model(container),\n            resolution_in_hours * sum(p_out[name, t] for t in time_steps) <=\n            week_import_limit * (hours_in_horizon / HOURS_IN_WEEK)\n        )\n        constraint_export[name] = JuMP.@constraint(\n            get_jump_model(container),\n            resolution_in_hours * sum(p_in[name, t] for t in time_steps) <=\n            week_export_limit * (hours_in_horizon / HOURS_IN_WEEK)\n        )\n    end\n    return\nend\n\nfunction add_constraints!(\n    container::OptimizationContainer,\n    ::Type{ActivePowerOutVariableTimeSeriesLimitsConstraint},\n    U::Type{<:Union{ActivePowerOutVariable, ActivePowerRangeExpressionUB}},\n    devices::IS.FlattenIteratorWrapper{V},\n    model::DeviceModel{V, W},\n    ::NetworkModel{X},\n) where {\n    V <: PSY.Source,\n    W <: AbstractSourceFormulation,\n    X <: PM.AbstractPowerModel,\n}\n    add_parameterized_upper_bound_range_constraints(\n        container,\n        ActivePowerOutVariableTimeSeriesLimitsConstraint,\n        U,\n        ActivePowerOutTimeSeriesParameter,\n        devices,\n        model,\n        X,\n    )\n    return\nend\n\nfunction add_constraints!(\n    container::OptimizationContainer,\n    ::Type{ActivePowerInVariableTimeSeriesLimitsConstraint},\n    U::Type{<:Union{ActivePowerInVariable, ActivePowerRangeExpressionUB}},\n    devices::IS.FlattenIteratorWrapper{V},\n    model::DeviceModel{V, W},\n    ::NetworkModel{X},\n) where {\n    V <: PSY.Source,\n    W <: AbstractSourceFormulation,\n    X <: PM.AbstractPowerModel,\n}\n    add_parameterized_upper_bound_range_constraints(\n        container,\n        ActivePowerInVariableTimeSeriesLimitsConstraint,\n        U,\n        ActivePowerInTimeSeriesParameter,\n        devices,\n        model,\n        X,\n    )\n    return\nend\n\nfunction PSI.objective_function!(\n    container::PSI.OptimizationContainer,\n    devices::IS.FlattenIteratorWrapper{T},\n    ::PSI.DeviceModel{T, U},\n    ::Type{V},\n) where {T <: PSY.Source, U <: AbstractSourceFormulation, V <: PM.AbstractPowerModel}\n    PSI.add_variable_cost!(container, PSI.ActivePowerOutVariable(), devices, U())\n    PSI.add_variable_cost!(container, PSI.ActivePowerInVariable(), devices, U())\n    return\nend\n\nget_initial_conditions_device_model(\n    ::OperationModel,\n    model::DeviceModel{PSY.Source, T},\n) where {T <: AbstractSourceFormulation} = DeviceModel(PSY.Source, T)\n"
  },
  {
    "path": "src/devices_models/devices/thermal_generation.jl",
    "content": "#! format: off\n\nrequires_initialization(::AbstractThermalFormulation) = false\nrequires_initialization(::AbstractThermalUnitCommitment) = true\nrequires_initialization(::ThermalStandardDispatch) = true\nrequires_initialization(::ThermalBasicCompactUnitCommitment) = false\nrequires_initialization(::ThermalBasicUnitCommitment) = false\n\nget_variable_multiplier(_, ::Type{<:PSY.ThermalGen}, ::AbstractThermalFormulation) = 1.0\nget_variable_multiplier(::OnVariable, d::PSY.ThermalGen, ::Union{AbstractCompactUnitCommitment, ThermalCompactDispatch}) = PSY.get_active_power_limits(d).min\nget_expression_type_for_reserve(::ActivePowerReserveVariable, ::Type{<:PSY.ThermalGen}, ::Type{<:PSY.Reserve{PSY.ReserveUp}}) = ActivePowerRangeExpressionUB\nget_expression_type_for_reserve(::ActivePowerReserveVariable, ::Type{<:PSY.ThermalGen}, ::Type{<:PSY.Reserve{PSY.ReserveDown}}) = ActivePowerRangeExpressionLB\n\n############## ActivePowerVariable, ThermalGen ####################\nget_variable_binary(::ActivePowerVariable, ::Type{<:PSY.ThermalGen}, ::AbstractThermalFormulation) = false\nget_variable_warm_start_value(::ActivePowerVariable, d::PSY.ThermalGen, ::AbstractThermalFormulation) = PSY.get_active_power(d)\nget_variable_lower_bound(::ActivePowerVariable, d::PSY.ThermalGen, ::AbstractThermalFormulation) = PSY.get_must_run(d) ? PSY.get_active_power_limits(d).min : 0.0\nget_variable_upper_bound(::ActivePowerVariable, d::PSY.ThermalGen, ::AbstractThermalFormulation) = PSY.get_active_power_limits(d).max\nget_variable_lower_bound(::ActivePowerVariable, d::PSY.ThermalGen, ::ThermalDispatchNoMin) = 0.0\n\n############## PowerAboveMinimumVariable, ThermalGen ####################\nget_variable_binary(::PowerAboveMinimumVariable, ::Type{<:PSY.ThermalGen}, ::AbstractThermalFormulation) = false\nget_variable_warm_start_value(::PowerAboveMinimumVariable, d::PSY.ThermalGen, ::AbstractCompactUnitCommitment) = max(0.0, PSY.get_active_power(d) - PSY.get_active_power_limits(d).min)\nget_variable_lower_bound(::PowerAboveMinimumVariable, d::PSY.ThermalGen, ::AbstractThermalFormulation) = 0.0\nget_variable_upper_bound(::PowerAboveMinimumVariable, d::PSY.ThermalGen, ::AbstractThermalFormulation) = PSY.get_active_power_limits(d).max - PSY.get_active_power_limits(d).min\n\n############## ReactivePowerVariable, ThermalGen ####################\nget_variable_binary(::ReactivePowerVariable, ::Type{<:PSY.ThermalGen}, ::AbstractThermalFormulation) = false\nget_variable_warm_start_value(::ReactivePowerVariable, d::PSY.ThermalGen, ::AbstractThermalFormulation) = PSY.get_reactive_power(d)\nget_variable_lower_bound(::ReactivePowerVariable, d::PSY.ThermalGen, ::AbstractThermalFormulation) = PSY.get_reactive_power_limits(d).min\nget_variable_upper_bound(::ReactivePowerVariable, d::PSY.ThermalGen, ::AbstractThermalFormulation) = PSY.get_reactive_power_limits(d).max\n\n############## OnVariable, ThermalGen ####################\nget_variable_binary(::OnVariable, ::Type{<:PSY.ThermalGen}, ::AbstractThermalFormulation) = true\nget_variable_warm_start_value(::OnVariable, d::PSY.ThermalGen, ::AbstractThermalFormulation) = PSY.get_status(d) ? 1.0 : 0.0\nget_variable_lower_bound(::OnVariable, d::PSY.ThermalGen, ::AbstractThermalUnitCommitment) = PSY.get_must_run(d) ? 1.0 : 0.0\n\n############## StopVariable, ThermalGen ####################\nget_variable_binary(::StopVariable, ::Type{<:PSY.ThermalGen}, ::AbstractThermalFormulation) = true\nget_variable_lower_bound(::StopVariable, d::PSY.ThermalGen, ::AbstractThermalFormulation) = 0.0\nget_variable_upper_bound(::StopVariable, d::PSY.ThermalGen, ::AbstractThermalFormulation) = 1.0\n\n############## StartVariable, ThermalGen ####################\nget_variable_binary(::StartVariable, d::Type{<:PSY.ThermalGen}, ::AbstractThermalFormulation) = true\nget_variable_lower_bound(::StartVariable, d::PSY.ThermalGen, ::AbstractThermalFormulation) = 0.0\nget_variable_upper_bound(::StartVariable, d::PSY.ThermalGen, ::AbstractThermalFormulation) = 1.0\n\n############## ColdStartVariable, WarmStartVariable, HotStartVariable ############\nget_variable_binary(::Union{ColdStartVariable, WarmStartVariable, HotStartVariable}, ::Type{PSY.ThermalMultiStart}, ::AbstractThermalFormulation) = true\n\n############## SlackVariables, ThermalGen ####################\n# LB Slack #\nget_variable_binary(::RateofChangeConstraintSlackDown, ::Type{<:PSY.ThermalGen}, ::AbstractThermalFormulation) = false\nget_variable_lower_bound(::RateofChangeConstraintSlackDown, d::PSY.ThermalGen, ::AbstractThermalFormulation) = 0.0\n# UB Slack #\nget_variable_binary(::RateofChangeConstraintSlackUp, ::Type{<:PSY.ThermalGen}, ::AbstractThermalFormulation) = false\nget_variable_lower_bound(::RateofChangeConstraintSlackUp, d::PSY.ThermalGen, ::AbstractThermalFormulation) = 0.0\n\n########################### Parameter related set functions ################################\nget_multiplier_value(::ActivePowerTimeSeriesParameter, d::PSY.ThermalGen, ::AbstractThermalFormulation) = PSY.get_max_active_power(d)\nget_multiplier_value(::FuelCostParameter, d::PSY.ThermalGen, ::AbstractThermalFormulation) = 1.0\nget_parameter_multiplier(::VariableValueParameter, d::PSY.ThermalGen, ::AbstractThermalFormulation) = 1.0\nget_initial_parameter_value(::VariableValueParameter, d::PSY.ThermalGen, ::AbstractThermalFormulation) = 1.0\nget_expression_multiplier(::OnStatusParameter, ::ActivePowerRangeExpressionUB, d::PSY.ThermalGen, ::AbstractThermalFormulation) = PSY.get_active_power_limits(d).max\nget_expression_multiplier(::OnStatusParameter, ::ActivePowerRangeExpressionLB, d::PSY.ThermalGen, ::AbstractThermalFormulation) = PSY.get_active_power_limits(d).min\nget_expression_multiplier(::OnStatusParameter, ::ActivePowerRangeExpressionUB, d::PSY.ThermalGen, ::AbstractCompactUnitCommitment) = PSY.get_active_power_limits(d).max - PSY.get_active_power_limits(d).min\nget_expression_multiplier(::OnStatusParameter, ::ActivePowerRangeExpressionLB, d::PSY.ThermalGen, ::AbstractCompactUnitCommitment) = 0.0\nget_expression_multiplier(::OnStatusParameter, ::ActivePowerRangeExpressionUB, d::PSY.ThermalGen, ::ThermalCompactDispatch) = PSY.get_active_power_limits(d).max - PSY.get_active_power_limits(d).min\nget_expression_multiplier(::OnStatusParameter, ::ActivePowerRangeExpressionLB, d::PSY.ThermalGen, ::ThermalCompactDispatch) = 0.0\nget_expression_multiplier(::OnStatusParameter, ::ActivePowerBalance, d::PSY.ThermalGen, ::AbstractThermalFormulation) = PSY.get_active_power_limits(d).min\n\n#################### Initial Conditions for models ###############\ninitial_condition_default(::DeviceStatus, d::PSY.ThermalGen, ::AbstractThermalFormulation) = PSY.get_status(d) ? 1.0 : 0.0\ninitial_condition_variable(::DeviceStatus, d::PSY.ThermalGen, ::AbstractThermalFormulation) = OnVariable()\ninitial_condition_default(::DevicePower, d::PSY.ThermalGen, ::AbstractThermalFormulation) = PSY.get_active_power(d)\ninitial_condition_variable(::DevicePower, d::PSY.ThermalGen, ::AbstractThermalFormulation) = ActivePowerVariable()\ninitial_condition_default(::DeviceAboveMinPower, d::PSY.ThermalGen, ::AbstractThermalFormulation) = max(0.0, PSY.get_active_power(d) - PSY.get_active_power_limits(d).min)\ninitial_condition_variable(::DeviceAboveMinPower, d::PSY.ThermalGen, ::AbstractCompactUnitCommitment) = PowerAboveMinimumVariable()\ninitial_condition_variable(::DeviceAboveMinPower, d::PSY.ThermalGen, ::ThermalCompactDispatch) = PowerAboveMinimumVariable()\ninitial_condition_default(::InitialTimeDurationOn, d::PSY.ThermalGen, ::AbstractThermalFormulation) = PSY.get_status(d) ? PSY.get_time_at_status(d) : 0.0\ninitial_condition_variable(::InitialTimeDurationOn, d::PSY.ThermalGen, ::AbstractThermalFormulation) = OnVariable()\ninitial_condition_default(::InitialTimeDurationOff, d::PSY.ThermalGen, ::AbstractThermalFormulation) = !PSY.get_status(d) ? PSY.get_time_at_status(d) : 0.0\ninitial_condition_variable(::InitialTimeDurationOff, d::PSY.ThermalGen, ::AbstractThermalFormulation) = OnVariable()\n\n########################Objective Function##################################################\n# TODO: Decide what is the cost for OnVariable, if fixed or constant term in variable\nfunction proportional_cost(container::OptimizationContainer, cost::PSY.ThermalGenerationCost, S::OnVariable, T::PSY.ThermalGen, U::AbstractThermalFormulation, t::Int)\n    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)\nend\nis_time_variant_term(::OptimizationContainer, ::PSY.ThermalGenerationCost, ::OnVariable, ::PSY.ThermalGen, ::AbstractThermalFormulation, t::Int) = false\n\nproportional_cost(container::OptimizationContainer, cost::PSY.MarketBidCost, ::OnVariable, comp::PSY.ThermalGen, ::AbstractThermalFormulation, t::Int) =\n    _lookup_maybe_time_variant_param(container, comp, t,\n    Val(is_time_variant(PSY.get_incremental_initial_input(cost))),\n    PSY.get_initial_input ∘ PSY.get_incremental_offer_curves ∘ PSY.get_operation_cost,\n    IncrementalCostAtMinParameter())\nis_time_variant_term(::OptimizationContainer, cost::PSY.MarketBidCost, ::OnVariable, ::PSY.ThermalGen, ::AbstractThermalFormulation, t::Int) =\n    is_time_variant(PSY.get_incremental_initial_input(cost))\n\nproportional_cost(::Union{PSY.MarketBidCost, PSY.ThermalGenerationCost}, ::Union{RateofChangeConstraintSlackUp, RateofChangeConstraintSlackDown}, ::PSY.ThermalGen, ::AbstractThermalFormulation) = CONSTRAINT_VIOLATION_SLACK_COST\n\n\nhas_multistart_variables(::PSY.ThermalGen, ::AbstractThermalFormulation)=false\nhas_multistart_variables(::PSY.ThermalMultiStart, ::ThermalMultiStartUnitCommitment)=true\n\nobjective_function_multiplier(::VariableType, ::AbstractThermalFormulation)=OBJECTIVE_FUNCTION_POSITIVE\n\nsos_status(::PSY.ThermalGen, ::AbstractThermalDispatchFormulation)=SOSStatusVariable.NO_VARIABLE\nsos_status(::PSY.ThermalGen, ::AbstractThermalUnitCommitment)=SOSStatusVariable.VARIABLE\nsos_status(::PSY.ThermalMultiStart, ::AbstractStandardUnitCommitment)=SOSStatusVariable.VARIABLE\nsos_status(::PSY.ThermalMultiStart, ::ThermalMultiStartUnitCommitment)=SOSStatusVariable.VARIABLE\n\n# Startup cost interpretations!\n# Validators: check that the types match (formulation is optional) and redirect to the simpler methods\nstart_up_cost(cost, ::PSY.ThermalGen, ::T, ::Union{AbstractThermalFormulation, Nothing} = nothing) where {T <: StartVariable} =\n    start_up_cost(cost, T())\nstart_up_cost(cost, ::PSY.ThermalMultiStart, ::T, ::ThermalMultiStartUnitCommitment = ThermalMultiStartUnitCommitment()) where {T <: MultiStartVariable} =\n    start_up_cost(cost, T())\n\n# Implementations: given a single number, tuple, or StartUpStages and a variable, do the right thing\n# Single number to anything\nstart_up_cost(cost::Float64, ::StartVariable) = cost\n# 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?\nstart_up_cost(cost::Float64, ::T) where {T <: MultiStartVariable} =\n    start_up_cost((hot = cost, warm = cost, cold = cost), T())\n\n# 3-tuple to anything\nstart_up_cost(cost::NTuple{3, Float64}, ::T) where {T <: VariableType} =\n    start_up_cost(StartUpStages(cost), T())\n\n# `StartUpStages` to anything\nstart_up_cost(cost::StartUpStages, ::ColdStartVariable) = cost.cold\nstart_up_cost(cost::StartUpStages, ::WarmStartVariable) = cost.warm\nstart_up_cost(cost::StartUpStages, ::HotStartVariable) = cost.hot\n# TODO in the opposite case, do we want to get the maximum or the hot?\nstart_up_cost(cost::StartUpStages, ::StartVariable) = maximum(cost)\n\nuses_compact_power(::PSY.ThermalGen, ::AbstractThermalFormulation)=false\nuses_compact_power(::PSY.ThermalGen, ::AbstractCompactUnitCommitment )=true\nuses_compact_power(::PSY.ThermalGen, ::ThermalCompactDispatch)=true\n\nvariable_cost(cost::PSY.OperationalCost, ::ActivePowerVariable, ::PSY.ThermalGen, ::AbstractThermalFormulation)=PSY.get_variable(cost)\nvariable_cost(cost::PSY.OperationalCost, ::PowerAboveMinimumVariable, ::PSY.ThermalGen, ::AbstractThermalFormulation)=PSY.get_variable(cost)\n\n\"\"\"\nTheoretical Cost at power output zero. Mathematically is the intercept with the y-axis\n\"\"\"\nfunction onvar_cost(container::OptimizationContainer, cost::PSY.ThermalGenerationCost, ::OnVariable, d::PSY.ThermalGen, ::AbstractThermalFormulation, t::Int)\n    return _onvar_cost(container, PSY.get_variable(cost), d, t)\nend\n\nfunction _onvar_cost(::OptimizationContainer, cost_function::PSY.FuelCurve{PSY.PiecewisePointCurve}, d::PSY.ThermalGen, ::Int)\n    # OnVariableCost is included in the Point itself for PiecewisePointCurve\n    return 0.0\nend\n\nfunction _onvar_cost(::OptimizationContainer, cost_function::PSY.FuelCurve{PSY.PiecewiseIncrementalCurve}, d::PSY.ThermalGen, ::Int)\n    # Input at min is used to transform to InputOutputCurve\n    return 0.0\nend\n\nfunction _onvar_cost(::OptimizationContainer, cost_function::PSY.FuelCurve{PSY.PiecewiseAverageCurve}, d::PSY.ThermalGen, ::Int)\n    # Converted to InputOutputCurve, OnVariableCost handled in transformation\n    return 0.0\nend\n\n# this one implementation is thermal-specific, and requires the component.\nfunction _onvar_cost(container::OptimizationContainer, cost_function::Union{PSY.FuelCurve{PSY.LinearCurve}, PSY.FuelCurve{PSY.QuadraticCurve}}, d::T, t::Int) where {T <: PSY.ThermalGen}\n    value_curve = PSY.get_value_curve(cost_function)\n    cost_component = PSY.get_function_data(value_curve)\n    # In Unit/h\n    constant_term = PSY.get_constant_term(cost_component)\n    fuel_cost = PSY.get_fuel_cost(cost_function)\n    if typeof(fuel_cost) <: Float64\n        return constant_term * fuel_cost\n    else\n        parameter_array = get_parameter_array(container, FuelCostParameter(), T)\n        parameter_multiplier =\n            get_parameter_multiplier_array(container, FuelCostParameter(), T)\n        name = PSY.get_name(d)\n        return constant_term * parameter_array[name, t] * parameter_multiplier[name, t]\n    end\nend\n\n#! format: on\nfunction get_initial_conditions_device_model(\n    model::OperationModel,\n    ::DeviceModel{T, D},\n) where {T <: PSY.ThermalGen, D <: AbstractThermalDispatchFormulation}\n    if supports_milp(get_optimization_container(model))\n        return DeviceModel(T, ThermalBasicUnitCommitment)\n    else\n        return DeviceModel(T, ThermalBasicDispatch)\n    end\nend\n\nfunction get_initial_conditions_device_model(\n    ::OperationModel,\n    ::DeviceModel{T, D},\n) where {T <: PSY.ThermalGen, D <: ThermalDispatchNoMin}\n    return DeviceModel(T, ThermalDispatchNoMin)\nend\n\nfunction get_initial_conditions_device_model(\n    ::OperationModel,\n    ::DeviceModel{T, D},\n) where {T <: PSY.ThermalGen, D <: AbstractThermalUnitCommitment}\n    return DeviceModel(T, ThermalBasicUnitCommitment)\nend\n\nfunction get_initial_conditions_device_model(\n    ::OperationModel,\n    ::DeviceModel{T, D},\n) where {T <: PSY.ThermalGen, D <: AbstractCompactUnitCommitment}\n    return DeviceModel(T, ThermalBasicCompactUnitCommitment)\nend\n\nfunction get_default_time_series_names(\n    ::Type{U},\n    ::Type{V},\n) where {U <: PSY.ThermalGen, V <: Union{FixedOutput, AbstractThermalFormulation}}\n    return Dict{Any, String}(\n        FuelCostParameter => \"fuel_cost\",\n    )\nend\n\nfunction get_default_attributes(\n    ::Type{U},\n    ::Type{V},\n) where {U <: PSY.ThermalGen, V <: Union{FixedOutput, AbstractThermalFormulation}}\n    return Dict{String, Any}()\nend\n\n######## THERMAL GENERATION CONSTRAINTS ############\n\n# active power limits of generators when there are no CommitmentVariables\n\"\"\"\nMin and max active power limits of generators for thermal dispatch formulations\n\"\"\"\nfunction get_min_max_limits(\n    device,\n    ::Type{ActivePowerVariableLimitsConstraint},\n    ::Type{<:AbstractThermalDispatchFormulation},\n)\n    return PSY.get_active_power_limits(device)\nend\n\n# active power limits of generators when there are CommitmentVariables\n\"\"\"\nMin and max active power limits of generators for thermal unit commitment formulations\n\"\"\"\nfunction get_min_max_limits(\n    device,\n    ::Type{ActivePowerVariableLimitsConstraint},\n    ::Type{<:AbstractThermalUnitCommitment},\n)\n    return PSY.get_active_power_limits(device)\nend\n\n\"\"\"\nRange constraints for thermal compact dispatch\n\"\"\"\nfunction add_constraints!(\n    container::OptimizationContainer,\n    T::Type{<:PowerVariableLimitsConstraint},\n    U::Type{<:Union{PowerAboveMinimumVariable, ExpressionType}},\n    devices::IS.FlattenIteratorWrapper{V},\n    model::DeviceModel{V, W},\n    network_model::NetworkModel{X},\n) where {V <: PSY.ThermalGen, W <: ThermalCompactDispatch, X <: PM.AbstractPowerModel}\n    if !has_semicontinuous_feedforward(model, PowerAboveMinimumVariable)\n        add_range_constraints!(container, T, U, devices, model, X)\n    end\n    return\nend\n\n\"\"\"\nMin and max active power limits of generators for thermal dispatch compact formulations\n\"\"\"\nfunction get_min_max_limits(\n    device,\n    ::Type{ActivePowerVariableLimitsConstraint},\n    ::Type{ThermalCompactDispatch},\n)\n    return (\n        min = 0.0,\n        max = PSY.get_active_power_limits(device).max -\n              PSY.get_active_power_limits(device).min,\n    )\nend\n\n\"\"\"\nMin and max active power limits of generators for thermal dispatch no minimum formulations\n\"\"\"\nfunction get_min_max_limits(\n    device,\n    ::Type{ActivePowerVariableLimitsConstraint},\n    ::Type{ThermalDispatchNoMin},\n)\n    return (min = 0.0, max = PSY.get_active_power_limits(device).max)\nend\n\n\"\"\"\nSemicontinuous range constraints for thermal dispatch formulations\n\"\"\"\nfunction add_constraints!(\n    container::OptimizationContainer,\n    T::Type{<:PowerVariableLimitsConstraint},\n    U::Type{<:Union{VariableType, ExpressionType}},\n    devices::IS.FlattenIteratorWrapper{V},\n    model::DeviceModel{V, W},\n    ::NetworkModel{X},\n) where {\n    V <: PSY.ThermalGen,\n    W <: AbstractThermalDispatchFormulation,\n    X <: PM.AbstractPowerModel,\n}\n    if !has_semicontinuous_feedforward(model, U)\n        add_range_constraints!(container, T, U, devices, model, X)\n    end\n    return\nend\n\n\"\"\"\nMin and max active power limits for multi-start unit commitment formulations\n\"\"\"\nfunction get_min_max_limits(\n    device,\n    ::Type{ActivePowerVariableLimitsConstraint},\n    ::Type{ThermalMultiStartUnitCommitment},\n) #  -> Union{Nothing, NamedTuple{(:startup, :shutdown), Tuple{Float64, Float64}}}\n    return (\n        min = 0.0,\n        max = PSY.get_active_power_limits(device).max -\n              PSY.get_active_power_limits(device).min,\n    )\nend\n\n\"\"\"\nAdds a variable to the optimization model for the OnVariable of Thermal Units\n\"\"\"\nfunction add_variable!(\n    container::OptimizationContainer,\n    variable_type::T,\n    devices::U,\n    formulation::AbstractThermalFormulation,\n) where {\n    T <: Union{OnVariable, StartVariable, StopVariable},\n    U <: Union{Vector{D}, IS.FlattenIteratorWrapper{D}},\n} where {D <: PSY.ThermalGen}\n    @assert !isempty(devices)\n    time_steps = get_time_steps(container)\n    settings = get_settings(container)\n    binary = get_variable_binary(variable_type, D, formulation)\n\n    variable = add_variable_container!(\n        container,\n        variable_type,\n        D,\n        [PSY.get_name(d) for d in devices if !PSY.get_must_run(d)],\n        time_steps,\n    )\n\n    for d in devices\n        if PSY.get_must_run(d)\n            continue\n        end\n        name = PSY.get_name(d)\n        for t in time_steps\n            variable[name, t] = JuMP.@variable(\n                get_jump_model(container),\n                base_name = \"$(T)_$(D)_{$(name), $(t)}\",\n                binary = binary\n            )\n            if get_warm_start(settings)\n                init = get_variable_warm_start_value(variable_type, d, formulation)\n                init !== nothing && JuMP.set_start_value(variable[name, t], init)\n            end\n        end\n    end\n\n    return\nend\n\n\"\"\"\nSemicontinuous range constraints for unit commitment formulations\n\"\"\"\nfunction add_constraints!(\n    container::OptimizationContainer,\n    T::Type{<:PowerVariableLimitsConstraint},\n    U::Type{<:Union{VariableType, ExpressionType}},\n    devices::IS.FlattenIteratorWrapper{V},\n    model::DeviceModel{V, W},\n    ::NetworkModel{X},\n) where {\n    V <: PSY.ThermalGen,\n    W <: AbstractThermalUnitCommitment,\n    X <: PM.AbstractPowerModel,\n}\n    add_semicontinuous_range_constraints!(container, T, U, devices, model, X)\n    return\nend\n\n\"\"\"\nStartup and shutdown active power limits for Compact Unit Commitment\n\"\"\"\nfunction get_startup_shutdown_limits(\n    device::PSY.ThermalMultiStart,\n    ::Type{ActivePowerVariableLimitsConstraint},\n    ::Type{ThermalMultiStartUnitCommitment},\n)\n    startup_shutdown = PSY.get_power_trajectory(device)\n    if isnothing(startup_shutdown)\n        @warn(\n            \"Generator $(summary(device)) has a Nothing startup_shutdown property. Using active power limits.\"\n        )\n        return (\n            startup = PSY.get_active_power_limits(device).max,\n            shutdown = PSY.get_active_power_limits(device).max,\n        )\n    end\n    return startup_shutdown\nend\n\n\"\"\"\nMin and Max active power limits for Compact Unit Commitment\n\"\"\"\nfunction get_min_max_limits(\n    device,\n    ::Type{ActivePowerVariableLimitsConstraint},\n    ::Type{<:AbstractCompactUnitCommitment},\n) #  -> Union{Nothing, NamedTuple{(:min, :max), Tuple{Float64, Float64}}}\n    return (\n        min = 0,\n        max = PSY.get_active_power_limits(device).max -\n              PSY.get_active_power_limits(device).min,\n    )\nend\n\n\"\"\"\nStartup shutdown limits for Compact Unit Commitment\n\"\"\"\nfunction get_startup_shutdown_limits(\n    device,\n    ::Type{ActivePowerVariableLimitsConstraint},\n    ::Type{<:AbstractCompactUnitCommitment},\n)\n    return (\n        startup = PSY.get_active_power_limits(device).max,\n        shutdown = PSY.get_active_power_limits(device).max,\n    )\nend\n\nfunction _get_data_for_range_ic(\n    initial_conditions_power::Vector{<:InitialCondition},\n    initial_conditions_status::Vector{<:InitialCondition},\n)\n    lenght_devices_power = length(initial_conditions_power)\n    lenght_devices_status = length(initial_conditions_status)\n    IS.@assert_op lenght_devices_power == lenght_devices_status\n    ini_conds = Matrix{InitialCondition}(undef, lenght_devices_power, 2)\n    idx = 0\n    for (ix, ic) in enumerate(initial_conditions_power)\n        g = get_component(ic)\n        IS.@assert_op g == get_component(initial_conditions_status[ix])\n        idx += 1\n        ini_conds[idx, 1] = ic\n        ini_conds[idx, 2] = initial_conditions_status[ix]\n    end\n    return ini_conds\nend\n\nfunction add_constraints!(\n    container::OptimizationContainer,\n    ::Type{ActivePowerVariableTimeSeriesLimitsConstraint},\n    U::Type{<:Union{ActivePowerVariable, ActivePowerRangeExpressionUB}},\n    devices::IS.FlattenIteratorWrapper{V},\n    model::DeviceModel{V, W},\n    ::NetworkModel{X},\n) where {\n    V <: PSY.ThermalGen,\n    W <: AbstractThermalUnitCommitment,\n    X <: PM.AbstractPowerModel,\n}\n    add_parameterized_upper_bound_range_constraints(\n        container,\n        ActivePowerVariableTimeSeriesLimitsConstraint,\n        U,\n        ActivePowerTimeSeriesParameter,\n        devices,\n        model,\n        X,\n    )\n    return\nend\n\n\"\"\"\nThis function adds range constraint for the first time period. Constraint (10) from PGLIB formulation\n\"\"\"\nfunction add_constraints!(\n    container::OptimizationContainer,\n    T::Type{<:ActivePowerVariableLimitsConstraint},\n    U::Type{<:VariableType},\n    devices::IS.FlattenIteratorWrapper{V},\n    ::DeviceModel{V, W},\n    ::NetworkModel{X},\n) where {\n    V <: PSY.ThermalMultiStart,\n    W <: ThermalMultiStartUnitCommitment,\n    X <: PM.AbstractPowerModel,\n}\n    time_steps = get_time_steps(container)\n    constraint_type = T()\n    variable_type = U()\n    component_type = V\n    varp = get_variable(container, variable_type, component_type)\n    varstatus = get_variable(container, OnVariable(), component_type)\n    varon = get_variable(container, StartVariable(), component_type)\n    varoff = get_variable(container, StopVariable(), component_type)\n\n    names = [PSY.get_name(x) for x in devices]\n    con_on = add_constraints_container!(\n        container,\n        constraint_type,\n        component_type,\n        names,\n        time_steps;\n        meta = \"on\",\n    )\n    con_off = add_constraints_container!(\n        container,\n        constraint_type,\n        component_type,\n        names,\n        time_steps[1:(end - 1)];\n        meta = \"off\",\n    )\n    con_lb = add_constraints_container!(\n        container,\n        constraint_type,\n        component_type,\n        names,\n        time_steps;\n        meta = \"lb\",\n    )\n\n    for device in devices\n        name = PSY.get_name(device)\n        limits = get_min_max_limits(device, T, W) # depends on constraint type and formulation type\n        startup_shutdown_limits = get_startup_shutdown_limits(device, T, W)\n\n        if JuMP.has_lower_bound(varp[name, t])\n            JuMP.set_lower_bound(varp[name, t], 0.0)\n        end\n        for t in time_steps\n            con_on[name, t] = JuMP.@constraint(\n                get_jump_model(container),\n                varp[name, t] <=\n                (limits.max - limits.min) * varstatus[name, t] -\n                max(limits.max - startup_shutdown_limits.startup, 0.0) * varon[name, t]\n            )\n\n            con_lb[name, t] =\n                JuMP.@constraint(get_jump_model(container), varp[name, t] >= 0.0)\n\n            if t != length(time_steps)\n                con_off[name, t] = JuMP.@constraint(\n                    get_jump_model(container),\n                    varp[name, t] <=\n                    (limits.max - limits.min) * varstatus[name, t] -\n                    max(limits.max - startup_shutdown_limits.shutdown, 0.0) *\n                    varoff[name, t + 1]\n                )\n            end\n        end\n    end\n    return\nend\n\nfunction add_constraints!(\n    container::OptimizationContainer,\n    T::Type{<:ActivePowerVariableLimitsConstraint},\n    U::Type{ActivePowerRangeExpressionLB},\n    devices::IS.FlattenIteratorWrapper{V},\n    ::DeviceModel{V, W},\n    ::NetworkModel{X},\n) where {\n    V <: PSY.ThermalMultiStart,\n    W <: ThermalMultiStartUnitCommitment,\n    X <: PM.AbstractPowerModel,\n}\n    time_steps = get_time_steps(container)\n    constraint_type = T()\n    expression_type = U()\n    component_type = V\n    expression_products = get_expression(container, expression_type, component_type)\n    varp = get_variable(container, PowerAboveMinimumVariable(), component_type)\n\n    names = [PSY.get_name(x) for x in devices]\n    con_lb = add_constraints_container!(\n        container,\n        constraint_type,\n        component_type,\n        names,\n        time_steps;\n        meta = \"lb\",\n    )\n\n    for device in devices\n        name = PSY.get_name(device)\n        for t in time_steps\n            if JuMP.has_lower_bound(varp[name, t])\n                JuMP.set_lower_bound(varp[name, t], 0.0)\n            end\n            con_lb[name, t] =\n                JuMP.@constraint(\n                    get_jump_model(container),\n                    expression_products[name, t] >= 0\n                )\n        end\n    end\n    return\nend\n\nfunction add_constraints!(\n    container::OptimizationContainer,\n    T::Type{<:ActivePowerVariableLimitsConstraint},\n    U::Type{ActivePowerRangeExpressionUB},\n    devices::IS.FlattenIteratorWrapper{V},\n    ::DeviceModel{V, W},\n    ::NetworkModel{X},\n) where {\n    V <: PSY.ThermalMultiStart,\n    W <: ThermalMultiStartUnitCommitment,\n    X <: PM.AbstractPowerModel,\n}\n    time_steps = get_time_steps(container)\n    constraint_type = T()\n    expression_type = U()\n    component_type = V\n    expression_products = get_expression(container, expression_type, component_type)\n    varstatus = get_variable(container, OnVariable(), component_type)\n    varon = get_variable(container, StartVariable(), component_type)\n    varoff = get_variable(container, StopVariable(), component_type)\n    varp = get_variable(container, PowerAboveMinimumVariable(), component_type)\n\n    names = [PSY.get_name(x) for x in devices]\n    con_on = add_constraints_container!(\n        container,\n        constraint_type,\n        component_type,\n        names,\n        time_steps;\n        meta = \"ubon\",\n    )\n    con_off = add_constraints_container!(\n        container,\n        constraint_type,\n        component_type,\n        names,\n        time_steps[1:(end - 1)];\n        meta = \"uboff\",\n    )\n\n    for device in devices\n        name = PSY.get_name(device)\n        limits = get_min_max_limits(device, T, W) # depends on constraint type and formulation type\n        startup_shutdown_limits = get_startup_shutdown_limits(device, T, W)\n        @assert !isnothing(startup_shutdown_limits) \"$(name)\"\n        for t in time_steps\n            if JuMP.has_lower_bound(varp[name, t])\n                JuMP.set_lower_bound(varp[name, t], 0.0)\n            end\n            con_on[name, t] = JuMP.@constraint(\n                get_jump_model(container),\n                expression_products[name, t] <=\n                (limits.max - limits.min) * varstatus[name, t] -\n                max(limits.max - startup_shutdown_limits.startup, 0) * varon[name, t]\n            )\n            if t != length(time_steps)\n                con_off[name, t] = JuMP.@constraint(\n                    get_jump_model(container),\n                    expression_products[name, t] <=\n                    (limits.max - limits.min) * varstatus[name, t] -\n                    max(limits.max - startup_shutdown_limits.shutdown, 0) *\n                    varoff[name, t + 1]\n                )\n            end\n        end\n    end\n    return\nend\n\nfunction add_constraints!(\n    container::OptimizationContainer,\n    ::Type{ActiveRangeICConstraint},\n    devices::IS.FlattenIteratorWrapper{T},\n    model::DeviceModel{T, S},\n    network_model::NetworkModel{X},\n) where {\n    T <: PSY.ThermalGen,\n    S <: AbstractCompactUnitCommitment,\n    X <: PM.AbstractPowerModel,\n}\n    initial_conditions_power = get_initial_condition(container, DeviceAboveMinPower(), T)\n    initial_conditions_status = get_initial_condition(container, DeviceStatus(), T)\n    ini_conds = _get_data_for_range_ic(initial_conditions_power, initial_conditions_status)\n\n    if !isempty(ini_conds)\n        varstop = get_variable(container, StopVariable(), T)\n        device_name_set = PSY.get_name.(devices)\n        con = add_constraints_container!(\n            container,\n            ActiveRangeICConstraint(),\n            T,\n            device_name_set,\n        )\n\n        for (ix, ic) in enumerate(ini_conds[:, 1])\n            name = get_component_name(ic)\n            device = get_component(ic)\n            limits = PSY.get_active_power_limits(device)\n            lag_ramp_limits = PSY.get_power_trajectory(device)\n            val = max(limits.max - lag_ramp_limits.shutdown, 0)\n            con[name] = JuMP.@constraint(\n                get_jump_model(container),\n                val * varstop[name, 1] <=\n                ini_conds[ix, 2].value * (limits.max - limits.min) - get_value(ic)\n            )\n        end\n    else\n        @warn \"Data doesn't contain generators with ramp limits, consider adjusting your formulation\"\n    end\n    return\nend\n\n\"\"\"\nReactive power limits of generators for all dispatch formulations\n\"\"\"\nfunction get_min_max_limits(\n    device,\n    ::Type{ReactivePowerVariableLimitsConstraint},\n    ::Type{<:AbstractThermalDispatchFormulation},\n)\n    return PSY.get_reactive_power_limits(device)\nend\n\n\"\"\"\nReactive power limits of generators when there CommitmentVariables\n\"\"\"\nfunction get_min_max_limits(\n    device,\n    ::Type{ReactivePowerVariableLimitsConstraint},\n    ::Type{<:AbstractThermalUnitCommitment},\n)\n    return PSY.get_reactive_power_limits(device)\nend\n\nfunction add_constraints!(\n    container::OptimizationContainer,\n    T::Type{CommitmentConstraint},\n    devices::IS.FlattenIteratorWrapper{U},\n    model::DeviceModel{U, V},\n    network_model::NetworkModel{X},\n) where {\n    U <: PSY.ThermalGen,\n    V <: AbstractThermalUnitCommitment,\n    X <: PM.AbstractPowerModel,\n}\n    time_steps = get_time_steps(container)\n    varstart = get_variable(container, StartVariable(), U)\n    varstop = get_variable(container, StopVariable(), U)\n    varon = get_variable(container, OnVariable(), U)\n    names = axes(varstart, 1)\n    initial_conditions = get_initial_condition(container, DeviceStatus(), U)\n    constraint =\n        add_constraints_container!(container, CommitmentConstraint(), U, names, time_steps)\n    aux_constraint = add_constraints_container!(\n        container,\n        CommitmentConstraint(),\n        U,\n        names,\n        time_steps;\n        meta = \"aux\",\n    )\n\n    for ic in initial_conditions\n        name = PSY.get_name(get_component(ic))\n        if !PSY.get_must_run(get_component(ic))\n            constraint[name, 1] = JuMP.@constraint(\n                get_jump_model(container),\n                varon[name, 1] == get_value(ic) + varstart[name, 1] - varstop[name, 1]\n            )\n            aux_constraint[name, 1] = JuMP.@constraint(\n                get_jump_model(container),\n                varstart[name, 1] + varstop[name, 1] <= 1.0\n            )\n        end\n    end\n\n    for ic in initial_conditions\n        if PSY.get_must_run(get_component(ic))\n            continue\n        else\n            name = get_component_name(ic)\n            for t in time_steps[2:end]\n                constraint[name, t] = JuMP.@constraint(\n                    get_jump_model(container),\n                    varon[name, t] ==\n                    varon[name, t - 1] + varstart[name, t] - varstop[name, t]\n                )\n                aux_constraint[name, t] = JuMP.@constraint(\n                    get_jump_model(container),\n                    varstart[name, t] + varstop[name, t] <= 1.0\n                )\n            end\n        end\n    end\n    return\nend\n\n########################## Make initial Conditions for a Model #############################\nfunction initial_conditions!(\n    container::OptimizationContainer,\n    devices::IS.FlattenIteratorWrapper{T},\n    formulation::AbstractThermalUnitCommitment,\n) where {T <: PSY.ThermalGen}\n    add_initial_condition!(container, devices, formulation, DeviceStatus())\n    add_initial_condition!(container, devices, formulation, DevicePower())\n    add_initial_condition!(container, devices, formulation, InitialTimeDurationOn())\n    add_initial_condition!(container, devices, formulation, InitialTimeDurationOff())\n\n    return\nend\n\nfunction initial_conditions!(\n    container::OptimizationContainer,\n    devices::IS.FlattenIteratorWrapper{T},\n    formulation::AbstractCompactUnitCommitment,\n) where {T <: PSY.ThermalGen}\n    add_initial_condition!(container, devices, formulation, DeviceStatus())\n    add_initial_condition!(container, devices, formulation, DeviceAboveMinPower())\n    add_initial_condition!(container, devices, formulation, InitialTimeDurationOn())\n    add_initial_condition!(container, devices, formulation, InitialTimeDurationOff())\n\n    return\nend\n\nfunction initial_conditions!(\n    container::OptimizationContainer,\n    devices::IS.FlattenIteratorWrapper{T},\n    formulation::Union{ThermalBasicUnitCommitment, ThermalBasicCompactUnitCommitment},\n) where {T <: PSY.ThermalGen}\n    add_initial_condition!(container, devices, formulation, DeviceStatus())\n    add_initial_condition!(container, devices, formulation, InitialTimeDurationOn())\n    add_initial_condition!(container, devices, formulation, InitialTimeDurationOff())\n    return\nend\n\nfunction initial_conditions!(\n    container::OptimizationContainer,\n    devices::IS.FlattenIteratorWrapper{T},\n    formulation::AbstractThermalDispatchFormulation,\n) where {T <: PSY.ThermalGen}\n    add_initial_condition!(container, devices, formulation, DevicePower())\n    return\nend\n\nfunction initial_conditions!(\n    container::OptimizationContainer,\n    devices::IS.FlattenIteratorWrapper{T},\n    formulation::ThermalCompactDispatch,\n) where {T <: PSY.ThermalGen}\n    add_initial_condition!(container, devices, formulation, DeviceAboveMinPower())\n    return\nend\n############################ Auxiliary Variables Calculation ################################\nfunction calculate_aux_variable_value!(\n    container::OptimizationContainer,\n    ::AuxVarKey{TimeDurationOn, T},\n    ::PSY.System,\n) where {T <: PSY.ThermalGen}\n    on_variable_results = get_variable(container, OnVariable(), T)\n    aux_variable_container = get_aux_variable(container, TimeDurationOn(), T)\n    ini_cond = get_initial_condition(container, InitialTimeDurationOn(), T)\n\n    time_steps = get_time_steps(container)\n\n    for ix in eachindex(JuMP.axes(aux_variable_container)[1])\n        # if its nothing it means the thermal unit was on must run\n        # so there is nothing to do but to add the total number of time steps\n        # to the count\n        if isnothing(get_value(ini_cond[ix]))\n            sum_on_var = time_steps[end]\n        else\n            on_var_name = get_component_name(ini_cond[ix])\n            ini_cond_value = get_condition(ini_cond[ix])\n            # On Var doesn't exist for a unit that has must_run = true\n            on_var = jump_value.(on_variable_results[on_var_name, :])\n            aux_variable_container.data[ix, :] .= ini_cond_value\n            sum_on_var = sum(on_var)\n        end\n        if sum_on_var == time_steps[end] # Unit was always on\n            aux_variable_container.data[ix, :] += time_steps\n        elseif sum_on_var == 0.0 # Unit was always off\n            aux_variable_container.data[ix, :] .= 0.0\n        else\n            previous_condition = ini_cond_value\n            for (t, v) in enumerate(on_var)\n                if v < 0.99 # Unit turn off\n                    time_value = 0.0\n                elseif isapprox(v, 1.0; atol = ABSOLUTE_TOLERANCE) # Unit is on\n                    time_value = previous_condition + 1.0\n                else\n                    error(\"Binary condition returned $v\")\n                end\n                previous_condition = aux_variable_container.data[ix, t] = time_value\n            end\n        end\n    end\n\n    return\nend\n\nfunction calculate_aux_variable_value!(\n    container::OptimizationContainer,\n    ::AuxVarKey{TimeDurationOff, T},\n    ::PSY.System,\n) where {T <: PSY.ThermalGen}\n    on_variable_results = get_variable(container, OnVariable(), T)\n    aux_variable_container = get_aux_variable(container, TimeDurationOff(), T)\n    ini_cond = get_initial_condition(container, InitialTimeDurationOff(), T)\n\n    time_steps = get_time_steps(container)\n    for ix in eachindex(JuMP.axes(aux_variable_container)[1])\n        # if its nothing it means the thermal unit was on must_run = true\n        # so there is nothing to do but continue\n        if isnothing(get_value(ini_cond[ix]))\n            sum_on_var = 0.0\n        else\n            on_var_name = get_component_name(ini_cond[ix])\n            # On Var doesn't exist for a unit that has must run\n            on_var = jump_value.(on_variable_results[on_var_name, :])\n            ini_cond_value = get_condition(ini_cond[ix])\n            aux_variable_container.data[ix, :] .= ini_cond_value\n            sum_on_var = sum(on_var)\n        end\n        if sum_on_var == time_steps[end] # Unit was always on\n            aux_variable_container.data[ix, :] .= 0.0\n        elseif sum_on_var == 0.0 # Unit was always off\n            aux_variable_container.data[ix, :] += time_steps\n        else\n            previous_condition = ini_cond_value\n            for (t, v) in enumerate(on_var)\n                if v < 0.99 # Unit turn off\n                    time_value = previous_condition + 1.0\n                elseif isapprox(v, 1.0; atol = ABSOLUTE_TOLERANCE) # Unit is on\n                    time_value = 0.0\n                else\n                    error(\"Binary condition returned $v\")\n                end\n                previous_condition = aux_variable_container.data[ix, t] = time_value\n            end\n        end\n    end\n\n    return\nend\n\nfunction calculate_aux_variable_value!(\n    container::OptimizationContainer,\n    ::AuxVarKey{PowerOutput, T},\n    system::PSY.System,\n) where {T <: PSY.ThermalGen}\n    time_steps = get_time_steps(container)\n    if has_container_key(container, OnVariable, T)\n        on_variable_results = get_variable(container, OnVariable(), T)\n    elseif has_container_key(container, OnStatusParameter, T)\n        on_variable_results = get_parameter_array(container, OnStatusParameter(), T)\n    else\n        error(\n            \"$T formulation is NOT supported without a Feedforward for CommitmentDecisions,\n      please consider changing your simulation setup or adding a SemiContinuousFeedforward.\",\n        )\n    end\n    p_variable_results = get_variable(container, PowerAboveMinimumVariable(), T)\n    device_name = axes(p_variable_results, 1)\n    aux_variable_container = get_aux_variable(container, PowerOutput(), T)\n    for d_name in device_name\n        d = PSY.get_component(T, system, d_name)\n        name = PSY.get_name(d)\n        min = PSY.get_active_power_limits(d).min\n        for t in time_steps\n            aux_variable_container[name, t] =\n                jump_value(on_variable_results[name, t]) * min +\n                jump_value(p_variable_results[name, t])\n        end\n    end\n\n    return\nend\n########################### Ramp/Rate of Change Constraints ################################\n\"\"\"\nThis function gets the data for the generators for ramping constraints of thermal generators\n\"\"\"\n_get_initial_condition_type(\n    ::Type{RampConstraint},\n    ::Type{<:PSY.ThermalGen},\n    ::Type{<:AbstractThermalFormulation},\n) = DevicePower\n_get_initial_condition_type(\n    ::Type{RampConstraint},\n    ::Type{<:PSY.ThermalGen},\n    ::Type{<:AbstractCompactUnitCommitment},\n) = DeviceAboveMinPower\n_get_initial_condition_type(\n    ::Type{RampConstraint},\n    ::Type{<:PSY.ThermalGen},\n    ::Type{ThermalCompactDispatch},\n) = DeviceAboveMinPower\n\n\"\"\"\nThis function adds the ramping limits of generators when there are CommitmentVariables\n\"\"\"\nfunction add_constraints!(\n    container::OptimizationContainer,\n    T::Type{RampConstraint},\n    devices::IS.FlattenIteratorWrapper{U},\n    model::DeviceModel{U, V},\n    ::NetworkModel{W},\n) where {\n    U <: PSY.ThermalGen,\n    V <: AbstractThermalUnitCommitment,\n    W <: PM.AbstractPowerModel,\n}\n    add_semicontinuous_ramp_constraints!(\n        container,\n        T,\n        ActivePowerVariable,\n        devices,\n        model,\n        W,\n    )\n    return\nend\n\nfunction add_constraints!(\n    container::OptimizationContainer,\n    T::Type{RampConstraint},\n    devices::IS.FlattenIteratorWrapper{U},\n    model::DeviceModel{U, V},\n    ::NetworkModel{W},\n) where {\n    U <: PSY.ThermalGen,\n    V <: AbstractCompactUnitCommitment,\n    W <: PM.AbstractPowerModel,\n}\n    add_semicontinuous_ramp_constraints!(\n        container,\n        T,\n        PowerAboveMinimumVariable,\n        devices,\n        model,\n        W,\n    )\n    return\nend\n\nfunction add_constraints!(\n    container::OptimizationContainer,\n    T::Type{RampConstraint},\n    devices::IS.FlattenIteratorWrapper{U},\n    model::DeviceModel{U, ThermalCompactDispatch},\n    ::NetworkModel{V},\n) where {U <: PSY.ThermalGen, V <: PM.AbstractPowerModel}\n    add_linear_ramp_constraints!(container, T, PowerAboveMinimumVariable, devices, model, V)\n    return\nend\n\nfunction add_constraints!(\n    container::OptimizationContainer,\n    T::Type{RampConstraint},\n    devices::IS.FlattenIteratorWrapper{U},\n    model::DeviceModel{U, V},\n    ::NetworkModel{W},\n) where {\n    U <: PSY.ThermalGen,\n    V <: AbstractThermalDispatchFormulation,\n    W <: PM.AbstractPowerModel,\n}\n    add_linear_ramp_constraints!(container, T, ActivePowerVariable, devices, model, W)\n    return\nend\n\nfunction add_constraints!(\n    container::OptimizationContainer,\n    T::Type{RampConstraint},\n    devices::IS.FlattenIteratorWrapper{PSY.ThermalMultiStart},\n    model::DeviceModel{PSY.ThermalMultiStart, ThermalMultiStartUnitCommitment},\n    ::NetworkModel{U},\n) where {U <: PM.AbstractPowerModel}\n    add_linear_ramp_constraints!(container, T, PowerAboveMinimumVariable, devices, model, U)\n    return\nend\n\n########################### start up trajectory constraints ######################################\n\nfunction _convert_hours_to_timesteps(\n    start_times_hr::StartUpStages,\n    resolution::Dates.TimePeriod,\n)\n    _start_times_ts = (\n        round((hr * MINUTES_IN_HOUR) / Dates.value(Dates.Minute(resolution)), RoundUp) for\n        hr in start_times_hr\n    )\n    start_times_ts = StartUpStages(_start_times_ts)\n    return start_times_ts\nend\n\n@doc raw\"\"\"\nConstructs contraints for different types of starts based on generator down-time\n\n# Equations\nfor t in time_limits[s+1]:T\n\n``` var_starts[name, s, t] <= sum( var_stop[name, t-i] for i in time_limits[s]:(time_limits[s+1]-1)  ```\n\n# LaTeX\n\n``  δ^{s}(t)  \\leq \\sum_{i=TS^{s}_{g}}^{TS^{s+1}_{g}} x^{stop}(t-i) ``\n\"\"\"\nfunction add_constraints!(\n    container::OptimizationContainer,\n    ::Type{StartupTimeLimitTemperatureConstraint},\n    devices::IS.FlattenIteratorWrapper{T},\n    model::DeviceModel{T, ThermalMultiStartUnitCommitment},\n    ::NetworkModel{<:PM.AbstractPowerModel},\n) where {T <: PSY.ThermalMultiStart}\n    resolution = get_resolution(container)\n    time_steps = get_time_steps(container)\n    start_vars = [\n        get_variable(container, HotStartVariable(), T),\n        get_variable(container, WarmStartVariable(), T),\n    ]\n    varstop = get_variable(container, StopVariable(), T)\n\n    names = PSY.get_name.(devices)\n\n    con = [\n        add_constraints_container!(\n            container,\n            StartupTimeLimitTemperatureConstraint(),\n            T,\n            names,\n            time_steps;\n            sparse = true,\n            meta = \"hot\",\n        ),\n        add_constraints_container!(\n            container,\n            StartupTimeLimitTemperatureConstraint(),\n            T,\n            names,\n            time_steps;\n            sparse = true,\n            meta = \"warm\",\n        ),\n    ]\n\n    for t in time_steps, d in devices\n        name = PSY.get_name(d)\n        startup_types = PSY.get_start_types(d)\n        time_limits = _convert_hours_to_timesteps(PSY.get_start_time_limits(d), resolution)\n        for ix in 1:(startup_types - 1)\n            if t >= time_limits[ix + 1]\n                con[ix][name, t] = JuMP.@constraint(\n                    get_jump_model(container),\n                    start_vars[ix][name, t] <= sum(\n                        varstop[name, t - i] for i in UnitRange{Int}(\n                            Int(time_limits[ix]),\n                            Int(time_limits[ix + 1] - 1),\n                        )\n                    )\n                )\n            end\n        end\n    end\n    for c in con\n        # Workaround to remove invalid key combinations\n        filter!(x -> x.second !== nothing, c.data)\n    end\n    return\nend\n\n@doc raw\"\"\"\n\nConstructs contraints that restricts devices to one type of start at a time\n\n# Equations\n\n``` sum(var_starts[name, s, t] for s in starts) = var_start[name, t]  ```\n\n# LaTeX\n\n``  \\sum^{S_g}_{s=1} δ^{s}(t)  \\eq  x^{start}(t) ``\n\n\"\"\"\nfunction add_constraints!(\n    container::OptimizationContainer,\n    ::Type{StartTypeConstraint},\n    devices::IS.FlattenIteratorWrapper{T},\n    model::DeviceModel{T, ThermalMultiStartUnitCommitment},\n    ::NetworkModel{<:PM.AbstractPowerModel},\n) where {T <: PSY.ThermalMultiStart}\n    time_steps = get_time_steps(container)\n    varstart = get_variable(container, StartVariable(), T)\n    start_vars = [\n        get_variable(container, HotStartVariable(), T),\n        get_variable(container, WarmStartVariable(), T),\n        get_variable(container, ColdStartVariable(), T),\n    ]\n\n    device_name_set = PSY.get_name.(devices)\n    con = add_constraints_container!(\n        container,\n        StartTypeConstraint(),\n        T,\n        device_name_set,\n        time_steps,\n    )\n\n    for t in time_steps, d in devices\n        name = PSY.get_name(d)\n        startup_types = PSY.get_start_types(d)\n        con[name, t] = JuMP.@constraint(\n            get_jump_model(container),\n            varstart[name, t] == sum(start_vars[ix][name, t] for ix in 1:(startup_types))\n        )\n    end\n    return\nend\n\n@doc raw\"\"\"\nConstructs contraints that restricts devices to one type of start at a time\n\n# Equations\nub:\n``` (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  ```\nlb:\n``` (time_limits[st]-1)*δ^{s}(t) =< sum(1-varbin[name, i]) for i in 1:t) + initial_condition_offtime  ```\n\n# LaTeX\n\n`` 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}``\n\n`` 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}``\n\n\"\"\"\nfunction add_constraints!(\n    container::OptimizationContainer,\n    ::Type{StartupInitialConditionConstraint},\n    devices::IS.FlattenIteratorWrapper{T},\n    model::DeviceModel{T, ThermalMultiStartUnitCommitment},\n    ::NetworkModel{<:PM.AbstractPowerModel},\n) where {T <: PSY.ThermalMultiStart}\n    resolution = get_resolution(container)\n    initial_conditions_offtime =\n        get_initial_condition(container, InitialTimeDurationOff(), PSY.ThermalMultiStart)\n\n    time_steps = get_time_steps(container)\n    device_name_set = [get_component_name(ic) for ic in initial_conditions_offtime]\n    varbin = get_variable(container, OnVariable(), T)\n    varstarts = [\n        get_variable(container, HotStartVariable(), T),\n        get_variable(container, WarmStartVariable(), T),\n    ]\n\n    con_ub = add_constraints_container!(\n        container,\n        StartupInitialConditionConstraint(),\n        T,\n        device_name_set,\n        time_steps,\n        1:(MAX_START_STAGES - 1);\n        sparse = true,\n        meta = \"ub\",\n    )\n    con_lb = add_constraints_container!(\n        container,\n        StartupInitialConditionConstraint(),\n        T,\n        device_name_set,\n        time_steps,\n        1:(MAX_START_STAGES - 1);\n        sparse = true,\n        meta = \"lb\",\n    )\n\n    for t in time_steps, (ix, ic) in enumerate(initial_conditions_offtime)\n        name = PSY.get_name(get_component(ic))\n        startup_types = PSY.get_start_types(get_component(ic))\n        time_limits = _convert_hours_to_timesteps(\n            PSY.get_start_time_limits(get_component(ic)),\n            resolution,\n        )\n        ic = initial_conditions_offtime[ix]\n        for st in 1:(startup_types - 1)\n            var = varstarts[st]\n            if t < (time_limits[st + 1] - 1)\n                con_ub[name, t, st] = JuMP.@constraint(\n                    get_jump_model(container),\n                    (time_limits[st + 1] - 1) * var[name, t] +\n                    (1 - var[name, t]) * M_VALUE >=\n                    sum((1 - varbin[name, i]) for i in 1:t) + get_value(ic)\n                )\n                con_lb[name, t, st] = JuMP.@constraint(\n                    get_jump_model(container),\n                    time_limits[st] * var[name, t] <=\n                    sum((1 - varbin[name, i]) for i in 1:t) + get_value(ic)\n                )\n            end\n        end\n    end\n    for c in [con_ub, con_lb]\n        # Workaround to remove invalid key combinations\n        filter!(x -> x.second !== nothing, c.data)\n    end\n    return\nend\n\n########################### time duration constraints ######################################\n\"\"\"\nIf the fraction of hours that a generator has a duration constraint is less than\nthe fraction of hours that a single time_step represents then it is not binding.\n\"\"\"\nfunction _get_data_for_tdc(\n    initial_conditions_on::Vector{T},\n    initial_conditions_off::Vector{U},\n    resolution::Dates.TimePeriod,\n) where {T <: InitialCondition, U <: InitialCondition}\n    steps_per_hour = 60 / Dates.value(Dates.Minute(resolution))\n    fraction_of_hour = 1 / steps_per_hour\n    lenght_devices_on = length(initial_conditions_on)\n    lenght_devices_off = length(initial_conditions_off)\n    IS.@assert_op lenght_devices_off == lenght_devices_on\n    time_params = Vector{UpDown}(undef, lenght_devices_on)\n    ini_conds = Matrix{InitialCondition}(undef, lenght_devices_on, 2)\n    idx = 0\n    for (ix, ic) in enumerate(initial_conditions_on)\n        g = get_component(ic)\n        IS.@assert_op g == get_component(initial_conditions_off[ix])\n        time_limits = PSY.get_time_limits(g)\n        name = PSY.get_name(g)\n        if time_limits !== nothing\n            if (time_limits.up <= fraction_of_hour) & (time_limits.down <= fraction_of_hour)\n                @debug \"Generator $(name) has a nonbinding time limits. Constraints Skipped\"\n                continue\n            else\n                idx += 1\n            end\n            ini_conds[idx, 1] = ic\n            ini_conds[idx, 2] = initial_conditions_off[ix]\n            up_val = round(time_limits.up * steps_per_hour, RoundUp)\n            down_val = round(time_limits.down * steps_per_hour, RoundUp)\n            time_params[idx] = (up = up_val, down = down_val)\n        end\n    end\n    if idx < lenght_devices_on\n        ini_conds = ini_conds[1:idx, :]\n        deleteat!(time_params, (idx + 1):lenght_devices_on)\n    end\n    return ini_conds, time_params\nend\n\nfunction add_constraints!(\n    container::OptimizationContainer,\n    ::Type{DurationConstraint},\n    ::IS.FlattenIteratorWrapper{U},\n    ::DeviceModel{U, V},\n    ::NetworkModel{<:PM.AbstractPowerModel},\n) where {U <: PSY.ThermalGen, V <: AbstractThermalUnitCommitment}\n    parameters = built_for_recurrent_solves(container)\n    resolution = get_resolution(container)\n    # Use getter functions that don't require creating the keys here\n    initial_conditions_on = get_initial_condition(container, InitialTimeDurationOn(), U)\n    initial_conditions_off = get_initial_condition(container, InitialTimeDurationOff(), U)\n    ini_conds, time_params =\n        _get_data_for_tdc(initial_conditions_on, initial_conditions_off, resolution)\n    if !(isempty(ini_conds))\n        if parameters\n            device_duration_parameters!(\n                container,\n                time_params,\n                ini_conds,\n                DurationConstraint(),\n                (OnVariable(), StartVariable(), StopVariable()),\n                U,\n            )\n        else\n            device_duration_retrospective!(\n                container,\n                time_params,\n                ini_conds,\n                DurationConstraint(),\n                (OnVariable(), StartVariable(), StopVariable()),\n                U,\n            )\n        end\n    else\n        @warn \"Data doesn't contain generators with time-up/down limits, consider adjusting your formulation\"\n    end\n    return\nend\n\nfunction add_constraints!(\n    container::OptimizationContainer,\n    ::Type{DurationConstraint},\n    devices::IS.FlattenIteratorWrapper{U},\n    model::DeviceModel{U, ThermalMultiStartUnitCommitment},\n    ::NetworkModel{<:PM.AbstractPowerModel},\n) where {U <: PSY.ThermalGen}\n    parameters = built_for_recurrent_solves(container)\n    resolution = get_resolution(container)\n    initial_conditions_on = get_initial_condition(container, InitialTimeDurationOn(), U)\n    initial_conditions_off = get_initial_condition(container, InitialTimeDurationOff(), U)\n    ini_conds, time_params =\n        _get_data_for_tdc(initial_conditions_on, initial_conditions_off, resolution)\n    if !(isempty(ini_conds))\n        if parameters\n            device_duration_parameters!(\n                container,\n                time_params,\n                ini_conds,\n                DurationConstraint(),\n                (OnVariable(), StartVariable(), StopVariable()),\n                U,\n            )\n        else\n            device_duration_compact_retrospective!(\n                container,\n                time_params,\n                ini_conds,\n                DurationConstraint(),\n                (OnVariable(), StartVariable(), StopVariable()),\n                U,\n            )\n        end\n    else\n        @warn \"Data doesn't contain generators with time-up/down limits, consider adjusting your formulation\"\n    end\n    return\nend\n\n########################### Objective Function Calls#############################################\n# These functions are custom implementations of the cost data. In the file objective_functions.jl there are default implementations. Define these only if needed.\n\nfunction objective_function!(\n    container::OptimizationContainer,\n    devices::IS.FlattenIteratorWrapper{T},\n    device_model::DeviceModel{T, U},\n    ::Type{<:PM.AbstractPowerModel},\n) where {T <: PSY.ThermalGen, U <: AbstractThermalUnitCommitment}\n    add_variable_cost!(container, ActivePowerVariable(), devices, U())\n    add_start_up_cost!(container, StartVariable(), devices, U())\n    add_shut_down_cost!(container, StopVariable(), devices, U())\n    add_proportional_cost!(container, OnVariable(), devices, U())\n    if get_use_slacks(device_model)\n        add_proportional_cost!(container, RateofChangeConstraintSlackUp(), devices, U())\n        add_proportional_cost!(container, RateofChangeConstraintSlackDown(), devices, U())\n    end\n    return\nend\n\nfunction objective_function!(\n    container::OptimizationContainer,\n    devices::IS.FlattenIteratorWrapper{T},\n    device_model::DeviceModel{T, U},\n    ::Type{<:PM.AbstractPowerModel},\n) where {T <: PSY.ThermalGen, U <: AbstractCompactUnitCommitment}\n    add_variable_cost!(container, PowerAboveMinimumVariable(), devices, U())\n    add_start_up_cost!(container, StartVariable(), devices, U())\n    add_shut_down_cost!(container, StopVariable(), devices, U())\n    add_proportional_cost!(container, OnVariable(), devices, U())\n    if get_use_slacks(device_model)\n        add_proportional_cost!(container, RateofChangeConstraintSlackUp(), devices, U())\n        add_proportional_cost!(container, RateofChangeConstraintSlackDown(), devices, U())\n    end\n    return\nend\n\nfunction objective_function!(\n    container::OptimizationContainer,\n    devices::IS.FlattenIteratorWrapper{PSY.ThermalMultiStart},\n    device_model::DeviceModel{PSY.ThermalMultiStart, U},\n    ::Type{<:PM.AbstractPowerModel},\n) where {U <: ThermalMultiStartUnitCommitment}\n    add_variable_cost!(container, PowerAboveMinimumVariable(), devices, U())\n    for var_type in MULTI_START_VARIABLES\n        add_start_up_cost!(container, var_type(), devices, U())\n    end\n    add_shut_down_cost!(container, StopVariable(), devices, U())\n    add_proportional_cost!(container, OnVariable(), devices, U())\n    if get_use_slacks(device_model)\n        add_proportional_cost!(container, RateofChangeConstraintSlackUp(), devices, U())\n        add_proportional_cost!(container, RateofChangeConstraintSlackDown(), devices, U())\n    end\n    return\nend\n\nfunction objective_function!(\n    container::OptimizationContainer,\n    devices::IS.FlattenIteratorWrapper{T},\n    device_model::DeviceModel{T, U},\n    ::Type{<:PM.AbstractPowerModel},\n) where {T <: PSY.ThermalGen, U <: AbstractThermalDispatchFormulation}\n    add_variable_cost!(container, ActivePowerVariable(), devices, U())\n    if get_use_slacks(device_model)\n        add_proportional_cost!(container, RateofChangeConstraintSlackUp(), devices, U())\n        add_proportional_cost!(container, RateofChangeConstraintSlackDown(), devices, U())\n    end\n    return\nend\n\nfunction objective_function!(\n    container::OptimizationContainer,\n    devices::IS.FlattenIteratorWrapper{T},\n    device_model::DeviceModel{T, U},\n    ::Type{<:PM.AbstractPowerModel},\n) where {T <: PSY.ThermalGen, U <: ThermalCompactDispatch}\n    add_variable_cost!(container, PowerAboveMinimumVariable(), devices, U())\n    if get_use_slacks(device_model)\n        add_proportional_cost!(container, RateofChangeConstraintSlackUp(), devices, U())\n        add_proportional_cost!(container, RateofChangeConstraintSlackDown(), devices, U())\n    end\n    return\nend\n\nfunction objective_function!(\n    ::OptimizationContainer,\n    ::IS.FlattenIteratorWrapper{PSY.ThermalMultiStart},\n    ::DeviceModel{PSY.ThermalMultiStart, ThermalDispatchNoMin},\n    ::Type{<:PM.AbstractPowerModel},\n)\n    throw(\n        IS.ConflictingInputsError(\n            \"ThermalDispatchNoMin cost function is not compatible with ThermalMultiStart Devices.\",\n        ),\n    )\nend\n"
  },
  {
    "path": "src/feedforward/feedforward_arguments.jl",
    "content": "function add_feedforward_arguments!(\n    container::OptimizationContainer,\n    model::DeviceModel,\n    devices::IS.FlattenIteratorWrapper{V},\n) where {V <: PSY.Component}\n    for ff in get_feedforwards(model)\n        @debug \"arguments\" ff V _group = LOG_GROUP_FEEDFORWARDS_CONSTRUCTION\n        _add_feedforward_arguments!(container, model, devices, ff)\n    end\n    return\nend\n\nfunction add_feedforward_arguments!(\n    container::OptimizationContainer,\n    model::ServiceModel,\n    service::V,\n) where {V <: PSY.AbstractReserve}\n    for ff in get_feedforwards(model)\n        @debug \"arguments\" ff V _group = LOG_GROUP_FEEDFORWARDS_CONSTRUCTION\n        contributing_devices = get_contributing_devices(model)\n        _add_feedforward_arguments!(container, model, contributing_devices, ff)\n    end\n    return\nend\n\nfunction add_feedforward_arguments!(\n    ::OptimizationContainer,\n    model::ServiceModel,\n    ::PSY.TransmissionInterface,\n)\n    # Currently we do not support feedforwards for TransmissionInterface\n    ffs = get_feedforwards(model)\n    if !isempty(ffs)\n        throw(\n            ArgumentError(\n                \"TransmissionInterface data types currently do not support feedforwards.\",\n            ),\n        )\n    end\n    return\nend\n\nfunction _add_feedforward_arguments!(\n    container::OptimizationContainer,\n    model::DeviceModel{T, U},\n    devices::IS.FlattenIteratorWrapper{T},\n    ff::AbstractAffectFeedforward,\n) where {T <: PSY.Device, U <: AbstractDeviceFormulation}\n    parameter_type = get_default_parameter_type(ff, T)\n    add_parameters!(container, parameter_type, ff, model, devices)\n    return\nend\n\nfunction _add_feedforward_arguments!(\n    container::OptimizationContainer,\n    model::ServiceModel{T, U},\n    contributing_devices::Vector,\n    ff::AbstractAffectFeedforward,\n) where {T <: PSY.AbstractReserve, U <: AbstractServiceFormulation}\n    parameter_type = get_default_parameter_type(ff, U)\n    add_parameters!(container, parameter_type, ff, model, contributing_devices)\n    return\nend\n\nfunction _add_feedforward_slack_variables!(container::OptimizationContainer,\n    ::T,\n    ff::Union{LowerBoundFeedforward, UpperBoundFeedforward},\n    model::ServiceModel{U, V},\n    devices::Vector,\n) where {\n    T <: Union{LowerBoundFeedForwardSlack, UpperBoundFeedForwardSlack},\n    U <: PSY.AbstractReserve,\n    V <: AbstractReservesFormulation,\n}\n    time_steps = get_time_steps(container)\n    for var in get_affected_values(ff)\n        variable = get_variable(container, var)\n        device_name_set, set_time = JuMP.axes(variable)\n        device_names = PSY.get_name.(devices)\n        @assert issetequal(device_name_set, device_names)\n        IS.@assert_op set_time == time_steps\n        service_name = get_service_name(model)\n        var_type = get_entry_type(var)\n        variable_container = add_variable_container!(\n            container,\n            T(),\n            U,\n            device_names,\n            time_steps;\n            meta = \"$(var_type)_$(service_name)\",\n        )\n\n        for t in time_steps, name in device_name_set\n            variable_container[name, t] = JuMP.@variable(\n                get_jump_model(container),\n                base_name = \"$(T)_$(U)_{$(name), $(t)}\",\n                lower_bound = 0.0\n            )\n            add_to_objective_invariant_expression!(\n                container,\n                variable_container[name, t] * BALANCE_SLACK_COST,\n            )\n        end\n    end\n    return\nend\n\nfunction _add_feedforward_slack_variables!(\n    container::OptimizationContainer,\n    ::T,\n    ff::Union{LowerBoundFeedforward, UpperBoundFeedforward},\n    model::DeviceModel{U, V},\n    devices::IS.FlattenIteratorWrapper{U},\n) where {\n    T <: Union{LowerBoundFeedForwardSlack, UpperBoundFeedForwardSlack},\n    U <: PSY.Device,\n    V <: AbstractDeviceFormulation,\n}\n    time_steps = get_time_steps(container)\n    for var in get_affected_values(ff)\n        variable = get_variable(container, var)\n        device_name_set, set_time = JuMP.axes(variable)\n        devices_names = PSY.get_name.(devices)\n        @assert issetequal(device_name_set, devices_names)\n        IS.@assert_op set_time == time_steps\n\n        var_type = get_entry_type(var)\n        variable = add_variable_container!(\n            container,\n            T(),\n            U,\n            devices_names,\n            time_steps;\n            meta = \"$(var_type)\",\n        )\n\n        for t in time_steps, name in device_name_set\n            variable[name, t] = JuMP.@variable(\n                get_jump_model(container),\n                base_name = \"$(T)_$(U)_{$(name), $(t)}\",\n                lower_bound = 0.0\n            )\n        end\n    end\n    return\nend\n\nfunction _add_feedforward_arguments!(\n    container::OptimizationContainer,\n    model::DeviceModel{T, U},\n    devices::IS.FlattenIteratorWrapper{T},\n    ff::UpperBoundFeedforward,\n) where {T <: PSY.Device, U <: AbstractDeviceFormulation}\n    parameter_type = get_default_parameter_type(ff, T)\n    add_parameters!(container, parameter_type, ff, model, devices)\n    if get_slacks(ff)\n        _add_feedforward_slack_variables!(\n            container,\n            UpperBoundFeedForwardSlack(),\n            ff,\n            model,\n            devices,\n        )\n    end\n    return\nend\n\nfunction _add_feedforward_arguments!(\n    container::OptimizationContainer,\n    model::ServiceModel{T, U},\n    contributing_devices::Vector,\n    ff::UpperBoundFeedforward,\n) where {T <: PSY.AbstractReserve, U <: AbstractServiceFormulation}\n    parameter_type = get_default_parameter_type(ff, SR)\n    add_parameters!(container, parameter_type, ff, model, contributing_devices)\n    if get_slacks(ff)\n        _add_feedforward_slack_variables!(\n            container,\n            UpperBoundFeedForwardSlack(),\n            ff,\n            model,\n            contributing_devices,\n        )\n    end\n    return\nend\n\nfunction _add_feedforward_arguments!(\n    container::OptimizationContainer,\n    model::DeviceModel{T, U},\n    devices::IS.FlattenIteratorWrapper{T},\n    ff::LowerBoundFeedforward,\n) where {T <: PSY.Device, U <: AbstractDeviceFormulation}\n    parameter_type = get_default_parameter_type(ff, T)\n    add_parameters!(container, parameter_type, ff, model, devices)\n    if get_slacks(ff)\n        _add_feedforward_slack_variables!(\n            container,\n            LowerBoundFeedForwardSlack(),\n            ff,\n            model,\n            devices,\n        )\n    end\n    return\nend\n\nfunction _add_feedforward_arguments!(\n    container::OptimizationContainer,\n    model::ServiceModel{T, U},\n    contributing_devices::Vector{V},\n    ff::LowerBoundFeedforward,\n) where {T <: PSY.AbstractReserve, U <: AbstractReservesFormulation, V <: PSY.Component}\n    parameter_type = get_default_parameter_type(ff, T)\n    add_parameters!(container, parameter_type, ff, model, contributing_devices)\n    if get_slacks(ff)\n        _add_feedforward_slack_variables!(\n            container,\n            LowerBoundFeedForwardSlack(),\n            ff,\n            model,\n            contributing_devices,\n        )\n    end\n    return\nend\n\nfunction _add_feedforward_arguments!(\n    container::OptimizationContainer,\n    model::DeviceModel{T, U},\n    devices::IS.FlattenIteratorWrapper{T},\n    ff::SemiContinuousFeedforward,\n) where {T <: PSY.Device, U <: AbstractDeviceFormulation}\n    parameter_type = get_default_parameter_type(ff, T)\n    add_parameters!(container, parameter_type, ff, model, devices)\n    add_to_expression!(\n        container,\n        ActivePowerRangeExpressionUB,\n        parameter_type(),\n        devices,\n        model,\n    )\n    add_to_expression!(\n        container,\n        ActivePowerRangeExpressionLB,\n        parameter_type(),\n        devices,\n        model,\n    )\n    return\nend\n"
  },
  {
    "path": "src/feedforward/feedforward_constraints.jl",
    "content": "function add_feedforward_constraints!(\n    container::OptimizationContainer,\n    model::DeviceModel,\n    devices::IS.FlattenIteratorWrapper{V},\n) where {V <: PSY.Component}\n    for ff in get_feedforwards(model)\n        @debug \"constraints\" ff V _group = LOG_GROUP_FEEDFORWARDS_CONSTRUCTION\n        add_feedforward_constraints!(container, model, devices, ff)\n    end\n    return\nend\n\nfunction add_feedforward_constraints!(\n    container::OptimizationContainer,\n    model::ServiceModel{V, <:AbstractReservesFormulation},\n    ::V,\n) where {V <: PSY.AbstractReserve}\n    for ff in get_feedforwards(model)\n        @debug \"constraints\" ff V _group = LOG_GROUP_FEEDFORWARDS_CONSTRUCTION\n        contributing_devices = get_contributing_devices(model)\n        add_feedforward_constraints!(container, model, contributing_devices, ff)\n    end\n    return\nend\n\nfunction add_feedforward_constraints!(\n    container::OptimizationContainer,\n    model::ServiceModel,\n    ::V,\n) where {V <: PSY.Service}\n    for ff in get_feedforwards(model)\n        @debug \"constraints\" ff V _group = LOG_GROUP_FEEDFORWARDS_CONSTRUCTION\n        contributing_devices = get_contributing_devices(model)\n        add_feedforward_constraints!(container, model, contributing_devices, ff)\n    end\n    return\nend\n\nfunction _add_feedforward_constraints!(\n    container::OptimizationContainer,\n    ::Type{T},\n    param::P,\n    ::VariableKey{U, V},\n    devices::IS.FlattenIteratorWrapper{V},\n    model::DeviceModel,\n) where {\n    T <: ConstraintType,\n    P <: ParameterType,\n    U <: VariableType,\n    V <: PSY.Component,\n}\n    time_steps = get_time_steps(container)\n    names = PSY.get_name.(devices)\n    constraint_lb =\n        add_constraints_container!(container, T(), V, names, time_steps; meta = \"$(U)_lb\")\n    constraint_ub =\n        add_constraints_container!(container, T(), V, names, time_steps; meta = \"$(U)_ub\")\n    array = get_variable(container, U(), V)\n    upper_bound_range_with_parameter!(\n        container,\n        constraint_ub,\n        array,\n        param,\n        devices,\n        model,\n    )\n    lower_bound_range_with_parameter!(\n        container,\n        constraint_lb,\n        array,\n        param,\n        devices,\n        model,\n    )\n    return\nend\n\nfunction _add_sc_feedforward_constraints!(\n    container::OptimizationContainer,\n    ::Type{T},\n    ::P,\n    ::VariableKey{U, V},\n    devices::IS.FlattenIteratorWrapper{V},\n    model::DeviceModel{V, W},\n) where {\n    T <: FeedforwardSemiContinuousConstraint,\n    P <: OnStatusParameter,\n    U <: Union{ActivePowerVariable, PowerAboveMinimumVariable},\n    V <: PSY.Component,\n    W <: AbstractDeviceFormulation,\n}\n    time_steps = get_time_steps(container)\n    names = PSY.get_name.(devices)\n    constraint_lb =\n        add_constraints_container!(container, T(), V, names, time_steps; meta = \"$(U)_lb\")\n    constraint_ub =\n        add_constraints_container!(container, T(), V, names, time_steps; meta = \"$(U)_ub\")\n    array_lb = get_expression(container, ActivePowerRangeExpressionLB(), V)\n    array_ub = get_expression(container, ActivePowerRangeExpressionUB(), V)\n    parameter = get_parameter_array(container, P(), V)\n    mult_ub = DenseAxisArray(zeros(length(devices), time_steps[end]), names, time_steps)\n    mult_lb = DenseAxisArray(zeros(length(devices), time_steps[end]), names, time_steps)\n    jump_model = get_jump_model(container)\n    _upper_bound_range_with_parameter!(\n        jump_model,\n        constraint_ub,\n        array_ub,\n        mult_ub,\n        parameter,\n        devices,\n    )\n    _lower_bound_range_with_parameter!(\n        jump_model,\n        constraint_lb,\n        array_lb,\n        mult_lb,\n        parameter,\n        devices,\n    )\n    return\nend\n\nfunction _add_sc_feedforward_constraints!(\n    container::OptimizationContainer,\n    ::Type{T},\n    ::P,\n    ::VariableKey{U, V},\n    devices::IS.FlattenIteratorWrapper{V},\n    model::DeviceModel{V, W},\n) where {\n    T <: FeedforwardSemiContinuousConstraint,\n    P <: ParameterType,\n    U <: VariableType,\n    V <: PSY.Component,\n    W <: AbstractDeviceFormulation,\n}\n    time_steps = get_time_steps(container)\n    names = PSY.get_name.(devices)\n    constraint_lb =\n        add_constraints_container!(container, T(), V, names, time_steps; meta = \"$(U)_lb\")\n    constraint_ub =\n        add_constraints_container!(container, T(), V, names, time_steps; meta = \"$(U)_ub\")\n    variable = get_variable(container, U(), V)\n    parameter = get_parameter_array(container, P(), V)\n    upper_bounds = [get_variable_upper_bound(U(), d, W()) for d in devices]\n    lower_bounds = [get_variable_lower_bound(U(), d, W()) for d in devices]\n    if any(isnothing.(upper_bounds)) || any(isnothing.(lower_bounds))\n        throw(IS.InvalidValueError(\"Bounds for variable $U $V not defined correctly\"))\n    end\n    mult_ub = DenseAxisArray(repeat(upper_bounds, 1, time_steps[end]), names, time_steps)\n    mult_lb = DenseAxisArray(repeat(lower_bounds, 1, time_steps[end]), names, time_steps)\n    jump_model = get_jump_model(container)\n    _upper_bound_range_with_parameter!(\n        jump_model,\n        constraint_ub,\n        variable,\n        mult_ub,\n        parameter,\n        devices,\n    )\n    _lower_bound_range_with_parameter!(\n        jump_model,\n        constraint_lb,\n        variable,\n        mult_lb,\n        parameter,\n        devices,\n    )\n    return\nend\n\nfunction _lower_bound_range_with_parameter!(\n    jump_model::JuMP.Model,\n    constraint_container::JuMPConstraintArray,\n    lhs_array,\n    param_multiplier::JuMPFloatArray,\n    param_array::Union{JuMPVariableArray, JuMPFloatArray},\n    devices::IS.FlattenIteratorWrapper{V},\n) where {V <: PSY.Component}\n    time_steps = axes(constraint_container)[2]\n    for device in devices\n        if hasmethod(PSY.get_must_run, Tuple{V})\n            PSY.get_must_run(device) && continue\n        end\n        name = PSY.get_name(device)\n        for t in time_steps\n            constraint_container[name, t] = JuMP.@constraint(\n                jump_model,\n                lhs_array[name, t] >= param_multiplier[name, t] * param_array[name, t]\n            )\n        end\n    end\n    return\nend\n\nfunction _upper_bound_range_with_parameter!(\n    jump_model::JuMP.Model,\n    constraint_container::JuMPConstraintArray,\n    lhs_array,\n    param_multiplier::JuMPFloatArray,\n    param_array::Union{JuMPVariableArray, JuMPFloatArray},\n    devices::IS.FlattenIteratorWrapper{V},\n) where {V <: PSY.Component}\n    time_steps = axes(constraint_container)[2]\n    for device in devices\n        if hasmethod(PSY.get_must_run, Tuple{V})\n            PSY.get_must_run(device) && continue\n        end\n        name = PSY.get_name(device)\n        for t in time_steps\n            constraint_container[name, t] = JuMP.@constraint(\n                jump_model,\n                lhs_array[name, t] <= param_multiplier[name, t] * param_array[name, t]\n            )\n        end\n    end\n    return\nend\n\nfunction add_feedforward_constraints!(\n    container::OptimizationContainer,\n    model::DeviceModel,\n    devices::IS.FlattenIteratorWrapper{T},\n    ff::SemiContinuousFeedforward,\n) where {T <: PSY.Component}\n    parameter_type = get_default_parameter_type(ff, T)\n    time_steps = get_time_steps(container)\n    for var in get_affected_values(ff)\n        variable = get_variable(container, var)\n        axes = JuMP.axes(variable)\n        @assert issetequal(axes[1], PSY.get_name.(devices))\n        IS.@assert_op axes[2] == time_steps\n        # If the variable was a lower bound != 0, not removing the LB can cause infeasibilities\n        for v in variable\n            if JuMP.has_lower_bound(v) && JuMP.lower_bound(v) > 0.0\n                @debug \"lb reset\" JuMP.lower_bound(v) v _group =\n                    LOG_GROUP_FEEDFORWARDS_CONSTRUCTION\n                JuMP.set_lower_bound(v, 0.0)\n            end\n        end\n        _add_sc_feedforward_constraints!(\n            container,\n            FeedforwardSemiContinuousConstraint,\n            parameter_type(),\n            var,\n            devices,\n            model,\n        )\n    end\n    return\nend\n\nfunction add_feedforward_constraints!(\n    container::OptimizationContainer,\n    model::DeviceModel{T, U},\n    devices::IS.FlattenIteratorWrapper{T},\n    ff::SemiContinuousFeedforward,\n) where {T <: PSY.ThermalGen, U <: AbstractThermalFormulation}\n    parameter_type = get_default_parameter_type(ff, T)\n    time_steps = get_time_steps(container)\n    for var in get_affected_values(ff)\n        variable = get_variable(container, var)\n        axes = JuMP.axes(variable)\n        @assert issetequal(axes[1], PSY.get_name.(devices))\n        IS.@assert_op axes[2] == time_steps\n        # If the variable was a lower bound != 0, not removing the LB can cause infeasibilities\n        for d in devices\n            PSY.get_must_run(d) && continue\n            for v in variable[PSY.get_name(d), :]\n                if JuMP.has_lower_bound(v) && JuMP.lower_bound(v) > 0.0\n                    @debug \"lb reset $(PSY.get_name(d))\" JuMP.lower_bound(v) v _group =\n                        LOG_GROUP_FEEDFORWARDS_CONSTRUCTION\n                    JuMP.set_lower_bound(v, 0.0)\n                end\n            end\n        end\n        _add_sc_feedforward_constraints!(\n            container,\n            FeedforwardSemiContinuousConstraint,\n            parameter_type(),\n            var,\n            devices,\n            model,\n        )\n    end\n    return\nend\n\n@doc raw\"\"\"\n        ub_ff(container::OptimizationContainer,\n              cons_name::Symbol,\n              constraint_infos,\n              param_reference,\n              var_key::VariableKey)\n\nConstructs a parameterized upper bound constraint to implement feedforward from other models.\nThe Parameters are initialized using the uppper boundary values of the provided variables.\n\n\n``` variable[var_name, t] <= param_reference[var_name] ```\n\n# LaTeX\n\n`` x \\leq param^{max}``\n\n# Arguments\n* container::OptimizationContainer : the optimization_container model built in PowerSimulations\n* cons_name::Symbol : name of the constraint\n* param_reference : Reference to the JuMP.VariableRef used to determine the upperbound\n* var_key::VariableKey : the name of the continuous variable\n\"\"\"\nfunction add_feedforward_constraints!(\n    container::OptimizationContainer,\n    ::DeviceModel,\n    devices::IS.FlattenIteratorWrapper{T},\n    ff::UpperBoundFeedforward,\n) where {T <: PSY.Component}\n    time_steps = get_time_steps(container)\n    parameter_type = get_default_parameter_type(ff, T)\n    param_ub = get_parameter_array(container, parameter_type(), T)\n    multiplier_ub = get_parameter_multiplier_array(container, parameter_type(), T)\n    for var in get_affected_values(ff)\n        variable = get_variable(container, var)\n        device_name_set, set_time = JuMP.axes(variable)\n        @assert issetequal(device_name_set, PSY.get_name.(devices))\n        IS.@assert_op set_time == time_steps\n\n        var_type = get_entry_type(var)\n        con_ub = add_constraints_container!(\n            container,\n            FeedforwardUpperBoundConstraint(),\n            T,\n            device_name_set,\n            time_steps;\n            meta = \"$(var_type)ub\",\n        )\n\n        for t in time_steps, name in device_name_set\n            con_ub[name, t] = JuMP.@constraint(\n                container.JuMPmodel,\n                variable[name, t] <= param_ub[name, t] * multiplier_ub[name, t]\n            )\n        end\n    end\n    return\nend\n\n@doc raw\"\"\"\n        lb_ff(container::OptimizationContainer,\n              cons_name::Symbol,\n              constraint_infos,\n              param_reference,\n              var_key::VariableKey)\n\nConstructs a parameterized upper bound constraint to implement feedforward from other models.\nThe Parameters are initialized using the uppper boundary values of the provided variables.\n\n\n``` variable[var_name, t] <= param_reference[var_name] ```\n\n# LaTeX\n\n`` x \\leq param^{max}``\n\n# Arguments\n* container::OptimizationContainer : the optimization_container model built in PowerSimulations\n* cons_name::Symbol : name of the constraint\n* param_reference : Reference to the JuMP.VariableRef used to determine the upperbound\n* var_key::VariableKey : the name of the continuous variable\n\"\"\"\nfunction add_feedforward_constraints!(\n    container::OptimizationContainer,\n    ::DeviceModel{T, U},\n    devices::IS.FlattenIteratorWrapper{T},\n    ff::LowerBoundFeedforward,\n) where {T <: PSY.Component, U <: AbstractDeviceFormulation}\n    time_steps = get_time_steps(container)\n    parameter_type = get_default_parameter_type(ff, T)\n    param_ub = get_parameter_array(container, parameter_type(), T)\n    multiplier_ub = get_parameter_multiplier_array(container, parameter_type(), T)\n    for var in get_affected_values(ff)\n        variable = get_variable(container, var)\n        device_name_set, set_time = JuMP.axes(variable)\n        @assert issetequal(device_name_set, PSY.get_name.(devices))\n        IS.@assert_op set_time == time_steps\n\n        var_type = get_entry_type(var)\n        con_ub = add_constraints_container!(\n            container,\n            FeedforwardLowerBoundConstraint(),\n            T,\n            device_name_set,\n            time_steps;\n            meta = \"$(var_type)lb\",\n        )\n\n        use_slacks = get_slacks(ff)\n        for t in time_steps, name in device_name_set\n            if use_slacks\n                slack_var =\n                    get_variable(container, LowerBoundFeedForwardSlack(), T, \"$(var_type)\")\n                con_ub[name, t] = JuMP.@constraint(\n                    get_jump_model(container),\n                    variable[name, t] + slack_var[name, t] >=\n                    param_ub[name, t] * multiplier_ub[name, t]\n                )\n            else\n                con_ub[name, t] = JuMP.@constraint(\n                    get_jump_model(container),\n                    variable[name, t] >= param_ub[name, t] * multiplier_ub[name, t]\n                )\n            end\n        end\n    end\n    return\nend\n\nfunction add_feedforward_constraints!(\n    container::OptimizationContainer,\n    model::ServiceModel{T, U},\n    contributing_devices::Vector{V},\n    ff::LowerBoundFeedforward,\n) where {T <: PSY.Service, U <: AbstractServiceFormulation, V <: PSY.Component}\n    time_steps = get_time_steps(container)\n    parameter_type = get_default_parameter_type(ff, T)\n    param_ub = get_parameter_array(container, parameter_type(), T, get_service_name(model))\n    service_name = get_service_name(model)\n    multiplier_ub = get_parameter_multiplier_array(\n        container,\n        parameter_type(),\n        T,\n        service_name,\n    )\n    use_slacks = get_slacks(ff)\n    for var in get_affected_values(ff)\n        variable = get_variable(container, var)\n        device_name_set, set_time = JuMP.axes(variable)\n        IS.@assert_op device_name_set == [PSY.get_name(d) for d in contributing_devices]\n        IS.@assert_op set_time == time_steps\n\n        var_type = get_entry_type(var)\n        con_lb = add_constraints_container!(\n            container,\n            FeedforwardLowerBoundConstraint(),\n            T,\n            device_name_set,\n            time_steps;\n            meta = \"$(var_type)_$(service_name)\",\n        )\n\n        for t in time_steps, name in device_name_set\n            if use_slacks\n                slack_var = get_variable(\n                    container,\n                    LowerBoundFeedForwardSlack(),\n                    T,\n                    \"$(var_type)_$(service_name)\",\n                )\n                slack_var[name, t]\n                con_lb[name, t] = JuMP.@constraint(\n                    get_jump_model(container),\n                    variable[name, t] + slack_var[name, t] >=\n                    param_ub[name, t] * multiplier_ub[name, t]\n                )\n            else\n                con_lb[name, t] = JuMP.@constraint(\n                    get_jump_model(container),\n                    variable[name, t] >= param_ub[name, t] * multiplier_ub[name, t]\n                )\n            end\n        end\n    end\n    return\nend\n\n@doc raw\"\"\"\n        add_feedforward_constraints(\n            container::OptimizationContainer,\n            ::DeviceModel,\n            devices::IS.FlattenIteratorWrapper{T},\n            ff::FixValueFeedforward,\n        ) where {T <: PSY.Component}\n\nConstructs a equality constraint to a fix a variable in one model using the variable value from other model results.\n\n\n``` variable[var_name, t] == param[var_name, t] ```\n\n# LaTeX\n\n`` x == param``\n\n# Arguments\n* container::OptimizationContainer : the optimization_container model built in PowerSimulations\n* model::DeviceModel : the device model\n* devices::IS.FlattenIteratorWrapper{T} : list of devices\n* ff::FixValueFeedforward : a instance of the FixValue Feedforward\n\"\"\"\nfunction add_feedforward_constraints!(\n    container::OptimizationContainer,\n    ::DeviceModel,\n    devices::Union{Vector{T}, IS.FlattenIteratorWrapper{T}},\n    ff::FixValueFeedforward,\n) where {T <: PSY.Component}\n    parameter_type = get_default_parameter_type(ff, T)\n    source_key = get_optimization_container_key(ff)\n    var_type = get_entry_type(source_key)\n    param = get_parameter_array(container, parameter_type(), T, \"$var_type\")\n    multiplier = get_parameter_multiplier_array(container, parameter_type(), T, \"$var_type\")\n    for var in get_affected_values(ff)\n        variable = get_variable(container, var)\n        device_name_set, set_time = JuMP.axes(variable)\n        IS.@assert_op device_name_set == PSY.get_name.(devices)\n\n        for t in set_time, name in device_name_set\n            JuMP.fix(variable[name, t], param[name, t] * multiplier[name, t]; force = true)\n        end\n    end\n    return\nend\n\nfunction add_feedforward_constraints!(\n    container::OptimizationContainer,\n    model::ServiceModel{T, U},\n    devices::Vector{V},\n    ff::FixValueFeedforward,\n) where {T, U, V <: PSY.Component}\n    time_steps = get_time_steps(container)\n    parameter_type = get_default_parameter_type(ff, T)\n    param = get_parameter_array(container, parameter_type(), T, get_service_name(model))\n    multiplier = get_parameter_multiplier_array(\n        container,\n        parameter_type(),\n        T,\n        get_service_name(model),\n    )\n    for var in get_affected_values(ff)\n        variable = get_variable(container, var)\n        device_name_set, set_time = JuMP.axes(variable)\n        @assert issetequal(device_name_set, PSY.get_name.(devices))\n        IS.@assert_op set_time == time_steps\n        for t in time_steps, name in device_name_set\n            JuMP.fix(variable[name, t], param[name, t] * multiplier[name, t]; force = true)\n        end\n    end\n    return\nend\n"
  },
  {
    "path": "src/feedforward/feedforwards.jl",
    "content": "function get_affected_values(ff::AbstractAffectFeedforward)\n    return ff.affected_values\nend\n\nfunction attach_feedforward!(\n    model::DeviceModel,\n    ff::T,\n) where {T <: AbstractAffectFeedforward}\n    if !isempty(model.feedforwards)\n        ff_k = [get_optimization_container_key(v) for v in model.feedforwards if isa(v, T)]\n        if get_optimization_container_key(ff) ∈ ff_k\n            return\n        end\n    end\n    push!(model.feedforwards, ff)\n    return\nend\n\nfunction attach_feedforward!(\n    model::ServiceModel,\n    ff::T,\n) where {T <: AbstractAffectFeedforward}\n    if get_feedforward_meta(ff) != NO_SERVICE_NAME_PROVIDED\n        ff_ = ff\n    else\n        ff_ = T(;\n            component_type = get_component_type(ff),\n            source = get_entry_type(get_optimization_container_key(ff)),\n            affected_values = affected_values =\n                get_entry_type.(get_affected_values(ff)),\n            meta = model.service_name,\n        )\n    end\n    if !isempty(model.feedforwards)\n        ff_k = [get_optimization_container_key(v) for v in model.feedforwards if isa(v, T)]\n        if get_optimization_container_key(ff_) ∈ ff_k\n            return\n        end\n    end\n    push!(model.feedforwards, ff_)\n    return\nend\n\nfunction get_component_type(ff::AbstractAffectFeedforward)\n    return get_component_type(get_optimization_container_key(ff))\nend\n\nfunction get_feedforward_meta(ff::AbstractAffectFeedforward)\n    return get_optimization_container_key(ff).meta\nend\n\n\"\"\"\n    UpperBoundFeedforward(\n        component_type::Type{<:PSY.Component},\n        source::Type{T},\n        affected_values::Vector{DataType},\n        add_slacks::Bool = false,\n        meta = CONTAINER_KEY_EMPTY_META\n    ) where {T}\n\nConstructs a parameterized upper bound constraint to implement feedforward from other models.\n\n# Arguments:\n* `component_type::Type{<:PSY.Component}` : Specify the type of component on which the Feedforward will be applied\n* `source::Type{T}` : Specify the VariableType, ParameterType or AuxVariableType as the source of values for the Feedforward\n* `affected_values::Vector{DataType}` : Specify the variable on which the upper bound will be applied using the source values\n* `add_slacks::Bool = false` : Add slacks variables to relax the upper bound constraint.\n\n\"\"\"\nstruct UpperBoundFeedforward <: AbstractAffectFeedforward\n    optimization_container_key::OptimizationContainerKey\n    affected_values::Vector\n    add_slacks::Bool\n    function UpperBoundFeedforward(;\n        component_type::Type{<:PSY.Component},\n        source::Type{T},\n        affected_values::Vector{DataType},\n        add_slacks::Bool = false,\n        meta = ISOPT.CONTAINER_KEY_EMPTY_META,\n    ) where {T}\n        values_vector = Vector(undef, length(affected_values))\n        for (ix, v) in enumerate(affected_values)\n            if v <: VariableType\n                values_vector[ix] =\n                    get_optimization_container_key(v(), component_type, meta)\n            else\n                error(\n                    \"UpperBoundFeedforward is only compatible with VariableType affected values\",\n                )\n            end\n        end\n        new(\n            get_optimization_container_key(T(), component_type, meta),\n            values_vector,\n            add_slacks,\n        )\n    end\nend\n\nget_default_parameter_type(::UpperBoundFeedforward, _) = UpperBoundValueParameter\nget_optimization_container_key(ff::UpperBoundFeedforward) = ff.optimization_container_key\nget_slacks(ff::UpperBoundFeedforward) = ff.add_slacks\n\n\"\"\"\n    LowerBoundFeedforward(\n        component_type::Type{<:PSY.Component},\n        source::Type{T},\n        affected_values::Vector{DataType},\n        add_slacks::Bool = false,\n        meta = CONTAINER_KEY_EMPTY_META\n    ) where {T}\n\nConstructs a parameterized lower bound constraint to implement feedforward from other models.\n\n# Arguments:\n* `component_type::Type{<:PSY.Component}` : Specify the type of component on which the Feedforward will be applied\n* `source::Type{T}` : Specify the VariableType, ParameterType or AuxVariableType as the source of values for the Feedforward\n* `affected_values::Vector{DataType}` : Specify the variable on which the lower bound will be applied using the source values\n* `add_slacks::Bool = false` : Add slacks variables to relax the lower bound constraint.\n\n\"\"\"\nstruct LowerBoundFeedforward <: AbstractAffectFeedforward\n    optimization_container_key::OptimizationContainerKey\n    affected_values::Vector{<:OptimizationContainerKey}\n    add_slacks::Bool\n    function LowerBoundFeedforward(;\n        component_type::Type{<:PSY.Component},\n        source::Type{T},\n        affected_values::Vector{DataType},\n        add_slacks::Bool = false,\n        meta = ISOPT.CONTAINER_KEY_EMPTY_META,\n    ) where {T}\n        values_vector = Vector{VariableKey}(undef, length(affected_values))\n        for (ix, v) in enumerate(affected_values)\n            if v <: VariableType\n                values_vector[ix] =\n                    get_optimization_container_key(v(), component_type, meta)\n            else\n                error(\n                    \"LowerBoundFeedforward is only compatible with VariableType affected values\",\n                )\n            end\n        end\n        new(\n            get_optimization_container_key(T(), component_type, meta),\n            values_vector,\n            add_slacks,\n        )\n    end\nend\n\nget_default_parameter_type(::LowerBoundFeedforward, _) = LowerBoundValueParameter\nget_optimization_container_key(ff::LowerBoundFeedforward) = ff.optimization_container_key\nget_slacks(ff::LowerBoundFeedforward) = ff.add_slacks\n\nfunction attach_feedforward!(\n    model::ServiceModel,\n    ff::T,\n) where {T <: Union{LowerBoundFeedforward, UpperBoundFeedforward}}\n    if get_feedforward_meta(ff) != NO_SERVICE_NAME_PROVIDED\n        ff_ = ff\n    else\n        ff_ = T(;\n            component_type = get_component_type(ff),\n            source = get_entry_type(get_optimization_container_key(ff)),\n            affected_values = get_entry_type.(get_affected_values(ff)),\n            meta = model.service_name,\n            add_slacks = ff.add_slacks,\n        )\n    end\n    if !isempty(model.feedforwards)\n        ff_k = [get_optimization_container_key(v) for v in model.feedforwards if isa(v, T)]\n        if get_optimization_container_key(ff_) ∈ ff_k\n            return\n        end\n    end\n    push!(model.feedforwards, ff_)\n    return\nend\n\n\"\"\"\n    SemiContinuousFeedforward(\n        component_type::Type{<:PSY.Component},\n        source::Type{T},\n        affected_values::Vector{DataType},\n        meta = CONTAINER_KEY_EMPTY_META\n    ) where {T}\n\nIt allows to enable/disable bounds to 0.0 for a specified variable. Commonly used to limit the\n`ActivePowerVariable` in an Economic Dispatch problem by the commitment decision taken in\nan another problem (typically a Unit Commitment problem).\n\n# Arguments:\n* `component_type::Type{<:PSY.Component}` : Specify the type of component on which the Feedforward will be applied\n* `source::Type{T}` : Specify the VariableType, ParameterType or AuxVariableType as the source of values for the Feedforward\n* `affected_values::Vector{DataType}` : Specify the variable on which the semicontinuous limit will be applied using the source values\n\"\"\"\nstruct SemiContinuousFeedforward <: AbstractAffectFeedforward\n    optimization_container_key::OptimizationContainerKey\n    affected_values::Vector{<:OptimizationContainerKey}\n    function SemiContinuousFeedforward(;\n        component_type::Type{<:PSY.Component},\n        source::Type{T},\n        affected_values::Vector{DataType},\n        meta = ISOPT.CONTAINER_KEY_EMPTY_META,\n    ) where {T}\n        values_vector = Vector{VariableKey}(undef, length(affected_values))\n        for (ix, v) in enumerate(affected_values)\n            if v <: VariableType\n                values_vector[ix] =\n                    get_optimization_container_key(v(), component_type, meta)\n            else\n                error(\n                    \"SemiContinuousFeedforward is only compatible with VariableType affected values\",\n                )\n            end\n        end\n        new(get_optimization_container_key(T(), component_type, meta), values_vector)\n    end\nend\n\nget_default_parameter_type(::SemiContinuousFeedforward, _) = OnStatusParameter\nget_optimization_container_key(f::SemiContinuousFeedforward) = f.optimization_container_key\n\nfunction has_semicontinuous_feedforward(\n    model::DeviceModel,\n    ::Type{T},\n)::Bool where {T <: Union{VariableType, ExpressionType}}\n    if isempty(model.feedforwards)\n        return false\n    end\n    sc_feedforwards = [x for x in model.feedforwards if isa(x, SemiContinuousFeedforward)]\n    if isempty(sc_feedforwards)\n        return false\n    end\n\n    keys = get_affected_values(sc_feedforwards[1])\n\n    return T ∈ get_entry_type.(keys)\nend\n\nfunction has_semicontinuous_feedforward(\n    model::DeviceModel,\n    ::Type{T},\n)::Bool where {T <: Union{ActivePowerRangeExpressionUB, ActivePowerRangeExpressionLB}}\n    return has_semicontinuous_feedforward(model, ActivePowerVariable)\nend\n\n\"\"\"\n    FixValueFeedforward(\n        component_type::Type{<:PSY.Component},\n        source::Type{T},\n        affected_values::Vector{DataType},\n        meta = CONTAINER_KEY_EMPTY_META\n    ) where {T}\n\nFixes a Variable or Parameter Value in the model from another problem. Is the only Feed Forward that can be used\nwith a Parameter or a Variable as the affected value.\n\n# Arguments:\n* `component_type::Type{<:PSY.Component}` : Specify the type of component on which the Feedforward will be applied\n* `source::Type{T}` : Specify the VariableType, ParameterType or AuxVariableType as the source of values for the Feedforward\n* `affected_values::Vector{DataType}` : Specify the variable on which the fix value will be applied using the source values\n\"\"\"\nstruct FixValueFeedforward <: AbstractAffectFeedforward\n    optimization_container_key::OptimizationContainerKey\n    affected_values::Vector\n    function FixValueFeedforward(;\n        component_type::Type{<:PSY.Component},\n        source::Type{T},\n        affected_values::Vector{DataType},\n        meta = ISOPT.CONTAINER_KEY_EMPTY_META,\n    ) where {T}\n        values_vector = Vector(undef, length(affected_values))\n        for (ix, v) in enumerate(affected_values)\n            if v <: VariableType || v <: ParameterType\n                values_vector[ix] =\n                    get_optimization_container_key(v(), component_type, meta)\n            else\n                error(\n                    \"UpperBoundFeedforward is only compatible with VariableType affected values\",\n                )\n            end\n        end\n        new(get_optimization_container_key(T(), component_type, meta), values_vector)\n    end\nend\n\nget_default_parameter_type(::FixValueFeedforward, _) = FixValueParameter\nget_optimization_container_key(ff::FixValueFeedforward) = ff.optimization_container_key\n"
  },
  {
    "path": "src/initial_conditions/add_initial_condition.jl",
    "content": "function _get_initial_conditions_value(\n    ::Vector{T},\n    component::W,\n    ::U,\n    ::V,\n    container::OptimizationContainer,\n) where {\n    T <: InitialCondition{U, Nothing},\n    V <: Union{AbstractDeviceFormulation, AbstractServiceFormulation},\n    W <: PSY.Component,\n} where {U <: InitialConditionType}\n    return InitialCondition{U, Nothing}(component, nothing)\nend\n\nfunction _get_initial_conditions_value(\n    ::Vector{T},\n    component::W,\n    ::U,\n    ::V,\n    container::OptimizationContainer,\n) where {\n    T <: Union{InitialCondition{U, Float64}, InitialCondition{U, Nothing}},\n    V <: Union{AbstractDeviceFormulation, AbstractServiceFormulation},\n    W <: PSY.Component,\n} where {U <: InitialConditionType}\n    ic_data = get_initial_conditions_data(container)\n    var_type = initial_condition_variable(U(), component, V())\n    if !has_initial_condition_value(ic_data, var_type, W)\n        val = initial_condition_default(U(), component, V())\n    else\n        val = get_initial_condition_value(ic_data, var_type, W)[PSY.get_name(component), 1]\n    end\n    @debug \"Device $(PSY.get_name(component)) initialized $U as $val\" _group =\n        LOG_GROUP_BUILD_INITIAL_CONDITIONS\n    return InitialCondition{U, Float64}(component, val)\nend\n\nfunction _get_initial_conditions_value(\n    ::Vector{T},\n    component::W,\n    ::U,\n    ::V,\n    container::OptimizationContainer,\n) where {\n    T <: Union{InitialCondition{U, JuMP.VariableRef}, InitialCondition{U, Nothing}},\n    V <: AbstractDeviceFormulation,\n    W <: PSY.Component,\n} where {U <: InitialConditionType}\n    ic_data = get_initial_conditions_data(container)\n    var_type = initial_condition_variable(U(), component, V())\n    if !has_initial_condition_value(ic_data, var_type, W)\n        val = initial_condition_default(U(), component, V())\n    else\n        val = get_initial_condition_value(ic_data, var_type, W)[PSY.get_name(component), 1]\n    end\n    @debug \"Device $(PSY.get_name(component)) initialized $U as $val\" _group =\n        LOG_GROUP_BUILD_INITIAL_CONDITIONS\n    return InitialCondition{U, JuMP.VariableRef}(\n        component,\n        add_jump_parameter(get_jump_model(container), val),\n    )\nend\n\nfunction _get_initial_conditions_value(\n    ::Vector{Union{InitialCondition{U, Float64}, InitialCondition{U, Nothing}}},\n    component::W,\n    ::U,\n    ::V,\n    container::OptimizationContainer,\n) where {\n    V <: AbstractThermalFormulation,\n    W <: PSY.Component,\n} where {U <: InitialTimeDurationOff}\n    ic_data = get_initial_conditions_data(container)\n    var_type = initial_condition_variable(U(), component, V())\n    if !has_initial_condition_value(ic_data, var_type, W)\n        val = initial_condition_default(U(), component, V())\n    else\n        var = get_initial_condition_value(ic_data, var_type, W)[PSY.get_name(component), 1]\n        val = 0.0\n        if !PSY.get_status(component) && !(var > ABSOLUTE_TOLERANCE)\n            val = PSY.get_time_at_status(component)\n        end\n    end\n    @debug \"Device $(PSY.get_name(component)) initialized $U as $val\" _group =\n        LOG_GROUP_BUILD_INITIAL_CONDITIONS\n    return InitialCondition{U, Float64}(component, val)\nend\n\nfunction _get_initial_conditions_value(\n    ::Vector{Union{InitialCondition{U, JuMP.VariableRef}, InitialCondition{U, Nothing}}},\n    component::W,\n    ::U,\n    ::V,\n    container::OptimizationContainer,\n) where {\n    V <: AbstractThermalFormulation,\n    W <: PSY.ThermalGen,\n} where {U <: InitialTimeDurationOff}\n    ic_data = get_initial_conditions_data(container)\n    var_type = initial_condition_variable(U(), component, V())\n    if !has_initial_condition_value(ic_data, var_type, W)\n        val = initial_condition_default(U(), component, V())\n    else\n        var = get_initial_condition_value(ic_data, var_type, W)[PSY.get_name(component), 1]\n        val = 0.0\n        if !PSY.get_status(component) && !(var > ABSOLUTE_TOLERANCE)\n            val = PSY.get_time_at_status(component)\n        end\n    end\n    @debug \"Device $(PSY.get_name(component)) initialized $U as $val\" _group =\n        LOG_GROUP_BUILD_INITIAL_CONDITIONS\n    return InitialCondition{U, JuMP.VariableRef}(\n        component,\n        add_jump_parameter(get_jump_model(container), val),\n    )\nend\n\nfunction _get_initial_conditions_value(\n    ::Vector{Union{InitialCondition{U, Float64}, InitialCondition{U, Nothing}}},\n    component::W,\n    ::U,\n    ::V,\n    container::OptimizationContainer,\n) where {\n    V <: AbstractThermalFormulation,\n    W <: PSY.ThermalGen,\n} where {U <: InitialTimeDurationOn}\n    ic_data = get_initial_conditions_data(container)\n    var_type = initial_condition_variable(U(), component, V())\n    if !has_initial_condition_value(ic_data, var_type, W)\n        val = initial_condition_default(U(), component, V())\n    else\n        var = get_initial_condition_value(ic_data, var_type, W)[PSY.get_name(component), 1]\n        val = 0.0\n        if PSY.get_status(component) && (var > ABSOLUTE_TOLERANCE)\n            val = PSY.get_time_at_status(component)\n        end\n    end\n    @debug \"Device $(PSY.get_name(component)) initialized $U as $val\" _group =\n        LOG_GROUP_BUILD_INITIAL_CONDITIONS\n    return InitialCondition{U, Float64}(component, val)\nend\n\nfunction _get_initial_conditions_value(\n    ::Vector{Union{InitialCondition{U, JuMP.VariableRef}, InitialCondition{U, Nothing}}},\n    component::W,\n    ::U,\n    ::V,\n    container::OptimizationContainer,\n) where {\n    V <: AbstractThermalFormulation,\n    W <: PSY.ThermalGen,\n} where {U <: InitialTimeDurationOn}\n    ic_data = get_initial_conditions_data(container)\n    var_type = initial_condition_variable(U(), component, V())\n    if !has_initial_condition_value(ic_data, var_type, W)\n        val = initial_condition_default(U(), component, V())\n    else\n        var = get_initial_condition_value(ic_data, var_type, W)[PSY.get_name(component), 1]\n        val = 0.0\n        if PSY.get_status(component) && (var > ABSOLUTE_TOLERANCE)\n            val = PSY.get_time_at_status(component)\n        end\n    end\n    @debug \"Device $(PSY.get_name(component)) initialized $U as $val\" _group =\n        LOG_GROUP_BUILD_INITIAL_CONDITIONS\n    return InitialCondition{U, JuMP.VariableRef}(\n        component,\n        add_jump_parameter(get_jump_model(container), val),\n    )\nend\n\nfunction _get_initial_conditions_value(\n    ::Vector{T},\n    component::W,\n    ::U,\n    ::V,\n    container::OptimizationContainer,\n) where {\n    T <: InitialCondition{U, JuMP.VariableRef},\n    V <: AbstractDeviceFormulation,\n    W <: PSY.Component,\n} where {U <: InitialEnergyLevel}\n    var_type = initial_condition_variable(U(), component, V())\n    val = initial_condition_default(U(), component, V())\n    @debug \"Device $(PSY.get_name(component)) initialized $U as $val\" _group =\n        LOG_GROUP_BUILD_INITIAL_CONDITIONS\n    return T(component, add_jump_parameter(get_jump_model(container), val))\nend\n\nfunction _get_initial_conditions_value(\n    ::Vector{T},\n    component::W,\n    ::U,\n    ::V,\n    container::OptimizationContainer,\n) where {\n    T <: InitialCondition{U, Float64},\n    V <: AbstractDeviceFormulation,\n    W <: PSY.Component,\n} where {U <: InitialEnergyLevel}\n    var_type = initial_condition_variable(U(), component, V())\n    val = initial_condition_default(U(), component, V())\n    @debug \"Device $(PSY.get_name(component)) initialized $U as $val\" _group =\n        LOG_GROUP_BUILD_INITIAL_CONDITIONS\n    return T(component, val)\nend\n\nfunction add_initial_condition!(\n    container::OptimizationContainer,\n    components::Union{Vector{T}, IS.FlattenIteratorWrapper{T}},\n    ::U,\n    ::D,\n) where {\n    T <: PSY.Component,\n    U <: Union{AbstractDeviceFormulation, AbstractServiceFormulation},\n    D <: InitialConditionType,\n}\n    if get_rebuild_model(get_settings(container)) && has_container_key(container, D, T)\n        return\n    end\n\n    ini_cond_vector = add_initial_condition_container!(container, D(), T, components)\n    for (ix, component) in enumerate(components)\n        ini_cond_vector[ix] =\n            _get_initial_conditions_value(ini_cond_vector, component, D(), U(), container)\n    end\n    return\nend\n\nfunction add_initial_condition!(\n    container::OptimizationContainer,\n    components::Union{Vector{T}, IS.FlattenIteratorWrapper{T}},\n    ::U,\n    ::D,\n) where {\n    T <: PSY.ThermalGen,\n    U <: AbstractThermalFormulation,\n    D <: Union{InitialTimeDurationOff, InitialTimeDurationOn, DeviceStatus},\n}\n    if get_rebuild_model(get_settings(container)) && has_container_key(container, D, T)\n        return\n    end\n\n    ini_cond_vector = add_initial_condition_container!(container, D(), T, components)\n    for (ix, component) in enumerate(components)\n        if PSY.get_must_run(component)\n            ini_cond_vector[ix] = InitialCondition{D, Nothing}(component, nothing)\n        else\n            ini_cond_vector[ix] =\n                _get_initial_conditions_value(\n                    ini_cond_vector,\n                    component,\n                    D(),\n                    U(),\n                    container,\n                )\n        end\n    end\n    return\nend\n"
  },
  {
    "path": "src/initial_conditions/calculate_initial_condition.jl",
    "content": "\"\"\"\nDefault implementation of set_initial_condition_value\n\"\"\"\nfunction set_ic_quantity!(\n    ic::InitialCondition{T, JuMP.VariableRef},\n    var_value::Float64,\n) where {T <: InitialConditionType}\n    @assert isfinite(var_value) ic\n    fix_parameter_value(ic.value, var_value)\n    return\nend\n\n\"\"\"\nDefault implementation of set_initial_condition_value\n\"\"\"\nfunction set_ic_quantity!(\n    ic::InitialCondition{T, Float64},\n    var_value::Float64,\n) where {T <: InitialConditionType}\n    @assert isfinite(var_value) ic\n    @debug \"Initial condition value set with Float64. Won't update the model until rebuild\" _group =\n        LOG_GROUP_BUILD_INITIAL_CONDITIONS\n    ic.value = var_value\n    return\nend\n\nfunction set_ic_quantity!(\n    ::InitialCondition{T, Nothing},\n    ::Float64,\n) where {T <: InitialConditionType}\n    return\nend\n"
  },
  {
    "path": "src/initial_conditions/initial_condition_chronologies.jl",
    "content": "\"\"\" Supertype for initial condition chronologies \"\"\"\nabstract type InitialConditionChronology end\n\n\"\"\"\n    InterProblemChronology()\n\nType 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\n\nSee also: [`IntraProblemChronology`](@ref)\n\"\"\"\nstruct InterProblemChronology <: InitialConditionChronology end\n\n\"\"\"\n    IntraProblemChronology()\n\nType 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.\n\nSee also: [`InterProblemChronology`](@ref)\n\"\"\"\nstruct IntraProblemChronology <: InitialConditionChronology end\n"
  },
  {
    "path": "src/initial_conditions/initialization.jl",
    "content": "function get_initial_conditions_template(model::OperationModel, number_of_steps::Int)\n    # This is done to avoid passing the duals but also not re-allocating the PTDF when it\n    # exists\n\n    network_model = NetworkModel(\n        get_network_formulation(model.template);\n        use_slacks = get_use_slacks(get_network_model(model.template)),\n        PTDF_matrix = get_PTDF_matrix(get_network_model(model.template)),\n        reduce_radial_branches = get_reduce_radial_branches(\n            get_network_model(model.template),\n        ),\n    )\n    set_hvdc_network_model!(\n        network_model,\n        deepcopy(get_hvdc_network_model(model.template)),\n    )\n    network_model.network_reduction =\n        deepcopy(get_network_reduction(get_network_model(model.template)))\n    network_model.subnetworks = get_subnetworks(get_network_model(model.template))\n    # Initialization does not support PowerFlow evaluation\n    network_model.power_flow_evaluation = Vector{PFS.PowerFlowEvaluationModel}[]\n    bus_area_map = get_bus_area_map(get_network_model(model.template))\n\n    if !isempty(bus_area_map)\n        network_model.bus_area_map = get_bus_area_map(get_network_model(model.template))\n    end\n    network_model.modeled_ac_branch_types =\n        get_network_model(model.template).modeled_ac_branch_types\n    ic_template = ProblemTemplate(network_model)\n    # Do not copy events here for initialization\n    for device_model in values(model.template.devices)\n        base_model = get_initial_conditions_device_model(model, device_model)\n        base_model.use_slacks = device_model.use_slacks\n        base_model.time_series_names = device_model.time_series_names\n        base_model.attributes = device_model.attributes\n        set_device_model!(ic_template, base_model)\n    end\n    for device_model in values(model.template.branches)\n        base_model = get_initial_conditions_device_model(model, device_model)\n        base_model.use_slacks = device_model.use_slacks\n        base_model.time_series_names = device_model.time_series_names\n        base_model.attributes = device_model.attributes\n        set_device_model!(ic_template, base_model)\n    end\n\n    for service_model in values(model.template.services)\n        base_model = get_initial_conditions_service_model(model, service_model)\n        base_model.service_name = service_model.service_name\n        base_model.contributing_devices_map = service_model.contributing_devices_map\n        base_model.use_slacks = service_model.use_slacks\n        base_model.time_series_names = service_model.time_series_names\n        base_model.attributes = service_model.attributes\n        set_service_model!(ic_template, get_service_name(service_model), base_model)\n    end\n    set_number_of_steps!(network_model.reduced_branch_tracker, number_of_steps)\n    if !isempty(model.template.services)\n        _add_services_to_device_model!(ic_template)\n    end\n    return ic_template\nend\n\nfunction _make_init_jump_model(ic_settings::Settings)\n    optimizer = get_optimizer(ic_settings)\n    JuMPmodel = JuMP.Model(optimizer)\n    warm_start_enabled = get_warm_start(ic_settings)\n    solver_supports_warm_start = _validate_warm_start_support(JuMPmodel, warm_start_enabled)\n    set_warm_start!(ic_settings, solver_supports_warm_start)\n    if get_optimizer_solve_log_print(ic_settings)\n        JuMP.unset_silent(JuMPmodel)\n        @debug \"optimizer unset to silent\" _group = LOG_GROUP_OPTIMIZATION_CONTAINER\n    else\n        JuMP.set_silent(JuMPmodel)\n        @debug \"optimizer set to silent\" _group = LOG_GROUP_OPTIMIZATION_CONTAINER\n    end\n    return JuMPmodel\nend\n\nfunction build_initial_conditions_model!(model::T) where {T <: OperationModel}\n    internal = get_internal(model)\n    ISOPT.set_initial_conditions_model_container!(\n        internal,\n        deepcopy(get_optimization_container(model)),\n    )\n    ic_container = ISOPT.get_initial_conditions_model_container(internal)\n    ic_settings = deepcopy(get_settings(ic_container))\n    main_problem_horizon = get_horizon(ic_settings)\n    # TODO: add an interface to allow user to configure initial_conditions problem\n    ic_container.JuMPmodel = _make_init_jump_model(ic_settings)\n    resolution = get_resolution(ic_settings)\n    init_horizon = INITIALIZATION_PROBLEM_HORIZON_COUNT * resolution\n    number_of_steps = min(init_horizon, main_problem_horizon)\n    template = get_initial_conditions_template(model, number_of_steps ÷ resolution)\n    ic_container.settings = ic_settings\n    ic_container.built_for_recurrent_solves = false\n    set_horizon!(ic_settings, number_of_steps)\n    init_optimization_container!(\n        ISOPT.get_initial_conditions_model_container(internal),\n        get_network_model(get_template(model)),\n        get_system(model),\n    )\n    JuMP.set_string_names_on_creation(\n        get_jump_model(ISOPT.get_initial_conditions_model_container(internal)),\n        false,\n    )\n    TimerOutputs.disable_timer!(BUILD_PROBLEMS_TIMER)\n\n    build_impl!(\n        model.internal.initial_conditions_model_container,\n        template,\n        get_system(model),\n    )\n    TimerOutputs.enable_timer!(BUILD_PROBLEMS_TIMER)\n    return\nend\n"
  },
  {
    "path": "src/initial_conditions/update_initial_conditions.jl",
    "content": "function _update_initial_conditions!(\n    model::OperationModel,\n    key::InitialConditionKey{T, U},\n    source, # Store or State are used in simulations by default\n) where {T <: InitialConditionType, U <: PSY.Component}\n    if get_execution_count(model) < 1\n        return\n    end\n    container = get_optimization_container(model)\n    model_resolution = get_resolution(get_store_params(model))\n    ini_conditions_vector = get_initial_condition(container, key)\n    timestamp = get_current_timestamp(model)\n    previous_values = get_condition.(ini_conditions_vector)\n    # The implementation of specific update_initial_conditions! is located in the files\n    # update_initial_conditions_in_memory_store.jl and update_initial_conditions_simulation.jl\n    update_initial_conditions!(ini_conditions_vector, source, model_resolution)\n    for (i, initial_condition) in enumerate(ini_conditions_vector)\n        IS.@record :execution InitialConditionUpdateEvent(\n            timestamp,\n            initial_condition,\n            previous_values[i],\n            get_name(model),\n        )\n    end\n    return\nend\n\n# Note to devs: Implemented this way to avoid ambiguities and future proof custom ic updating\nfunction update_initial_conditions!(\n    model::DecisionModel,\n    key::InitialConditionKey{T, U},\n    source, # Store or State are used in simulations by default\n) where {T <: InitialConditionType, U <: PSY.Component}\n    _update_initial_conditions!(model, key, source)\n    return\nend\n\nfunction update_initial_conditions!(\n    model::EmulationModel,\n    key::InitialConditionKey{T, U},\n    source, # Store or State are used in simulations by default\n) where {T <: InitialConditionType, U <: PSY.Component}\n    _update_initial_conditions!(model, key, source)\n    return\nend\n"
  },
  {
    "path": "src/network_models/area_balance_model.jl",
    "content": "function add_constraints!(\n    container::OptimizationContainer,\n    ::Type{CopperPlateBalanceConstraint},\n    sys::PSY.System,\n    model::NetworkModel{AreaBalancePowerModel},\n)\n    expressions = get_expression(container, ActivePowerBalance(), PSY.Area)\n    area_names, time_steps = axes(expressions)\n\n    constraints = add_constraints_container!(\n        container,\n        CopperPlateBalanceConstraint(),\n        PSY.Area,\n        area_names,\n        time_steps,\n    )\n\n    for a in area_names, t in time_steps\n        constraints[a, t] =\n            JuMP.@constraint(get_jump_model(container), expressions[a, t] == 0.0)\n    end\n    return\nend\n\n# Unavailable Feature\n#=\nfunction agc_area_balance(\n    container::OptimizationContainer,\n    expression::ExpressionKey,\n    area_mapping::Dict{String, Array{PSY.ACBus, 1}},\n    branches,\n)\n    time_steps = get_time_steps(container)\n    nodal_net_balance = get_expression(container, expression)\n\n    constraint = add_constraints_container!(\n        container,\n        CopperPlateBalanceConstraint(),\n        PSY.Area,\n        keys(area_mapping),\n        time_steps,\n    )\n    area_balance = get_variable(container, ActivePowerVariable(), PSY.Area)\n    for (k, buses_in_area) in area_mapping\n        for t in time_steps\n            area_net = JuMP.AffExpr(0.0)\n            for b in buses_in_area\n                JuMP.add_to_expression!(area_net, nodal_net_balance[PSY.get_number(b), t])\n            end\n            constraint[k, t] =\n                JuMP.@constraint(get_jump_model(container), area_balance[k, t] == area_net)\n        end\n    end\n\n    expr_up = get_expression(container, EmergencyUp(), PSY.Area)\n    expr_dn = get_expression(container, EmergencyDown(), PSY.Area)\n\n    participation_assignment_up = add_constraints_container!(\n        container,\n        AreaParticipationAssignmentConstraint(),\n        PSY.Area,\n        keys(area_mapping),\n        time_steps;\n        meta = \"up\",\n    )\n    participation_assignment_dn = add_constraints_container!(\n        container,\n        AreaParticipationAssignmentConstraint(),\n        PSY.Area,\n        keys(area_mapping),\n        time_steps;\n        meta = \"dn\",\n    )\n\n    for area in keys(area_mapping), t in time_steps\n        participation_assignment_up[area, t] =\n            JuMP.@constraint(get_jump_model(container), expr_up[area, t] == 0)\n        participation_assignment_dn[area, t] =\n            JuMP.@constraint(get_jump_model(container), expr_dn[area, t] == 0)\n    end\n\n    return\nend\n=#\n"
  },
  {
    "path": "src/network_models/copperplate_model.jl",
    "content": "function add_constraints!(\n    container::OptimizationContainer,\n    ::Type{T},\n    sys::U,\n    model::NetworkModel{V},\n) where {\n    T <: CopperPlateBalanceConstraint,\n    U <: PSY.System,\n    V <: Union{CopperPlatePowerModel, PTDFPowerModel},\n}\n    time_steps = get_time_steps(container)\n    expressions = get_expression(container, ActivePowerBalance(), U)\n    subnets = collect(keys(model.subnetworks))\n    constraint = add_constraints_container!(container, T(), U, subnets, time_steps)\n    for t in time_steps, k in keys(model.subnetworks)\n        constraint[k, t] =\n            JuMP.@constraint(get_jump_model(container), expressions[k, t] == 0)\n    end\n\n    return\nend\n\nfunction add_constraints!(\n    container::OptimizationContainer,\n    ::Type{T},\n    sys::U,\n    network_model::NetworkModel{V},\n) where {\n    T <: CopperPlateBalanceConstraint,\n    U <: PSY.System,\n    V <: AreaPTDFPowerModel,\n}\n    time_steps = get_time_steps(container)\n    expressions = get_expression(container, ActivePowerBalance(), PSY.Area)\n    area_names = PSY.get_name.(get_available_components(network_model, PSY.Area, sys))\n    constraint =\n        add_constraints_container!(container, T(), PSY.Area, area_names, time_steps)\n    jm = get_jump_model(container)\n    for t in time_steps, k in area_names\n        constraint[k, t] = JuMP.@constraint(jm, expressions[k, t] == 0)\n    end\n\n    return\nend\n"
  },
  {
    "path": "src/network_models/hvdc_network_constructor.jl",
    "content": "function construct_hvdc_network!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    transmission_model::NetworkModel{T},\n    hvdc_model::Nothing,\n    ::ProblemTemplate,\n) where {T <: PM.AbstractPowerModel}\n    return\nend\n\nfunction construct_hvdc_network!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    transmission_model::NetworkModel{T},\n    hvdc_model::TransportHVDCNetworkModel,\n    ::ProblemTemplate,\n) where {T <: PM.AbstractPowerModel}\n    add_constraints!(\n        container,\n        NodalBalanceActiveConstraint,\n        sys,\n        transmission_model,\n        hvdc_model,\n    )\n    # TODO: duals\n    #add_constraint_dual!(container, sys, hvdc_model)\n    return\nend\n\nfunction construct_hvdc_network!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    transmission_model::NetworkModel{T},\n    hvdc_model::VoltageDispatchHVDCNetworkModel,\n    ::ProblemTemplate,\n) where {T <: PM.AbstractPowerModel}\n    add_constraints!(\n        container,\n        NodalBalanceCurrentConstraint,\n        sys,\n        transmission_model,\n        hvdc_model,\n    )\n    # TODO: duals\n    #add_constraint_dual!(container, sys, hvdc_model)\n    return\nend\n"
  },
  {
    "path": "src/network_models/hvdc_networks.jl",
    "content": "## To add method of upper_bounds and lower_bounds for DCVoltage\nget_variable_binary(::DCVoltage, ::Type{PSY.DCBus}, ::AbstractHVDCNetworkModel) = false\nget_variable_lower_bound(::DCVoltage, d::PSY.DCBus, ::AbstractHVDCNetworkModel) =\n    PSY.get_voltage_limits(d).min\nget_variable_upper_bound(::DCVoltage, d::PSY.DCBus, ::AbstractHVDCNetworkModel) =\n    PSY.get_voltage_limits(d).max\n\nfunction add_constraints!(\n    container::OptimizationContainer,\n    ::Type{NodalBalanceActiveConstraint},\n    sys::PSY.System,\n    model::NetworkModel{V},\n    hvdc_model::W,\n) where {V <: PM.AbstractPowerModel, W <: TransportHVDCNetworkModel}\n    dc_buses = PSY.get_components(PSY.DCBus, sys)\n    if isempty(dc_buses)\n        return\n    end\n\n    time_steps = get_time_steps(container)\n    dc_expr = get_expression(container, ActivePowerBalance(), PSY.DCBus)\n    balance_constraint = add_constraints_container!(\n        container,\n        NodalBalanceActiveConstraint(),\n        PSY.DCBus,\n        axes(dc_expr)[1],\n        time_steps,\n    )\n    for d in dc_buses\n        dc_bus_no = PSY.get_number(d)\n        for t in time_steps\n            balance_constraint[dc_bus_no, t] =\n                JuMP.@constraint(get_jump_model(container), dc_expr[dc_bus_no, t] == 0)\n        end\n    end\n    return\nend\n\nfunction add_constraints!(\n    container::OptimizationContainer,\n    ::Type{NodalBalanceCurrentConstraint},\n    sys::PSY.System,\n    model::NetworkModel{V},\n    hvdc_model::W,\n) where {V <: PM.AbstractPowerModel, W <: VoltageDispatchHVDCNetworkModel}\n    dc_buses = PSY.get_components(PSY.DCBus, sys)\n    if isempty(dc_buses)\n        return\n    end\n\n    time_steps = get_time_steps(container)\n    dc_expr = get_expression(container, DCCurrentBalance(), PSY.DCBus)\n    balance_constraint = add_constraints_container!(\n        container,\n        NodalBalanceCurrentConstraint(),\n        PSY.DCBus,\n        axes(dc_expr)[1],\n        time_steps,\n    )\n    for d in dc_buses\n        dc_bus_no = PSY.get_number(d)\n        for t in time_steps\n            balance_constraint[dc_bus_no, t] =\n                JuMP.@constraint(get_jump_model(container), dc_expr[dc_bus_no, t] == 0)\n        end\n    end\n    return\nend\n"
  },
  {
    "path": "src/network_models/network_constructor.jl",
    "content": "function construct_network!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    model::NetworkModel{CopperPlatePowerModel},\n    ::ProblemTemplate,\n)\n    if get_use_slacks(model)\n        add_variables!(container, SystemBalanceSlackUp, sys, model)\n        add_variables!(container, SystemBalanceSlackDown, sys, model)\n        add_to_expression!(container, ActivePowerBalance, SystemBalanceSlackUp, sys, model)\n        add_to_expression!(\n            container,\n            ActivePowerBalance,\n            SystemBalanceSlackDown,\n            sys,\n            model,\n        )\n        objective_function!(container, sys, model)\n    end\n\n    add_constraints!(container, CopperPlateBalanceConstraint, sys, model)\n\n    add_constraint_dual!(container, sys, model)\n    return\nend\n\nfunction construct_network!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    model::NetworkModel{AreaBalancePowerModel},\n    ::ProblemTemplate,\n)\n    if get_use_slacks(model)\n        add_variables!(container, SystemBalanceSlackUp, sys, model)\n        add_variables!(container, SystemBalanceSlackDown, sys, model)\n        add_to_expression!(container, ActivePowerBalance, SystemBalanceSlackUp, sys, model)\n        add_to_expression!(\n            container,\n            ActivePowerBalance,\n            SystemBalanceSlackDown,\n            sys,\n            model,\n        )\n        objective_function!(container, sys, model)\n    end\n\n    add_constraints!(container, CopperPlateBalanceConstraint, sys, model)\n    add_constraint_dual!(container, sys, model)\n    return\nend\n\nfunction construct_network!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    model::NetworkModel{<:AbstractPTDFModel},\n    ::ProblemTemplate,\n)\n    if get_use_slacks(model)\n        add_variables!(container, SystemBalanceSlackUp, sys, model)\n        add_variables!(container, SystemBalanceSlackDown, sys, model)\n        add_to_expression!(container, ActivePowerBalance, SystemBalanceSlackUp, sys, model)\n        add_to_expression!(\n            container,\n            ActivePowerBalance,\n            SystemBalanceSlackDown,\n            sys,\n            model,\n        )\n        objective_function!(container, sys, model)\n    end\n    add_constraints!(container, CopperPlateBalanceConstraint, sys, model)\n    add_constraint_dual!(container, sys, model)\n    return\nend\n\nfunction construct_network!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    model::NetworkModel{T},\n    template::ProblemTemplate;\n) where {T <: PM.AbstractActivePowerModel}\n    if T in UNSUPPORTED_POWERMODELS\n        throw(\n            ArgumentError(\n                \"$(T) formulation is not currently supported in PowerSimulations\",\n            ),\n        )\n    end\n\n    if get_use_slacks(model)\n        add_variables!(container, SystemBalanceSlackUp, sys, model)\n        add_variables!(container, SystemBalanceSlackDown, sys, model)\n        add_to_expression!(container, ActivePowerBalance, SystemBalanceSlackUp, sys, model)\n        add_to_expression!(\n            container,\n            ActivePowerBalance,\n            SystemBalanceSlackDown,\n            sys,\n            model,\n        )\n        objective_function!(container, sys, model)\n    end\n\n    @debug \"Building the $T network with instantiate_nip_expr_model method\" _group =\n        LOG_GROUP_NETWORK_CONSTRUCTION\n    powermodels_network!(container, T, sys, template, instantiate_nip_expr_model)\n    add_pm_variable_refs!(container, T, sys, model)\n    add_pm_constraint_refs!(container, T, sys)\n\n    add_constraint_dual!(container, sys, model)\n    return\nend\n\nfunction construct_network!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    model::NetworkModel{T},\n    template::ProblemTemplate;\n) where {T <: PM.AbstractPowerModel}\n    if T in UNSUPPORTED_POWERMODELS\n        throw(\n            ArgumentError(\n                \"$(T) formulation is not currently supported in PowerSimulations\",\n            ),\n        )\n    end\n\n    if get_use_slacks(model)\n        add_variables!(container, SystemBalanceSlackUp, sys, model)\n        add_variables!(container, SystemBalanceSlackDown, sys, model)\n        add_to_expression!(container, ActivePowerBalance, SystemBalanceSlackUp, sys, model)\n        add_to_expression!(\n            container,\n            ActivePowerBalance,\n            SystemBalanceSlackDown,\n            sys,\n            model,\n        )\n        add_to_expression!(\n            container,\n            ReactivePowerBalance,\n            SystemBalanceSlackUp,\n            sys,\n            model,\n        )\n        add_to_expression!(\n            container,\n            ReactivePowerBalance,\n            SystemBalanceSlackDown,\n            sys,\n            model,\n        )\n        objective_function!(container, sys, model)\n    end\n\n    @debug \"Building the $T network with instantiate_nip_expr_model method\" _group =\n        LOG_GROUP_NETWORK_CONSTRUCTION\n    powermodels_network!(container, T, sys, template, instantiate_nip_expr_model)\n    add_pm_variable_refs!(container, T, sys, model)\n    add_pm_constraint_refs!(container, T, sys)\n\n    add_constraint_dual!(container, sys, model)\n    return\nend\n\nfunction construct_network!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    model::NetworkModel{T},\n    template::ProblemTemplate,\n) where {T <: PM.AbstractBFModel}\n    if T in UNSUPPORTED_POWERMODELS\n        throw(\n            ArgumentError(\n                \"$(T) formulation is not currently supported in PowerSimulations\",\n            ),\n        )\n    end\n\n    if get_use_slacks(model)\n        add_variables!(container, SystemBalanceSlackUp, sys, model)\n        add_variables!(container, SystemBalanceSlackDown, sys, model)\n        add_to_expression!(\n            container,\n            ActivePowerBalance,\n            SystemBalanceSlackUp,\n            sys,\n            model,\n        )\n        add_to_expression!(\n            container,\n            ActivePowerBalance,\n            SystemBalanceSlackDown,\n            sys,\n            model,\n        )\n        add_to_expression!(\n            container,\n            ReactivePowerBalance,\n            SystemBalanceSlackUp,\n            sys,\n            model,\n        )\n        add_to_expression!(\n            container,\n            ReactivePowerBalance,\n            SystemBalanceSlackDown,\n            sys,\n            model,\n        )\n        objective_function!(container, sys, model)\n    end\n\n    @debug \"Building the $T network with instantiate_bfp_expr_model method\" _group =\n        LOG_GROUP_NETWORK_CONSTRUCTION\n    powermodels_network!(container, T, sys, template, instantiate_bfp_expr_model)\n    add_pm_variable_refs!(container, T, sys, model)\n    add_pm_constraint_refs!(container, T, sys)\n    add_constraint_dual!(container, sys, model)\n    return\nend\n\n#=\n# AbstractIVRModel models not currently supported\nfunction construct_network!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    model::NetworkModel{T},\n    template::ProblemTemplate;\n) where {T <: PM.AbstractIVRModel}\n    if T in UNSUPPORTED_POWERMODELS\n        throw(\n            ArgumentError(\n                \"$(T) formulation is not currently supported in PowerSimulations\",\n            ),\n        )\n    end\n\n    if get_use_slacks(model)\n        add_variables!(container, SystemBalanceSlackUp, sys, T)\n        add_variables!(container, SystemBalanceSlackDown, sys, T)\n        add_to_expression!(\n            container,\n            ActivePowerBalance,\n            SystemBalanceSlackUp,\n            sys,\n            model,\n            T,\n        )\n        add_to_expression!(\n            container,\n            ActivePowerBalance,\n            SystemBalanceSlackDown,\n            sys,\n            model,\n            T,\n        )\n        add_to_expression!(\n            container,\n            ReactivePowerBalance,\n            SystemBalanceSlackUp,\n            sys,\n            model,\n            T,\n        )\n        add_to_expression!(\n            container,\n            ReactivePowerBalance,\n            SystemBalanceSlackDown,\n            sys,\n            model,\n            T,\n        )\n        objective_function!(container, sys, model)\n    end\n\n    @debug \"Building the $T network with instantiate_vip_expr_model method\" _group =\n        LOG_GROUP_NETWORK_CONSTRUCTION\n    #Constraints in case the model has DC Buses\n    add_constraints!(container, NodalBalanceActiveConstraint, sys, model)\n    powermodels_network!(container, T, sys, template, instantiate_vip_expr_model)\n    add_pm_variable_refs!(container, T, sys, model)\n    add_pm_constraint_refs!(container, T, sys)\n    add_constraint_dual!(container, sys, model)\n    return\nend\n=#\n"
  },
  {
    "path": "src/network_models/network_slack_variables.jl",
    "content": "#! format: off\nget_variable_multiplier(::SystemBalanceSlackUp, ::Type{<: Union{PSY.ACBus, PSY.Area, PSY.System}}, _) = 1.0\nget_variable_multiplier(::SystemBalanceSlackDown, ::Type{<: Union{PSY.ACBus, PSY.Area, PSY.System}}, _) = -1.0\n#! format: on\n\nfunction add_variables!(\n    container::OptimizationContainer,\n    ::Type{T},\n    ::PSY.System,\n    network_model::NetworkModel{U},\n) where {\n    T <: Union{SystemBalanceSlackUp, SystemBalanceSlackDown},\n    U <: Union{CopperPlatePowerModel, PTDFPowerModel},\n}\n    time_steps = get_time_steps(container)\n    reference_buses = get_reference_buses(network_model)\n    variable =\n        add_variable_container!(container, T(), PSY.System, reference_buses, time_steps)\n\n    for t in time_steps, bus in reference_buses\n        variable[bus, t] = JuMP.@variable(\n            get_jump_model(container),\n            base_name = \"slack_{$(T), $(bus), $t}\",\n            lower_bound = 0.0\n        )\n    end\n    return\nend\n\nfunction add_variables!(\n    container::OptimizationContainer,\n    ::Type{T},\n    sys::PSY.System,\n    network_model::NetworkModel{U},\n) where {\n    T <: Union{SystemBalanceSlackUp, SystemBalanceSlackDown},\n    U <: Union{AreaBalancePowerModel, AreaPTDFPowerModel},\n}\n    time_steps = get_time_steps(container)\n    areas = get_name.(get_available_components(network_model, PSY.Area, sys))\n    variable =\n        add_variable_container!(container, T(), PSY.Area, areas, time_steps)\n\n    for t in time_steps, area in areas\n        variable[area, t] = JuMP.@variable(\n            get_jump_model(container),\n            base_name = \"slack_{$(T), $(area), $t}\",\n            lower_bound = 0.0\n        )\n    end\n\n    return\nend\n\nfunction add_variables!(\n    container::OptimizationContainer,\n    ::Type{T},\n    sys::PSY.System,\n    network_model::NetworkModel{U},\n) where {\n    T <: Union{SystemBalanceSlackUp, SystemBalanceSlackDown},\n    U <: PM.AbstractActivePowerModel,\n}\n    time_steps = get_time_steps(container)\n    network_reduction = get_network_reduction(network_model)\n    if isempty(network_reduction)\n        bus_numbers =\n            PSY.get_number.(get_available_components(network_model, PSY.ACBus, sys))\n    else\n        bus_numbers = collect(keys(PNM.get_bus_reduction_map(network_reduction)))\n    end\n\n    variable = add_variable_container!(container, T(), PSY.ACBus, bus_numbers, time_steps)\n    for t in time_steps, n in bus_numbers\n        variable[n, t] = JuMP.@variable(\n            get_jump_model(container),\n            base_name = \"slack_{$(T), $n, $t}\",\n            lower_bound = 0.0\n        )\n    end\n    return\nend\n\nfunction add_variables!(\n    container::OptimizationContainer,\n    ::Type{T},\n    sys::PSY.System,\n    network_model::NetworkModel{U},\n) where {\n    T <: Union{SystemBalanceSlackUp, SystemBalanceSlackDown},\n    U <: PM.AbstractPowerModel,\n}\n    time_steps = get_time_steps(container)\n    network_reduction = get_network_reduction(network_model)\n    if isempty(network_reduction)\n        bus_numbers =\n            PSY.get_number.(get_available_components(network_model, PSY.ACBus, sys))\n    else\n        bus_numbers = collect(keys(PNM.get_bus_reduction_map(network_reduction)))\n    end\n    variable_active =\n        add_variable_container!(container, T(), PSY.ACBus, \"P\", bus_numbers, time_steps)\n    variable_reactive =\n        add_variable_container!(container, T(), PSY.ACBus, \"Q\", bus_numbers, time_steps)\n\n    for t in time_steps, n in bus_numbers\n        variable_active[n, t] = JuMP.@variable(\n            get_jump_model(container),\n            base_name = \"slack_{p, $(T), $n, $t}\",\n            lower_bound = 0.0\n        )\n        variable_reactive[n, t] = JuMP.@variable(\n            get_jump_model(container),\n            base_name = \"slack_{q, $(T), $n, $t}\",\n            lower_bound = 0.0\n        )\n    end\n    return\nend\n\nfunction objective_function!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    network_model::NetworkModel{T},\n) where {T <: Union{CopperPlatePowerModel, PTDFPowerModel}}\n    variable_up = get_variable(container, SystemBalanceSlackUp(), PSY.System)\n    variable_dn = get_variable(container, SystemBalanceSlackDown(), PSY.System)\n    reference_buses = get_reference_buses(network_model)\n\n    for t in get_time_steps(container), n in reference_buses\n        add_to_objective_invariant_expression!(\n            container,\n            (variable_dn[n, t] + variable_up[n, t]) * BALANCE_SLACK_COST,\n        )\n    end\n    return\nend\n\nfunction objective_function!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    network_model::NetworkModel{T},\n) where {T <: Union{AreaBalancePowerModel, AreaPTDFPowerModel}}\n    variable_up = get_variable(container, SystemBalanceSlackUp(), PSY.Area)\n    variable_dn = get_variable(container, SystemBalanceSlackDown(), PSY.Area)\n    areas = PSY.get_name.(get_available_components(network_model, PSY.Area, sys))\n\n    for t in get_time_steps(container), n in areas\n        add_to_objective_invariant_expression!(\n            container,\n            (variable_dn[n, t] + variable_up[n, t]) * BALANCE_SLACK_COST,\n        )\n    end\n    return\nend\n\nfunction objective_function!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    network_model::NetworkModel{T},\n) where {T <: PM.AbstractActivePowerModel}\n    variable_up = get_variable(container, SystemBalanceSlackUp(), PSY.ACBus)\n    variable_dn = get_variable(container, SystemBalanceSlackDown(), PSY.ACBus)\n    bus_numbers = axes(variable_up)[1]\n    @assert_op bus_numbers == axes(variable_dn)[1]\n    for t in get_time_steps(container), n in bus_numbers\n        add_to_objective_invariant_expression!(\n            container,\n            (variable_dn[n, t] + variable_up[n, t]) * BALANCE_SLACK_COST,\n        )\n    end\n    return\nend\n\nfunction objective_function!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    network_model::NetworkModel{T},\n) where {T <: PM.AbstractPowerModel}\n    variable_p_up = get_variable(container, SystemBalanceSlackUp(), PSY.ACBus, \"P\")\n    variable_p_dn = get_variable(container, SystemBalanceSlackDown(), PSY.ACBus, \"P\")\n    variable_q_up = get_variable(container, SystemBalanceSlackUp(), PSY.ACBus, \"Q\")\n    variable_q_dn = get_variable(container, SystemBalanceSlackDown(), PSY.ACBus, \"Q\")\n    bus_numbers = axes(variable_p_up)[1]\n    @assert_op bus_numbers == axes(variable_q_dn)[1]\n    for t in get_time_steps(container), n in bus_numbers\n        add_to_objective_invariant_expression!(\n            container,\n            (\n                variable_p_dn[n, t] +\n                variable_p_up[n, t] +\n                variable_q_dn[n, t] +\n                variable_q_up[n, t]\n            ) * BALANCE_SLACK_COST,\n        )\n    end\n    return\nend\n"
  },
  {
    "path": "src/network_models/pm_translator.jl",
    "content": "const PM_MAP_TUPLE =\n    NamedTuple{(:from_to, :to_from), Tuple{Tuple{Int, Int, Int}, Tuple{Int, Int, Int}}}\n\nconst PM_BUSTYPES = Dict{PSY.ACBusTypes, Int}(\n    PSY.ACBusTypes.ISOLATED => 4,\n    PSY.ACBusTypes.PQ => 1,\n    PSY.ACBusTypes.PV => 2,\n    PSY.ACBusTypes.REF => 3,\n    PSY.ACBusTypes.SLACK => 3,\n)\n\nstruct PMmap\n    bus::Dict{Int, PSY.ACBus}\n    arcs::Dict{Tuple{Int, Int}, PM_MAP_TUPLE}\n    arcs_dc::Dict{PM_MAP_TUPLE, PSY.TwoTerminalHVDC}\nend\n\nfunction get_branch_to_pm(\n    ix::Int,\n    ::Tuple{Int, Int},\n    branch::PSY.PhaseShiftingTransformer,\n    ::Type{PhaseAngleControl},\n    ::Type{<:PM.AbstractDCPModel},\n)\n    # we allocate the transformer shunt values to primary side only\n    yt = PSY.get_primary_shunt(branch)\n    PM_branch = Dict{String, Any}(\n        \"br_r\" => PSY.get_r(branch),\n        \"rate_a\" => PSY.get_rating(branch),\n        \"shift\" => PSY.get_α(branch),\n        \"rate_b\" => PSY.get_rating(branch),\n        \"br_x\" => PSY.get_x(branch),\n        \"rate_c\" => PSY.get_rating(branch),\n        \"g_to\" => 0.0,\n        \"b_to\" => 0.0,\n        \"g_fr\" => real(yt),\n        \"b_fr\" => imag(yt),\n        \"f_bus\" => PSY.get_number(PSY.get_arc(branch).from),\n        \"br_status\" => 0.0, # Turn off the branch while keeping the function type stable\n        \"t_bus\" => PSY.get_number(PSY.get_arc(branch).to),\n        \"index\" => ix,\n        \"angmin\" => -π / 2,\n        \"angmax\" => π / 2,\n        \"transformer\" => true,\n        \"tap\" => PSY.get_tap(branch),\n    )\n    return PM_branch\nend\n\nfunction get_branch_to_pm(\n    ix::Int,\n    ::Tuple{Int, Int},\n    branch::PSY.PhaseShiftingTransformer,\n    ::Type{D},\n    ::Type{<:PM.AbstractPowerModel},\n) where {D <: AbstractBranchFormulation}\n    # we allocate the transformer shunt values to primary side only\n    yt = PSY.get_primary_shunt(branch)\n    PM_branch = Dict{String, Any}(\n        \"br_r\" => PSY.get_r(branch),\n        \"rate_a\" => PSY.get_rating(branch),\n        \"shift\" => PSY.get_α(branch),\n        \"rate_b\" => PSY.get_rating(branch),\n        \"br_x\" => PSY.get_x(branch),\n        \"rate_c\" => PSY.get_rating(branch),\n        \"g_to\" => 0.0,\n        \"b_to\" => 0.0,\n        \"g_fr\" => real(yt),\n        \"b_fr\" => imag(yt),\n        \"f_bus\" => PSY.get_number(PSY.get_arc(branch).from),\n        \"br_status\" => Float64(PSY.get_available(branch)),\n        \"t_bus\" => PSY.get_number(PSY.get_arc(branch).to),\n        \"index\" => ix,\n        \"angmin\" => -π / 2,\n        \"angmax\" => π / 2,\n        \"transformer\" => true,\n        \"tap\" => PSY.get_tap(branch),\n    )\n    return PM_branch\nend\n\nfunction get_branch_to_pm(\n    ix::Int,\n    ::Tuple{Int, Int},\n    branch::PSY.PhaseShiftingTransformer,\n    ::Type{StaticBranchUnbounded},\n    ::Type{<:PM.AbstractPowerModel},\n)\n    # we allocate the transformer shunt values to primary side only\n    yt = PSY.get_primary_shunt(branch)\n    PM_branch = Dict{String, Any}(\n        \"br_r\" => PSY.get_r(branch),\n        \"shift\" => PSY.get_α(branch),\n        \"br_x\" => PSY.get_x(branch),\n        \"g_to\" => 0.0,\n        \"b_to\" => 0.0,\n        \"g_fr\" => real(yt),\n        \"b_fr\" => imag(yt),\n        \"f_bus\" => PSY.get_number(PSY.get_arc(branch).from),\n        \"br_status\" => Float64(PSY.get_available(branch)),\n        \"t_bus\" => PSY.get_number(PSY.get_arc(branch).to),\n        \"index\" => ix,\n        \"angmin\" => -π / 2,\n        \"angmax\" => π / 2,\n        \"transformer\" => true,\n        \"tap\" => PSY.get_tap(branch),\n    )\n    return PM_branch\nend\n\nfunction get_branch_to_pm(\n    ix::Int,\n    ::Tuple{Int, Int},\n    branch::PSY.Transformer2W,\n    ::Type{<:AbstractBranchFormulation},\n    ::Type{<:PM.AbstractPowerModel},\n)\n    # we allocate the transformer shunt values to primary side only\n    yt = PSY.get_primary_shunt(branch)\n    PM_branch = Dict{String, Any}(\n        \"br_r\" => PSY.get_r(branch),\n        \"rate_a\" => PSY.get_rating(branch),\n        \"shift\" => 0.0,\n        \"rate_b\" => PSY.get_rating(branch),\n        \"br_x\" => PSY.get_x(branch),\n        \"rate_c\" => PSY.get_rating(branch),\n        \"g_to\" => 0.0,\n        \"b_to\" => 0.0,\n        \"g_fr\" => real(yt),\n        \"b_fr\" => imag(yt),\n        \"f_bus\" => PSY.get_number(PSY.get_arc(branch).from),\n        \"br_status\" => Float64(PSY.get_available(branch)),\n        \"t_bus\" => PSY.get_number(PSY.get_arc(branch).to),\n        \"index\" => ix,\n        \"angmin\" => -π / 2,\n        \"angmax\" => π / 2,\n        \"transformer\" => true,\n        \"tap\" => 1.0,\n    )\n    return PM_branch\nend\n\nfunction get_branch_to_pm(\n    ix::Int,\n    ::Tuple{Int, Int},\n    branch::PSY.Transformer2W,\n    ::Type{StaticBranchUnbounded},\n    ::Type{<:PM.AbstractPowerModel},\n)\n    # we allocate the transformer shunt values to primary side only\n    yt = PSY.get_primary_shunt(branch)\n    PM_branch = Dict{String, Any}(\n        \"br_r\" => PSY.get_r(branch),\n        \"shift\" => 0.0,\n        \"br_x\" => PSY.get_x(branch),\n        \"g_to\" => 0.0,\n        \"b_to\" => 0.0,\n        \"g_fr\" => real(yt),\n        \"b_fr\" => imag(yt),\n        \"f_bus\" => PSY.get_number(PSY.get_arc(branch).from),\n        \"br_status\" => Float64(PSY.get_available(branch)),\n        \"t_bus\" => PSY.get_number(PSY.get_arc(branch).to),\n        \"index\" => ix,\n        \"angmin\" => -π / 2,\n        \"angmax\" => π / 2,\n        \"transformer\" => true,\n        \"tap\" => 1.0,\n    )\n    return PM_branch\nend\n\nfunction get_branch_to_pm(\n    ix::Int,\n    ::Tuple{Int, Int},\n    branch::PSY.TapTransformer,\n    ::Type{<:AbstractBranchFormulation},\n    ::Type{<:PM.AbstractPowerModel},\n)\n    # we allocate the transformer shunt values to primary side only\n    yt = PSY.get_primary_shunt(branch)\n    PM_branch = Dict{String, Any}(\n        \"br_r\" => PSY.get_r(branch),\n        \"rate_a\" => PSY.get_rating(branch),\n        \"shift\" => 0.0,\n        \"rate_b\" => PSY.get_rating(branch),\n        \"br_x\" => PSY.get_x(branch),\n        \"rate_c\" => PSY.get_rating(branch),\n        \"g_to\" => 0.0,\n        \"b_to\" => 0.0,\n        \"g_fr\" => real(yt),\n        \"b_fr\" => imag(yt),\n        \"f_bus\" => PSY.get_number(PSY.get_arc(branch).from),\n        \"br_status\" => Float64(PSY.get_available(branch)),\n        \"t_bus\" => PSY.get_number(PSY.get_arc(branch).to),\n        \"index\" => ix,\n        \"angmin\" => -π / 2,\n        \"angmax\" => π / 2,\n        \"transformer\" => true,\n        \"tap\" => PSY.get_tap(branch),\n    )\n    return PM_branch\nend\n\nfunction get_branch_to_pm(\n    ix::Int,\n    ::Tuple{Int, Int},\n    branch::PSY.TapTransformer,\n    ::Type{StaticBranchUnbounded},\n    ::Type{<:PM.AbstractPowerModel},\n)\n    # we allocate the transformer shunt values to primary side only\n    yt = PSY.get_primary_shunt(branch)\n    PM_branch = Dict{String, Any}(\n        \"br_r\" => PSY.get_r(branch),\n        \"shift\" => 0.0,\n        \"br_x\" => PSY.get_x(branch),\n        \"g_to\" => 0.0,\n        \"b_to\" => 0.0,\n        \"g_fr\" => real(yt),\n        \"b_fr\" => imag(yt),\n        \"f_bus\" => PSY.get_number(PSY.get_arc(branch).from),\n        \"br_status\" => Float64(PSY.get_available(branch)),\n        \"t_bus\" => PSY.get_number(PSY.get_arc(branch).to),\n        \"index\" => ix,\n        \"angmin\" => -π / 2,\n        \"angmax\" => π / 2,\n        \"transformer\" => true,\n        \"tap\" => PSY.get_tap(branch),\n    )\n    return PM_branch\nend\n\nfunction get_branch_to_pm(\n    ix::Int,\n    ::Tuple{Int, Int},\n    branch::PSY.ACTransmission,\n    ::Type{<:AbstractBranchFormulation},\n    ::Type{<:PM.AbstractPowerModel},\n)\n    PM_branch = Dict{String, Any}(\n        \"br_r\" => PSY.get_r(branch),\n        \"rate_a\" => PSY.get_rating(branch),\n        \"shift\" => 0.0,\n        \"rate_b\" => PSY.get_rating(branch),\n        \"br_x\" => PSY.get_x(branch),\n        \"rate_c\" => PSY.get_rating(branch),\n        \"g_to\" => 0.0,\n        \"g_fr\" => 0.0,\n        \"b_fr\" => PSY.get_b(branch).from,\n        \"f_bus\" => PSY.get_number(PSY.get_arc(branch).from),\n        \"br_status\" => Float64(PSY.get_available(branch)),\n        \"t_bus\" => PSY.get_number(PSY.get_arc(branch).to),\n        \"b_to\" => PSY.get_b(branch).to,\n        \"index\" => ix,\n        \"angmin\" => PSY.get_angle_limits(branch).min,\n        \"angmax\" => PSY.get_angle_limits(branch).max,\n        \"transformer\" => false,\n        \"tap\" => 1.0,\n    )\n    return PM_branch\nend\n\nfunction get_branch_to_pm(\n    ix::Int,\n    ::Tuple{Int, Int},\n    branch::PSY.ACTransmission,\n    ::Type{StaticBranchUnbounded},\n    ::Type{<:PM.AbstractPowerModel},\n)\n    PM_branch = Dict{String, Any}(\n        \"br_r\" => PSY.get_r(branch),\n        \"shift\" => 0.0,\n        \"br_x\" => PSY.get_x(branch),\n        \"g_to\" => 0.0,\n        \"g_fr\" => 0.0,\n        \"b_fr\" => PSY.get_b(branch).from,\n        \"f_bus\" => PSY.get_number(PSY.get_arc(branch).from),\n        \"br_status\" => Float64(PSY.get_available(branch)),\n        \"t_bus\" => PSY.get_number(PSY.get_arc(branch).to),\n        \"b_to\" => PSY.get_b(branch).to,\n        \"index\" => ix,\n        \"angmin\" => PSY.get_angle_limits(branch).min,\n        \"angmax\" => PSY.get_angle_limits(branch).max,\n        \"transformer\" => false,\n        \"tap\" => 1.0,\n    )\n    return PM_branch\nend\n\nfunction get_branch_to_pm(\n    ix::Int,\n    arc_tuple::Tuple{Int, Int},\n    branch::PNM.ThreeWindingTransformerWinding{PSY.Transformer3W},\n    ::Type{StaticBranchUnbounded},\n    ::Type{<:PM.AbstractPowerModel},\n)\n    arc_tuple\n    PM_branch = Dict{String, Any}(\n        \"br_r\" => PNM.get_equivalent_r(branch),\n        \"shift\" => 0.0,\n        \"br_x\" => PNM.get_equivalent_x(branch),\n        \"g_to\" => 0.0,\n        \"g_fr\" => 0.0,\n        \"b_fr\" => PNM.get_equivalent_b(branch).from,\n        \"f_bus\" => arc_tuple[1],\n        \"br_status\" => Float64(PNM.get_equivalent_available(branch)),\n        \"t_bus\" => arc_tuple[2],\n        \"b_to\" => PNM.get_equivalent_b(branch).to,\n        \"index\" => ix,\n        \"angmin\" => -π / 2,\n        \"angmax\" => π / 2,\n        \"transformer\" => true,\n        \"tap\" => 1.0,\n    )\n    return PM_branch\nend\n\nfunction get_branch_to_pm(\n    ix::Int,\n    arc_tuple::Tuple{Int, Int},\n    branch::PNM.ThreeWindingTransformerWinding{PSY.Transformer3W},\n    ::Type{<:AbstractBranchFormulation},\n    ::Type{<:PM.AbstractPowerModel},\n)\n    PM_branch = Dict{String, Any}(\n        \"br_r\" => PNM.get_equivalent_r(branch),\n        \"rate_a\" => PNM.get_equivalent_rating(branch),\n        \"shift\" => 0.0,\n        \"rate_b\" => PNM.get_equivalent_rating(branch),\n        \"br_x\" => PNM.get_equivalent_x(branch),\n        \"rate_c\" => PNM.get_equivalent_rating(branch),\n        \"g_to\" => 0.0,\n        \"g_fr\" => 0.0,\n        \"b_fr\" => PNM.get_equivalent_b(branch).from,\n        \"f_bus\" => arc_tuple[1],\n        \"br_status\" => Float64(PNM.get_equivalent_available(branch)),\n        \"t_bus\" => arc_tuple[2],\n        \"b_to\" => PNM.get_equivalent_b(branch).to,\n        \"index\" => ix,\n        \"angmin\" => -π / 2,\n        \"angmax\" => π / 2,\n        \"transformer\" => true,\n        \"tap\" => 1.0,\n    )\n    return PM_branch\nend\n\nfunction get_branch_to_pm(\n    ix::Int,\n    arc_tuple::Tuple{Int, Int},\n    branch::PNM.ThreeWindingTransformerWinding{PSY.PhaseShiftingTransformer3W},\n    ::Type{<:AbstractBranchFormulation},\n    ::Type{<:PM.AbstractPowerModel},\n)\n    PM_branch = Dict{String, Any}(\n        \"br_r\" => PNM.get_equivalent_r(branch),\n        \"rate_a\" => PNM.get_equivalent_rating(branch),\n        \"shift\" => 0.0,\n        \"rate_b\" => PNM.get_equivalent_rating(branch),\n        \"br_x\" => PNM.get_equivalent_x(branch),\n        \"rate_c\" => PNM.get_equivalent_rating(branch),\n        \"g_to\" => 0.0,\n        \"g_fr\" => 0.0,\n        \"b_fr\" => PNM.get_equivalent_b(branch).from,\n        \"f_bus\" => arc_tuple[1],\n        \"br_status\" => Float64(PNM.get_equivalent_available(branch)),\n        \"t_bus\" => arc_tuple[2],\n        \"b_to\" => PNM.get_equivalent_b(branch).to,\n        \"index\" => ix,\n        \"angmin\" => -π / 2,\n        \"angmax\" => π / 2,\n        \"transformer\" => true,\n        \"tap\" => PNM.get_equivalent_tap(branch),\n    )\n    return PM_branch\nend\n\nfunction get_branch_to_pm(\n    ix::Int,\n    arc_tuple::Tuple{Int, Int},\n    branch::PNM.ThreeWindingTransformerWinding{PSY.PhaseShiftingTransformer3W},\n    ::Type{StaticBranchUnbounded},\n    ::Type{<:PM.AbstractPowerModel},\n)\n    PM_branch = Dict{String, Any}(\n        \"br_r\" => PNM.get_equivalent_r(branch),\n        \"shift\" => 0.0,\n        \"br_x\" => PNM.get_equivalent_x(branch),\n        \"g_to\" => 0.0,\n        \"g_fr\" => 0.0,\n        \"b_fr\" => PNM.get_equivalent_b(branch).from,\n        \"f_bus\" => arc_tuple[1],\n        \"br_status\" => Float64(PNM.get_equivalent_available(branch)),\n        \"t_bus\" => arc_tuple[2],\n        \"b_to\" => PNM.get_equivalent_b(branch).to,\n        \"index\" => ix,\n        \"angmin\" => -π / 2,\n        \"angmax\" => π / 2,\n        \"transformer\" => true,\n        \"tap\" => PNM.get_equivalent_tap(branch),\n    )\n    return PM_branch\nend\n\nfunction get_branch_to_pm(\n    ix::Int,\n    arc_tuple::Tuple{Int, Int},\n    double_circuit::PNM.BranchesParallel,\n    T::Type{<:AbstractBranchFormulation},\n    U::Type{<:PM.AbstractPowerModel},\n)\n    equivalent_branch = PNM.get_equivalent_physical_branch_parameters(double_circuit)\n    PM_branch = Dict{String, Any}(\n        \"br_r\" => PNM.get_equivalent_r(equivalent_branch),\n        \"shift\" => 0.0,\n        \"rate_a\" => PNM.get_equivalent_rating(double_circuit),\n        \"rate_b\" => PNM.get_equivalent_rating(double_circuit),\n        \"rate_c\" => PNM.get_equivalent_rating(double_circuit),\n        \"br_x\" => PNM.get_equivalent_x(equivalent_branch),\n        \"g_to\" => 0.0,\n        \"g_fr\" => 0.0,\n        \"b_fr\" => PNM.get_equivalent_b_from(equivalent_branch),\n        \"f_bus\" => arc_tuple[1],\n        \"br_status\" => Float64(PNM.get_equivalent_available(double_circuit)),\n        \"t_bus\" => arc_tuple[2],\n        \"b_to\" => PNM.get_equivalent_b_to(equivalent_branch),\n        \"index\" => ix,\n        \"angmin\" => -π / 2,\n        \"angmax\" => π / 2,\n        \"transformer\" => false,\n        \"tap\" => 1.0,\n    )\n\n    return PM_branch\nend\n\nfunction get_branch_to_pm(\n    ix::Int,\n    arc_tuple::Tuple{Int, Int},\n    double_circuit::PNM.BranchesParallel,\n    T::Type{StaticBranchUnbounded},\n    U::Type{<:PM.AbstractPowerModel},\n)\n    equivalent_branch = PNM.get_equivalent_physical_branch_parameters(double_circuit)\n    PM_branch = Dict{String, Any}(\n        \"br_r\" => PNM.get_equivalent_r(equivalent_branch),\n        \"shift\" => 0.0,\n        \"br_x\" => PNM.get_equivalent_x(equivalent_branch),\n        \"g_to\" => 0.0,\n        \"g_fr\" => 0.0,\n        \"rate_a\" => PNM.get_equivalent_rating(double_circuit),\n        \"rate_b\" => PNM.get_equivalent_rating(double_circuit),\n        \"rate_c\" => PNM.get_equivalent_rating(double_circuit),\n        \"b_fr\" => PNM.get_equivalent_b_from(equivalent_branch),\n        \"f_bus\" => arc_tuple[1],\n        \"br_status\" => Float64(PNM.get_equivalent_available(double_circuit)),\n        \"t_bus\" => arc_tuple[2],\n        \"b_to\" => PNM.get_equivalent_b_to(equivalent_branch),\n        \"index\" => ix,\n        \"angmin\" => -π / 2,\n        \"angmax\" => π / 2,\n        \"transformer\" => false,\n        \"tap\" => 1.0,\n    )\n\n    return PM_branch\nend\n\nfunction get_branch_to_pm(\n    ix::Int,\n    arc_tuple::Tuple{Int, Int},\n    series_chain::PNM.BranchesSeries,\n    T::Type{<:AbstractBranchFormulation},\n    U::Type{<:PM.AbstractPowerModel},\n)\n    equivalent_branch = PNM.get_equivalent_physical_branch_parameters(series_chain)\n    PM_branch = Dict{String, Any}(\n        \"br_r\" => PNM.get_equivalent_r(equivalent_branch),\n        \"shift\" => 0.0,\n        \"br_x\" => PNM.get_equivalent_x(equivalent_branch),\n        \"g_to\" => 0.0,\n        \"g_fr\" => 0.0,\n        \"b_fr\" => PNM.get_equivalent_b_from(equivalent_branch),\n        \"f_bus\" => arc_tuple[1],\n        \"br_status\" => Float64(PNM.get_equivalent_available(series_chain)),\n        \"t_bus\" => arc_tuple[2],\n        \"b_to\" => PNM.get_equivalent_b_to(equivalent_branch),\n        \"index\" => ix,\n        \"angmin\" => -π / 2,\n        \"angmax\" => π / 2,\n        \"transformer\" => false,\n        \"tap\" => 1.0,\n    )\n    return PM_branch\nend\n\nfunction get_branch_to_pm(\n    ix::Int,\n    arc_tuple::Tuple{Int, Int},\n    series_chain::PNM.BranchesSeries,\n    T::Type{StaticBranchUnbounded},\n    U::Type{<:PM.AbstractPowerModel},\n)\n    equivalent_branch = PNM.get_equivalent_physical_branch_parameters(series_chain)\n    PM_branch = Dict{String, Any}(\n        \"br_r\" => PNM.get_equivalent_r(equivalent_branch),\n        \"shift\" => 0.0,\n        \"br_x\" => PNM.get_equivalent_x(equivalent_branch),\n        \"g_to\" => 0.0,\n        \"g_fr\" => 0.0,\n        \"b_fr\" => PNM.get_equivalent_b_from(equivalent_branch),\n        \"f_bus\" => arc_tuple[1],\n        \"br_status\" => Float64(PNM.get_equivalent_available(double_circuit)),\n        \"t_bus\" => arc_tuple[2],\n        \"b_to\" => PNM.get_equivalent_b_to(equivalent_branch),\n        \"index\" => ix,\n        \"angmin\" => -π / 2,\n        \"angmax\" => π / 2,\n        \"transformer\" => false,\n        \"tap\" => 1.0,\n    )\n    return PM_branch\nend\n\nfunction get_branch_to_pm(\n    ix::Int,\n    branch::PSY.TwoTerminalGenericHVDCLine,\n    ::Type{HVDCTwoTerminalDispatch},\n    ::Type{<:PM.AbstractPowerModel},\n)\n    check_hvdc_line_limits_unidirectional(branch)\n    PM_branch = Dict{String, Any}(\n        \"loss1\" => PSY.get_proportional_term(PSY.get_loss(branch)),\n        \"mp_pmax\" => PSY.get_reactive_power_limits_from(branch).max,\n        \"model\" => 2,\n        \"shutdown\" => 0.0,\n        \"pmaxt\" => PSY.get_active_power_limits_to(branch).max,\n        \"pmaxf\" => PSY.get_active_power_limits_from(branch).max,\n        \"startup\" => 0.0,\n        \"loss0\" => PSY.get_constant_term(PSY.get_loss(branch)),\n        \"pt\" => 0.0,\n        \"vt\" => PSY.get_magnitude(PSY.get_arc(branch).to),\n        \"qmaxf\" => PSY.get_reactive_power_limits_from(branch).max,\n        \"pmint\" => PSY.get_active_power_limits_to(branch).min,\n        \"f_bus\" => PSY.get_number(PSY.get_arc(branch).from),\n        \"mp_pmin\" => PSY.get_reactive_power_limits_from(branch).min,\n        \"br_status\" => Float64(PSY.get_available(branch)),\n        \"t_bus\" => PSY.get_number(PSY.get_arc(branch).to),\n        \"index\" => ix,\n        \"qmint\" => PSY.get_reactive_power_limits_to(branch).min,\n        \"qf\" => 0.0,\n        \"cost\" => 0.0,\n        \"pminf\" => PSY.get_active_power_limits_from(branch).min,\n        \"qt\" => 0.0,\n        \"qminf\" => PSY.get_reactive_power_limits_from(branch).min,\n        \"vf\" => PSY.get_magnitude(PSY.get_arc(branch).from),\n        \"qmaxt\" => PSY.get_reactive_power_limits_to(branch).max,\n        \"ncost\" => 0,\n        \"pf\" => 0.0,\n    )\n    return PM_branch\nend\n\nfunction get_branch_to_pm(\n    ix::Int,\n    branch::PSY.TwoTerminalGenericHVDCLine,\n    ::Type{<:AbstractTwoTerminalDCLineFormulation},\n    ::Type{<:PM.AbstractPowerModel},\n)\n    PM_branch = Dict{String, Any}(\n        \"loss1\" => PSY.get_proportional_term(PSY.get_loss(branch)),\n        \"mp_pmax\" => PSY.get_reactive_power_limits_from(branch).max,\n        \"model\" => 2,\n        \"shutdown\" => 0.0,\n        \"pmaxt\" => PSY.get_active_power_limits_to(branch).max,\n        \"pmaxf\" => PSY.get_active_power_limits_from(branch).max,\n        \"startup\" => 0.0,\n        \"loss0\" => PSY.get_constant_term(PSY.get_loss(branch)),\n        \"pt\" => 0.0,\n        \"vt\" => PSY.get_magnitude(PSY.get_arc(branch).to),\n        \"qmaxf\" => PSY.get_reactive_power_limits_from(branch).max,\n        \"pmint\" => PSY.get_active_power_limits_to(branch).min,\n        \"f_bus\" => PSY.get_number(PSY.get_arc(branch).from),\n        \"mp_pmin\" => PSY.get_reactive_power_limits_from(branch).min,\n        \"br_status\" => Float64(PSY.get_available(branch)),\n        \"t_bus\" => PSY.get_number(PSY.get_arc(branch).to),\n        \"index\" => ix,\n        \"qmint\" => PSY.get_reactive_power_limits_to(branch).min,\n        \"qf\" => 0.0,\n        \"cost\" => 0.0,\n        \"pminf\" => PSY.get_active_power_limits_from(branch).min,\n        \"qt\" => 0.0,\n        \"qminf\" => PSY.get_reactive_power_limits_from(branch).min,\n        \"vf\" => PSY.get_magnitude(PSY.get_arc(branch).from),\n        \"qmaxt\" => PSY.get_reactive_power_limits_to(branch).max,\n        \"ncost\" => 0,\n        \"pf\" => 0.0,\n    )\n    return PM_branch\nend\n\nfunction get_branch_to_pm(\n    ix::Int,\n    branch::PSY.TwoTerminalGenericHVDCLine,\n    ::Type{HVDCTwoTerminalDispatch},\n    ::Type{<:PM.AbstractDCPModel},\n)\n    PM_branch = Dict{String, Any}(\n        \"loss1\" => PSY.get_proportional_term(PSY.get_loss(branch)),\n        \"mp_pmax\" => PSY.get_reactive_power_limits_from(branch).max,\n        \"model\" => 2,\n        \"shutdown\" => 0.0,\n        \"pmaxt\" => PSY.get_active_power_limits_to(branch).max,\n        \"pmaxf\" => PSY.get_active_power_limits_from(branch).max,\n        \"startup\" => 0.0,\n        \"loss0\" => PSY.get_constant_term(PSY.get_loss(branch)),\n        \"pt\" => 0.0,\n        \"vt\" => PSY.get_magnitude(PSY.get_arc(branch).to),\n        \"qmaxf\" => PSY.get_reactive_power_limits_from(branch).max,\n        \"pmint\" => PSY.get_active_power_limits_to(branch).min,\n        \"f_bus\" => PSY.get_number(PSY.get_arc(branch).from),\n        \"mp_pmin\" => PSY.get_reactive_power_limits_from(branch).min,\n        \"br_status\" => 0.0,\n        \"t_bus\" => PSY.get_number(PSY.get_arc(branch).to),\n        \"index\" => ix,\n        \"qmint\" => PSY.get_reactive_power_limits_to(branch).min,\n        \"qf\" => 0.0,\n        \"cost\" => 0.0,\n        \"pminf\" => PSY.get_active_power_limits_from(branch).min,\n        \"qt\" => 0.0,\n        \"qminf\" => PSY.get_reactive_power_limits_from(branch).min,\n        \"vf\" => PSY.get_magnitude(PSY.get_arc(branch).from),\n        \"qmaxt\" => PSY.get_reactive_power_limits_to(branch).max,\n        \"ncost\" => 0,\n        \"pf\" => 0.0,\n    )\n    return PM_branch\nend\n\nfunction get_branch_to_pm(\n    ix::Int,\n    branch::PSY.TwoTerminalLCCLine,\n    ::Type{HVDCTwoTerminalLCC},\n    ::Type{<:PM.AbstractPowerModel},\n)\n    return Dict{String, Any}()\nend\n\nfunction get_branches_to_pm(\n    sys::PSY.System,\n    network_model::NetworkModel{S},\n    ::Type{T},\n    branch_template::BranchModelContainer,\n    start_idx = 0,\n) where {T <: PSY.ACTransmission, S <: PM.AbstractPowerModel}\n    PM_branches = Dict{String, Any}()\n    PMmap_br = Dict{Tuple{Int, Int}, PM_MAP_TUPLE}()\n    net_reduction_data = get_network_reduction(network_model)\n    all_branch_maps_by_type = net_reduction_data.all_branch_maps_by_type\n    name_to_arc_maps = PNM.get_name_to_arc_maps(net_reduction_data)\n    ix = 1\n    @assert !isempty(branch_template)\n    modeled_arc_tuples = Set{Tuple{Int, Int}}()\n    for (d, device_model) in branch_template\n        comp_type = get_component_type(device_model)\n        if comp_type <: PSY.TwoTerminalHVDC || !haskey(name_to_arc_maps, comp_type)\n            @info \"No $d Branches to process in PowerModels data.\"\n            continue\n        end\n        name_to_arc_map = PNM.get_name_to_arc_map(net_reduction_data, comp_type)\n        for (_, (arc_tuple, reduction)) in name_to_arc_map\n            arc_tuple ∈ modeled_arc_tuples && continue # This is the PowerModels equivalent of the branch and constraint tracker.\n            reduction_entry = all_branch_maps_by_type[reduction][comp_type][arc_tuple]\n            PM_branches[\"$(ix)\"] = get_branch_to_pm(\n                ix,\n                arc_tuple,\n                reduction_entry,\n                get_formulation(device_model),\n                S,\n            )\n            if PM_branches[\"$(ix)\"][\"br_status\"] == true\n                f = PM_branches[\"$(ix)\"][\"f_bus\"]\n                t = PM_branches[\"$(ix)\"][\"t_bus\"]\n                PMmap_br[arc_tuple] = (from_to = (ix, f, t), to_from = (ix, t, f))\n            end\n            push!(modeled_arc_tuples, arc_tuple)\n            ix += 1\n        end\n    end\n    return PM_branches, PMmap_br\nend\n\nfunction get_branches_to_pm(\n    sys::PSY.System,\n    network_model::NetworkModel{S},\n    ::Type{T},\n    branch_template::BranchModelContainer,\n    start_idx = 0,\n) where {T <: PSY.TwoTerminalHVDC, S <: PM.AbstractPowerModel}\n    PM_branches = Dict{String, Any}()\n    PMmap_br = Dict{PM_MAP_TUPLE, T}()\n\n    for (d, device_model) in branch_template\n        comp_type = get_component_type(device_model)\n        !(comp_type <: T) && continue\n        if comp_type <: PSY.TwoTerminalLCCLine &&\n           get_formulation(device_model) <: HVDCTwoTerminalLCC\n            continue\n        end\n        start_idx += length(PM_branches)\n        for (i, branch) in enumerate(get_available_components(device_model, sys))\n            ix = i + start_idx\n            PM_branches[\"$(ix)\"] =\n                get_branch_to_pm(ix, branch, get_formulation(device_model), S)\n            if PM_branches[\"$(ix)\"][\"br_status\"] == true\n                f = PM_branches[\"$(ix)\"][\"f_bus\"]\n                t = PM_branches[\"$(ix)\"][\"t_bus\"]\n                PMmap_br[(from_to = (ix, f, t), to_from = (ix, t, f))] = branch\n            end\n        end\n    end\n    return PM_branches, PMmap_br\nend\n\nfunction get_buses_to_pm(buses::IS.FlattenIteratorWrapper{PSY.ACBus})\n    PM_buses = Dict{String, Any}()\n    PMmap_buses = Dict{Int, PSY.ACBus}()\n\n    for bus in buses\n        if PSY.get_bustype(bus) == PSY.ACBusTypes.ISOLATED\n            continue\n        end\n        number = PSY.get_number(bus)\n        PM_bus = Dict{String, Any}(\n            \"zone\" => 1,\n            \"bus_i\" => number,\n            \"bus_type\" => PM_BUSTYPES[PSY.get_bustype(bus)],\n            \"vmax\" => PSY.get_voltage_limits(bus).max,\n            \"area\" => 1,\n            \"vmin\" => PSY.get_voltage_limits(bus).min,\n            \"index\" => PSY.get_number(bus),\n            \"va\" => PSY.get_angle(bus),\n            \"vm\" => PSY.get_magnitude(bus),\n            \"base_kv\" => PSY.get_base_voltage(bus),\n            \"inj_p\" => 0.0,\n            \"inj_q\" => 0.0,\n            \"name\" => PSY.get_name(bus),\n        )\n        PM_buses[\"$(number)\"] = PM_bus\n        PMmap_buses[number] = bus\n    end\n    return PM_buses, PMmap_buses\nend\n\nfunction pass_to_pm(sys::PSY.System, template::ProblemTemplate, time_periods::Int)\n    ac_lines, PMmap_ac = get_branches_to_pm(\n        sys,\n        get_network_model(template),\n        PSY.ACTransmission,\n        template.branches,\n    )\n    two_terminal_dc_lines, PMmap_dc = get_branches_to_pm(\n        sys,\n        get_network_model(template),\n        PSY.TwoTerminalHVDC,\n        template.branches,\n        length(ac_lines),\n    )\n    network_model = get_network_model(template)\n    buses = get_available_components(network_model, PSY.ACBus, sys)\n    pm_buses, PMmap_buses = get_buses_to_pm(buses)\n    PM_translation = Dict{String, Any}(\n        \"bus\" => pm_buses,\n        \"branch\" => ac_lines,\n        \"baseMVA\" => PSY.get_base_power(sys),\n        \"per_unit\" => true,\n        \"storage\" => Dict{String, Any}(),\n        \"dcline\" => two_terminal_dc_lines,\n        \"gen\" => Dict{String, Any}(),\n        \"switch\" => Dict{String, Any}(),\n        \"shunt\" => Dict{String, Any}(),\n        \"load\" => Dict{String, Any}(),\n    )\n\n    # TODO: this function adds overhead in large number of time_steps\n    # We can do better later.\n\n    PM_translation = PM.replicate(PM_translation, time_periods)\n\n    PM_map = PMmap(PMmap_buses, PMmap_ac, PMmap_dc)\n\n    return PM_translation, PM_map\nend\n"
  },
  {
    "path": "src/network_models/power_flow_evaluation.jl",
    "content": "# Defines the order of precedence for each type of information that could be sent to PowerFlows.jl\nconst PF_INPUT_KEY_PRECEDENCES = Dict(\n    :active_power => [ActivePowerVariable, PowerOutput, ActivePowerTimeSeriesParameter],\n    :active_power_in => [ActivePowerInVariable, ActivePowerInTimeSeriesParameter],\n    :active_power_out => [ActivePowerOutVariable, ActivePowerOutTimeSeriesParameter],\n    :reactive_power => [ReactivePowerVariable, ReactivePowerTimeSeriesParameter],\n    :voltage_angle_export => [PowerFlowVoltageAngle, VoltageAngle],\n    :voltage_magnitude_export => [PowerFlowVoltageMagnitude, VoltageMagnitude],\n    :voltage_angle_opf => [VoltageAngle],\n    :voltage_magnitude_opf => [VoltageMagnitude],\n    :active_power_hvdc_pst_from_to =>\n        [FlowActivePowerFromToVariable, FlowActivePowerVariable],\n    :active_power_hvdc_pst_to_from =>\n        [FlowActivePowerToFromVariable, FlowActivePowerVariable],\n)\n\nconst RELEVANT_COMPONENTS_SELECTOR =\n    PSY.make_selector(Union{PSY.StaticInjection, PSY.Bus, PSY.Branch})\n\nfunction _add_aux_variables!(\n    container::OptimizationContainer,\n    component_map::Dict{Type{<:AuxVariableType}, <:Set{<:Tuple{DataType, Any}}},\n)\n    for (var_type, components) in pairs(component_map)\n        component_types = unique(first.(components))\n        for component_type in component_types\n            component_names = [v for (k, v) in components if k <: component_type]\n            sort!(component_names)\n            add_aux_variable_container!(\n                container,\n                var_type(),\n                component_type,\n                component_names,\n                get_time_steps(container),\n            )\n        end\n    end\nend\n\n# Trait that determines which types of information are needed for each type of power flow\npf_input_keys(::PFS.ABAPowerFlowData) =\n    [:active_power, :active_power_in, :active_power_out]\npf_input_keys(::PFS.PTDFPowerFlowData) =\n    [:active_power, :active_power_in, :active_power_out]\npf_input_keys(::PFS.vPTDFPowerFlowData) =\n    [:active_power, :active_power_in, :active_power_out]\npf_input_keys(::PFS.ACPowerFlowData) =\n    [\n        :active_power,\n        :active_power_in,\n        :active_power_out,\n        :reactive_power,\n        :voltage_angle_opf,\n        :voltage_magnitude_opf,\n    ]\npf_input_keys(::PFS.PSSEExporter) =\n    [\n        :active_power,\n        :active_power_in,\n        :active_power_out,\n        :reactive_power,\n        :voltage_angle_export,\n        :voltage_magnitude_export,\n    ]\npf_input_keys_hvdc_pst(::PFS.PowerFlowData) = DataType[]\npf_input_keys_hvdc_pst(::PFS.ACPowerFlowData) =\n    [:active_power_hvdc_pst_from_to, :active_power_hvdc_pst_to_from]\n\n_get_component_bus_for_map(component::PSY.Branch, ::Val{:from}) =\n    PSY.get_from_bus(component)\n_get_component_bus_for_map(component::PSY.Branch, ::Val{:to}) = PSY.get_to_bus(component)\n_get_component_bus_for_map(component::PSY.Component, ::Nothing) = PSY.get_bus(component)\n\n# Generalized function to create component maps by name to the index in the PowerFlowData bus arrays\nfunction _make_temp_component_map(\n    pf_data::PFS.PowerFlowData,\n    sys::PSY.System,\n    component_type::DataType,\n    side::Union{Val{:from}, Val{:to}, Nothing},\n)\n    nrd = PFS.get_network_reduction_data(pf_data)\n    temp_component_map = Dict{DataType, Dict{String, Int}}()\n    components = PSY.get_available_components(component_type, sys)\n    bus_lookup = PFS.get_bus_lookup(pf_data)\n    for comp in components\n        comp_type = typeof(comp)\n        bus_dict = get!(temp_component_map, comp_type, Dict{String, Int}())\n        bus_number = PSY.get_number(_get_component_bus_for_map(comp, side))\n        bus_dict[PSY.get_name(comp)] = PNM.get_bus_index(bus_number, bus_lookup, nrd)\n    end\n    return temp_component_map\nend\n\n# Maps the StaticInjection component type by name to the\n# index in the PowerFlow data arrays going from Bus number to bus index\nfunction _make_temp_component_map(pf_data::PFS.PowerFlowData, sys::PSY.System)\n    temp_component_map = _make_temp_component_map(\n        pf_data,\n        sys,\n        PSY.StaticInjection,\n        nothing,\n    )\n    # Add ACBus components for voltage magnitude and angle export\n    bus_lookup = PFS.get_bus_lookup(pf_data)\n    nrd = PFS.get_network_reduction_data(pf_data)\n    temp_component_map[PSY.ACBus] =\n        Dict(\n            PSY.get_name(c) => PNM.get_bus_index(PSY.get_number(c), bus_lookup, nrd) for\n            c in get_available_components(PSY.ACBus, sys)\n        )\n    return temp_component_map\nend\n\n_get_temp_component_map_lhs(comp::PSY.Component) = PSY.get_name(comp)\n_get_temp_component_map_lhs(comp::PSY.Bus) = PSY.get_number(comp)\n\n# Creates dicts of components by type\nfunction _make_temp_component_map(::PFS.SystemPowerFlowContainer, sys::PSY.System)\n    temp_component_map =\n        Dict{DataType, Dict{Union{String, Int64}, String}}()\n    relevant_components = PSY.get_available_components(RELEVANT_COMPONENTS_SELECTOR, sys)\n    for comp_type in unique(typeof.(relevant_components))\n        # NOTE we avoid using bus numbers here because PSY.get_bus(system, number) is O(n)\n        temp_component_map[comp_type] =\n            Dict(\n                _get_temp_component_map_lhs(c) => PSY.get_name(c) for\n                c in relevant_components if c isa comp_type\n            )\n    end\n    return temp_component_map\nend\n\nfunction _make_pf_input_map!(\n    pf_e_data::PowerFlowEvaluationData,\n    container::OptimizationContainer,\n    sys::PSY.System,\n)\n    pf_data = get_power_flow_data(pf_e_data)\n    temp_component_map = _make_temp_component_map(pf_data, sys)\n    map_type = valtype(temp_component_map)  # Dict{String, Int} for PowerFlowData, Dict{Union{String, Int64}, String} for SystemPowerFlowContainer\n    pf_e_data.input_key_map = Dict{Symbol, Dict{OptimizationContainerKey, map_type}}()\n\n    # available_keys is a vector of Pair{OptimizationContainerKey, data} containing all possibly relevant data sources to iterate over\n    available_keys = vcat(\n        [\n            collect(pairs(f(container))) for\n            f in [get_variables, get_aux_variables, get_parameters]\n        ]...,\n    )\n    # Separate map for each category\n    for category in pf_input_keys(pf_data)\n        # Map that persists to store the bus index to which the variable maps in the PowerFlowData, etc.\n        pf_data_opt_container_map = Dict{OptimizationContainerKey, map_type}()\n        @info \"Adding input map to send $category to $(nameof(typeof(pf_data)))\"\n        precedence = PF_INPUT_KEY_PRECEDENCES[category]\n        _add_category_to_map!(\n            precedence,\n            available_keys,\n            temp_component_map,\n            pf_data_opt_container_map,\n        )\n        pf_e_data.input_key_map[category] = pf_data_opt_container_map\n    end\n    _add_two_terminal_elements_map!(sys, pf_data, available_keys, pf_e_data.input_key_map)\n    return\nend\n\n\"\"\"\n    _add_category_to_map!(\n        precedence::Vector{DataType},\n        available_keys::Vector{Pair{OptimizationContainerKey, Any}},\n        temp_component_map::Union{\n            Dict{DataType, Dict{String, Int}},\n            Dict{DataType, Dict{Union{Int64, String}, String}},\n        },\n        pf_data_opt_container_map::Union{\n            Dict{OptimizationContainerKey, Dict{String, Int}},\n            Dict{OptimizationContainerKey, Dict{Union{Int64, String}, String}},\n        },\n    )\n\nHelper function that is used in _make_pf_input_map! and _add_two_terminal_elements_map! to configure which variables from the\noptimization results get written to the PowerFlowData. For every results variable from the optimization, it finds the corresponding\nmapping between the optimization variable and the PowerFlowData variable.\nThe mappings are added to the `pf_data_opt_container_map` Dict.\nThis step is executed during the build stage of the optimization. The results are written to the PowerFlowData in the\nsolve stage, before the power flow is solved.\n\n# Arguments\n- `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`).\n- `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.\n- `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).\n- `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.\n\"\"\"\nfunction _add_category_to_map!(\n    precedence::Vector{DataType},\n    available_keys::Vector{Pair{OptimizationContainerKey, Any}},\n    temp_component_map::Dict{DataType, <:Dict},\n    pf_data_opt_container_map::Dict{OptimizationContainerKey, <:Dict},\n)\n    added_injection_types = DataType[]\n    for entry_type in precedence\n        for (key, val) in available_keys\n            if get_entry_type(key) === entry_type\n                comp_type = get_component_type(key)\n                # Skip types that have already been handled by something of higher precedence\n                if comp_type ∈ added_injection_types || comp_type ∉ keys(temp_component_map)\n                    continue\n                end\n                push!(added_injection_types, comp_type)\n\n                name_bus_ix_map = valtype(temp_component_map)()\n                comp_names =\n                    if (key isa ParameterKey)\n                        get_component_names(get_attributes(val))\n                    else\n                        axes(val)[1]\n                    end\n                for comp_name in comp_names\n                    name_bus_ix_map[comp_name] =\n                        temp_component_map[comp_type][comp_name]\n                end\n                pf_data_opt_container_map[key] = name_bus_ix_map\n            end\n        end\n    end\nend\n\n# the function to map HVDC power transfers as bus injections is not applicable to PSSEExporter:\n_add_two_terminal_elements_map!(\n    ::PSY.System,\n    ::PFS.PSSEExporter,\n    ::Vector{Pair{OptimizationContainerKey, Any}},\n    ::Dict{Symbol, <:Dict{OptimizationContainerKey, <:Dict}},\n) = nothing\n\n\"\"\"\n    _add_two_terminal_elements_map!(\n        sys::PSY.System,\n        pf_data::PFS.PowerFlowData,\n        available_keys::Vector{Pair{OptimizationContainerKey, Any}},\n        input_key_map::Dict{Symbol, Dict{OptimizationContainerKey, Dict{String, Int64}}}\n    )\n\nAdds mappings for two-terminal elements (HVDC components) that connect the power flow results (from -> to, to -> from)\nto be added to the mappings for all component types.\nThe results for these elements are added as bus injections in the `PowerFlowData` as a simplified representation of\nthese components.\n\n# Arguments\n- `sys::PSY.System`: `System` instance representing the power system model.\n- `pf_data::PFS.PowerFlowData`: The power flow data used internally for power flow calculations.\n- `available_keys::Vector{Pair{OptimizationContainerKey, Any}}`: A vector of available optimization container keys and their associated values.\n- `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.\n\"\"\"\nfunction _add_two_terminal_elements_map!(\n    sys::PSY.System,\n    pf_data::PFS.PowerFlowData,\n    available_keys::Vector{Pair{OptimizationContainerKey, Any}},\n    input_key_map::Dict{Symbol, <:Dict{OptimizationContainerKey, <:Dict}},\n)\n    for element_type in (PSY.TwoTerminalHVDC, PSY.PhaseShiftingTransformer)\n        for (category, side) in zip(\n            [:active_power_hvdc_pst_from_to, :active_power_hvdc_pst_to_from],\n            [Val(:from), Val(:to)],\n        )\n            category ∈ pf_input_keys_hvdc_pst(pf_data) || continue\n\n            temp_component_map = _make_temp_component_map(\n                pf_data,\n                sys,\n                element_type,\n                side,\n            )\n            isempty(temp_component_map) && continue\n\n            precedence = PF_INPUT_KEY_PRECEDENCES[category]\n            pf_data_opt_container_map =\n                Dict{OptimizationContainerKey, valtype(temp_component_map)}()\n            _add_category_to_map!(\n                precedence,\n                available_keys,\n                temp_component_map,\n                pf_data_opt_container_map,\n            )\n            category_map = get!(\n                input_key_map,\n                category,\n                Dict{OptimizationContainerKey, valtype(temp_component_map)}(),\n            )\n            merge!(category_map, pf_data_opt_container_map)\n        end\n    end\n    return\nend\n\n# Trait that determines what branch aux vars we can get from each PowerFlowContainer\nbranch_aux_vars(::PFS.ACPowerFlowData) =\n    [PowerFlowBranchReactivePowerFromTo, PowerFlowBranchReactivePowerToFrom,\n        PowerFlowBranchActivePowerFromTo, PowerFlowBranchActivePowerToFrom,\n        PowerFlowBranchActivePowerLoss]\nbranch_aux_vars(::PFS.ABAPowerFlowData) =\n    [PowerFlowBranchActivePowerFromTo, PowerFlowBranchActivePowerToFrom]\nbranch_aux_vars(::PFS.PTDFPowerFlowData) =\n    [PowerFlowBranchActivePowerFromTo, PowerFlowBranchActivePowerToFrom]\nbranch_aux_vars(::PFS.vPTDFPowerFlowData) =\n    [PowerFlowBranchActivePowerFromTo, PowerFlowBranchActivePowerToFrom]\nbranch_aux_vars(::PFS.PSSEExporter) = DataType[]\n\n# Same for bus aux vars\nfunction bus_aux_vars(data::PFS.ACPowerFlowData)\n    vars = [PowerFlowVoltageAngle, PowerFlowVoltageMagnitude]\n    if PFS.get_calculate_loss_factors(data)\n        push!(vars, PowerFlowLossFactors)\n    end\n    if PFS.get_calculate_voltage_stability_factors(data)\n        push!(vars, PowerFlowVoltageStabilityFactors)\n    end\n    return vars\nend\n\nbus_aux_vars(::PFS.ABAPowerFlowData) = [PowerFlowVoltageAngle]\nbus_aux_vars(::PFS.PTDFPowerFlowData) = DataType[]\nbus_aux_vars(::PFS.vPTDFPowerFlowData) = DataType[]\nbus_aux_vars(::PFS.PSSEExporter) = DataType[]\n\n# TODO: Needs update for MultiTerminal HVDC\n_get_branch_component_tuples(sys::PSY.System) = [\n    (typeof(c), get_name(c)) for\n    c in PSY.get_available_components(PSY.ACBranch, sys)\n]\n\n_get_bus_component_tuples(pfd::PFS.PowerFlowData) =\n    tuple.(PSY.ACBus, keys(PFS.get_bus_lookup(pfd)))  # get_bus_type returns a ACBusTypes, not the DataType we need here\n\n_get_bus_component_tuples(pfd::PFS.SystemPowerFlowContainer) =\n    [\n        (typeof(c), PSY.get_number(c)) for\n        c in PSY.get_available_components(PSY.ACBus, PFS.get_system(pfd))\n    ]\n\nfunction _with_time_steps(pf::T, n::Int) where {T <: PFS.PowerFlowEvaluationModel}\n    fields = Dict(fn => getfield(pf, fn) for fn in fieldnames(T))\n    fields[:time_steps] = n\n    return T(; fields...)\nend\n\n_with_time_steps(pf::PFS.PSSEExportPowerFlow, ::Int) = pf # exporter doesn't use time_steps\n\nfunction add_power_flow_data!(\n    container::OptimizationContainer,\n    evaluators::Vector{PFS.PowerFlowEvaluationModel},\n    sys::PSY.System,\n)\n    container.power_flow_evaluation_data = Vector{PowerFlowEvaluationData}()\n    sizehint!(container.power_flow_evaluation_data, length(evaluators))\n    # For each output key, what components are we working with?\n    branch_aux_var_components =\n        Dict{Type{<:AuxVariableType}, Set{Tuple{<:DataType, String}}}()\n    bus_aux_var_components = Dict{Type{<:AuxVariableType}, Set{Tuple{<:DataType, <:Int}}}()\n    # we ought to be providing the time_steps when constructing the PF evaluation model,\n    # but that value isn't known until runtime (and PF evaluation model is immutable).\n    n_time_steps = length(get_time_steps(container))\n    for evaluator in evaluators\n        evaluator = _with_time_steps(evaluator, n_time_steps)\n        @info \"Building PowerFlow evaluator using $(evaluator)\"\n        pf_data = PFS.make_power_flow_container(evaluator, sys)\n        pf_e_data = PowerFlowEvaluationData(pf_data)\n        my_branch_aux_vars = branch_aux_vars(pf_data)\n        my_bus_aux_vars = bus_aux_vars(pf_data)\n\n        my_branch_components = _get_branch_component_tuples(sys)\n        for branch_aux_var in my_branch_aux_vars\n            to_add_to = get!(\n                branch_aux_var_components,\n                branch_aux_var,\n                Set{Tuple{<:DataType, String}}(),\n            )\n            push!.(Ref(to_add_to), my_branch_components)\n        end\n\n        my_bus_components = _get_bus_component_tuples(pf_data)\n        for bus_aux_var in my_bus_aux_vars\n            to_add_to =\n                get!(bus_aux_var_components, bus_aux_var, Set{Tuple{<:DataType, <:Int}}())\n            push!.(Ref(to_add_to), my_bus_components)\n        end\n        push!(container.power_flow_evaluation_data, pf_e_data)\n    end\n\n    _add_aux_variables!(container, branch_aux_var_components)\n    _add_aux_variables!(container, bus_aux_var_components)\n\n    # Make the input maps after adding aux vars so output of one power flow can be input of another\n    for pf_e_data in get_power_flow_evaluation_data(container)\n        _make_pf_input_map!(pf_e_data, container, sys)\n    end\n    return\nend\n\n# How to update the PowerFlowData given a component type. A bit duplicative of code in PowerFlows.jl.\n_update_pf_data_component!(\n    pf_data::PFS.PowerFlowData,\n    ::Val{:active_power},\n    ::Type{<:PSY.StaticInjection},\n    index,\n    t,\n    value,\n) = (pf_data.bus_active_power_injections[index, t] += value)\n_update_pf_data_component!(\n    pf_data::PFS.PowerFlowData,\n    ::Val{:active_power},\n    ::Type{<:PSY.ElectricLoad},\n    index,\n    t,\n    value,\n) = (pf_data.bus_active_power_withdrawals[index, t] -= value)\n# ActivePowerOutVariable represents power output (positive injection into the grid)\n_update_pf_data_component!(\n    pf_data::PFS.PowerFlowData,\n    ::Val{:active_power_out},\n    ::Type{<:PSY.StaticInjection},\n    index,\n    t,\n    value,\n) = (pf_data.bus_active_power_injections[index, t] += value)\n# ActivePowerInVariable represents power input (withdrawal from the grid, e.g. storage charging)\n_update_pf_data_component!(\n    pf_data::PFS.PowerFlowData,\n    ::Val{:active_power_in},\n    ::Type{<:PSY.StaticInjection},\n    index,\n    t,\n    value,\n) = (pf_data.bus_active_power_injections[index, t] -= value)\n_update_pf_data_component!(\n    pf_data::PFS.PowerFlowData,\n    ::Val{:reactive_power},\n    ::Type{<:PSY.StaticInjection},\n    index,\n    t,\n    value,\n) = (pf_data.bus_reactive_power_injections[index, t] += value)\n_update_pf_data_component!(\n    pf_data::PFS.PowerFlowData,\n    ::Val{:reactive_power},\n    ::Type{<:PSY.ElectricLoad},\n    index,\n    t,\n    value,\n) = (pf_data.bus_reactive_power_withdrawals[index, t] -= value)\n_update_pf_data_component!(\n    pf_data::PFS.PowerFlowData,\n    ::Union{Val{:voltage_angle_export}, Val{:voltage_angle_opf}},\n    ::Type{<:PSY.ACBus},\n    index,\n    t,\n    value,\n) = (pf_data.bus_angles[index, t] = value)\n_update_pf_data_component!(\n    pf_data::PFS.PowerFlowData,\n    ::Union{Val{:voltage_magnitude_export}, Val{:voltage_magnitude_opf}},\n    ::Type{<:PSY.ACBus},\n    index,\n    t,\n    value,\n) = (pf_data.bus_magnitude[index, t] = value)\n_update_pf_data_component!(\n    pf_data::PFS.PowerFlowData,\n    ::Val{:active_power_hvdc_pst_from_to},\n    ::Type{<:PSY.TwoTerminalHVDC},\n    index,\n    t,\n    value,\n) = (pf_data.bus_active_power_injections[index, t] -= value)\n# FlowActivePowerToFromVariable is signed negative when power flows from→to (since\n# `tf_var + ft_var == losses ≥ 0`), so subtracting yields the correct positive\n# injection at the receiving bus.\n_update_pf_data_component!(\n    pf_data::PFS.PowerFlowData,\n    ::Val{:active_power_hvdc_pst_to_from},\n    ::Type{<:PSY.TwoTerminalHVDC},\n    index,\n    t,\n    value,\n) = (pf_data.bus_active_power_injections[index, t] -= value)\n_update_pf_data_component!(\n    pf_data::PFS.PowerFlowData,\n    ::Val{:active_power_hvdc_pst_from_to},\n    ::Type{<:PSY.PhaseShiftingTransformer},\n    index,\n    t,\n    value,\n) = (pf_data.bus_active_power_injections[index, t] -= value)\n_update_pf_data_component!(\n    pf_data::PFS.PowerFlowData,\n    ::Val{:active_power_hvdc_pst_to_from},\n    ::Type{<:PSY.PhaseShiftingTransformer},\n    index,\n    t,\n    value,\n) = (pf_data.bus_active_power_injections[index, t] += value)\n\nfunction _write_value_to_pf_data!(\n    pf_data::PFS.PowerFlowData,\n    category::Symbol,\n    container::OptimizationContainer,\n    key::OptimizationContainerKey,\n    component_map)\n    result = lookup_value(container, key)\n    for (device_name, index) in component_map\n        injection_values = result[device_name, :]\n        for t in get_time_steps(container)\n            value = jump_value(injection_values[t])\n            _update_pf_data_component!(\n                pf_data,\n                Val(category),\n                get_component_type(key),\n                index,\n                t,\n                value,\n            )\n        end\n    end\n    return\nend\n\nfunction update_pf_data!(\n    pf_e_data::PowerFlowEvaluationData{<:PFS.PowerFlowData},\n    container::OptimizationContainer,\n)\n    pf_data = get_power_flow_data(pf_e_data)\n    PFS.clear_injection_data!(pf_data)\n    input_map = get_input_key_map(pf_e_data)\n    for (category, inputs) in input_map\n        @debug \"Writing $category to $(nameof(typeof(pf_data)))\"\n        for (key, component_map) in inputs\n            _write_value_to_pf_data!(pf_data, category, container, key, component_map)\n        end\n    end\n    return\nend\n\n# PERF we use direct dot access here, and implement our own unit conversions, for performance and convenience\n_update_component!(comp::PSY.Component, ::Val{:active_power}, value, sys_base) =\n    (comp.active_power = value * sys_base / PSY.get_base_power(comp))\n# Sign is flipped for loads (TODO can we rely on some existing function that encodes this information?)\n_update_component!(comp::PSY.ElectricLoad, ::Val{:active_power}, value, sys_base) =\n    (comp.active_power = -value * sys_base / PSY.get_base_power(comp))\n_update_component!(comp::PSY.Component, ::Val{:reactive_power}, value, sys_base) =\n    (comp.reactive_power = value * sys_base / PSY.get_base_power(comp))\n_update_component!(comp::PSY.ElectricLoad, ::Val{:reactive_power}, value, sys_base) =\n    (comp.reactive_power = -value * sys_base / PSY.get_base_power(comp))\n# ActivePowerOutVariable represents power output (positive contribution to active_power)\n_update_component!(comp::PSY.Component, ::Val{:active_power_out}, value, sys_base) =\n    (comp.active_power += value * sys_base / PSY.get_base_power(comp))\n# ActivePowerInVariable represents power input / withdrawal (negative contribution to active_power)\n_update_component!(comp::PSY.Component, ::Val{:active_power_in}, value, sys_base) =\n    (comp.active_power -= value * sys_base / PSY.get_base_power(comp))\n_update_component!(\n    comp::PSY.ACBus,\n    ::Union{Val{:voltage_angle_export}, Val{:voltage_angle_opf}},\n    value, sys_base,\n) =\n    comp.angle = value\n_update_component!(\n    comp::PSY.ACBus,\n    ::Union{Val{:voltage_magnitude_export}, Val{:voltage_magnitude_opf}},\n    value, sys_base,\n) =\n    comp.magnitude = value\n\nfunction update_pf_system!(\n    sys::PSY.System,\n    container::OptimizationContainer,\n    input_map::Dict{Symbol, <:Dict{OptimizationContainerKey, <:Any}},\n    time_step::Int,\n)\n    # Reset active_power to zero for components that use separate in/out variables\n    # (e.g. storage, import/export sources) before the additive += / -= updates.\n    # Collect unique (type, name) pairs to avoid resetting the same component twice.\n    reset_components = Set{Tuple{DataType, String}}()\n    for category in (:active_power_in, :active_power_out)\n        haskey(input_map, category) || continue\n        for (key, component_map) in input_map[category]\n            for (_, device_name) in component_map\n                push!(reset_components, (get_component_type(key), device_name))\n            end\n        end\n    end\n    for (comp_type, device_name) in reset_components\n        comp = PSY.get_component(comp_type, sys, device_name)\n        comp.active_power = 0.0\n    end\n    for (category, inputs) in input_map\n        @debug \"Writing $category to (possibly internal) System\"\n        for (key, component_map) in inputs\n            result = lookup_value(container, key)\n            for (device_id, device_name) in component_map\n                injection_values = result[device_id, :]\n                comp = PSY.get_component(get_component_type(key), sys, device_name)\n                val = jump_value(injection_values[time_step])\n                _update_component!(comp, Val(category), val, get_base_power(container))\n            end\n        end\n    end\nend\n\n\"\"\"\nUpdate a `PowerFlowEvaluationData` containing a `PowerFlowContainer` that does not\n`supports_multi_period` using a single `time_step` of the `OptimizationContainer`. To\nproperly keep track of outer step number, time steps must be passed in sequentially,\nstarting with 1.\n\"\"\"\nfunction update_pf_data!(\n    pf_e_data::PowerFlowEvaluationData{PFS.PSSEExporter},\n    container::OptimizationContainer,\n    time_step::Int,\n)\n    pf_data = get_power_flow_data(pf_e_data)\n    input_map = get_input_key_map(pf_e_data)\n    update_pf_system!(PFS.get_system(pf_data), container, input_map, time_step)\n    if !isnothing(pf_data.step)\n        outer_step, _... = pf_data.step\n        # time_step == 1 means we have rolled over to a new outer step\n        # NOTE this is a bit brittle but there is currently no way of getting this\n        # information from upstream, may change in the future\n        (time_step == 1) && (outer_step += 1)\n        pf_data.step = (outer_step, time_step)\n    end\n    return\nend\n\n# ParameterKey → FixedOutput formulation; dispatch is externally determined,\n# should not participate in slack.\n_accumulate_headroom!(::PFS.PowerFlowData,\n    ::OptimizationContainer,\n    ::PSY.System,\n    ::OptimizationContainerKey{<:ParameterType, <:PSY.Component},\n    ::Dict{String, Int},\n    ::Int,\n    ::Matrix{PSY.ACBusTypes},\n    ::Vector{Dict{Tuple{DataType, String}, Float64}},\n) = nothing\n\n# Storage uses split In/Out active power variables; its headroom contribution comes\n# from `_accumulate_in_out_headroom!` below. These skips guard against any (currently\n# unused) `:active_power` mapping for Storage that would otherwise double-count.\n_accumulate_headroom!(::PFS.PowerFlowData,\n    ::OptimizationContainer,\n    ::PSY.System,\n    ::OptimizationContainerKey{<:ISOPT.OptimizationKeyType, <:PSY.Storage},\n    ::Dict{String, Int},\n    ::Int,\n    ::Matrix{PSY.ACBusTypes},\n    ::Vector{Dict{Tuple{DataType, String}, Float64}},\n) = nothing\n\n# needed to fix an ambiguity: ParameterType with Storage.\n_accumulate_headroom!(::PFS.PowerFlowData,\n    ::OptimizationContainer,\n    ::PSY.System,\n    ::OptimizationContainerKey{<:ISOPT.ParameterType, <:PSY.Storage},\n    ::Dict{String, Int},\n    ::Int,\n    ::Matrix{PSY.ACBusTypes},\n    ::Vector{Dict{Tuple{DataType, String}, Float64}},\n) = nothing\n\n\"\"\"\nAccumulate headroom for a single OptimizationContainerKey into `pf_data` and `computed_gspf`.\nThe `where {U}` parameter makes the component type a compile-time constant, so\n`PSY.get_component(U, ...)`, `has_container_key(..., U)`, and the `(U, device_name)` Dict\nkey all dispatch concretely. Note that `result` and `ts_param_values` remain abstractly\ntyped because `OptimizationContainer.variables` and `.parameters` have abstract value types\nin their dict signatures — the inner indexing into them still goes through dynamic dispatch.\n\"\"\"\nfunction _accumulate_headroom!(\n    pf_data::PFS.PowerFlowData,\n    container::OptimizationContainer,\n    sys::PSY.System,\n    key::OptimizationContainerKey{<:ISOPT.OptimizationKeyType, U},\n    component_map::Dict{String, Int},\n    n_time_steps::Int,\n    bus_types::Matrix{PSY.ACBusTypes},\n    computed_gspf::Vector{Dict{Tuple{DataType, String}, Float64}},\n) where {U <: PSY.Component}\n    result = lookup_value(container, key)\n\n    # Time-varying active power limits (e.g. renewable availability profiles).\n    # Precompute the axis as a Set so the per-(device, t) membership test is O(1).\n    ts_param_values, ts_axis =\n        if has_container_key(\n            container, ActivePowerTimeSeriesParameter, U)\n            vals = lookup_value(container, ParameterKey(ActivePowerTimeSeriesParameter, U))\n            (vals, Set{String}(axes(vals, 1)))\n        else\n            (nothing, nothing)\n        end\n\n    for (device_name, bus_ix) in component_map\n        comp = PSY.get_component(U, sys, device_name)\n        PFS.contributes_active_power(comp) || continue\n        PFS.active_power_contribution_type(comp) ==\n        PFS.PowerContributionType.INJECTION || continue\n\n        # limits.max is already in SYSTEM_BASE because PSI sets units at init\n        p_max_static = PFS.get_active_power_limits_for_power_flow(comp).max\n        has_ts = ts_axis !== nothing && device_name ∈ ts_axis\n\n        for t in 1:n_time_steps\n            bus_types[bus_ix, t] ∈ (PSY.ACBusTypes.REF, PSY.ACBusTypes.PV) || continue\n\n            p_setpoint = jump_value(result[device_name, t])\n            p_max_t = if has_ts\n                min(p_max_static, jump_value(ts_param_values[device_name, t]))\n            else\n                p_max_static\n            end\n            headroom = p_max_t - p_setpoint\n            headroom <= 0.0 && continue\n\n            computed_gspf[t][(U, device_name)] = headroom\n            pf_data.bus_active_power_range[bus_ix, t] += headroom\n        end\n    end\n    return\nend\n\n# Maximum discharge active power (system-base PU) for devices that use split\n# `ActivePowerInVariable` / `ActivePowerOutVariable`. PFS's\n# `get_active_power_limits_for_power_flow(::Source)` returns `(min=-Inf, max=Inf)`,\n# which is unusable for headroom math, so we read the device-level limits directly.\n_pf_in_out_discharge_max(comp::PSY.Storage) = PSY.get_output_active_power_limits(comp).max\n_pf_in_out_discharge_max(comp::PSY.Source) = PSY.get_active_power_limits(comp).max\n\n\"\"\"\nAccumulate headroom for devices that use split `ActivePowerInVariable` /\n`ActivePowerOutVariable` (e.g. Storage `BookKeeping`, Source `ImportExportSourceModel`).\n\n`net = p_out - p_in` is the device's signed contribution at time `t`. With net > 0 the\ndevice is dispatching and its headroom is `p_max_out - net`; with net <= 0 the device is\ncharging (or idle) and contributes no upward slack.\n\"\"\"\nfunction _accumulate_in_out_headroom!(\n    pf_data::PFS.PowerFlowData,\n    container::OptimizationContainer,\n    sys::PSY.System,\n    in_inputs::Dict{OptimizationContainerKey, Dict{String, Int}},\n    out_inputs::Dict{OptimizationContainerKey, Dict{String, Int}},\n    n_time_steps::Int,\n    bus_types::Matrix{PSY.ACBusTypes},\n    computed_gspf::Vector{Dict{Tuple{DataType, String}, Float64}},\n)\n    for (in_key, in_cmap) in in_inputs\n        out_key, out_cmap = _find_paired_out(out_inputs, get_component_type(in_key))\n        _accumulate_in_out_headroom_one_type!(\n            pf_data, container, sys,\n            in_key, in_cmap, out_key, out_cmap,\n            n_time_steps, bus_types, computed_gspf,\n        )\n    end\n    return\nend\n\nfunction _find_paired_out(\n    out_inputs::Dict{OptimizationContainerKey, Dict{String, Int}},\n    comp_type::DataType,\n)\n    for (key, cmap) in out_inputs\n        get_component_type(key) === comp_type && return (key, cmap)\n    end\n    error(\n        \"`:active_power_out` map missing for $comp_type — a formulation added \" *\n        \"`ActivePowerInVariable` without a paired `ActivePowerOutVariable`.\",\n    )\nend\n\n# Function barrier: the parametric key types specialize `lookup_value` and `result[...]`\n# indexing on the concrete component type `U`.\nfunction _accumulate_in_out_headroom_one_type!(\n    pf_data::PFS.PowerFlowData,\n    container::OptimizationContainer,\n    sys::PSY.System,\n    in_key::OptimizationContainerKey{<:ISOPT.OptimizationKeyType, U},\n    in_cmap::Dict{String, Int},\n    out_key::OptimizationContainerKey{<:ISOPT.OptimizationKeyType, U},\n    out_cmap::Dict{String, Int},\n    n_time_steps::Int,\n    bus_types::Matrix{PSY.ACBusTypes},\n    computed_gspf::Vector{Dict{Tuple{DataType, String}, Float64}},\n) where {U <: PSY.Component}\n    result_in = lookup_value(container, in_key)\n    result_out = lookup_value(container, out_key)\n    for (device_name, bus_ix) in in_cmap\n        comp = PSY.get_component(U, sys, device_name)\n        PFS.contributes_active_power(comp) || continue\n        PFS.active_power_contribution_type(comp) ==\n        PFS.PowerContributionType.INJECTION || continue\n        p_max_out = _pf_in_out_discharge_max(comp)\n        for t in 1:n_time_steps\n            bus_types[bus_ix, t] ∈ (PSY.ACBusTypes.REF, PSY.ACBusTypes.PV) || continue\n            net =\n                jump_value(result_out[device_name, t]) -\n                jump_value(result_in[device_name, t])\n            # Net <= 0 means charging or idle — per spec, no upward slack contribution.\n            net < 0.0 && continue\n            headroom = p_max_out - net\n            headroom <= 0.0 && continue\n            computed_gspf[t][(U, device_name)] = headroom\n            pf_data.bus_active_power_range[bus_ix, t] += headroom\n        end\n    end\n    return\nend\n\n\"\"\"\nRecompute per-time-step headroom-proportional generator slack participation factors using\noptimization results. Only runs if headroom proportional slack was enabled during\ninitialization.\n\nFor each generator at a REF or PV bus, headroom is `P_max(t) - P_setpoint(t)`, where\n`P_setpoint(t)` comes from the optimization result and `P_max(t)` is the minimum of the\nstatic device limit and any `ActivePowerTimeSeriesParameter` at time `t`. Devices that use\nsplit In/Out active power variables are handled separately via\n`_accumulate_in_out_headroom!`. This overwrites the PF-initialized values (which were\ncomputed once from static system data) with time-varying factors.\n\"\"\"\nfunction _update_headroom_participation_factors!(\n    pf_data::PFS.PowerFlowData,\n    container::OptimizationContainer,\n    sys::PSY.System,\n    input_key_map::Dict{Symbol, Dict{OptimizationContainerKey, Dict{String, Int}}},\n)\n    PFS.get_distribute_slack_proportional_to_headroom(PFS.get_pf(pf_data)) || return\n    computed_gspf =\n        PFS.get_computed_gspf(pf_data)::Vector{Dict{Tuple{DataType, String}, Float64}}\n\n    n_time_steps = length(get_time_steps(container))\n    bus_types = PFS.get_bus_type(pf_data)::Matrix{PSY.ACBusTypes}\n    bus_slack_pf =\n        PFS.get_bus_slack_participation_factors(\n            pf_data,\n        )::SparseArrays.SparseMatrixCSC{\n            Float64,\n            Int,\n        }\n\n    # Reset with fresh dicts per time step (init may share references)\n    for t in 1:n_time_steps\n        computed_gspf[t] = Dict{Tuple{DataType, String}, Float64}()\n    end\n    pf_data.bus_active_power_range .= 0.0\n\n    # Function barrier so `_accumulate_headroom!` specializes per concrete key type\n    # encountered at runtime — the outer Dict iterates abstract `OptimizationContainerKey`s.\n    for (key, component_map) in input_key_map[:active_power]\n        _accumulate_headroom!(\n            pf_data, container, sys, key, component_map,\n            n_time_steps, bus_types, computed_gspf)\n    end\n\n    # Devices with split `ActivePowerInVariable` / `ActivePowerOutVariable`\n    # (e.g. Storage `BookKeeping`, Source `ImportExportSourceModel`) accumulate\n    # headroom from the net of out − in.\n    _accumulate_in_out_headroom!(\n        pf_data, container, sys,\n        input_key_map[:active_power_in], input_key_map[:active_power_out],\n        n_time_steps, bus_types, computed_gspf)\n\n    # Rebuild bus_slack_pf in one pass. Per-cell writes into the existing CSC matrix\n    # would trigger O(nnz) structural inserts whenever runtime headroom appears at\n    # (bus, t) pairs outside the t=1-derived sparsity pattern PFS init creates — which\n    # is the common case for renewables with intermittent availability. PowerFlowData\n    # is immutable, so we mutate the CSC's internal arrays in place to preserve identity.\n    n_buses = size(pf_data.bus_active_power_range, 1)\n    nnz_hint = count(>(0.0), pf_data.bus_active_power_range)\n    I_idx = Int[]\n    J_idx = Int[]\n    V_val = Float64[]\n    sizehint!(I_idx, nnz_hint)\n    sizehint!(J_idx, nnz_hint)\n    sizehint!(V_val, nnz_hint)\n    for t in 1:n_time_steps, bus_ix in 1:n_buses\n        R_k = pf_data.bus_active_power_range[bus_ix, t]\n        R_k > 0.0 || continue\n        push!(I_idx, bus_ix)\n        push!(J_idx, t)\n        push!(V_val, R_k)\n    end\n    new_sparse = SparseArrays.sparse(I_idx, J_idx, V_val, n_buses, n_time_steps)\n    resize!(bus_slack_pf.nzval, length(new_sparse.nzval))\n    copyto!(bus_slack_pf.nzval, new_sparse.nzval)\n    resize!(bus_slack_pf.rowval, length(new_sparse.rowval))\n    copyto!(bus_slack_pf.rowval, new_sparse.rowval)\n    resize!(bus_slack_pf.colptr, length(new_sparse.colptr))\n    copyto!(bus_slack_pf.colptr, new_sparse.colptr)\n    return\nend\n\n\"Fetch the most recently solved `PowerFlowEvaluationData`\"\nfunction latest_solved_power_flow_evaluation_data(container::OptimizationContainer)\n    datas = get_power_flow_evaluation_data(container)\n    return datas[findlast(x -> x.is_solved, datas)]\nend\n\nfunction solve_power_flow!(\n    pf_e_data::PowerFlowEvaluationData,\n    container::OptimizationContainer,\n    sys::PSY.System)\n    pf_data = get_power_flow_data(pf_e_data)\n    if PFS.supports_multi_period(pf_data)\n        update_pf_data!(pf_e_data, container)\n        _update_headroom_participation_factors!(\n            pf_data, container, sys, get_input_key_map(pf_e_data))\n        PFS.solve_power_flow!(pf_data)\n    else\n        for t in get_time_steps(container)\n            update_pf_data!(pf_e_data, container, t)\n            PFS.solve_power_flow!(pf_data)\n        end\n    end\n    pf_e_data.is_solved = true\n    return\nend\n\n# Currently nothing to write back to the optimization container from a PSSEExporter\ncalculate_aux_variable_value!(::OptimizationContainer,\n    ::AuxVarKey{T, <:Any} where {T <: PowerFlowAuxVariableType},\n    ::PSY.System, ::PowerFlowEvaluationData{PFS.PSSEExporter}) = nothing\n\n_get_pf_result(::Type{PowerFlowVoltageAngle}, pf_data::PFS.PowerFlowData) =\n    PFS.get_bus_angles(pf_data)\n_get_pf_result(::Type{PowerFlowVoltageMagnitude}, pf_data::PFS.PowerFlowData) =\n    PFS.get_bus_magnitude(pf_data)\n_get_pf_result(::Type{PowerFlowBranchReactivePowerFromTo}, pf_data::PFS.PowerFlowData) =\n    PFS.get_arc_reactive_power_flow_from_to(pf_data)\n_get_pf_result(::Type{PowerFlowBranchReactivePowerToFrom}, pf_data::PFS.PowerFlowData) =\n    PFS.get_arc_reactive_power_flow_to_from(pf_data)\n_get_pf_result(::Type{PowerFlowBranchActivePowerFromTo}, pf_data::PFS.PowerFlowData) =\n    PFS.get_arc_active_power_flow_from_to(pf_data)\n_get_pf_result(::Type{PowerFlowBranchActivePowerToFrom}, pf_data::PFS.PowerFlowData) =\n    PFS.get_arc_active_power_flow_to_from(pf_data)\n_get_pf_result(::Type{PowerFlowLossFactors}, pf_data::PFS.PowerFlowData) =\n    PFS.get_loss_factors(pf_data)\n_get_pf_result(::Type{PowerFlowVoltageStabilityFactors}, pf_data::PFS.PowerFlowData) =\n    PFS.get_voltage_stability_factors(pf_data)\n# PERF: unlike the others, this one requires a bit of computation.\n_get_pf_result(::Type{PowerFlowBranchActivePowerLoss}, pf_data::PFS.PowerFlowData) =\n    PFS.get_arc_active_power_flow_from_to(pf_data) .+\n    PFS.get_arc_active_power_flow_to_from(pf_data)\n\nfunction calculate_aux_variable_value!(container::OptimizationContainer,\n    key::AuxVarKey{T, <:PSY.ACBus},\n    ::PSY.System,\n    pf_e_data::PowerFlowEvaluationData{<:PFS.PowerFlowData},\n) where {T <: PowerFlowAuxVariableType}\n    @debug \"Updating $key from PowerFlowData\"\n    pf_data = get_power_flow_data(pf_e_data)\n    nrd = PFS.get_network_reduction_data(pf_data)\n    src = _get_pf_result(T, pf_data)\n    bus_lookup = PFS.get_bus_lookup(pf_data)\n    dest = get_aux_variable(container, key)\n    for bus_number in axes(dest, 1)\n        bus_ix = PNM.get_bus_index(bus_number, bus_lookup, nrd)\n        dest[bus_number, :] = src[bus_ix, :]\n    end\n    return\nend\n\nfunction calculate_aux_variable_value!(container::OptimizationContainer,\n    key::AuxVarKey{T, U},\n    ::PSY.System,\n    pf_e_data::PowerFlowEvaluationData{<:PFS.PowerFlowData},\n) where {T <: PowerFlowAuxVariableType, U <: PSY.Branch}\n    @debug \"Updating $key from PowerFlowData\"\n    pf_data = get_power_flow_data(pf_e_data)\n    src = _get_pf_result(T, pf_data)\n    dest = get_aux_variable(container, key)\n    nrd = PFS.get_network_reduction_data(pf_data)\n    arc_lookup = PFS.get_arc_lookup(pf_data)\n    # PERF: could pre-compute a Dict of branch type to arcs, then intersect the arcs\n    # for the type U with the keys of the branch maps.\n    for (arc, br) in PNM.get_direct_branch_map(nrd)\n        if br isa U # always a concrete type, so same as: typeof(br) == U\n            name = PSY.get_name(br)\n            arc_ix = arc_lookup[arc]\n            dest[name, :] = src[arc_ix, :]\n        end\n    end\n    for (arc, parallel_brs) in PNM.get_parallel_branch_map(nrd) # parallel_brs is Set{ACTransmission}\n        for br in parallel_brs\n            sample_line = first(parallel_brs)\n            impedance = PSY.get_r(sample_line) + im * PSY.get_x(sample_line)\n            first_name = PSY.get_name(sample_line)\n            if br isa U\n                name = PSY.get_name(br)\n                IS.@assert_op T <: BranchFlowAuxVariableType ||\n                              (T == PowerFlowBranchActivePowerLoss)\n                if !isapprox(PSY.get_r(br) + im * PSY.get_x(br), impedance)\n                    @debug \"Parallel branches with different impedances found: \" *\n                           \"$name and $first_name. Check your data inputs.\"\n                end\n                multiplier = PNM.compute_parallel_multiplier(parallel_brs, name)\n                arc_ix = arc_lookup[arc]\n                dest[name, :] = multiplier .* src[arc_ix, :]\n            end\n        end\n    end\n    return\nend\n\nfunction calculate_aux_variable_value!(container::OptimizationContainer,\n    key::AuxVarKey{<:PowerFlowAuxVariableType, <:PSY.Component},\n    system::PSY.System)\n    # Skip the aux vars that the current power flow isn't meant to update\n    pf_e_data = latest_solved_power_flow_evaluation_data(container)\n    pf_data = get_power_flow_data(pf_e_data)\n    (key in branch_aux_vars(pf_data) || key in bus_aux_vars(pf_data)) && return\n    calculate_aux_variable_value!(container, key, system, pf_e_data)\n    return\nend\n"
  },
  {
    "path": "src/network_models/powermodels_interface.jl",
    "content": "#################################################################################\n# Comments\n#\n# - Ideally the net_injection variables would be bounded.  This can be done using an adhoc data model extension\n# - the `instantiate_*_expr_model` functions combine `PM.instantiate_model` and the `build_*` methods\n#################################################################################\n# Model Definitions\n\nconst UNSUPPORTED_POWERMODELS =\n    [\n        PM.SOCBFPowerModel,\n        PM.SOCBFConicPowerModel,\n        PM.IVRPowerModel,\n        PM.SparseSDPWRMPowerModel,\n    ]\n\nfunction instantiate_nip_expr_model(data::Dict{String, Any}, model_constructor; kwargs...)\n    return PM.instantiate_model(data, model_constructor, instantiate_nip_expr; kwargs...)\nend\n\n# replicates PM.build_mn_opf\nfunction instantiate_nip_expr(pm::PM.AbstractPowerModel)\n    for n in eachindex(PM.nws(pm))\n        PM.variable_bus_voltage(pm; nw = n)\n        PM.variable_branch_power(pm; nw = n, bounded = false)\n        PM.variable_dcline_power(pm; nw = n, bounded = false)\n\n        PM.constraint_model_voltage(pm; nw = n)\n\n        for i in PM.ids(pm, :ref_buses; nw = n)\n            PM.constraint_theta_ref(pm, i; nw = n)\n        end\n\n        for i in PM.ids(pm, :bus; nw = n)\n            constraint_power_balance_ni_expr(pm, i; nw = n)\n        end\n\n        for i in PM.ids(pm, :branch; nw = n)\n            PM.constraint_ohms_yt_from(pm, i; nw = n)\n            PM.constraint_ohms_yt_to(pm, i; nw = n)\n\n            PM.constraint_voltage_angle_difference(pm, i; nw = n)\n        end\n\n        for i in PM.ids(pm, :dcline; nw = n)\n            PM.constraint_dcline_power_losses(pm, i; nw = n)\n        end\n    end\n\n    return\nend\n\nfunction instantiate_bfp_expr_model(data::Dict{String, Any}, model_constructor; kwargs...)\n    return PM.instantiate_model(data, model_constructor, instantiate_bfp_expr; kwargs...)\nend\n\n# replicates PM.build_mn_opf_bf_strg\nfunction instantiate_bfp_expr(pm::PM.AbstractPowerModel)\n    for n in eachindex(PM.nws(pm))\n        PM.variable_bus_voltage(pm; nw = n)\n        PM.variable_branch_power(pm; nw = n, bounded = false)\n        PM.variable_dcline_power(pm; nw = n, bounded = false)\n\n        PM.constraint_model_current(pm; nw = n)\n\n        for i in PM.ids(pm, :ref_buses; nw = n)\n            PM.constraint_theta_ref(pm, i; nw = n)\n        end\n\n        for i in PM.ids(pm, :bus; nw = n)\n            constraint_power_balance_ni_expr(pm, i; nw = n)\n        end\n\n        for i in PM.ids(pm, :branch; nw = n)\n            PM.constraint_power_losses(pm, i; nw = n)\n            PM.constraint_voltage_magnitude_difference(pm, i; nw = n)\n\n            PM.constraint_voltage_angle_difference(pm, i; nw = n)\n        end\n\n        for i in PM.ids(pm, :dcline; nw = n)\n            PM.constraint_dcline_power_losses(pm, i; nw = n)\n        end\n    end\n\n    return\nend\n\n#=\n# VI Methdos not supported currently\nfunction instantiate_vip_expr_model(data::Dict{String, Any}, model_constructor; kwargs...)\n    throw(error(\"VI Models not currently supported\"))\nend\n=#\n\n#################################################################################\n# Model Extention Functions\n\nfunction constraint_power_balance_ni_expr(\n    pm::PM.AbstractPowerModel,\n    i::Int;\n    nw::Int = pm.cnw,\n)\n    if !haskey(PM.con(pm, nw), :power_balance_p)\n        PM.con(pm, nw)[:power_balance_p] = Dict{Int, JuMP.ConstraintRef}()\n    end\n    if !haskey(PM.con(pm, nw), :power_balance_q)\n        PM.con(pm, nw)[:power_balance_q] = Dict{Int, JuMP.ConstraintRef}()\n    end\n\n    bus_arcs = PM.ref(pm, nw, :bus_arcs, i)\n    bus_arcs_dc = PM.ref(pm, nw, :bus_arcs_dc, i)\n\n    inj_p_expr = PM.ref(pm, nw, :bus, i, \"inj_p\")\n    inj_q_expr = PM.ref(pm, nw, :bus, i, \"inj_q\")\n\n    constraint_power_balance_ni_expr(\n        pm,\n        nw,\n        i,\n        bus_arcs,\n        bus_arcs_dc,\n        inj_p_expr,\n        inj_q_expr,\n    )\n\n    return\nend\n\nfunction constraint_power_balance_ni_expr(\n    pm::PM.AbstractPowerModel,\n    n::Int,\n    i::Int,\n    bus_arcs,\n    bus_arcs_dc,\n    inj_p_expr,\n    inj_q_expr,\n)\n    p = PM.var(pm, n, :p)\n    q = PM.var(pm, n, :q)\n    p_dc = PM.var(pm, n, :p_dc)\n    q_dc = PM.var(pm, n, :q_dc)\n\n    PM.con(pm, n, :power_balance_p)[i] = JuMP.@constraint(\n        pm.model,\n        sum(p[a] for a in bus_arcs) + sum(p_dc[a_dc] for a_dc in bus_arcs_dc) == inj_p_expr\n    )\n    PM.con(pm, n, :power_balance_q)[i] = JuMP.@constraint(\n        pm.model,\n        sum(q[a] for a in bus_arcs) + sum(q_dc[a_dc] for a_dc in bus_arcs_dc) == inj_q_expr\n    )\n\n    return\nend\n\n#=\n# VI Methdos not supported currently\nfunction constraint_current_balance_ni_expr(\n    pm::PM.AbstractPowerModel,\n    i::Int;\n    nw::Int = pm.cnw,\n)\n    if !haskey(PM.con(pm, nw), :kcl_cr)\n        PM.con(pm, nw)[:kcl_cr] = Dict{Int, JuMP.ConstraintRef}()\n    end\n    if !haskey(PM.con(pm, nw), :kcl_ci)\n        PM.con(pm, nw)[:kcl_ci] = Dict{Int, JuMP.ConstraintRef}()\n    end\n\n    bus_arcs = PM.ref(pm, nw, :bus_arcs, i)\n    bus_arcs_dc = PM.ref(pm, nw, :bus_arcs_dc, i)\n\n    inj_p_expr = PM.ref(pm, nw, :bus, i, \"inj_p\")\n    inj_q_expr = PM.ref(pm, nw, :bus, i, \"inj_q\")\n\n    constraint_current_balance_ni_expr(\n        pm,\n        nw,\n        i,\n        bus_arcs,\n        bus_arcs_dc,\n        inj_p_expr,\n        inj_q_expr,\n    )\n\n    return\nend\n\nfunction constraint_current_balance_ni_expr(\n    pm::PM.AbstractPowerModel,\n    n::Int,\n    i::Int,\n    bus_arcs,\n    bus_arcs_dc,\n    inj_p_expr,\n    inj_q_expr,\n)\n    p = PM.var(pm, n, :p)\n    q = PM.var(pm, n, :q)\n    p_dc = PM.var(pm, n, :p_dc)\n    q_dc = PM.var(pm, n, :q_dc)\n\n    PM.con(pm, n, :power_balance_p)[i] = JuMP.@constraint(\n        pm.model,\n        sum(p[a] for a in bus_arcs) + sum(p_dc[a_dc] for a_dc in bus_arcs_dc) == inj_p_expr\n    )\n    PM.con(pm, n, :power_balance_q)[i] = JuMP.@constraint(\n        pm.model,\n        sum(q[a] for a in bus_arcs) + sum(q_dc[a_dc] for a_dc in bus_arcs_dc) == inj_q_expr\n    )\n\n    return\nend\n=#\n\n\"\"\"\nactive power only models ignore reactive power variables\n\"\"\"\nfunction variable_reactive_net_injection(pm::PM.AbstractActivePowerModel; kwargs...)\n    return\nend\n\nfunction constraint_power_balance_ni_expr(\n    pm::PM.AbstractActivePowerModel,\n    n::Int,\n    i::Int,\n    bus_arcs,\n    bus_arcs_dc,\n    inj_p_expr,\n    _,\n)\n    p = PM.var(pm, n, :p)\n    p_dc = PM.var(pm, n, :p_dc)\n\n    PM.con(pm, n, :power_balance_p)[i] = JuMP.@constraint(\n        pm.model,\n        sum(p[a] for a in bus_arcs) + sum(p_dc[a_dc] for a_dc in bus_arcs_dc) == inj_p_expr\n    )\n\n    return\nend\n\nfunction powermodels_network!(\n    container::OptimizationContainer,\n    system_formulation::Type{S},\n    sys::PSY.System,\n    template::ProblemTemplate,\n    instantiate_model,\n) where {S <: PM.AbstractPowerModel}\n    time_steps = get_time_steps(container)\n    pm_data, PM_map = pass_to_pm(sys, template, time_steps[end])\n\n    network_model = get_network_model(template)\n    network_reduction = get_network_reduction(network_model)\n    if isempty(network_reduction)\n        ac_bus_numbers =\n            PSY.get_number.(get_available_components(network_model, PSY.ACBus, sys))\n    else\n        bus_reduction_map = PNM.get_bus_reduction_map(network_reduction)\n        ac_bus_numbers = collect(keys(bus_reduction_map))\n    end\n\n    for t in time_steps, bus_no in ac_bus_numbers\n        pm_data[\"nw\"][\"$(t)\"][\"bus\"][\"$bus_no\"][\"inj_p\"] =\n            container.expressions[ExpressionKey(ActivePowerBalance, PSY.ACBus)][\n                bus_no,\n                t,\n            ]\n        pm_data[\"nw\"][\"$(t)\"][\"bus\"][\"$bus_no\"][\"inj_q\"] =\n            container.expressions[ExpressionKey(ReactivePowerBalance, PSY.ACBus)][\n                bus_no,\n                t,\n            ]\n    end\n\n    container.pm =\n        instantiate_model(pm_data, system_formulation; jump_model = container.JuMPmodel)\n    container.pm.ext[:PMmap] = PM_map\n\n    return\nend\n\nfunction powermodels_network!(\n    container::OptimizationContainer,\n    system_formulation::Type{S},\n    sys::PSY.System,\n    template::ProblemTemplate,\n    instantiate_model,\n) where {S <: PM.AbstractActivePowerModel}\n    time_steps = get_time_steps(container)\n    pm_data, PM_map = pass_to_pm(sys, template, time_steps[end])\n\n    network_model = get_network_model(template)\n    network_reduction = get_network_reduction(network_model)\n    if isempty(network_reduction)\n        ac_bus_numbers =\n            PSY.get_number.(get_available_components(network_model, PSY.ACBus, sys))\n    else\n        bus_reduction_map = PNM.get_bus_reduction_map(network_reduction)\n        ac_bus_numbers = collect(keys(bus_reduction_map))\n    end\n\n    for t in time_steps, bus_no in ac_bus_numbers\n        pm_data[\"nw\"][\"$(t)\"][\"bus\"][\"$bus_no\"][\"inj_p\"] =\n            container.expressions[ExpressionKey(ActivePowerBalance, PSY.ACBus)][\n                bus_no,\n                t,\n            ]\n        # pm_data[\"nw\"][\"$(t)\"][\"bus\"][\"$(bus.number)\"][\"inj_q\"] = 0.0\n    end\n\n    container.pm =\n        instantiate_model(\n            pm_data,\n            system_formulation;\n            jump_model = get_jump_model(container),\n        )\n    container.pm.ext[:PMmap] = PM_map\n\n    return\nend\n\n#### PM accessor functions ########\n\nfunction PMvarmap(::Type{S}) where {S <: PM.AbstractDCPModel}\n    pm_variable_map = Dict{Type, Dict{Symbol, Union{VariableType, NamedTuple}}}()\n\n    pm_variable_map[PSY.ACBus] = Dict(:va => VoltageAngle())\n    pm_variable_map[PSY.ACTransmission] =\n        Dict(:p => (from_to = FlowActivePowerVariable(), to_from = nothing))\n    pm_variable_map[PSY.TwoTerminalHVDC] =\n        Dict(:p_dc => (from_to = FlowActivePowerVariable(), to_from = nothing))\n\n    return pm_variable_map\nend\n\nfunction PMvarmap(::Type{S}) where {S <: PM.AbstractActivePowerModel}\n    pm_variable_map = Dict{Type, Dict{Symbol, Union{VariableType, NamedTuple}}}()\n\n    pm_variable_map[PSY.ACBus] = Dict(:va => VoltageAngle())\n    pm_variable_map[PSY.ACTransmission] = Dict(:p => FlowActivePowerFromToVariable())\n    pm_variable_map[PSY.TwoTerminalHVDC] = Dict(\n        :p_dc => (\n            from_to = FlowActivePowerFromToVariable(),\n            to_from = FlowActivePowerToFromVariable(),\n        ),\n    )\n\n    return pm_variable_map\nend\n\nfunction PMvarmap(::Type{S}) where {S <: PM.AbstractPowerModel}\n    pm_variable_map = Dict{Type, Dict{Symbol, Union{VariableType, NamedTuple}}}()\n\n    pm_variable_map[PSY.ACBus] = Dict(:va => VoltageAngle(), :vm => VoltageMagnitude())\n    pm_variable_map[PSY.ACTransmission] = Dict(\n        :p => (\n            from_to = FlowActivePowerFromToVariable(),\n            to_from = FlowActivePowerToFromVariable(),\n        ),\n        :q => (\n            from_to = FlowReactivePowerFromToVariable(),\n            to_from = FlowReactivePowerToFromVariable(),\n        ),\n    )\n    pm_variable_map[PSY.TwoTerminalHVDC] = Dict(\n        :p_dc => (from_to = FlowActivePowerVariable(), to_from = nothing),\n        :q_dc => (\n            from_to = FlowReactivePowerFromToVariable(),\n            to_from = FlowReactivePowerToFromVariable(),\n        ),\n    )\n\n    return pm_variable_map\nend\n\nfunction PMconmap(::Type{S}) where {S <: PM.AbstractActivePowerModel}\n    pm_constraint_map = Dict{Type, Dict{Symbol, <:ConstraintType}}()\n\n    pm_constraint_map[PSY.ACBus] = Dict(:power_balance_p => NodalBalanceActiveConstraint())\n    return pm_constraint_map\nend\n\nfunction PMconmap(::Type{S}) where {S <: PM.AbstractPowerModel}\n    pm_constraint_map = Dict{Type, Dict{Symbol, ConstraintType}}()\n\n    pm_constraint_map[PSY.ACBus] = Dict(\n        :power_balance_p => NodalBalanceActiveConstraint(),\n        :power_balance_q => NodalBalanceReactiveConstraint(),\n    )\n    return pm_constraint_map\nend\n\nfunction PMexprmap(::Type{S}) where {S <: PM.AbstractPowerModel}\n    pm_expr_map = Dict{\n        Type,\n        NamedTuple{\n            (:pm_expr, :psi_con),\n            Tuple{Dict{Symbol, Union{VariableType, NamedTuple}}, Symbol},\n        },\n    }()\n\n    return pm_expr_map\nend\n\nfunction add_pm_variable_refs!(\n    container::OptimizationContainer,\n    system_formulation::Type{S},\n    ::PSY.System,\n    model::NetworkModel,\n) where {S <: PM.AbstractPowerModel}\n    time_steps = get_time_steps(container)\n    bus_dict = container.pm.ext[:PMmap].bus\n    ACbranch_dict = container.pm.ext[:PMmap].arcs\n    ACbranch_types = PNM.get_ac_transmission_types(model.network_reduction)\n    DCbranch_dict = container.pm.ext[:PMmap].arcs_dc\n    DCbranch_types = Set(typeof.(values(DCbranch_dict)))\n\n    pm_variable_types = keys(PM.var(container.pm, 1))\n\n    pm_variable_map = PMvarmap(system_formulation)\n    bus_names = [PSY.get_name(b) for b in values(bus_dict)]\n    for (pm_v, ps_v) in pm_variable_map[PSY.ACBus]\n        if pm_v in pm_variable_types\n            var_container =\n                add_variable_container!(container, ps_v, PSY.ACBus, bus_names, time_steps)\n            for t in time_steps, (pm_bus, bus) in bus_dict\n                name = PSY.get_name(bus)\n                var_container[name, t] = PM.var(container.pm, t, pm_v)[pm_bus] # pm_vars[pm_v][pm_bus]\n            end\n        end\n    end\n\n    add_pm_variable_refs!(\n        container,\n        model,\n        PSY.ACTransmission,\n        ACbranch_types,\n        ACbranch_dict,\n        pm_variable_map,\n        pm_variable_types,\n        time_steps,\n    )\n    add_pm_variable_refs!(\n        container,\n        model,\n        PSY.TwoTerminalHVDC,\n        DCbranch_types,\n        DCbranch_dict,\n        pm_variable_map,\n        pm_variable_types,\n        time_steps,\n    )\n    return\nend\n\nfunction add_pm_variable_refs!(\n    container::OptimizationContainer,\n    model::NetworkModel,\n    d_class::Type{PSY.ACTransmission},\n    device_types::Set,\n    pm_map::Dict,\n    pm_variable_map::Dict,\n    pm_variable_types::Base.KeySet,\n    time_steps::UnitRange{Int},\n)\n    net_reduction_data = get_network_reduction(model)\n    reduced_branch_tracker = get_reduced_branch_tracker(model)\n    for d_type in device_types, (pm_v, ps_v) in pm_variable_map[d_class]\n        if pm_v in pm_variable_types\n            for direction in fieldnames(typeof(ps_v))\n                var_type = getfield(ps_v, direction)\n                var_type === nothing && continue\n                branch_names =\n                    get_branch_argument_variable_axis(net_reduction_data, d_type)\n                var_container = add_variable_container!(\n                    container,\n                    var_type,\n                    d_type,\n                    branch_names,\n                    time_steps,\n                )\n                for (name, (arc_tuple, reduction)) in\n                    PNM.get_name_to_arc_map(net_reduction_data, d_type)\n                    has_entry, tracker_container = search_for_reduced_branch_argument!(\n                        reduced_branch_tracker,\n                        arc_tuple,\n                        typeof(var_type), # TODO: Make the mapping not rely on instances but types\n                    )\n                    if has_entry\n                        @assert !isempty(tracker_container) name arc_tuple reduction\n                    end\n                    for t in time_steps\n                        if !has_entry\n                            pm_d = pm_map[arc_tuple]\n                            var = PM.var(container.pm, t, pm_v, getfield(pm_d, direction))\n                            tracker_container[t] = var\n                        end\n                        var_container[name, t] = tracker_container[t]\n                    end\n                end\n            end\n        end\n    end\n    return\nend\n\nfunction add_pm_variable_refs!(\n    container::OptimizationContainer,\n    ::NetworkModel,\n    d_class::Type{PSY.TwoTerminalHVDC},\n    device_types::Set,\n    pm_map::Dict,\n    pm_variable_map::Dict,\n    pm_variable_types::Base.KeySet,\n    time_steps::UnitRange{Int},\n)\n    for d_type in Set(device_types)\n        devices = [d for d in pm_map if typeof(d[2]) == d_type]\n        for (pm_v, ps_v) in pm_variable_map[d_class]\n            if pm_v in pm_variable_types\n                for dir in fieldnames(typeof(ps_v))\n                    var_type = getfield(ps_v, dir)\n                    var_type === nothing && continue\n                    var_container = add_variable_container!(\n                        container,\n                        var_type,\n                        d_type,\n                        [PSY.get_name(d[2]) for d in devices],\n                        time_steps,\n                    )\n                    for t in time_steps, (pm_d, d) in devices\n                        var = PM.var(container.pm, t, pm_v, getfield(pm_d, dir))\n                        var_container[PSY.get_name(d), t] = var\n                    end\n                end\n            end\n        end\n    end\n    return\nend\n\nfunction add_pm_constraint_refs!(\n    container::OptimizationContainer,\n    system_formulation::Type{S},\n    ::PSY.System,\n) where {S <: PM.AbstractPowerModel}\n    time_steps = get_time_steps(container)\n    bus_dict = container.pm.ext[:PMmap].bus\n\n    pm_constraint_names =\n        [k for k in keys(PM.con(container.pm, 1)) if !isempty(PM.con(container.pm, 1, k))]\n\n    pm_constraint_map = PMconmap(system_formulation)\n    for (pm_v, ps_v) in pm_constraint_map[PSY.ACBus]\n        if pm_v in pm_constraint_names\n            cons_container = add_constraints_container!(\n                container,\n                ps_v,\n                PSY.ACBus,\n                [PSY.get_name(b) for b in values(bus_dict)],\n                time_steps,\n            )\n            for t in time_steps, (pm_bus, bus) in bus_dict\n                name = PSY.get_name(bus)\n                cons_container[name, t] = PM.con(container.pm, t, pm_v)[pm_bus]\n            end\n        end\n    end\n    return\nend\n"
  },
  {
    "path": "src/operation/decision_model.jl",
    "content": "mutable struct DecisionModel{M <: DecisionProblem} <: OperationModel\n    name::Symbol\n    template::AbstractProblemTemplate\n    sys::PSY.System\n    internal::Union{Nothing, ISOPT.ModelInternal}\n    simulation_info::SimulationInfo\n    store::DecisionModelStore\n    ext::Dict{String, Any}\nend\n\n\"\"\"\n    DecisionModel{M}(\n        template::AbstractProblemTemplate,\n        sys::PSY.System,\n        jump_model::Union{Nothing, JuMP.Model}=nothing;\n        kwargs...) where {M<:DecisionProblem}\n\nBuild the optimization problem of type M with the specific system and template.\n\n# Arguments\n\n  - `::Type{M} where M<:DecisionProblem`: The abstract operation model type\n  - `template::AbstractProblemTemplate`: The model reference made up of transmission, devices, branches, and services.\n  - `sys::PSY.System`: the system created using Power Systems\n  - `jump_model::Union{Nothing, JuMP.Model}`: Enables passing a custom JuMP model. Use with care\n  - `name = nothing`: name of model, string or symbol; defaults to the type of template converted to a symbol.\n  - `optimizer::Union{Nothing,MOI.OptimizerWithAttributes} = nothing` : The optimizer does\n    not get serialized. Callers should pass whatever they passed to the original problem.\n  - `horizon::Dates.Period = UNSET_HORIZON`: Manually specify the length of the forecast Horizon\n  - `resolution::Dates.Period = UNSET_RESOLUTION`: Manually specify the model's resolution\n  - `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.\n  - `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\n  - `check_components::Bool = true`: True to check the components valid fields when building\n  - `initialize_model::Bool = true`: Option to decide to initialize the model or not.\n  - `initialization_file::String = \"\"`: This allows to pass pre-existing initialization values to avoid the solution of an optimization problem to find feasible initial conditions.\n  - `deserialize_initial_conditions::Bool = false`: Option to deserialize conditions\n  - `export_pwl_vars::Bool = false`: True to export all the pwl intermediate variables. It can slow down significantly the build and solve time.\n  - `allow_fails::Bool = false`: True to allow the simulation to continue even if the optimization step fails. Use with care.\n  - `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()\n  - `detailed_optimizer_stats::Bool = false`: True to save detailed optimizer stats log.\n  - `calculate_conflict::Bool = false`: True to use solver to calculate conflicts for infeasible problems. Only specific solvers are able to calculate conflicts.\n  - `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).\n  - `store_variable_names::Bool = false`: to store variable names in optimization model. Decreases the build times.\n  - `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.\n  - `initial_time::Dates.DateTime = UNSET_INI_TIME`: Initial Time for the model solve.\n  - `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.\n\n# Example\n\n```julia\ntemplate = ProblemTemplate(CopperPlatePowerModel, devices, branches, services)\nOpModel = DecisionModel(MockOperationProblem, template, system)\n```\n\"\"\"\nfunction DecisionModel{M}(\n    template::AbstractProblemTemplate,\n    sys::PSY.System,\n    settings::Settings,\n    jump_model::Union{Nothing, JuMP.Model} = nothing;\n    name = nothing,\n) where {M <: DecisionProblem}\n    if name === nothing\n        name = nameof(M)\n    elseif name isa String\n        name = Symbol(name)\n    end\n    auto_transform_time_series!(sys, settings)\n    ts_type = get_deterministic_time_series_type(sys)\n    internal = ISOPT.ModelInternal(\n        OptimizationContainer(sys, settings, jump_model, ts_type),\n    )\n\n    template_ = deepcopy(template)\n    finalize_template!(template_, sys)\n    model = DecisionModel{M}(\n        name,\n        template_,\n        sys,\n        internal,\n        SimulationInfo(),\n        DecisionModelStore(),\n        Dict{String, Any}(),\n    )\n    validate_time_series!(model)\n    return model\nend\n\nfunction DecisionModel{M}(\n    template::AbstractProblemTemplate,\n    sys::PSY.System,\n    jump_model::Union{Nothing, JuMP.Model} = nothing;\n    name = nothing,\n    optimizer = nothing,\n    horizon = UNSET_HORIZON,\n    resolution = UNSET_RESOLUTION,\n    interval = UNSET_INTERVAL,\n    warm_start = true,\n    check_components = true,\n    initialize_model = true,\n    initialization_file = \"\",\n    deserialize_initial_conditions = false,\n    export_pwl_vars = false,\n    allow_fails = false,\n    optimizer_solve_log_print = false,\n    detailed_optimizer_stats = false,\n    calculate_conflict = false,\n    direct_mode_optimizer = false,\n    store_variable_names = false,\n    rebuild_model = false,\n    export_optimization_model = false,\n    check_numerical_bounds = true,\n    initial_time = UNSET_INI_TIME,\n    time_series_cache_size::Int = IS.TIME_SERIES_CACHE_SIZE_BYTES,\n) where {M <: DecisionProblem}\n    settings = Settings(\n        sys;\n        horizon = horizon,\n        resolution = resolution,\n        interval = interval,\n        initial_time = initial_time,\n        optimizer = optimizer,\n        time_series_cache_size = time_series_cache_size,\n        warm_start = warm_start,\n        check_components = check_components,\n        initialize_model = initialize_model,\n        initialization_file = initialization_file,\n        deserialize_initial_conditions = deserialize_initial_conditions,\n        export_pwl_vars = export_pwl_vars,\n        allow_fails = allow_fails,\n        calculate_conflict = calculate_conflict,\n        optimizer_solve_log_print = optimizer_solve_log_print,\n        detailed_optimizer_stats = detailed_optimizer_stats,\n        direct_mode_optimizer = direct_mode_optimizer,\n        check_numerical_bounds = check_numerical_bounds,\n        store_variable_names = store_variable_names,\n        rebuild_model = rebuild_model,\n        export_optimization_model = export_optimization_model,\n    )\n    return DecisionModel{M}(template, sys, settings, jump_model; name = name)\nend\n\n\"\"\"\nBuild the optimization problem of type M with the specific system and template\n\n# Arguments\n\n  - `::Type{M} where M<:DecisionProblem`: The abstract operation model type\n  - `template::AbstractProblemTemplate`: The model reference made up of transmission, devices, branches, and services.\n  - `sys::PSY.System`: the system created using Power Systems\n  - `jump_model::Union{Nothing, JuMP.Model}` = nothing: Enables passing a custom JuMP model. Use with care.\n\n# Example\n\n```julia\ntemplate = ProblemTemplate(CopperPlatePowerModel, devices, branches, services)\nproblem = DecisionModel(MyOpProblemType, template, system, optimizer)\n```\n\"\"\"\nfunction DecisionModel(\n    ::Type{M},\n    template::AbstractProblemTemplate,\n    sys::PSY.System,\n    jump_model::Union{Nothing, JuMP.Model} = nothing;\n    kwargs...,\n) where {M <: DecisionProblem}\n    return DecisionModel{M}(template, sys, jump_model; kwargs...)\nend\n\nfunction DecisionModel(\n    template::AbstractProblemTemplate,\n    sys::PSY.System,\n    jump_model::Union{Nothing, JuMP.Model} = nothing;\n    kwargs...,\n)\n    return DecisionModel{GenericOpProblem}(template, sys, jump_model; kwargs...)\nend\n\n\"\"\"\nBuilds an empty decision model. This constructor is used for the implementation of custom\ndecision models that do not require a template.\n\n# Arguments\n\n  - `::Type{M} where M<:DecisionProblem`: The abstract operation model type\n  - `sys::PSY.System`: the system created using Power Systems\n  - `jump_model::Union{Nothing, JuMP.Model}` = nothing: Enables passing a custom JuMP model. Use with care.\n\n# Example\n\n```julia\nproblem = DecisionModel(system, optimizer)\n```\n\"\"\"\nfunction DecisionModel{M}(\n    sys::PSY.System,\n    jump_model::Union{Nothing, JuMP.Model} = nothing;\n    kwargs...,\n) where {M <: DecisionProblem}\n    return DecisionModel{M}(ProblemTemplate(), sys, jump_model; kwargs...)\nend\n\nfunction DecisionModel{M}(\n    sys::PSY.System,\n    jump_model::Union{Nothing, JuMP.Model} = nothing;\n    kwargs...,\n) where {M <: DefaultDecisionProblem}\n    IS.ArgumentError(\n        \"DefaultDecisionProblem subtypes require a template. Use DecisionModel subtyping instead.\",\n    )\nend\n\nget_problem_type(::DecisionModel{M}) where {M <: DecisionProblem} = M\n\nfunction validate_template(::DecisionModel{M}) where {M <: DecisionProblem}\n    error(\"validate_template is not implemented for DecisionModel{$M}\")\nend\n\nfunction validate_template(model::DecisionModel{<:DefaultDecisionProblem})\n    validate_template_impl!(model)\n    return\nend\n\n# Probably could be more efficient by storing the info in the internal\nfunction get_current_time(model::DecisionModel)\n    execution_count = get_execution_count(model)\n    initial_time = get_initial_time(model)\n    interval = get_interval(model)\n    return initial_time + interval * execution_count\nend\n\nfunction init_model_store_params!(model::DecisionModel)\n    num_executions = get_executions(model)\n    horizon = get_horizon(model)\n    system = get_system(model)\n    settings = get_settings(model)\n    model_interval = get_interval(settings)\n    if model_interval != UNSET_INTERVAL\n        interval = model_interval\n    else\n        interval = PSY.get_forecast_interval(system)\n    end\n    resolution = get_resolution(model)\n    base_power = PSY.get_base_power(system)\n    sys_uuid = IS.get_uuid(system)\n    store_params = ModelStoreParams(\n        num_executions,\n        horizon,\n        iszero(interval) ? resolution : interval,\n        resolution,\n        base_power,\n        sys_uuid,\n        get_metadata(get_optimization_container(model)),\n    )\n    ISOPT.set_store_params!(get_internal(model), store_params)\n    return\nend\n\nfunction validate_time_series!(model::DecisionModel{<:DefaultDecisionProblem})\n    sys = get_system(model)\n    settings = get_settings(model)\n    available_resolutions = PSY.get_time_series_resolutions(sys)\n\n    if get_resolution(settings) == UNSET_RESOLUTION && length(available_resolutions) != 1\n        throw(\n            IS.ConflictingInputsError(\n                \"Data contains multiple resolutions, the resolution keyword argument must be added to the Model. Time Series Resolutions: $(available_resolutions)\",\n            ),\n        )\n    elseif get_resolution(settings) != UNSET_RESOLUTION && length(available_resolutions) > 1\n        if get_resolution(settings) ∉ available_resolutions\n            throw(\n                IS.ConflictingInputsError(\n                    \"Resolution $(get_resolution(settings)) is not available in the system data. Time Series Resolutions: $(available_resolutions)\",\n                ),\n            )\n        end\n    else\n        set_resolution!(settings, first(available_resolutions))\n    end\n\n    model_interval = get_interval(settings)\n    available_intervals = get_forecast_intervals(sys)\n    if model_interval == UNSET_INTERVAL && length(available_intervals) > 1\n        throw(\n            IS.ConflictingInputsError(\n                \"The system contains multiple forecast intervals $(available_intervals). \" *\n                \"The `interval` keyword argument must be provided to the DecisionModel constructor \" *\n                \"to select which interval to use.\",\n            ),\n        )\n    elseif model_interval != UNSET_INTERVAL && !isempty(available_intervals)\n        if model_interval ∉ available_intervals\n            throw(\n                IS.ConflictingInputsError(\n                    \"Interval $(Dates.canonicalize(model_interval)) is not available in the system data. \" *\n                    \"Available forecast intervals: $(available_intervals)\",\n                ),\n            )\n        end\n    end\n    interval_kwarg =\n        model_interval == UNSET_INTERVAL ? (;) : (; interval = model_interval)\n    if get_horizon(settings) == UNSET_HORIZON\n        set_horizon!(settings, PSY.get_forecast_horizon(sys; interval_kwarg...))\n    end\n\n    counts = PSY.get_time_series_counts(sys)\n    if counts.forecast_count < 1\n        error(\n            \"The system does not contain forecast data. A DecisionModel can't be built.\",\n        )\n    end\n    return\nend\n\nfunction build_pre_step!(model::DecisionModel{<:DecisionProblem})\n    TimerOutputs.@timeit BUILD_PROBLEMS_TIMER \"Build pre-step\" begin\n        validate_template(model)\n        if !isempty(model)\n            @info \"OptimizationProblem status not ModelBuildStatus.EMPTY. Resetting\"\n\n            reset!(model)\n        end\n        # Initial time are set here because the information is specified in the\n        # Simulation Sequence object and not at the problem creation.\n        @info \"Initializing Optimization Container For a DecisionModel\"\n        init_optimization_container!(\n            get_optimization_container(model),\n            get_network_model(get_template(model)),\n            get_system(model),\n        )\n        @info \"Initializing ModelStoreParams\"\n        init_model_store_params!(model)\n        set_status!(model, ModelBuildStatus.IN_PROGRESS)\n    end\n    return\nend\n\nfunction build_impl!(model::DecisionModel{<:DecisionProblem})\n    build_pre_step!(model)\n    @info \"Instantiating Network Model\"\n    instantiate_network_model!(model)\n    handle_initial_conditions!(model)\n    build_model!(model)\n    serialize_metadata!(get_optimization_container(model), get_output_dir(model))\n    log_values(get_settings(model))\n    return\nend\n\nget_horizon(model::DecisionModel) = get_horizon(get_settings(model))\n\n\"\"\"\nBuild the Decision Model based on the specified DecisionProblem.\n\n# Arguments\n\n  - `model::DecisionModel{<:DecisionProblem}`: DecisionModel object\n  - `output_dir::String`: Output directory for results\n  - `recorders::Vector{Symbol} = []`: recorder names to register\n  - `console_level = Logging.Error`:\n  - `file_level = Logging.Info`:\n  - `disable_timer_outputs = false` : Enable/Disable timing outputs\n\"\"\"\nfunction build!(\n    model::DecisionModel{<:DecisionProblem};\n    output_dir::String,\n    recorders = [],\n    console_level = Logging.Error,\n    file_level = Logging.Info,\n    disable_timer_outputs = false,\n)\n    mkpath(output_dir)\n    set_output_dir!(model, output_dir)\n    set_console_level!(model, console_level)\n    set_file_level!(model, file_level)\n    TimerOutputs.reset_timer!(BUILD_PROBLEMS_TIMER)\n    disable_timer_outputs && TimerOutputs.disable_timer!(BUILD_PROBLEMS_TIMER)\n    file_mode = \"w\"\n    add_recorders!(model, recorders)\n    register_recorders!(model, file_mode)\n    logger = IS.configure_logging(get_internal(model), PROBLEM_LOG_FILENAME, file_mode)\n    try\n        Logging.with_logger(logger) do\n            try\n                TimerOutputs.@timeit BUILD_PROBLEMS_TIMER \"Problem $(get_name(model))\" begin\n                    build_impl!(model)\n                end\n                set_status!(model, ModelBuildStatus.BUILT)\n                @info \"\\n$(BUILD_PROBLEMS_TIMER)\\n\"\n            catch e\n                set_status!(model, ModelBuildStatus.FAILED)\n                bt = catch_backtrace()\n                @error \"DecisionModel Build Failed\" exception = e, bt\n            end\n        end\n    finally\n        unregister_recorders!(model)\n        close(logger)\n    end\n    return get_status(model)\nend\n\n\"\"\"\nDefault implementation of build method for Operational Problems for models conforming with\nDecisionProblem specification. Overload this function to implement a custom build method\n\"\"\"\nfunction build_model!(model::DecisionModel{<:DefaultDecisionProblem})\n    build_impl!(get_optimization_container(model), get_template(model), get_system(model))\n    return\nend\n\nfunction reset!(model::DecisionModel{<:DefaultDecisionProblem})\n    was_built_for_recurrent_solves = built_for_recurrent_solves(model)\n    if was_built_for_recurrent_solves\n        set_execution_count!(model, 0)\n    end\n    sys = get_system(model)\n    ts_type = get_deterministic_time_series_type(sys)\n    ISOPT.set_container!(\n        get_internal(model),\n        OptimizationContainer(\n            get_system(model),\n            get_settings(model),\n            nothing,\n            ts_type,\n        ),\n    )\n    get_optimization_container(model).built_for_recurrent_solves =\n        was_built_for_recurrent_solves\n    internal = get_internal(model)\n    ISOPT.set_initial_conditions_model_container!(internal, nothing)\n    empty_time_series_cache!(model)\n    empty!(get_store(model))\n    set_status!(model, ModelBuildStatus.EMPTY)\n    return\nend\n\n\"\"\"\nDefault solve method for models that conform to the requirements of\nDecisionModel{<: DecisionProblem}.\n\nThis will call `build!` on the model if it is not already built. It will forward all\nkeyword arguments to that function.\n\n# Arguments\n\n  - `model::OperationModel = model`: operation model\n  - `export_problem_results::Bool = false`: If true, export OptimizationProblemResults DataFrames to CSV files. Reduces solution times during simulation.\n  - `console_level = Logging.Error`:\n  - `file_level = Logging.Info`:\n  - `disable_timer_outputs = false` : Enable/Disable timing outputs\n  - `export_optimization_problem::Bool = true`: If true, serialize the model to a file to allow re-execution later.\n\n# Examples\n\n```julia\nresults = solve!(OpModel)\nresults = solve!(OpModel, export_problem_results = true)\n```\n\"\"\"\nfunction solve!(\n    model::DecisionModel{<:DecisionProblem};\n    export_problem_results = false,\n    console_level = Logging.Error,\n    file_level = Logging.Info,\n    disable_timer_outputs = false,\n    export_optimization_problem = true,\n    kwargs...,\n)\n    build_if_not_already_built!(\n        model;\n        console_level = console_level,\n        file_level = file_level,\n        disable_timer_outputs = disable_timer_outputs,\n        kwargs...,\n    )\n    set_console_level!(model, console_level)\n    set_file_level!(model, file_level)\n    TimerOutputs.reset_timer!(RUN_OPERATION_MODEL_TIMER)\n    disable_timer_outputs && TimerOutputs.disable_timer!(RUN_OPERATION_MODEL_TIMER)\n    file_mode = \"a\"\n    register_recorders!(model, file_mode)\n    logger = ISOPT.configure_logging(\n        get_internal(model),\n        PROBLEM_LOG_FILENAME,\n        file_mode,\n    )\n    optimizer = get(kwargs, :optimizer, nothing)\n    try\n        Logging.with_logger(logger) do\n            try\n                initialize_storage!(\n                    get_store(model),\n                    get_optimization_container(model),\n                    get_store_params(model),\n                )\n                TimerOutputs.@timeit RUN_OPERATION_MODEL_TIMER \"Solve\" begin\n                    _pre_solve_model_checks(model, optimizer)\n                    solve_impl!(model)\n                    current_time = get_initial_time(model)\n                    write_results!(get_store(model), model, current_time, current_time)\n                    write_optimizer_stats!(\n                        get_store(model),\n                        get_optimizer_stats(model),\n                        current_time,\n                    )\n                end\n                if export_optimization_problem\n                    TimerOutputs.@timeit RUN_OPERATION_MODEL_TIMER \"Serialize\" begin\n                        serialize_optimization_model(model)\n                    end\n                end\n                TimerOutputs.@timeit RUN_OPERATION_MODEL_TIMER \"Results processing\" begin\n                    # TODO: This could be more complicated than it needs to be\n                    results = OptimizationProblemResults(model)\n                    serialize_results(results, get_output_dir(model))\n                    export_problem_results && export_results(results)\n                end\n                @info \"\\n$(RUN_OPERATION_MODEL_TIMER)\\n\"\n            catch e\n                @error \"Decision Problem solve failed\" exception = (e, catch_backtrace())\n                set_run_status!(model, RunStatus.FAILED)\n            end\n        end\n    finally\n        unregister_recorders!(model)\n        close(logger)\n    end\n\n    return get_run_status(model)\nend\n\n\"\"\"\nDefault solve method for a DecisionModel used inside of a Simulation. Solves problems that conform to the requirements of DecisionModel{<: DecisionProblem}\n\n# Arguments\n\n  - `step::Int`: Simulation Step\n  - `model::OperationModel`: operation model\n  - `start_time::Dates.DateTime`: Initial Time of the simulation step in Simulation time.\n  - `store::SimulationStore`: Simulation output store\n\n# Accepted Key Words\n\n  - `exports`: realtime export of output. Use wisely, it can have negative impacts in the simulation times\n\"\"\"\nfunction solve!(\n    step::Int,\n    model::DecisionModel{<:DecisionProblem},\n    start_time::Dates.DateTime,\n    store::SimulationStore;\n    exports = nothing,\n)\n    # Note, we don't call solve!(decision_model) here because the solve call includes a lot of\n    # other logic used when solving the models separate from a simulation\n    solve_impl!(model)\n    IS.@assert_op get_current_time(model) == start_time\n    if get_run_status(model) == RunStatus.SUCCESSFULLY_FINALIZED\n        write_results!(store, model, start_time, start_time; exports = exports)\n        write_optimizer_stats!(store, model, start_time)\n        advance_execution_count!(model)\n    end\n    return get_run_status(model)\nend\n\nfunction handle_initial_conditions!(model::DecisionModel{<:DecisionProblem})\n    TimerOutputs.@timeit BUILD_PROBLEMS_TIMER \"Model Initialization\" begin\n        if isempty(get_template(model))\n            return\n        end\n        settings = get_settings(model)\n        initialize_model = get_initialize_model(settings)\n        deserialize_initial_conditions = get_deserialize_initial_conditions(settings)\n        serialized_initial_conditions_file = get_initial_conditions_file(model)\n        custom_init_file = get_initialization_file(settings)\n\n        if !initialize_model && deserialize_initial_conditions\n            throw(\n                IS.ConflictingInputsError(\n                    \"!initialize_model && deserialize_initial_conditions\",\n                ),\n            )\n        elseif !initialize_model && !isempty(custom_init_file)\n            throw(IS.ConflictingInputsError(\"!initialize_model && initialization_file\"))\n        end\n\n        if !initialize_model\n            @info \"Skip build of initial conditions\"\n            return\n        end\n\n        if !isempty(custom_init_file)\n            if !isfile(custom_init_file)\n                error(\"initialization_file = $custom_init_file does not exist\")\n            end\n            if abspath(custom_init_file) != abspath(serialized_initial_conditions_file)\n                cp(custom_init_file, serialized_initial_conditions_file; force = true)\n            end\n        end\n\n        if deserialize_initial_conditions && isfile(serialized_initial_conditions_file)\n            set_initial_conditions_data!(\n                get_optimization_container(model),\n                Serialization.deserialize(serialized_initial_conditions_file),\n            )\n            @info \"Deserialized initial_conditions_data\"\n        else\n            @info \"Make Initial Conditions Model\"\n            build_initial_conditions!(model)\n            initialize!(model)\n        end\n        ISOPT.set_initial_conditions_model_container!(\n            get_internal(model),\n            nothing,\n        )\n    end\n    return\nend\n\nfunction _make_device_cache(\n    filter_function::Function,\n    devices::IS.FlattenIteratorWrapper{T},\n    check_components::Bool,\n    sys::PSY.System,\n) where {T <: PSY.Device}\n    device_cache = sizehint!(Vector{T}(), length(devices))\n    for device in devices\n        if PSY.get_available(device) && filter_function(device)\n            check_components && PSY.check_component(sys, device)\n            push!(device_cache, device)\n        end\n    end\n    return device_cache\nend\n\nfunction _make_device_cache(\n    ::Nothing,\n    devices::IS.FlattenIteratorWrapper{T},\n    check_components::Bool,\n    sys::PSY.System,\n) where {T <: PSY.Device}\n    device_cache = sizehint!(Vector{T}(), length(devices))\n    for device in devices\n        if PSY.get_available(device)\n            check_components && PSY.check_component(sys, device)\n            push!(device_cache, device)\n        end\n    end\n    return device_cache\nend\n\nfunction make_device_cache!(\n    model::DeviceModel{T, <:AbstractDeviceFormulation},\n    system::PSY.System,\n    check_components::Bool,\n) where {T <: PSY.Device}\n    subsystem = get_subsystem(model)\n    !PSY.has_components(system, T) && return false\n    devices = PSY.get_components(T, system; subsystem_name = subsystem)\n    filt_func = get_attribute(model, \"filter_function\")\n    model.device_cache =\n        _make_device_cache(filt_func, devices, check_components, system)\n    return\nend\n"
  },
  {
    "path": "src/operation/decision_model_store.jl",
    "content": "\"\"\"\nStores results data for one DecisionModel\n\"\"\"\nmutable struct DecisionModelStore <: ISOPT.AbstractModelStore\n    # All DenseAxisArrays have axes (column names, row indexes)\n    duals::Dict{ConstraintKey, OrderedDict{Dates.DateTime, DenseAxisArray{Float64}}}\n    parameters::Dict{ParameterKey, OrderedDict{Dates.DateTime, DenseAxisArray{Float64}}}\n    variables::Dict{VariableKey, OrderedDict{Dates.DateTime, DenseAxisArray{Float64}}}\n    aux_variables::Dict{AuxVarKey, OrderedDict{Dates.DateTime, DenseAxisArray{Float64}}}\n    expressions::Dict{ExpressionKey, OrderedDict{Dates.DateTime, DenseAxisArray{Float64}}}\n    optimizer_stats::OrderedDict{Dates.DateTime, OptimizerStats}\nend\n\nfunction DecisionModelStore()\n    return DecisionModelStore(\n        Dict{ConstraintKey, OrderedDict{Dates.DateTime, DenseAxisArray{Float64}}}(),\n        Dict{ParameterKey, OrderedDict{Dates.DateTime, DenseAxisArray{Float64}}}(),\n        Dict{VariableKey, OrderedDict{Dates.DateTime, DenseAxisArray{Float64}}}(),\n        Dict{AuxVarKey, OrderedDict{Dates.DateTime, DenseAxisArray{Float64}}}(),\n        Dict{ExpressionKey, OrderedDict{Dates.DateTime, DenseAxisArray{Float64}}}(),\n        OrderedDict{Dates.DateTime, OptimizerStats}(),\n    )\nend\n\nfunction initialize_storage!(\n    store::DecisionModelStore,\n    container::ISOPT.AbstractOptimizationContainer,\n    params::ModelStoreParams,\n)\n    num_of_executions = get_num_executions(params)\n    if length(get_time_steps(container)) < 1\n        error(\"The time step count in the optimization container is not defined\")\n    end\n    time_steps_count = get_time_steps(container)[end]\n    initial_time = get_initial_time(container)\n    model_interval = get_interval(params)\n    for type in STORE_CONTAINERS\n        field_containers = getfield(container, type)\n        results_container = getfield(store, type)\n        for (key, field_container) in field_containers\n            !should_write_resulting_value(key) && continue\n            @debug \"Adding $(encode_key_as_string(key)) to DecisionModelStore\" _group =\n                LOG_GROUP_MODEL_STORE\n            column_names = get_column_names(container, type, field_container, key)\n            data = OrderedDict{\n                Dates.DateTime,\n                DenseAxisArray{Float64, length(column_names) + 1},\n            }()\n            for timestamp in\n                range(initial_time; step = model_interval, length = num_of_executions)\n                data[timestamp] = fill!(\n                    DenseAxisArray{Float64}(undef, column_names..., 1:time_steps_count),\n                    NaN,\n                )\n            end\n            results_container[key] = data\n        end\n    end\n    return\nend\n\nfunction write_result!(\n    store::DecisionModelStore,\n    name::Symbol,\n    key::OptimizationContainerKey,\n    index::DecisionModelIndexType,\n    update_timestamp::Dates.DateTime,\n    array::DenseAxisArray{T, 2, <:Tuple{Vector{String}, UnitRange}},\n) where {T}\n    container = getfield(store, get_store_container_type(key))\n    container[key][index] = array\n    return\nend\n\nfunction write_result!(\n    store::DecisionModelStore,\n    name::Symbol,\n    key::OptimizationContainerKey,\n    index::DecisionModelIndexType,\n    update_timestamp::Dates.DateTime,\n    array::DenseAxisArray{T, 2, <:Tuple{Vector{Int}, UnitRange}},\n) where {T}\n    columns = get_column_names_from_axis_array(array)\n    container = getfield(store, get_store_container_type(key))\n    container[key][index] = DenseAxisArray(array.data, columns..., 1:size(array, 2))\n    return\nend\n\nfunction write_result!(\n    store::DecisionModelStore,\n    name::Symbol,\n    key::OptimizationContainerKey,\n    index::DecisionModelIndexType,\n    update_timestamp::Dates.DateTime,\n    array::DenseAxisArray{T, 1, <:Tuple{Vector{String}}},\n) where {T}\n    container = getfield(store, get_store_container_type(key))\n    container[key][index] = array\n    return\nend\n\nfunction write_result!(\n    store::DecisionModelStore,\n    name::Symbol,\n    key::OptimizationContainerKey,\n    index::DecisionModelIndexType,\n    update_timestamp::Dates.DateTime,\n    array::DenseAxisArray{T, 1, <:Tuple{UnitRange}},\n) where {T}\n    container = getfield(store, get_store_container_type(key))\n    container[key][index] = array\n    return\nend\n\nfunction write_result!(\n    store::DecisionModelStore,\n    name::Symbol,\n    key::OptimizationContainerKey,\n    index::DecisionModelIndexType,\n    update_timestamp::Dates.DateTime,\n    array::DenseAxisArray{T, 3, <:Tuple{Vector{String}, Vector{String}, UnitRange{Int}}},\n) where {T}\n    container = getfield(store, get_store_container_type(key))\n    container[key][index] = array\n    return\nend\n\nfunction read_results(\n    store::DecisionModelStore,\n    key::OptimizationContainerKey;\n    index::Union{DecisionModelIndexType, Nothing} = nothing,\n)\n    container = getfield(store, get_store_container_type(key))\n    data = container[key]\n    if isnothing(index)\n        @assert_op length(data) == 1\n        index = first(keys(data))\n    end\n\n    # Return a copy because callers may mutate it.\n    return deepcopy(data[index])\nend\n\nfunction write_optimizer_stats!(\n    store::DecisionModelStore,\n    stats::OptimizerStats,\n    index::DecisionModelIndexType,\n)\n    # TODO: This check is incompatible with test calls to psi_checksolve_test\n    # Overwriting should not be allowed in normal operation.\n    # if index in keys(store.optimizer_stats)\n    #     error(\"Bug: Overwriting optimizer stats for index = $index\")\n    # end\n    store.optimizer_stats[index] = stats\n    return\nend\n\nfunction read_optimizer_stats(store::DecisionModelStore)\n    stats = [IS.to_namedtuple(x) for x in values(store.optimizer_stats)]\n    df = DataFrames.DataFrame(stats)\n    DataFrames.insertcols!(df, 1, :DateTime => keys(store.optimizer_stats))\n    return df\nend\n\nfunction get_column_names(store::DecisionModelStore, key::OptimizationContainerKey)\n    container = getfield(store, get_store_container_type(key))\n    return get_column_names_from_axis_array(key, first(values(container[key])))\nend\n"
  },
  {
    "path": "src/operation/emulation_model.jl",
    "content": "\"\"\"\n    EmulationModel{M}(\n        template::AbstractProblemTemplate,\n        sys::PSY.System,\n        jump_model::Union{Nothing, JuMP.Model}=nothing;\n        kwargs...) where {M<:EmulationProblem}\n\nBuild the optimization problem of type M with the specific system and template.\n\n# Arguments\n\n  - `::Type{M} where M<:EmulationProblem`: The abstract Emulation model type\n  - `template::AbstractProblemTemplate`: The model reference made up of transmission, devices, branches, and services.\n  - `sys::PSY.System`: the system created using Power Systems\n  - `jump_model::Union{Nothing, JuMP.Model}`: Enables passing a custom JuMP model. Use with care\n  - `name = nothing`: name of model, string or symbol; defaults to the type of template converted to a symbol.\n  - `optimizer::Union{Nothing,MOI.OptimizerWithAttributes} = nothing` : The optimizer does\n    not get serialized. Callers should pass whatever they passed to the original problem.\n  - `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\n  - `initialize_model::Bool = true`: Option to decide to initialize the model or not.\n  - `initialization_file::String = \"\"`: This allows to pass pre-existing initialization values to avoid the solution of an optimization problem to find feasible initial conditions.\n  - `deserialize_initial_conditions::Bool = false`: Option to deserialize conditions\n  - `export_pwl_vars::Bool = false`: True to export all the pwl intermediate variables. It can slow down significantly the build and solve time.\n  - `allow_fails::Bool = false`: True to allow the simulation to continue even if the optimization step fails. Use with care.\n  - `calculate_conflict::Bool = false`: True to use solver to calculate conflicts for infeasible problems. Only specific solvers are able to calculate conflicts.\n  - `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()\n  - `detailed_optimizer_stats::Bool = false`: True to save detailed optimizer stats log.\n  - `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).\n  - `store_variable_names::Bool = false`: True to store variable names in optimization model.\n  - `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.\n  - `initial_time::Dates.DateTime = UNSET_INI_TIME`: Initial Time for the model solve.\n  - `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.\n\n# Example\n\n```julia\ntemplate = ProblemTemplate(CopperPlatePowerModel, devices, branches, services)\nOpModel = EmulationModel(MockEmulationProblem, template, system)\n```\n\"\"\"\nmutable struct EmulationModel{M <: EmulationProblem} <: OperationModel\n    name::Symbol\n    template::AbstractProblemTemplate\n    sys::PSY.System\n    internal::ISOPT.ModelInternal\n    simulation_info::SimulationInfo\n    store::EmulationModelStore # might be extended to other stores for simulation\n    ext::Dict{String, Any}\n\n    function EmulationModel{M}(\n        template::AbstractProblemTemplate,\n        sys::PSY.System,\n        settings::Settings,\n        jump_model::Union{Nothing, JuMP.Model} = nothing;\n        name = nothing,\n    ) where {M <: EmulationProblem}\n        if name === nothing\n            name = nameof(M)\n        elseif name isa String\n            name = Symbol(name)\n        end\n        finalize_template!(template, sys)\n        internal = ISOPT.ModelInternal(\n            OptimizationContainer(sys, settings, jump_model, PSY.SingleTimeSeries),\n        )\n        new{M}(\n            name,\n            template,\n            sys,\n            internal,\n            SimulationInfo(),\n            EmulationModelStore(),\n            Dict{String, Any}(),\n        )\n    end\nend\n\nfunction EmulationModel{M}(\n    template::AbstractProblemTemplate,\n    sys::PSY.System,\n    jump_model::Union{Nothing, JuMP.Model} = nothing;\n    resolution = UNSET_RESOLUTION,\n    name = nothing,\n    optimizer = nothing,\n    warm_start = true,\n    initialize_model = true,\n    initialization_file = \"\",\n    deserialize_initial_conditions = false,\n    export_pwl_vars = false,\n    allow_fails = false,\n    calculate_conflict = false,\n    optimizer_solve_log_print = false,\n    detailed_optimizer_stats = false,\n    direct_mode_optimizer = false,\n    check_numerical_bounds = true,\n    store_variable_names = false,\n    rebuild_model = false,\n    initial_time = UNSET_INI_TIME,\n    time_series_cache_size::Int = IS.TIME_SERIES_CACHE_SIZE_BYTES,\n) where {M <: EmulationProblem}\n    settings = Settings(\n        sys;\n        initial_time = initial_time,\n        optimizer = optimizer,\n        time_series_cache_size = time_series_cache_size,\n        warm_start = warm_start,\n        initialize_model = initialize_model,\n        initialization_file = initialization_file,\n        deserialize_initial_conditions = deserialize_initial_conditions,\n        export_pwl_vars = export_pwl_vars,\n        allow_fails = allow_fails,\n        calculate_conflict = calculate_conflict,\n        optimizer_solve_log_print = optimizer_solve_log_print,\n        detailed_optimizer_stats = detailed_optimizer_stats,\n        direct_mode_optimizer = direct_mode_optimizer,\n        check_numerical_bounds = check_numerical_bounds,\n        store_variable_names = store_variable_names,\n        rebuild_model = rebuild_model,\n        horizon = resolution,\n        resolution = resolution,\n    )\n    model = EmulationModel{M}(template, sys, settings, jump_model; name = name)\n    validate_time_series!(model)\n    return model\nend\n\n\"\"\"\nBuild the optimization problem of type M with the specific system and template\n\n# Arguments\n\n  - `::Type{M} where M<:EmulationProblem`: The abstract Emulation model type\n  - `template::AbstractProblemTemplate`: The model reference made up of transmission, devices,\n    branches, and services.\n  - `sys::PSY.System`: the system created using Power Systems\n  - `jump_model::Union{Nothing, JuMP.Model}`: Enables passing a custom JuMP model. Use with care\n\n# Example\n\n```julia\ntemplate = ProblemTemplate(CopperPlatePowerModel, devices, branches, services)\nproblem = EmulationModel(MyEmProblemType, template, system, optimizer)\n```\n\"\"\"\nfunction EmulationModel(\n    ::Type{M},\n    template::AbstractProblemTemplate,\n    sys::PSY.System,\n    jump_model::Union{Nothing, JuMP.Model} = nothing;\n    kwargs...,\n) where {M <: EmulationProblem}\n    return EmulationModel{M}(template, sys, jump_model; kwargs...)\nend\n\nfunction EmulationModel(\n    template::AbstractProblemTemplate,\n    sys::PSY.System,\n    jump_model::Union{Nothing, JuMP.Model} = nothing;\n    kwargs...,\n)\n    return EmulationModel{GenericEmulationProblem}(template, sys, jump_model; kwargs...)\nend\n\n\"\"\"\nBuilds an empty emulation model. This constructor is used for the implementation of custom\nemulation models that do not require a template.\n\n# Arguments\n\n  - `::Type{M} where M<:EmulationProblem`: The abstract operation model type\n  - `sys::PSY.System`: the system created using Power Systems\n  - `jump_model::Union{Nothing, JuMP.Model}` = nothing: Enables passing a custom JuMP model. Use with care.\n\n# Example\n\n```julia\nproblem = EmulationModel(system, optimizer)\n```\n\"\"\"\nfunction EmulationModel{M}(\n    sys::PSY.System,\n    jump_model::Union{Nothing, JuMP.Model} = nothing;\n    kwargs...,\n) where {M <: EmulationProblem}\n    return EmulationModel{M}(template, sys, jump_model; kwargs...)\nend\n\nget_problem_type(::EmulationModel{M}) where {M <: EmulationProblem} = M\n\nfunction validate_template(::EmulationModel{M}) where {M <: EmulationProblem}\n    error(\"validate_template is not implemented for EmulationModel{$M}\")\nend\n\nfunction validate_template(model::EmulationModel{<:DefaultEmulationProblem})\n    validate_template_impl!(model)\n    return\nend\n\nfunction validate_time_series!(model::EmulationModel{<:DefaultEmulationProblem})\n    sys = get_system(model)\n    settings = get_settings(model)\n    available_resolutions = PSY.get_time_series_resolutions(sys)\n\n    if get_resolution(settings) == UNSET_RESOLUTION && length(available_resolutions) != 1\n        throw(\n            IS.ConflictingInputsError(\n                \"Data contains multiple resolutions, the resolution keyword argument must be added to the Model. Time Series Resolutions: $(available_resolutions)\",\n            ),\n        )\n    elseif get_resolution(settings) != UNSET_RESOLUTION && length(available_resolutions) > 1\n        if get_resolution(settings) ∉ available_resolutions\n            throw(\n                IS.ConflictingInputsError(\n                    \"Resolution $(get_resolution(settings)) is not available in the system data. Time Series Resolutions: $(available_resolutions)\",\n                ),\n            )\n        end\n    else\n        set_resolution!(settings, first(available_resolutions))\n    end\n\n    if get_horizon(settings) == UNSET_HORIZON\n        # Emulation Models Only solve one \"step\" so Horizon and Resolution must match\n        set_horizon!(settings, get_resolution(settings))\n    end\n\n    counts = PSY.get_time_series_counts(sys)\n    if counts.static_time_series_count < 1\n        error(\n            \"The system does not contain Static Time Series data. A EmulationModel can't be built.\",\n        )\n    end\n    return\nend\n\nfunction get_current_time(model::EmulationModel)\n    execution_count = get_execution_count(model)\n    initial_time = get_initial_time(model)\n    resolution = get_resolution(model)\n    return initial_time + resolution * execution_count\nend\n\nfunction init_model_store_params!(model::EmulationModel)\n    num_executions = get_executions(model)\n    system = get_system(model)\n    settings = get_settings(model)\n    horizon = interval = resolution = get_resolution(settings)\n    base_power = PSY.get_base_power(system)\n    sys_uuid = IS.get_uuid(system)\n    ISOPT.set_store_params!(\n        get_internal(model),\n        ModelStoreParams(\n            num_executions,\n            horizon,\n            interval,\n            resolution,\n            base_power,\n            sys_uuid,\n            get_metadata(get_optimization_container(model)),\n        ),\n    )\n    return\nend\n\nfunction build_pre_step!(model::EmulationModel)\n    TimerOutputs.@timeit BUILD_PROBLEMS_TIMER \"Build pre-step\" begin\n        validate_template(model)\n        if !isempty(model)\n            @info \"EmulationProblem status not ModelBuildStatus.EMPTY. Resetting\"\n            reset!(model)\n        end\n        container = get_optimization_container(model)\n        container.built_for_recurrent_solves = true\n\n        @info \"Initializing Optimization Container For an EmulationModel\"\n        init_optimization_container!(\n            get_optimization_container(model),\n            get_network_model(get_template(model)),\n            get_system(model),\n        )\n\n        @info \"Initializing ModelStoreParams\"\n        init_model_store_params!(model)\n        set_status!(model, ModelBuildStatus.IN_PROGRESS)\n    end\n    return\nend\n\nfunction build_impl!(model::EmulationModel{<:EmulationProblem})\n    build_pre_step!(model)\n    @info \"Instantiating Network Model\"\n    instantiate_network_model!(model)\n    handle_initial_conditions!(model)\n    build_model!(model)\n    serialize_metadata!(get_optimization_container(model), get_output_dir(model))\n    log_values(get_settings(model))\n    return\nend\n\n\"\"\"\nImplementation of build for any EmulationProblem\n\"\"\"\nfunction build!(\n    model::EmulationModel{<:EmulationProblem};\n    executions = 1,\n    output_dir::String,\n    recorders = [],\n    console_level = Logging.Error,\n    file_level = Logging.Info,\n    disable_timer_outputs = false,\n)\n    mkpath(output_dir)\n    set_output_dir!(model, output_dir)\n    set_console_level!(model, console_level)\n    set_file_level!(model, file_level)\n    TimerOutputs.reset_timer!(BUILD_PROBLEMS_TIMER)\n    disable_timer_outputs && TimerOutputs.disable_timer!(BUILD_PROBLEMS_TIMER)\n    file_mode = \"w\"\n    add_recorders!(model, recorders)\n    register_recorders!(model, file_mode)\n    logger = ISOPT.configure_logging(\n        get_internal(model),\n        PROBLEM_LOG_FILENAME,\n        file_mode,\n    )\n    try\n        Logging.with_logger(logger) do\n            try\n                set_executions!(model, executions)\n                TimerOutputs.@timeit BUILD_PROBLEMS_TIMER \"Problem $(get_name(model))\" begin\n                    build_impl!(model)\n                end\n                set_status!(model, ModelBuildStatus.BUILT)\n                @info \"\\n$(BUILD_PROBLEMS_TIMER)\\n\"\n            catch e\n                set_status!(model, ModelBuildStatus.FAILED)\n                bt = catch_backtrace()\n                @error \"EmulationModel Build Failed\" exception = e, bt\n            end\n        end\n    finally\n        unregister_recorders!(model)\n        close(logger)\n    end\n    return get_status(model)\nend\n\n\"\"\"\nDefault implementation of build method for Emulation Problems for models conforming with  DecisionProblem specification. Overload this function to implement a custom build method\n\"\"\"\nfunction build_model!(model::EmulationModel{<:EmulationProblem})\n    container = get_optimization_container(model)\n    system = get_system(model)\n    build_impl!(container, get_template(model), system)\n    return\nend\n\nfunction reset!(model::EmulationModel{<:EmulationProblem})\n    if built_for_recurrent_solves(model)\n        set_execution_count!(model, 0)\n    end\n    ISOPT.set_container!(\n        get_internal(model),\n        OptimizationContainer(\n            get_system(model),\n            get_settings(model),\n            nothing,\n            PSY.SingleTimeSeries,\n        ),\n    )\n    ISOPT.set_initial_conditions_model_container!(get_internal(model), nothing)\n    empty_time_series_cache!(model)\n    empty!(get_store(model))\n    set_status!(model, ModelBuildStatus.EMPTY)\n    return\nend\n\nfunction update_parameters!(model::EmulationModel, store::EmulationModelStore)\n    update_parameters!(model, store.data_container)\n    return\nend\n\nfunction update_parameters!(model::EmulationModel, data::DatasetContainer{InMemoryDataset})\n    cost_function_unsynch(get_optimization_container(model))\n    for key in keys(get_parameters(model))\n        update_parameter_values!(model, key, data)\n    end\n    if !is_synchronized(model)\n        update_objective_function!(get_optimization_container(model))\n        obj_func = get_objective_expression(get_optimization_container(model))\n        set_synchronized_status!(obj_func, true)\n    end\n    return\nend\n\nfunction update_initial_conditions!(\n    model::EmulationModel,\n    source::EmulationModelStore,\n    ::InterProblemChronology,\n)\n    for key in keys(get_initial_conditions(model))\n        update_initial_conditions!(model, key, source)\n    end\n    return\nend\n\nfunction update_model!(\n    model::EmulationModel,\n    source::EmulationModelStore,\n    ini_cond_chronology,\n)\n    TimerOutputs.@timeit RUN_SIMULATION_TIMER \"Parameter Updates\" begin\n        update_parameters!(model, source)\n    end\n    TimerOutputs.@timeit RUN_SIMULATION_TIMER \"Ini Cond Updates\" begin\n        update_initial_conditions!(model, source, ini_cond_chronology)\n    end\n    return\nend\n\n\"\"\"\nUpdate parameter function an OperationModel\n\"\"\"\nfunction update_parameter_values!(\n    model::EmulationModel,\n    key::ParameterKey{T, U},\n    input::DatasetContainer{InMemoryDataset},\n) where {T <: ParameterType, U <: PSY.Component}\n    # Enable again for detailed debugging\n    # TimerOutputs.@timeit RUN_SIMULATION_TIMER \"$T $U Parameter Update\" begin\n    optimization_container = get_optimization_container(model)\n    update_container_parameter_values!(optimization_container, model, key, input)\n    parameter_attributes = get_parameter_attributes(optimization_container, key)\n    IS.@record :execution ParameterUpdateEvent(\n        T,\n        U,\n        \"event\", # parameter_attributes,\n        get_current_timestamp(model),\n        get_name(model),\n    )\n    #end\n    return\nend\n\nfunction update_model!(model::EmulationModel)\n    update_model!(model, get_store(model), InterProblemChronology())\n    return\nend\n\nfunction run_impl!(\n    model::EmulationModel;\n    optimizer = nothing,\n    enable_progress_bar = progress_meter_enabled(),\n    kwargs...,\n)\n    _pre_solve_model_checks(model, optimizer)\n    internal = get_internal(model)\n    executions = ISOPT.get_executions(internal)\n    # Temporary check. Needs better way to manage re-runs of the same model\n    if internal.execution_count > 0\n        error(\"Call build! again\")\n    end\n    prog_bar = ProgressMeter.Progress(executions; enabled = enable_progress_bar)\n    initial_time = get_initial_time(model)\n    for execution in 1:executions\n        TimerOutputs.@timeit RUN_OPERATION_MODEL_TIMER \"Run execution\" begin\n            solve_impl!(model)\n            current_time = initial_time + (execution - 1) * PSI.get_resolution(model)\n            write_results!(get_store(model), model, execution, current_time)\n            write_optimizer_stats!(get_store(model), get_optimizer_stats(model), execution)\n            advance_execution_count!(model)\n            update_model!(model)\n            ProgressMeter.update!(\n                prog_bar,\n                get_execution_count(model);\n                showvalues = [(:Execution, execution)],\n            )\n        end\n    end\n    return\nend\n\n\"\"\"\nDefault run method for problems that conform to the requirements of\nEmulationModel{<: EmulationProblem}\n\nThis will call `build!` on the model if it is not already built. It will forward all\nkeyword arguments to that function.\n\n# Arguments\n\n  - `model::EmulationModel = model`: Emulation model\n  - `optimizer::MOI.OptimizerWithAttributes`: The optimizer that is used to solve the model\n  - `executions::Int`: Number of executions for the emulator run\n  - `export_problem_results::Bool`: If true, export OptimizationProblemResults DataFrames to CSV files.\n  - `output_dir::String`: Required if the model is not already built, otherwise ignored\n  - `enable_progress_bar::Bool`: Enables/Disable progress bar printing\n  - `export_optimization_model::Bool`: If true, serialize the model to a file to allow re-execution later.\n\n# Examples\n\n```julia\nstatus = run!(model; optimizer = HiGHS.Optimizer, executions = 10)\nstatus = run!(model; output_dir = ./model_output, optimizer = HiGHS.Optimizer, executions = 10)\n```\n\"\"\"\nfunction run!(\n    model::EmulationModel{<:EmulationProblem};\n    export_problem_results = false,\n    console_level = Logging.Error,\n    file_level = Logging.Info,\n    disable_timer_outputs = false,\n    export_optimization_model = true,\n    kwargs...,\n)\n    build_if_not_already_built!(\n        model;\n        console_level = console_level,\n        file_level = file_level,\n        disable_timer_outputs = disable_timer_outputs,\n        kwargs...,\n    )\n    set_console_level!(model, console_level)\n    set_file_level!(model, file_level)\n    TimerOutputs.reset_timer!(RUN_OPERATION_MODEL_TIMER)\n    disable_timer_outputs && TimerOutputs.disable_timer!(RUN_OPERATION_MODEL_TIMER)\n    file_mode = \"a\"\n    register_recorders!(model, file_mode)\n    logger = ISOPT.configure_logging(\n        get_internal(model),\n        PROBLEM_LOG_FILENAME,\n        file_mode,\n    )\n    try\n        Logging.with_logger(logger) do\n            try\n                initialize_storage!(\n                    get_store(model),\n                    get_optimization_container(model),\n                    get_store_params(model),\n                )\n                TimerOutputs.@timeit RUN_OPERATION_MODEL_TIMER \"Run\" begin\n                    run_impl!(model; kwargs...)\n                    set_run_status!(model, RunStatus.SUCCESSFULLY_FINALIZED)\n                end\n                if export_optimization_model\n                    TimerOutputs.@timeit RUN_OPERATION_MODEL_TIMER \"Serialize\" begin\n                        serialize_optimization_model(model)\n                    end\n                end\n                TimerOutputs.@timeit RUN_OPERATION_MODEL_TIMER \"Results processing\" begin\n                    results = OptimizationProblemResults(model)\n                    serialize_results(results, get_output_dir(model))\n                    export_problem_results && export_results(results)\n                end\n                @info \"\\n$(RUN_OPERATION_MODEL_TIMER)\\n\"\n            catch e\n                @error \"Emulation Problem Run failed\" exception = (e, catch_backtrace())\n                set_run_status!(model, RunStatus.FAILED)\n            end\n        end\n    finally\n        unregister_recorders!(model)\n        close(logger)\n    end\n    return get_run_status(model)\nend\n\n\"\"\"\nDefault solve method for an EmulationModel used inside of a Simulation. Solves problems that conform to the requirements of DecisionModel{<: DecisionProblem}\n\n# Arguments\n\n  - `step::Int`: Simulation Step\n  - `model::OperationModel`: operation model\n  - `start_time::Dates.DateTime`: Initial Time of the simulation step in Simulation time.\n  - `store::SimulationStore`: Simulation output store\n  - `exports = nothing`: realtime export of output. Use wisely, it can have negative impacts in the simulation times\n\"\"\"\nfunction solve!(\n    step::Int,\n    model::EmulationModel{<:EmulationProblem},\n    start_time::Dates.DateTime,\n    store::SimulationStore;\n    exports = nothing,\n)\n    # Note, we don't call solve!(decision_model) here because the solve call includes a lot of\n    # other logic used when solving the models separate from a simulation\n    solve_impl!(model)\n    @assert get_current_time(model) == start_time\n    if get_run_status(model) == RunStatus.SUCCESSFULLY_FINALIZED\n        advance_execution_count!(model)\n        write_results!(\n            store,\n            model,\n            get_execution_count(model),\n            start_time;\n            exports = exports,\n        )\n        write_optimizer_stats!(store, model, get_execution_count(model))\n    end\n    return get_run_status(model)\nend\n\nfunction handle_initial_conditions!(model::EmulationModel{<:EmulationProblem})\n    # This code is a duplicate of DecisionModel initial conditions handling.\n    # It should be refactored to better handle AGC emulator initial conditions\n    TimerOutputs.@timeit BUILD_PROBLEMS_TIMER \"Model Initialization\" begin\n        if isempty(get_template(model))\n            return\n        end\n        settings = get_settings(model)\n        initialize_model = get_initialize_model(settings)\n        deserialize_initial_conditions = get_deserialize_initial_conditions(settings)\n        serialized_initial_conditions_file = get_initial_conditions_file(model)\n        custom_init_file = get_initialization_file(settings)\n\n        if !initialize_model && deserialize_initial_conditions\n            throw(\n                IS.ConflictingInputsError(\n                    \"!initialize_model && deserialize_initial_conditions\",\n                ),\n            )\n        elseif !initialize_model && !isempty(custom_init_file)\n            throw(IS.ConflictingInputsError(\"!initialize_model && initialization_file\"))\n        end\n\n        if !initialize_model\n            @info \"Skip build of initial conditions\"\n            return\n        end\n\n        if !isempty(custom_init_file)\n            if !isfile(custom_init_file)\n                error(\"initialization_file = $custom_init_file does not exist\")\n            end\n            if abspath(custom_init_file) != abspath(serialized_initial_conditions_file)\n                cp(custom_init_file, serialized_initial_conditions_file; force = true)\n            end\n        end\n\n        if deserialize_initial_conditions && isfile(serialized_initial_conditions_file)\n            set_initial_conditions_data!(\n                get_optimization_container(model),\n                Serialization.deserialize(serialized_initial_conditions_file),\n            )\n            @info \"Deserialized initial_conditions_data\"\n        else\n            @info \"Make Initial Conditions Model\"\n            build_initial_conditions!(model)\n            initialize!(model)\n        end\n        ISOPT.set_initial_conditions_model_container!(\n            get_internal(model),\n            nothing,\n        )\n    end\n    return\nend\n"
  },
  {
    "path": "src/operation/emulation_model_store.jl",
    "content": "\"\"\"\nStores results data for one EmulationModel\n\"\"\"\nmutable struct EmulationModelStore <: ISOPT.AbstractModelStore\n    data_container::DatasetContainer{InMemoryDataset}\n    optimizer_stats::OrderedDict{Int, OptimizerStats}\nend\n\nget_data_field(store::EmulationModelStore, type::Symbol) =\n    getfield(store.data_container, type)\n\nfunction EmulationModelStore()\n    return EmulationModelStore(\n        DatasetContainer{InMemoryDataset}(),\n        OrderedDict{Int, OptimizerStats}(),\n    )\nend\n\n\"\"\"\n    Base.empty!(store::EmulationModelStore)\n\nEmpty the [`EmulationModelStore`](@ref)\n\"\"\"\nfunction Base.empty!(store::EmulationModelStore)\n    stype = DatasetContainer\n    for (name, _) in zip(fieldnames(stype), fieldtypes(stype))\n        if name ∉ [:values, :timestamps]\n            val = get_data_field(store, name)\n            try\n                empty!(val)\n            catch\n                @error \"Base.empty! must be customized for type $stype or skipped\"\n                rethrow()\n            end\n        elseif name == :update_timestamp\n            store.update_timestamp = UNSET_INI_TIME\n        else\n            setfield!(\n                store.data_container,\n                name,\n                zero(fieldtype(store.data_container, name)),\n            )\n        end\n    end\n    empty!(store.optimizer_stats)\n    return\nend\n\nfunction Base.isempty(store::EmulationModelStore)\n    stype = DatasetContainer\n    for (name, type) in zip(fieldnames(stype), fieldtypes(stype))\n        if name ∉ [:values, :timestamps]\n            val = get_data_field(store, name)\n            try\n                !isempty(val) && return false\n            catch\n                @error \"Base.isempty must be customized for type $stype or skipped\"\n                rethrow()\n            end\n        elseif name == :update_timestamp\n            store.update_timestamp != UNSET_INI_TIME && return false\n        else\n            val = get_data_field(store, name)\n            iszero(val) && return false\n        end\n    end\n    return isempty(store.optimizer_stats)\nend\n\nfunction initialize_storage!(\n    store::EmulationModelStore,\n    container::OptimizationContainer,\n    params::ModelStoreParams,\n)\n    num_of_executions = get_num_executions(params)\n    for type in STORE_CONTAINERS\n        field_containers = getfield(container, type)\n        results_container = get_data_field(store, type)\n        for (key, field_container) in field_containers\n            @debug \"Adding $(encode_key_as_string(key)) to EmulationModelStore\" _group =\n                LOG_GROUP_MODEL_STORE\n            column_names = get_column_names(container, type, field_container, key)\n            results_container[key] = InMemoryDataset(\n                fill!(\n                    DenseAxisArray{Float64}(undef, column_names..., 1:num_of_executions),\n                    NaN,\n                ),\n            )\n        end\n    end\n    return\nend\n\nfunction write_result!(\n    store::EmulationModelStore,\n    name::Symbol,\n    key::OptimizationContainerKey,\n    index::EmulationModelIndexType,\n    update_timestamp::Dates.DateTime,\n    array::DenseAxisArray{Float64, 2},\n)\n    if size(array, 2) == 1\n        write_result!(store, name, key, index, update_timestamp, array[:, 1])\n    else\n        container = get_data_field(store, get_store_container_type(key))\n        set_value!(\n            container[key],\n            array,\n            index,\n        )\n        set_last_recorded_row!(container[key], index)\n        set_update_timestamp!(container[key], update_timestamp)\n    end\n    return\nend\n\nfunction write_result!(\n    store::EmulationModelStore,\n    ::Symbol,\n    key::OptimizationContainerKey,\n    index::EmulationModelIndexType,\n    update_timestamp::Dates.DateTime,\n    array::DenseAxisArray{Float64, 1},\n)\n    container = get_data_field(store, get_store_container_type(key))\n    set_value!(\n        container[key],\n        array,\n        index,\n    )\n    set_last_recorded_row!(container[key], index)\n    set_update_timestamp!(container[key], update_timestamp)\n    return\nend\n\nfunction write_result!(\n    store::EmulationModelStore,\n    name::Symbol,\n    key::OptimizationContainerKey,\n    index::EmulationModelIndexType,\n    update_timestamp::Dates.DateTime,\n    array::DenseAxisArray{Float64, 3},\n)\n    # Handle 3D arrays by reducing to 2D when the last dimension is 1.\n    # This mirrors the 2D case above where size(array, 2) == 1 triggers a dimension reduction.\n    if size(array, 3) == 1\n        write_result!(store, name, key, index, update_timestamp, array[:, :, 1])\n    else\n        container = get_data_field(store, get_store_container_type(key))\n        set_value!(\n            container[key],\n            array,\n            index,\n        )\n        set_last_recorded_row!(container[key], index)\n        set_update_timestamp!(container[key], update_timestamp)\n    end\n    return\nend\n\nfunction read_results(\n    store::EmulationModelStore,\n    key::OptimizationContainerKey;\n    index::Union{Int, Nothing} = nothing,\n    len::Union{Int, Nothing} = nothing,\n)\n    container = get_data_field(store, get_store_container_type(key))\n    data = container[key].values\n    num_dims = ndims(data)\n    # Return a copy because callers may mutate it.\n    if num_dims == 2\n        if isnothing(index)\n            @assert_op len === nothing\n            return data[:, :]\n        elseif isnothing(len)\n            return data[:, index:end]\n        else\n            return data[:, index:(index + len - 1)]\n        end\n    elseif num_dims == 3\n        if isnothing(index)\n            @assert_op len === nothing\n            return data[:, :, :]\n        elseif isnothing(len)\n            return data[:, :, index:end]\n        else\n            return data[:, :, index:(index + len - 1)]\n        end\n    else\n        error(\"Unsupported number of dimensions for emulation dataset: $num_dims\")\n    end\nend\n\nfunction get_column_names(store::EmulationModelStore, key::OptimizationContainerKey)\n    container = get_data_field(store, get_store_container_type(key))\n    return get_column_names_from_axis_array(key, container[key].values)\nend\n\nfunction get_dataset_size(store::EmulationModelStore, key::OptimizationContainerKey)\n    container = get_data_field(store, get_store_container_type(key))\n    return size(container[key].values)\nend\n\nfunction get_last_updated_timestamp(\n    store::EmulationModelStore,\n    key::OptimizationContainerKey,\n)\n    container = get_data_field(store, get_store_container_type(key))\n    return get_update_timestamp(container[key])\nend\nfunction write_optimizer_stats!(\n    store::EmulationModelStore,\n    stats::OptimizerStats,\n    index::EmulationModelIndexType,\n)\n    @assert !(index in keys(store.optimizer_stats))\n    store.optimizer_stats[index] = stats\n    return\nend\n\nfunction read_optimizer_stats(store::EmulationModelStore)\n    return DataFrames.DataFrame([\n        IS.to_namedtuple(x) for x in values(store.optimizer_stats)\n    ])\nend\n\nfunction get_last_recorded_row(x::EmulationModelStore, key::OptimizationContainerKey)\n    return get_last_recorded_row(x.data_container, key)\nend\n"
  },
  {
    "path": "src/operation/initial_conditions_update_in_memory_store.jl",
    "content": "\n################## ic updates from store for emulation problems simulation #################\n\nfunction update_initial_conditions!(\n    ics::T,\n    store::EmulationModelStore,\n    ::Dates.Millisecond,\n) where {\n    T <: Union{\n        Vector{\n            Union{\n                InitialCondition{InitialTimeDurationOn, Nothing},\n                InitialCondition{InitialTimeDurationOn, Float64},\n            },\n        },\n        Vector{\n            Union{\n                InitialCondition{InitialTimeDurationOn, Nothing},\n                InitialCondition{InitialTimeDurationOn, JuMP.VariableRef},\n            },\n        },\n    },\n}\n    for ic in ics\n        var_val = get_value(store, TimeDurationOn(), get_component_type(ic))\n        set_ic_quantity!(ic, get_last_recorded_value(var_val)[get_component_name(ic)])\n    end\n    return\nend\n\nfunction update_initial_conditions!(\n    ics::T,\n    store::EmulationModelStore,\n    ::Dates.Millisecond,\n) where {\n    T <: Union{\n        Vector{\n            Union{\n                InitialCondition{InitialTimeDurationOff, Nothing},\n                InitialCondition{InitialTimeDurationOff, Float64},\n            },\n        },\n        Vector{\n            Union{\n                InitialCondition{InitialTimeDurationOff, Nothing},\n                InitialCondition{InitialTimeDurationOff, JuMP.VariableRef},\n            },\n        },\n    },\n}\n    for ic in ics\n        var_val = get_value(store, TimeDurationOff(), get_component_type(ic))\n        set_ic_quantity!(ic, get_last_recorded_value(var_val)[get_component_name(ic)])\n    end\n    return\nend\n\nfunction update_initial_conditions!(\n    ics::T,\n    store::EmulationModelStore,\n    ::Dates.Millisecond,\n) where {\n    T <: Union{\n        Vector{\n            Union{\n                InitialCondition{DevicePower, Nothing},\n                InitialCondition{DevicePower, Float64},\n            },\n        },\n        Vector{\n            Union{\n                InitialCondition{DevicePower, Nothing},\n                InitialCondition{DevicePower, JuMP.VariableRef},\n            },\n        },\n    },\n}\n    for ic in ics\n        var_val = get_value(store, ActivePowerVariable(), get_component_type(ic))\n        set_ic_quantity!(ic, get_last_recorded_value(var_val)[get_component_name(ic)])\n    end\n    return\nend\n\nfunction update_initial_conditions!(\n    ics::T,\n    store::EmulationModelStore,\n    ::Dates.Millisecond,\n) where {\n    T <: Union{\n        Vector{\n            Union{\n                InitialCondition{DeviceStatus, Nothing},\n                InitialCondition{DeviceStatus, Float64},\n            },\n        },\n        Vector{\n            Union{\n                InitialCondition{DeviceStatus, Nothing},\n                InitialCondition{DeviceStatus, JuMP.VariableRef},\n            },\n        },\n    },\n}\n    for ic in ics\n        var_val = get_value(store, OnVariable(), get_component_type(ic))\n        set_ic_quantity!(ic, get_last_recorded_value(var_val)[get_component_name(ic)])\n    end\n    return\nend\n\nfunction update_initial_conditions!(\n    ics::T,\n    store::EmulationModelStore,\n    ::Dates.Millisecond,\n) where {\n    T <: Union{\n        Vector{\n            Union{\n                InitialCondition{DeviceAboveMinPower, Nothing},\n                InitialCondition{DeviceAboveMinPower, Float64},\n            },\n        },\n        Vector{\n            Union{\n                InitialCondition{DeviceAboveMinPower, Nothing},\n                InitialCondition{DeviceAboveMinPower, JuMP.VariableRef},\n            },\n        },\n    },\n}\n    for ic in ics\n        var_val =\n            get_value(store, PowerAboveMinimumVariable(), get_component_type(ic))\n        set_ic_quantity!(ic, get_last_recorded_value(var_val)[get_component_name(ic)])\n    end\n    return\nend\n\n#= Unused without the AGC model enabled\nfunction update_initial_conditions!(\n    ics::Vector{T},\n    store::EmulationModelStore,\n    ::Dates.Millisecond,\n) where {\n    T <: InitialCondition{AreaControlError, S},\n} where {S <: Union{Float64, JuMP.VariableRef}}\n    for ic in ics\n        var_val = get_value(store, AreaMismatchVariable(), get_component_type(ic))\n        set_ic_quantity!(ic, get_last_recorded_value(var_val)[get_component_name(ic)])\n    end\n    return\nend\n=#\n\nfunction update_initial_conditions!(\n    ics::T,\n    store::EmulationModelStore,\n    ::Dates.Millisecond,\n) where {\n    T <: Union{\n        Vector{\n            Union{\n                InitialCondition{InitialEnergyLevel, Nothing},\n                InitialCondition{InitialEnergyLevel, Float64},\n            },\n        },\n        Vector{\n            Union{\n                InitialCondition{InitialEnergyLevel, Nothing},\n                InitialCondition{InitialEnergyLevel, JuMP.VariableRef},\n            },\n        },\n    },\n}\n    for ic in ics\n        var_val = get_value(store, EnergyVariable(), get_component_type(ic))\n        set_ic_quantity!(ic, get_last_recorded_value(var_val)[get_component_name(ic)])\n    end\n    return\nend\n"
  },
  {
    "path": "src/operation/model_numerical_analysis_utils.jl",
    "content": "# The Numerical stability checks code in this file is based on the code from the SDDP.jl package,\n# from the below mentioned commit and file.\n# commit :8cd305188caffc50a1734913053fc81bba613778\n# link to file :https://github.com/odow/SDDP.jl/blob/d353fe5a2903421e7fed6d609eb9377c35d715a1/src/print.jl#L190\n\nmutable struct NumericalBounds\n    min::Float64\n    max::Float64\n    min_index::Any\n    max_index::Any\nend\n\nNumericalBounds() = NumericalBounds(Inf, -Inf, nothing, nothing)\n\nset_min!(v::NumericalBounds, value::Real) = v.min = value\nset_max!(v::NumericalBounds, value::Real) = v.max = value\nset_min_index!(v::NumericalBounds, idx) = v.min_index = idx\nset_max_index!(v::NumericalBounds, idx) = v.max_index = idx\n\nmutable struct ConstraintBounds\n    coefficient::NumericalBounds\n    rhs::NumericalBounds\n    function ConstraintBounds()\n        return new(NumericalBounds(), NumericalBounds())\n    end\nend\n\nfunction update_coefficient_bounds(\n    v::ConstraintBounds,\n    constraint::JuMP.ScalarConstraint,\n    idx,\n)\n    update_numerical_bounds(v.coefficient, constraint.func, idx)\n    return\nend\n\nfunction update_rhs_bounds(v::ConstraintBounds, constraint::JuMP.ScalarConstraint, idx)\n    update_numerical_bounds(v.rhs, constraint.set, idx)\n    return\nend\n\nmutable struct VariableBounds\n    bounds::NumericalBounds\n    function VariableBounds()\n        return new(NumericalBounds())\n    end\nend\n\nfunction update_variable_bounds(v::VariableBounds, variable::JuMP.VariableRef, idx)\n    if JuMP.is_binary(variable)\n        set_min!(v.bounds, 0.0)\n        update_numerical_bounds(v.bounds, 1.0, idx)\n    else\n        if JuMP.has_lower_bound(variable)\n            update_numerical_bounds(v.bounds, JuMP.lower_bound(variable), idx)\n        end\n        if JuMP.has_upper_bound(variable)\n            update_numerical_bounds(v.bounds, JuMP.upper_bound(variable), idx)\n        end\n    end\n    return\nend\n\nfunction update_numerical_bounds(v::NumericalBounds, value::Real, idx)\n    if !isapprox(value, 0.0)\n        if v.min > abs(value)\n            set_min!(v, value)\n            set_min_index!(v, idx)\n        elseif v.max < abs(value)\n            set_max!(v, value)\n            set_max_index!(v, idx)\n        end\n    end\n    return\nend\n\nfunction update_numerical_bounds(bonuds::NumericalBounds, func::JuMP.GenericAffExpr, idx)\n    for coefficient in values(func.terms)\n        update_numerical_bounds(bonuds, coefficient, idx)\n    end\n    return\nend\n\nfunction update_numerical_bounds(bonuds::NumericalBounds, func::MOI.LessThan, idx)\n    return update_numerical_bounds(bonuds, func.upper, idx)\nend\n\nfunction update_numerical_bounds(bonuds::NumericalBounds, func::MOI.GreaterThan, idx)\n    return update_numerical_bounds(bonuds, func.lower, idx)\nend\n\nfunction update_numerical_bounds(bonuds::NumericalBounds, func::MOI.EqualTo, idx)\n    return update_numerical_bounds(bonuds, func.value, idx)\nend\n\nfunction update_numerical_bounds(bonuds::NumericalBounds, func::MOI.Interval, idx)\n    update_numerical_bounds(bonuds, func.upper, idx)\n    return update_numerical_bounds(bonuds, func.lower, idx)\nend\n\n# Default fallback for unsupported constraints.\nupdate_numerical_bounds(::NumericalBounds, func, idx) = nothing\n\nfunction get_constraint_numerical_bounds(model::OperationModel)\n    if !is_built(model)\n        error(\"Model not built, can't calculate constraint numerical bounds\")\n    end\n    bounds = ConstraintBounds()\n    for (const_key, constraint_array) in get_constraints(get_optimization_container(model))\n        # TODO: handle this at compile and not at run time\n        if isa(constraint_array, SparseAxisArray)\n            for idx in eachindex(constraint_array)\n                constraint_array[idx] == 0.0 && continue\n                con_obj = JuMP.constraint_object(constraint_array[idx])\n                update_coefficient_bounds(bounds, con_obj, (const_key, idx))\n                update_rhs_bounds(bounds, con_obj, (const_key, idx))\n            end\n        else\n            for idx in Iterators.product(constraint_array.axes...)\n                !isassigned(constraint_array, idx...) && continue\n                con_obj = JuMP.constraint_object(constraint_array[idx...])\n                update_coefficient_bounds(bounds, con_obj, (const_key, idx))\n                update_rhs_bounds(bounds, con_obj, (const_key, idx))\n            end\n        end\n    end\n    return bounds\nend\n\nfunction get_variable_numerical_bounds(model::OperationModel)\n    if !is_built(model)\n        error(\"Model not built, can't calculate variable numerical bounds\")\n    end\n    bounds = VariableBounds()\n    for (variable_key, variable_array) in get_variables(get_optimization_container(model))\n        if isa(variable_array, SparseAxisArray)\n            for idx in eachindex(variable_array)\n                var = variable_array[idx]\n                var == 0.0 && continue\n                update_variable_bounds(bounds, var, (variable_key, idx))\n            end\n        else\n            for idx in Iterators.product(variable_array.axes...)\n                var = variable_array[idx...]\n                update_variable_bounds(bounds, var, (variable_key, idx))\n            end\n        end\n    end\n    return bounds\nend\n"
  },
  {
    "path": "src/operation/operation_model_interface.jl",
    "content": "# Default implementations of getter/setter functions for OperationModel.\nis_built(model::OperationModel) =\n    ISOPT.get_status(get_internal(model)) == ModelBuildStatus.BUILT\nisempty(model::OperationModel) =\n    ISOPT.get_status(get_internal(model)) == ModelBuildStatus.EMPTY\nwarm_start_enabled(model::OperationModel) =\n    get_warm_start(get_optimization_container(model).settings)\nbuilt_for_recurrent_solves(model::OperationModel) =\n    get_optimization_container(model).built_for_recurrent_solves\nget_constraints(model::OperationModel) =\n    ISOPT.get_constraints(get_internal(model))\nget_execution_count(model::OperationModel) =\n    ISOPT.get_execution_count(get_internal(model))\nget_executions(model::OperationModel) = ISOPT.get_executions(get_internal(model))\nget_initial_time(model::OperationModel) = get_initial_time(get_settings(model))\nget_internal(model::OperationModel) = model.internal\n\nfunction get_jump_model(model::OperationModel)\n    return get_jump_model(ISOPT.get_container(get_internal(model)))\nend\n\nget_name(model::OperationModel) = model.name\nget_store(model::OperationModel) = model.store\nis_synchronized(model::OperationModel) = is_synchronized(get_optimization_container(model))\n\nfunction get_rebuild_model(model::OperationModel)\n    sim_info = model.simulation_info\n    if sim_info === nothing\n        error(\"Model not part of a simulation\")\n    end\n    return get_rebuild_model(get_optimization_container(model).settings)\nend\n\nfunction get_optimization_container(model::OperationModel)\n    return ISOPT.get_optimization_container(get_internal(model))\nend\n\nfunction get_resolution(model::OperationModel)\n    resolution = get_resolution(get_settings(model))\n    return resolution\nend\n\nget_problem_base_power(model::OperationModel) = PSY.get_base_power(model.sys)\nget_settings(model::OperationModel) = get_optimization_container(model).settings\n\nget_optimizer_stats(model::OperationModel) =\n# This deepcopy is important because the optimization container is overwritten\n# at each solve in a simulation.\n    deepcopy(get_optimizer_stats(get_optimization_container(model)))\n\nget_simulation_info(model::OperationModel) = model.simulation_info\nget_simulation_number(model::OperationModel) = get_number(get_simulation_info(model))\nset_simulation_number!(model::OperationModel, val) =\n    set_number!(get_simulation_info(model), val)\nget_sequence_uuid(model::OperationModel) = get_sequence_uuid(get_simulation_info(model))\nset_sequence_uuid!(model::OperationModel, val) =\n    set_sequence_uuid!(get_simulation_info(model), val)\nget_status(model::OperationModel) = ISOPT.get_status(get_internal(model))\nget_system(model::OperationModel) = model.sys\nget_template(model::OperationModel) = model.template\nget_log_file(model::OperationModel) = joinpath(get_output_dir(model), PROBLEM_LOG_FILENAME)\nget_store_params(model::OperationModel) =\n    ISOPT.get_store_params(get_internal(model))\nget_output_dir(model::OperationModel) = ISOPT.get_output_dir(get_internal(model))\nget_initial_conditions_file(model::OperationModel) =\n    joinpath(get_output_dir(model), \"initial_conditions.bin\")\nget_recorder_dir(model::OperationModel) =\n    joinpath(get_output_dir(model), \"recorder\")\nget_variables(model::OperationModel) = get_variables(get_optimization_container(model))\nget_parameters(model::OperationModel) = get_parameters(get_optimization_container(model))\nget_duals(model::OperationModel) = get_duals(get_optimization_container(model))\nget_initial_conditions(model::OperationModel) =\n    get_initial_conditions(get_optimization_container(model))\n\nget_interval(model::OperationModel) = get_store_params(model).interval\nget_run_status(model::OperationModel) = get_run_status(get_simulation_info(model))\nset_run_status!(model::OperationModel, status) =\n    set_run_status!(get_simulation_info(model), status)\nget_time_series_cache(model::OperationModel) =\n    ISOPT.get_time_series_cache(get_internal(model))\nempty_time_series_cache!(x::OperationModel) = empty!(get_time_series_cache(x))\n\nfunction get_current_timestamp(model::OperationModel)\n    # For EmulationModel interval and resolution are the same.\n    return get_initial_time(model) + get_execution_count(model) * get_interval(model)\nend\n\nfunction get_timestamps(model::OperationModel)\n    optimization_container = get_optimization_container(model)\n    start_time = get_initial_time(optimization_container)\n    resolution = get_resolution(model)\n    horizon_count = get_time_steps(optimization_container)[end]\n    return range(start_time; length = horizon_count, step = resolution)\nend\n\nfunction write_data(model::OperationModel, output_dir::AbstractString; kwargs...)\n    write_data(get_optimization_container(model), output_dir; kwargs...)\n    return\nend\n\nfunction get_initial_conditions(\n    model::OperationModel,\n    ::T,\n    ::U,\n) where {T <: InitialConditionType, U <: PSY.Device}\n    return get_initial_conditions(get_optimization_container(model), T, U)\nend\n\nfunction solve_impl!(model::OperationModel)\n    container = get_optimization_container(model)\n    model_name = get_name(model)\n    ts = get_current_timestamp(model)\n    output_dir = get_output_dir(model)\n\n    if get_export_optimization_model(get_settings(model))\n        model_output_dir = joinpath(output_dir, \"optimization_model_exports\")\n        mkpath(model_output_dir)\n        tss = replace(\"$(ts)\", \":\" => \"_\")\n        model_export_path = joinpath(model_output_dir, \"exported_$(model_name)_$(tss).json\")\n        serialize_optimization_model(container, model_export_path)\n        write_lp_file(\n            get_jump_model(container),\n            replace(model_export_path, \".json\" => \".lp\"),\n        )\n    end\n\n    status = solve_impl!(container, get_system(model))\n    set_run_status!(model, status)\n    if status != RunStatus.SUCCESSFULLY_FINALIZED\n        settings = get_settings(model)\n        infeasible_opt_path = joinpath(output_dir, \"infeasible_$(model_name).json\")\n        @error(\"Serializing Infeasible Problem at $(infeasible_opt_path)\")\n        serialize_optimization_model(container, infeasible_opt_path)\n        if !get_allow_fails(settings)\n            error(\"Solving model $(model_name) failed at $(ts)\")\n        else\n            @error \"Solving model $(model_name) failed at $(ts). Failure Allowed\"\n        end\n    end\n    return\nend\n\nset_console_level!(model::OperationModel, val) =\n    ISOPT.set_console_level!(get_internal(model), val)\nset_file_level!(model::OperationModel, val) =\n    ISOPT.set_file_level!(get_internal(model), val)\nfunction set_executions!(model::OperationModel, val::Int)\n    ISOPT.set_executions!(get_internal(model), val)\n    return\nend\n\nfunction set_execution_count!(model::OperationModel, val::Int)\n    ISOPT.set_execution_count!(get_internal(model), val)\n    return\nend\n\nset_initial_time!(model::OperationModel, val::Dates.DateTime) =\n    set_initial_time!(get_settings(model), val)\n\nget_simulation_info(model::OperationModel, val) = model.simulation_info = val\n\nfunction set_status!(model::OperationModel, status::ModelBuildStatus)\n    ISOPT.set_status!(get_internal(model), status)\n    return\nend\n\nfunction set_output_dir!(model::OperationModel, path::AbstractString)\n    ISOPT.set_output_dir!(get_internal(model), path)\n    return\nend\n\nfunction advance_execution_count!(model::OperationModel)\n    internal = get_internal(model)\n    internal.execution_count += 1\n    return\nend\n\nfunction build_initial_conditions!(model::OperationModel)\n    @assert ISOPT.get_initial_conditions_model_container(get_internal(model)) ===\n            nothing\n    requires_init = false\n    for (device_type, device_model) in get_device_models(get_template(model))\n        requires_init = requires_initialization(get_formulation(device_model)())\n        if requires_init\n            @debug \"initial_conditions required for $device_type\" _group =\n                LOG_GROUP_BUILD_INITIAL_CONDITIONS\n            build_initial_conditions_model!(model)\n            break\n        end\n    end\n    if !requires_init\n        @info \"No initial conditions in the model\"\n    end\n    return\nend\n\nfunction write_initial_conditions_data!(model::OperationModel)\n    write_initial_conditions_data!(\n        get_optimization_container(model),\n        ISOPT.get_initial_conditions_model_container(get_internal(model)),\n    )\n    return\nend\n\nfunction initialize!(model::OperationModel)\n    container = get_optimization_container(model)\n    if ISOPT.get_initial_conditions_model_container(get_internal(model)) ===\n       nothing\n        return\n    end\n    @info \"Solving Initialization Model for $(get_name(model))\"\n    status = solve_impl!(\n        ISOPT.get_initial_conditions_model_container(get_internal(model)),\n        get_system(model),\n    )\n    if status == RunStatus.FAILED\n        error(\"Model failed to initialize\")\n    end\n\n    write_initial_conditions_data!(\n        container,\n        ISOPT.get_initial_conditions_model_container(get_internal(model)),\n    )\n    init_file = get_initial_conditions_file(model)\n    Serialization.serialize(init_file, get_initial_conditions_data(container))\n    @info \"Serialized initial conditions to $init_file\"\n    return\nend\n\n# TODO: Document requirements for solve_impl\n# function solve_impl!(model::OperationModel)\n# end\n\nconst _TEMPLATE_VALIDATION_EXCLUSIONS = [PSY.Arc, PSY.Area, PSY.ACBus, PSY.LoadZone]\n\nfunction build_if_not_already_built!(model::OperationModel; kwargs...)\n    status = get_status(model)\n    if status == ModelBuildStatus.EMPTY\n        if !haskey(kwargs, :output_dir)\n            error(\n                \"'output_dir' must be provided as a kwarg if the model build status is $status\",\n            )\n        else\n            new_kwargs = Dict(k => v for (k, v) in kwargs if k != :optimizer)\n            status = build!(model; new_kwargs...)\n        end\n    end\n    if status != ModelBuildStatus.BUILT\n        error(\"build! of the $(typeof(model)) $(get_name(model)) failed: $status\")\n    end\n    return\nend\n\nfunction _check_numerical_bounds(model::OperationModel)\n    variable_bounds = get_variable_numerical_bounds(model)\n    if variable_bounds.bounds.max - variable_bounds.bounds.min > 1e9\n        @warn \"Variable bounds range is $(variable_bounds.bounds.max - variable_bounds.bounds.min) and can result in numerical problems for the solver. \\\\\n        max_bound_variable = $(encode_key_as_string(variable_bounds.bounds.max_index)) \\\\\n        min_bound_variable = $(encode_key_as_string(variable_bounds.bounds.min_index)) \\\\\n        Run get_detailed_variable_numerical_bounds on the model for a deeper analysis\"\n    else\n        @info \"Variable bounds range is [$(variable_bounds.bounds.min) $(variable_bounds.bounds.max)]\"\n    end\n\n    constraint_bounds = get_constraint_numerical_bounds(model)\n    if constraint_bounds.coefficient.max - constraint_bounds.coefficient.min > 1e9\n        @warn \"Constraint coefficient bounds range is $(constraint_bounds.coefficient.max - constraint_bounds.coefficient.min) and can result in numerical problems for the solver. \\\\\n        max_bound_constraint = $(encode_key_as_string(constraint_bounds.coefficient.max_index)) \\\\\n        min_bound_constraint = $(encode_key_as_string(constraint_bounds.coefficient.min_index)) \\\\\n        Run get_detailed_constraint_numerical_bounds on the model for a deeper analysis\"\n    else\n        @info \"Constraint coefficient bounds range is [$(constraint_bounds.coefficient.min) $(constraint_bounds.coefficient.max)]\"\n    end\n\n    if constraint_bounds.rhs.max - constraint_bounds.rhs.min > 1e9\n        @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. \\\\\n        max_bound_constraint = $(encode_key_as_string(constraint_bounds.rhs.max_index)) \\\\\n        min_bound_constraint = $(encode_key_as_string(constraint_bounds.rhs.min_index)) \\\\\n        Run get_detailed_constraint_numerical_bounds on the model for a deeper analysis\"\n    else\n        @info \"Constraint right-hand-side bounds [$(constraint_bounds.rhs.min) $(constraint_bounds.rhs.max)]\"\n    end\n    return\nend\n\nfunction _pre_solve_model_checks(model::OperationModel, optimizer = nothing)\n    jump_model = get_jump_model(model)\n    if optimizer !== nothing\n        JuMP.set_optimizer(jump_model, optimizer)\n    end\n\n    if JuMP.mode(jump_model) != JuMP.DIRECT\n        if JuMP.backend(jump_model).state == MOIU.NO_OPTIMIZER\n            error(\"No Optimizer has been defined, can't solve the operational problem\")\n        end\n    else\n        @assert get_direct_mode_optimizer(get_settings(model))\n    end\n\n    optimizer_name = JuMP.solver_name(jump_model)\n    @info \"$(get_name(model)) optimizer set to: $optimizer_name\"\n    settings = get_settings(model)\n    if get_check_numerical_bounds(settings)\n        @info \"Checking Numerical Bounds\"\n        TimerOutputs.@timeit BUILD_PROBLEMS_TIMER \"Numerical Bounds Check\" begin\n            _check_numerical_bounds(model)\n        end\n    end\n    return\nend\n\nfunction _list_names(model::OperationModel, container_type)\n    return encode_keys_as_strings(\n        ISOPT.list_keys(get_store(model), container_type),\n    )\nend\n\nread_dual(model::OperationModel, key::ConstraintKey) = _read_results(model, key)\nread_parameter(model::OperationModel, key::ParameterKey) = _read_results(model, key)\nread_aux_variable(model::OperationModel, key::AuxVarKey) = _read_results(model, key)\nread_variable(model::OperationModel, key::VariableKey) = _read_results(model, key)\nread_expression(model::OperationModel, key::ExpressionKey) = _read_results(model, key)\n\nfunction _read_results(model::OperationModel, key::OptimizationContainerKey)\n    array = read_results(get_store(model), key)\n    return to_results_dataframe(array, nothing, Val(TableFormat.LONG))\nend\n\nread_optimizer_stats(model::OperationModel) = read_optimizer_stats(get_store(model))\n\nfunction add_recorders!(model::OperationModel, recorders)\n    internal = get_internal(model)\n    for name in union(REQUIRED_RECORDERS, recorders)\n        ISOPT.add_recorder!(internal, name)\n    end\nend\n\nfunction register_recorders!(model::OperationModel, file_mode)\n    recorder_dir = get_recorder_dir(model)\n    mkpath(recorder_dir)\n    for name in ISOPT.get_recorders(get_internal(model))\n        IS.register_recorder!(name; mode = file_mode, directory = recorder_dir)\n    end\nend\n\nfunction unregister_recorders!(model::OperationModel)\n    for name in ISOPT.get_recorders(get_internal(model))\n        IS.unregister_recorder!(name)\n    end\nend\n\nconst _JUMP_MODEL_FILENAME = \"jump_model.json\"\n\nfunction serialize_optimization_model(model::OperationModel)\n    serialize_optimization_model(\n        get_optimization_container(model),\n        joinpath(get_output_dir(model), _JUMP_MODEL_FILENAME),\n    )\n    return\nend\n\nfunction instantiate_network_model!(model::OperationModel)\n    template = get_template(model)\n    network_model = get_network_model(template)\n    branch_models = get_branch_models(template)\n    number_of_steps = get_time_steps(get_optimization_container(model))[end]\n    instantiate_network_model!(\n        network_model,\n        branch_models,\n        number_of_steps,\n        get_system(model),\n    )\n    return\nend\n\nlist_aux_variable_keys(x::OperationModel) =\n    ISOPT.list_keys(get_store(x), STORE_CONTAINER_AUX_VARIABLES)\nlist_aux_variable_names(x::OperationModel) = _list_names(x, STORE_CONTAINER_AUX_VARIABLES)\nlist_variable_keys(x::OperationModel) =\n    ISOPT.list_keys(get_store(x), STORE_CONTAINER_VARIABLES)\nlist_variable_names(x::OperationModel) = _list_names(x, STORE_CONTAINER_VARIABLES)\nlist_parameter_keys(x::OperationModel) =\n    ISOPT.list_keys(get_store(x), STORE_CONTAINER_PARAMETERS)\nlist_parameter_names(x::OperationModel) = _list_names(x, STORE_CONTAINER_PARAMETERS)\nlist_dual_keys(x::OperationModel) =\n    ISOPT.list_keys(get_store(x), STORE_CONTAINER_DUALS)\nlist_dual_names(x::OperationModel) = _list_names(x, STORE_CONTAINER_DUALS)\nlist_expression_keys(x::OperationModel) =\n    ISOPT.list_keys(get_store(x), STORE_CONTAINER_EXPRESSIONS)\nlist_expression_names(x::OperationModel) = _list_names(x, STORE_CONTAINER_EXPRESSIONS)\n\nfunction list_all_keys(x::OperationModel)\n    return Iterators.flatten(\n        keys(get_data_field(get_store(x), f)) for f in STORE_CONTAINERS\n    )\nend\n\nfunction serialize_optimization_model(model::OperationModel, save_path::String)\n    serialize_jump_optimization_model(\n        get_jump_model(get_optimization_container(model)),\n        save_path,\n    )\n    return\nend\n"
  },
  {
    "path": "src/operation/operation_model_simulation_interface.jl",
    "content": "function update_model!(model::OperationModel, source::SimulationState, ini_cond_chronology)\n    TimerOutputs.@timeit RUN_SIMULATION_TIMER \"Parameter Updates\" begin\n        update_parameters!(model, source)\n    end\n    TimerOutputs.@timeit RUN_SIMULATION_TIMER \"Ini Cond Updates\" begin\n        update_initial_conditions!(model, source, ini_cond_chronology)\n    end\n    return\nend\n\nfunction update_parameters!(model::EmulationModel, state::SimulationState)\n    data = get_decision_states(state)\n    update_parameters!(model, data)\n    return\nend\n\nfunction update_parameters!(\n    model::DecisionModel,\n    simulation_state::SimulationState,\n)\n    cost_function_unsynch(get_optimization_container(model))\n    for key in keys(get_parameters(model))\n        update_parameter_values!(model, key, simulation_state)\n    end\n    if !is_synchronized(model)\n        update_objective_function!(get_optimization_container(model))\n        obj_func = get_objective_expression(get_optimization_container(model))\n        set_synchronized_status!(obj_func, true)\n    end\n    return\nend\n"
  },
  {
    "path": "src/operation/operation_model_types.jl",
    "content": "\"\"\"\nAbstract type for models than employ PowerSimulations methods. For custom decision problems\n    use DecisionProblem as the super type.\n\"\"\"\nabstract type DefaultDecisionProblem <: DecisionProblem end\n\n\"\"\"\nGeneric PowerSimulations Operation Problem Type for unspecified models\n\"\"\"\nstruct GenericOpProblem <: DefaultDecisionProblem end\n\n\"\"\"\nAbstract type for models than employ PowerSimulations methods. For custom emulation problems\n    use EmulationProblem as the super type.\n\"\"\"\nabstract type DefaultEmulationProblem <: EmulationProblem end\n\n\"\"\"\nDefault PowerSimulations Emulation Problem Type for unspecified problems\n\"\"\"\nstruct GenericEmulationProblem <: DefaultEmulationProblem end\n"
  },
  {
    "path": "src/operation/operation_problem_templates.jl",
    "content": "\nstruct EconomicDispatchProblem <: DefaultDecisionProblem end\nstruct UnitCommitmentProblem <: DefaultDecisionProblem end\nstruct AGCReserveDeployment <: DefaultDecisionProblem end\n\nfunction _default_devices_uc()\n    return [\n        DeviceModel(PSY.ThermalStandard, ThermalBasicUnitCommitment),\n        DeviceModel(PSY.RenewableDispatch, RenewableFullDispatch),\n        DeviceModel(PSY.RenewableNonDispatch, FixedOutput),\n        DeviceModel(PSY.PowerLoad, StaticPowerLoad),\n        DeviceModel(PSY.InterruptiblePowerLoad, PowerLoadInterruption),\n        DeviceModel(PSY.Line, StaticBranch),\n        DeviceModel(PSY.Transformer2W, StaticBranch),\n        DeviceModel(PSY.TapTransformer, StaticBranch),\n        DeviceModel(PSY.TwoTerminalGenericHVDCLine, HVDCTwoTerminalDispatch),\n    ]\nend\n\nfunction _default_devices_dispatch()\n    default = _default_devices_uc()\n    default[1] = DeviceModel(PSY.ThermalStandard, ThermalBasicDispatch)\n    return default\nend\n\nfunction _default_services()\n    return [\n        ServiceModel(PSY.VariableReserve{PSY.ReserveUp}, RangeReserve),\n        ServiceModel(PSY.VariableReserve{PSY.ReserveDown}, RangeReserve),\n    ]\nend\n\n\"\"\"\n    template_unit_commitment(; kwargs...)\n\nCreates a `ProblemTemplate` with default DeviceModels for a Unit Commitment\nproblem.\n\n# Example\n\ntemplate = template_unit_commitment()\n\n```\n\n# Accepted Key Words\n- `network::Type{<:PM.AbstractPowerModel}` : override default network model settings\n- `devices::Vector{DeviceModel}` : override default `DeviceModel` settings\n- `services::Vector{ServiceModel}` : override default `ServiceModel` settings\n```\n\"\"\"\nfunction template_unit_commitment(; kwargs...)\n    network = get(kwargs, :network, CopperPlatePowerModel)\n    template = ProblemTemplate(network)\n    for model in get(kwargs, :devices, _default_devices_uc())\n        set_device_model!(template, model)\n    end\n\n    for model in get(kwargs, :services, _default_services())\n        set_service_model!(template, model)\n    end\n    return template\nend\n\n\"\"\"\n    template_economic_dispatch(; kwargs...)\n\nCreates a `ProblemTemplate` with default DeviceModels for an Economic Dispatch\nproblem.\n\n# Example\n\ntemplate = template_economic_dispatch()\n\n```\n\n# Accepted Key Words\n- `network::Type{<:PM.AbstractPowerModel}` : override default network model settings\n- `devices::Vector{DeviceModel}` : override default `DeviceModel` settings\n- `services::Vector{ServiceModel}` : override default `ServiceModel` settings\n```\n\"\"\"\nfunction template_economic_dispatch(; kwargs...)\n    network = get(kwargs, :network, CopperPlatePowerModel)\n    template = ProblemTemplate(network)\n    for model in get(kwargs, :devices, _default_devices_dispatch())\n        set_device_model!(template, model)\n    end\n\n    for model in get(kwargs, :services, _default_services())\n        set_service_model!(template, model)\n    end\n\n    return template\nend\n\n#=\n\"\"\"\n    template_agc_reserve_deployment(; kwargs...)\n\nCreates a `ProblemTemplate` with default DeviceModels for an AGC Reserve Deployment Problem. This model doesn't support customization\n\n# Example\n\ntemplate = agc_reserve_deployment()\n\"\"\"\nfunction template_agc_reserve_deployment(; kwargs...)\n    if !isempty(kwargs)\n        throw(ArgumentError(\"AGC Template doesn't currently support customization\"))\n    end\n    template = ProblemTemplate(AreaBalancePowerModel)\n    set_device_model!(template, PSY.ThermalStandard, FixedOutput)\n    set_device_model!(template, PSY.RenewableDispatch, FixedOutput)\n    set_device_model!(template, PSY.PowerLoad, StaticPowerLoad)\n    set_device_model!(template, PSY.HydroEnergyReservoir, FixedOutput)\n    set_device_model!(template, PSY.HydroDispatch, FixedOutput)\n    set_device_model!(template, PSY.RenewableNonDispatch, FixedOutput)\n    set_device_model!(\n        template,\n        DeviceModel(PSY.RegulationDevice{PSY.ThermalStandard}, DeviceLimitedRegulation),\n    )\n    set_device_model!(\n        template,\n        DeviceModel(PSY.RegulationDevice{PSY.HydroDispatch}, ReserveLimitedRegulation),\n    )\n    set_device_model!(\n        template,\n        DeviceModel(\n            PSY.RegulationDevice{PSY.HydroEnergyReservoir},\n            ReserveLimitedRegulation,\n        ),\n    )\n    set_service_model!(template, ServiceModel(PSY.AGC, PIDSmoothACE))\n    return template\nend\n=#\n"
  },
  {
    "path": "src/operation/optimization_debugging.jl",
    "content": "\"\"\"\nEach Tuple corresponds to (con_name, internal_index, moi_index)\n\"\"\"\nfunction get_all_constraint_index(model::OperationModel)\n    con_index = Vector{Tuple{ConstraintKey, Int, Int}}()\n    container = get_optimization_container(model)\n    for (key, value) in get_constraints(container)\n        for (idx, constraint) in enumerate(value)\n            moi_index = JuMP.optimizer_index(constraint)\n            push!(con_index, (key, idx, moi_index.value))\n        end\n    end\n    return con_index\nend\n\n\"\"\"\nEach Tuple corresponds to (con_name, internal_index, moi_index)\n\"\"\"\nfunction get_all_variable_index(model::OperationModel)\n    var_keys = get_all_variable_keys(model)\n    return [(ISOPT.encode_key(v[1]), v[2], v[3]) for v in var_keys]\nend\n\nfunction get_all_variable_keys(model::OperationModel)\n    var_index = Vector{Tuple{VariableKey, Int, Int}}()\n    container = get_optimization_container(model)\n    for (key, value) in get_variables(container)\n        for (idx, variable) in enumerate(value)\n            moi_index = JuMP.optimizer_index(variable)\n            push!(var_index, (key, idx, moi_index.value))\n        end\n    end\n    return var_index\nend\n\nfunction get_constraint_index(model::OperationModel, index::Int)\n    container = get_optimization_container(model)\n    constraints = get_constraints(container)\n    for i in get_all_constraint_index(model)\n        if i[3] == index\n            return constraints[i[1]].data[i[2]]\n        end\n    end\n    @info \"Index not found\"\n    return\nend\n\nfunction get_variable_index(model::OperationModel, index::Int)\n    container = get_optimization_container(model)\n    variables = get_variables(container)\n    for i in get_all_variable_keys(model)\n        if i[3] == index\n            return variables[i[1]].data[i[2]]\n        end\n    end\n    @info \"Index not found\"\n    return\nend\n\nfunction get_detailed_constraint_numerical_bounds(model::OperationModel)\n    if !is_built(model)\n        error(\"Model not built, can't calculate constraint numerical bounds\")\n    end\n    constraint_bounds = Dict()\n    for (const_key, constraint_array) in get_constraints(get_optimization_container(model))\n        if isa(constraint_array, SparseAxisArray)\n            bounds = ConstraintBounds()\n            for idx in eachindex(constraint_array)\n                constraint_array[idx] == 0.0 && continue\n                con_obj = JuMP.constraint_object(constraint_array[idx])\n                update_coefficient_bounds(bounds, con_obj, idx)\n                update_rhs_bounds(bounds, con_obj, idx)\n            end\n            constraint_bounds[const_key] = bounds\n        else\n            bounds = ConstraintBounds()\n            for idx in Iterators.product(constraint_array.axes...)\n                con_obj = JuMP.constraint_object(constraint_array[idx...])\n                update_coefficient_bounds(bounds, con_obj, idx)\n                update_rhs_bounds(bounds, con_obj, idx)\n            end\n            constraint_bounds[const_key] = bounds\n        end\n    end\n    return constraint_bounds\nend\n\nfunction get_detailed_variable_numerical_bounds(model::OperationModel)\n    if !is_built(model)\n        error(\"Model not built, can't calculate variable numerical bounds\")\n    end\n    variable_bounds = Dict()\n    for (variable_key, variable_array) in get_variables(get_optimization_container(model))\n        bounds = VariableBounds()\n        if isa(variable_array, SparseAxisArray)\n            for idx in eachindex(variable_array)\n                var = variable_array[idx]\n                var == 0.0 && continue\n                update_variable_bounds(bounds, var, idx)\n            end\n        else\n            for idx in Iterators.product(variable_array.axes...)\n                var = variable_array[idx...]\n                update_variable_bounds(bounds, var, idx)\n            end\n        end\n        variable_bounds[variable_key] = bounds\n    end\n    return variable_bounds\nend\n"
  },
  {
    "path": "src/operation/problem_results.jl",
    "content": "\"\"\"\nConstruct OptimizationProblemResults from a solved DecisionModel.\n\"\"\"\nfunction OptimizationProblemResults(model::DecisionModel)\n    status = get_run_status(model)\n    status != RunStatus.SUCCESSFULLY_FINALIZED &&\n        error(\"problem was not solved successfully: $status\")\n\n    model_store = get_store(model)\n\n    if isempty(model_store)\n        error(\"Model Solved as part of a Simulation.\")\n    end\n\n    timestamps = get_timestamps(model)\n    optimizer_stats = ISOPT.to_dataframe(get_optimizer_stats(model))\n\n    aux_variable_values =\n        Dict(x => read_aux_variable(model, x) for x in list_aux_variable_keys(model))\n    variable_values = Dict(x => read_variable(model, x) for x in list_variable_keys(model))\n    dual_values = Dict(x => read_dual(model, x) for x in list_dual_keys(model))\n    parameter_values =\n        Dict(x => read_parameter(model, x) for x in list_parameter_keys(model))\n    expression_values =\n        Dict(x => read_expression(model, x) for x in list_expression_keys(model))\n\n    sys = get_system(model)\n\n    return OptimizationProblemResults(\n        get_problem_base_power(model),\n        timestamps,\n        sys,\n        IS.get_uuid(sys),\n        aux_variable_values,\n        variable_values,\n        dual_values,\n        parameter_values,\n        expression_values,\n        optimizer_stats,\n        get_metadata(get_optimization_container(model)),\n        IS.strip_module_name(typeof(model)),\n        get_output_dir(model),\n        mkpath(joinpath(get_output_dir(model), \"results\")),\n    )\nend\n\n\"\"\"\nConstruct OptimizationProblemResults from a solved EmulationModel.\n\"\"\"\nfunction OptimizationProblemResults(model::EmulationModel)\n    status = get_run_status(model)\n    status != RunStatus.SUCCESSFULLY_FINALIZED &&\n        error(\"problem was not solved successfully: $status\")\n\n    model_store = get_store(model)\n\n    if isempty(model_store)\n        error(\"Model Solved as part of a Simulation.\")\n    end\n\n    aux_variables =\n        Dict(x => read_aux_variable(model, x) for x in list_aux_variable_keys(model))\n    variables = Dict(x => read_variable(model, x) for x in list_variable_keys(model))\n    duals = Dict(x => read_dual(model, x) for x in list_dual_keys(model))\n    parameters = Dict(x => read_parameter(model, x) for x in list_parameter_keys(model))\n    expression = Dict(x => read_expression(model, x) for x in list_expression_keys(model))\n    optimizer_stats = read_optimizer_stats(model)\n    initial_time = get_initial_time(model)\n    container = get_optimization_container(model)\n    sys = get_system(model)\n\n    return OptimizationProblemResults(\n        get_problem_base_power(model),\n        StepRange(initial_time, get_resolution(model), initial_time),\n        sys,\n        IS.get_uuid(sys),\n        aux_variables,\n        variables,\n        duals,\n        parameters,\n        expression,\n        optimizer_stats,\n        get_metadata(container),\n        IS.strip_module_name(typeof(model)),\n        get_output_dir(model),\n        mkpath(joinpath(get_output_dir(model), \"results\")),\n    )\nend\n"
  },
  {
    "path": "src/operation/problem_template.jl",
    "content": "\nconst DevicesModelContainer = Dict{Symbol, DeviceModel}\nconst ServicesModelContainer = Dict{Tuple{String, Symbol}, ServiceModel}\n\nabstract type AbstractProblemTemplate end\n\n\"\"\"\n    ProblemTemplate(::Type{T}) where {T<:PM.AbstractPowerFormulation}\n\nCreates a model reference of the PowerSimulations Optimization Problem.\n\n# Arguments\n\n  - `model::Type{T<:PM.AbstractPowerFormulation}`:\n\n# Example\n\ntemplate = ProblemTemplate(CopperPlatePowerModel)\n\"\"\"\nmutable struct ProblemTemplate <: AbstractProblemTemplate\n    network_model::NetworkModel{<:PM.AbstractPowerModel}\n    devices::DevicesModelContainer\n    branches::BranchModelContainer\n    services::ServicesModelContainer\n    function ProblemTemplate(network::NetworkModel{T}) where {T <: PM.AbstractPowerModel}\n        new(\n            network,\n            DevicesModelContainer(),\n            BranchModelContainer(),\n            ServicesModelContainer(),\n        )\n    end\nend\n\nfunction Base.isempty(template::ProblemTemplate)\n    if !isempty(template.devices)\n        return false\n    elseif !isempty(template.branches)\n        return false\n    elseif !isempty(template.services)\n        return false\n    else\n        return true\n    end\nend\n\nProblemTemplate(::Type{T}) where {T <: PM.AbstractPowerModel} =\n    ProblemTemplate(NetworkModel(T))\nProblemTemplate() = ProblemTemplate(CopperPlatePowerModel)\n\nget_device_models(template::ProblemTemplate) = template.devices\nget_branch_models(template::ProblemTemplate) = template.branches\nget_service_models(template::ProblemTemplate) = template.services\nget_network_model(template::ProblemTemplate) = template.network_model\nget_network_formulation(template::ProblemTemplate) =\n    get_network_formulation(get_network_model(template))\nget_hvdc_network_model(template::ProblemTemplate) =\n    template.network_model.hvdc_network_model\n\nfunction get_component_types(template::ProblemTemplate)::Vector{DataType}\n    return vcat(\n        get_component_type.(values(get_device_models(template))),\n        get_component_type.(values(get_branch_models(template))),\n        get_component_type.(values(get_service_models(template))),\n    )\nend\n\nfunction get_model(template::ProblemTemplate, ::Type{T}) where {T <: PSY.Device}\n    if T <: PSY.Branch\n        return get(template.branches, Symbol(T), nothing)\n    elseif T <: PSY.Device\n        return get(template.devices, Symbol(T), nothing)\n    else\n        error(\"Component $T not present in the template\")\n    end\nend\n\nfunction get_model(\n    template::ProblemTemplate,\n    ::Type{T},\n    name::String = NO_SERVICE_NAME_PROVIDED,\n) where {T <: PSY.Service}\n    if haskey(template.services, (name, Symbol(T)))\n        return template.services[(name, Symbol(T))]\n    else\n        error(\"Service $T $name not present in the template\")\n    end\nend\n\n# Note to devs. PSY exports set_model! these names are chosen to avoid name clashes\n\n\"\"\"\nSets the network model in a template.\n\"\"\"\nfunction set_network_model!(\n    template::ProblemTemplate,\n    model::NetworkModel{<:PM.AbstractPowerModel},\n)\n    template.network_model = model\n    return\nend\n\n\"\"\"\nSets the network model in a template.\n\"\"\"\nfunction set_hvdc_network_model!(\n    template::ProblemTemplate,\n    model::Union{Nothing, AbstractHVDCNetworkModel},\n)\n    set_hvdc_network_model!(template.network_model, model)\n    return\nend\n\n\"\"\"\nSets the network model in a template.\n\"\"\"\nfunction set_hvdc_network_model!(\n    template::ProblemTemplate,\n    model::Type{U},\n) where {U <: AbstractHVDCNetworkModel}\n    set_hvdc_network_model!(template.network_model, model())\n    return\nend\n\n\"\"\"\nSets the device model in a template using the component type and formulation.\nBuilds a default DeviceModel\n\"\"\"\nfunction set_device_model!(\n    template::ProblemTemplate,\n    component_type::Type{<:PSY.Device},\n    formulation::Type{<:AbstractDeviceFormulation},\n)\n    set_device_model!(template, DeviceModel(component_type, formulation))\n    return\nend\n\n\"\"\"\nSets the device model in a template using a DeviceModel instance\n\"\"\"\nfunction set_device_model!(\n    template::ProblemTemplate,\n    model::DeviceModel{<:PSY.Device, <:AbstractDeviceFormulation},\n)\n    _set_model!(template.devices, model)\n    return\nend\n\nfunction set_device_model!(\n    template::ProblemTemplate,\n    model::DeviceModel{<:PSY.Branch, <:AbstractDeviceFormulation},\n)\n    _set_model!(template.branches, model)\n    return\nend\n\n\"\"\"\nSets the service model in a template using a name and the service type and formulation.\nBuilds a default ServiceModel with use_service_name set to true.\n\"\"\"\nfunction set_service_model!(\n    template::ProblemTemplate,\n    service_name::String,\n    service_type::Type{<:PSY.Service},\n    formulation::Type{<:AbstractServiceFormulation},\n)\n    set_service_model!(\n        template,\n        service_name,\n        ServiceModel(service_type, formulation; use_service_name = true),\n    )\n    return\nend\n\n\"\"\"\nSets the service model in a template using a ServiceModel instance.\n\"\"\"\nfunction set_service_model!(\n    template::ProblemTemplate,\n    service_type::Type{<:PSY.Service},\n    formulation::Type{<:AbstractServiceFormulation},\n)\n    set_service_model!(template, ServiceModel(service_type, formulation))\n    return\nend\n\nfunction set_service_model!(\n    template::ProblemTemplate,\n    service_name::String,\n    model::ServiceModel{T, <:AbstractServiceFormulation},\n) where {T <: PSY.Service}\n    _set_model!(template.services, (service_name, Symbol(T)), model)\n    return\nend\n\nfunction set_service_model!(\n    template::ProblemTemplate,\n    model::ServiceModel{<:PSY.Service, <:AbstractServiceFormulation},\n)\n    _set_model!(template.services, model)\n    return\nend\n\nfunction _add_contributing_device_by_type!(\n    service_model::ServiceModel,\n    contributing_device::T,\n    incompatible_device_types::Set{DataType},\n    modeled_devices::Set{DataType},\n) where {T <: PSY.Device}\n    !PSY.get_available(contributing_device) && return\n    if T ∈ incompatible_device_types || T ∉ modeled_devices\n        return\n    end\n    push!(get!(get_contributing_devices_map(service_model), T, T[]), contributing_device)\n    return\nend\n\nfunction _populate_contributing_devices!(template::ProblemTemplate, sys::PSY.System)\n    service_models = get_service_models(template)\n    isempty(service_models) && return\n\n    device_models = get_device_models(template)\n    branch_models = get_branch_models(template)\n    # Type stability: explicitly type the Set to avoid widening to Set{Type}\n    modeled_devices = Set{DataType}(get_component_type(m) for m in values(device_models))\n    union!(modeled_devices, (get_component_type(m) for m in values(branch_models)))\n    incompatible_device_types = get_incompatible_devices(device_models)\n    services_mapping = PSY.get_contributing_device_mapping(sys)\n    if isempty(keys(services_mapping))\n        @warn \"The system doesn't include any services. No services will be modeled, consider removing the service models from the template.\" _group =\n            LOG_GROUP_SERVICE_CONSTUCTORS\n        empty!(service_models)\n        return\n    end\n    for (service_key, service_model) in service_models\n        @debug \"Populating service $(service_key)\"\n        empty!(get_contributing_devices_map(service_model))\n        S = get_component_type(service_model)\n        service = PSY.get_component(S, sys, get_service_name(service_model))\n        if service === nothing\n            @info \"The data doesn't include services of type $(S) and name $(get_service_name(service_model)), consider changing the service models\" _group =\n                LOG_GROUP_SERVICE_CONSTUCTORS\n            continue\n        end\n        service_devices_key = (type = S, name = PSY.get_name(service))\n        contributing_devices_ =\n            services_mapping[service_devices_key].contributing_devices\n        for d in contributing_devices_\n            _add_contributing_device_by_type!(\n                service_model,\n                d,\n                incompatible_device_types,\n                modeled_devices,\n            )\n        end\n        if isempty(get_contributing_devices_map(service_model))\n            error(\n                \"The contributing devices for service $(PSY.get_name(service)) is empty. Add contributing devices to the service in the data to continue.\",\n            )\n        end\n    end\n    return\nend\n\nfunction _modify_device_model!(\n    devices_template::Dict{Symbol, DeviceModel},\n    service_model::ServiceModel{<:PSY.Reserve, <:AbstractReservesFormulation},\n    contributing_devices::Vector{<:PSY.Component},\n)\n    # Type stability: explicitly type the Set to avoid widening\n    for dt in Set{DataType}(typeof.(contributing_devices))\n        for device_model in values(devices_template)\n            # add message here when it exists\n            get_component_type(device_model) != dt && continue\n            service_model in device_model.services && continue\n            # type instability: pushing to vector of abstract type\n            push!(device_model.services, service_model)\n        end\n    end\n\n    return\nend\n\nfunction _modify_device_model!(\n    ::Dict{Symbol, DeviceModel},\n    ::ServiceModel{<:PSY.ReserveNonSpinning, <:AbstractReservesFormulation},\n    ::Vector{<:PSY.Component},\n)\n    return\nend\n\nfunction _modify_device_model!(\n    ::Dict{Symbol, DeviceModel},\n    ::ServiceModel{PSY.TransmissionInterface, ConstantMaxInterfaceFlow},\n    ::Vector,\n)\n    return\nend\n\nfunction _modify_device_model!(\n    ::Dict{Symbol, DeviceModel},\n    ::ServiceModel{PSY.TransmissionInterface, VariableMaxInterfaceFlow},\n    ::Vector,\n)\n    return\nend\n\nfunction _add_services_to_device_model!(template::ProblemTemplate)\n    service_models = get_service_models(template)\n    devices_template = get_device_models(template)\n    for (service_key, service_model) in service_models\n        S = get_component_type(service_model)\n        (S <: PSY.AGC || S <: PSY.ConstantReserveGroup) && continue\n        contributing_devices = get_contributing_devices(service_model)\n        isempty(contributing_devices) && continue\n        _modify_device_model!(devices_template, service_model, contributing_devices)\n    end\n    return\nend\n\nfunction _populate_aggregated_service_model!(template::ProblemTemplate, sys::PSY.System)\n    services_template = get_service_models(template)\n    for (key, service_model) in services_template\n        attributes = get_attributes(service_model)\n        use_slacks = service_model.use_slacks\n        duals = service_model.duals\n        if pop!(attributes, \"aggregated_service_model\", false)\n            delete!(services_template, key)\n            D = get_component_type(service_model)\n            B = get_formulation(service_model)\n            for service in get_available_components(service_model, sys)\n                new_key = (PSY.get_name(service), Symbol(D))\n                if !haskey(services_template, new_key)\n                    template.services[new_key] =\n                        ServiceModel(\n                            D,\n                            B,\n                            PSY.get_name(service);\n                            use_slacks = use_slacks,\n                            duals = duals,\n                            attributes = attributes,\n                        )\n                else\n                    error(\"Key $new_key already assigned in ServiceModel\")\n                end\n            end\n        end\n    end\n    return\nend\n\nfunction finalize_template!(template::ProblemTemplate, sys::PSY.System)\n    _populate_aggregated_service_model!(template, sys)\n    _populate_contributing_devices!(template, sys)\n    _add_services_to_device_model!(template)\n    return\nend\n"
  },
  {
    "path": "src/operation/template_validation.jl",
    "content": "function _check_branch_network_compatibility(\n    ::NetworkModel{T},\n    unmodeled_branch_types::Vector{DataType},\n) where {T <: PM.AbstractPowerModel}\n    if requires_all_branch_models(T) && !isempty(unmodeled_branch_types)\n        for d in unmodeled_branch_types\n            @error \"The system has a branch branch type $(d) but the DeviceModel is not included in the Template.\"\n        end\n        throw(\n            IS.ConflictingInputsError(\n                \"Network model $(T) requires all AC Transmission devices have a model\",\n            ),\n        )\n    end\n    return\nend\n\nfunction _validate_branch_models(\n    ::Type{T},\n    model_has_branch_filters::Bool,\n) where {T <: PM.AbstractPowerModel}\n    if supports_branch_filtering(T) || !model_has_branch_filters\n        return\n    elseif model_has_branch_filters\n        if ignores_branch_filtering(T)\n            @warn \"Branch filtering is ignored for network model $(T)\"\n        else\n            throw(\n                IS.ConflictingInputsError(\n                    \"Branch filtering is not supported for network model $(T). Remove branch \\\\\n                    filter functions from branch models or use a different network model.\",\n                ),\n            )\n        end\n    else\n        throw(\n            IS.ConflictingInputsError(\n                \"Network model $(T) can't be validated against branch models\",\n            ),\n        )\n    end\n    return\nend\n\nfunction validate_network_model(network_model::NetworkModel{T},\n    unmodeled_branch_types::Vector{DataType},\n    model_has_branch_filters::Bool,\n) where {T <: PM.AbstractPowerModel}\n    _check_branch_network_compatibility(network_model, unmodeled_branch_types)\n    _validate_branch_models(T, model_has_branch_filters)\n    return\nend\n\nfunction validate_template_impl!(model::OperationModel)\n    template = get_template(model)\n    settings = get_settings(model)\n    if isempty(template)\n        error(\"Template can't be empty for models $(get_problem_type(model))\")\n    end\n    system = get_system(model)\n    modeled_types = get_component_types(template)\n    system_component_types = PSY.get_existing_component_types(system)\n    network_model = get_network_model(template)\n    valid_device_types = union(modeled_types, _TEMPLATE_VALIDATION_EXCLUSIONS)\n    unmodeled_branch_types = DataType[]\n\n    for m in setdiff(system_component_types, valid_device_types)\n        @warn \"The template doesn't include models for components of type $(m), consider changing the template\" _group =\n            LOG_GROUP_MODELS_VALIDATION\n        if m <: PSY.ACTransmission\n            push!(unmodeled_branch_types, m)\n        end\n    end\n\n    device_keys_to_delete = Symbol[]\n    for (k, device_model) in model.template.devices\n        make_device_cache!(device_model, system, get_check_components(settings))\n        if isempty(get_device_cache(device_model))\n            @info \"The system data doesn't include devices of type $(k), consider changing the models in the template\" _group =\n                LOG_GROUP_MODELS_VALIDATION\n            push!(device_keys_to_delete, k)\n        end\n    end\n    for k in device_keys_to_delete\n        delete!(model.template.devices, k)\n    end\n\n    model_has_branch_filters = false\n    branch_keys_to_delete = Symbol[]\n    for (k, device_model) in model.template.branches\n        make_device_cache!(device_model, system, get_check_components(settings))\n        if isempty(get_device_cache(device_model))\n            @info \"The system data doesn't include Branches of type $(k), consider changing the models in the template\" _group =\n                LOG_GROUP_MODELS_VALIDATION\n            push!(branch_keys_to_delete, k)\n        else\n            push!(network_model.modeled_ac_branch_types, get_component_type(device_model))\n        end\n        if get_attribute(device_model, \"filter_function\") !== nothing\n            model_has_branch_filters = true\n        end\n    end\n    for k in branch_keys_to_delete\n        delete!(model.template.branches, k)\n    end\n    validate_network_model(network_model, unmodeled_branch_types, model_has_branch_filters)\n    return\nend\n"
  },
  {
    "path": "src/operation/time_series_interface.jl",
    "content": "function get_time_series_values!(\n    time_series_type::Type{T},\n    model::DecisionModel,\n    component,\n    name::String,\n    initial_time::Dates.DateTime,\n    horizon::Int;\n    ignore_scaling_factors = true,\n    interval::Dates.Millisecond = UNSET_INTERVAL,\n) where {T <: PSY.Forecast}\n    is_interval = _to_is_interval(interval)\n    settings = get_settings(model)\n    resolution = get_resolution(settings)\n    if !use_time_series_cache(settings)\n        return IS.get_time_series_values(\n            T,\n            component,\n            name;\n            start_time = initial_time,\n            len = horizon,\n            ignore_scaling_factors = ignore_scaling_factors,\n            interval = is_interval,\n        )\n    end\n\n    cache = get_time_series_cache(model)\n    key = IS.TimeSeriesCacheKey(IS.get_uuid(component), T, name, resolution, is_interval)\n    if haskey(cache, key)\n        ts_cache = cache[key]\n    else\n        ts_cache = IS.make_time_series_cache(\n            time_series_type,\n            component,\n            name,\n            initial_time,\n            horizon;\n            ignore_scaling_factors = ignore_scaling_factors,\n            interval = is_interval,\n            resolution = resolution,\n        )\n        cache[key] = ts_cache\n    end\n\n    ts = IS.get_time_series_array!(ts_cache, initial_time)\n    return TimeSeries.values(ts)\nend\n\nfunction get_time_series_values!(\n    ::Type{T},\n    model::EmulationModel,\n    component::U,\n    name::String,\n    initial_time::Dates.DateTime,\n    len::Int = 1;\n    ignore_scaling_factors = true,\n    resolution::Dates.Millisecond = UNSET_RESOLUTION,\n) where {T <: PSY.StaticTimeSeries, U <: PSY.Component}\n    settings = get_settings(model)\n    key_resolution =\n        resolution == UNSET_RESOLUTION ? get_resolution(settings) : resolution\n    is_resolution = _to_is_resolution(key_resolution)\n    if !use_time_series_cache(settings)\n        return IS.get_time_series_values(\n            T,\n            component,\n            name;\n            start_time = initial_time,\n            len = len,\n            ignore_scaling_factors = ignore_scaling_factors,\n            resolution = is_resolution,\n        )\n    end\n\n    cache = get_time_series_cache(model)\n    key = IS.TimeSeriesCacheKey(IS.get_uuid(component), T, name, key_resolution, nothing)\n    if haskey(cache, key)\n        ts_cache = cache[key]\n    else\n        ts_cache = IS.make_time_series_cache(\n            T,\n            component,\n            name,\n            initial_time,\n            len;\n            ignore_scaling_factors = ignore_scaling_factors,\n            resolution = is_resolution,\n        )\n        cache[key] = ts_cache\n    end\n\n    ts = IS.get_time_series_array!(ts_cache, initial_time)\n    return TimeSeries.values(ts)\nend\n"
  },
  {
    "path": "src/parameters/add_parameters.jl",
    "content": "function add_parameters!(\n    container::OptimizationContainer,\n    ::Type{T},\n    devices::U,\n    model::DeviceModel{D, W},\n) where {\n    T <: ParameterType,\n    U <: Union{Vector{D}, IS.FlattenIteratorWrapper{D}},\n    W <: AbstractDeviceFormulation,\n} where {D <: PSY.Component}\n    if get_rebuild_model(get_settings(container)) && has_container_key(container, T, D)\n        return\n    end\n    _add_parameters!(container, T(), devices, model)\n    return\nend\n\nfunction add_branch_parameters!(\n    container::OptimizationContainer,\n    ::Type{T},\n    devices::U,\n    model::DeviceModel{D, W},\n    network_model::NetworkModel{<:AbstractPTDFModel},\n) where {\n    T <: ParameterType,\n    U <: Union{Vector{D}, IS.FlattenIteratorWrapper{D}},\n    W <: AbstractDeviceFormulation,\n} where {D <: PSY.ACTransmission}\n    if get_rebuild_model(get_settings(container)) && has_container_key(container, T, D)\n        return\n    end\n    _add_time_series_parameters!(container, T(), network_model, devices, model)\n    return\nend\n\nfunction add_parameters!(\n    container::OptimizationContainer,\n    ::Type{T},\n    devices::U,\n    device_model::DeviceModel{D, W},\n    event_model::EventModel{V, X},\n) where {\n    T <: ParameterType,\n    U <: Vector{D},\n    V <: PSY.Contingency,\n    W <: AbstractDeviceFormulation,\n    X <: AbstractEventCondition,\n} where {D <: PSY.Component}\n    if get_rebuild_model(get_settings(container)) && has_container_key(container, T, D)\n        return\n    end\n    _add_parameters!(container, T(), devices, device_model, event_model)\n    return\nend\n\nfunction add_parameters!(\n    container::OptimizationContainer,\n    ::Type{T},\n    ff::LowerBoundFeedforward,\n    model::ServiceModel{S, W},\n    devices::V,\n) where {\n    S <: PSY.AbstractReserve,\n    T <: VariableValueParameter,\n    V <: Union{Vector{D}, IS.FlattenIteratorWrapper{D}},\n    W <: AbstractReservesFormulation,\n} where {D <: PSY.Component}\n    if get_rebuild_model(get_settings(container)) && has_container_key(container, T, S)\n        return\n    end\n    source_key = get_optimization_container_key(ff)\n    _add_parameters!(container, T(), source_key, model, devices)\n    return\nend\n\nfunction add_parameters!(\n    container::OptimizationContainer,\n    ::Type{T},\n    service::U,\n    model::ServiceModel{U, V},\n) where {T <: TimeSeriesParameter, U <: PSY.Service, V <: AbstractServiceFormulation}\n    if get_rebuild_model(get_settings(container)) &&\n       has_container_key(container, T, U, PSY.get_name(service))\n        return\n    end\n    _add_parameters!(container, T(), service, model)\n    return\nend\n\nfunction add_parameters!(\n    container::OptimizationContainer,\n    ::Type{T},\n    ff::AbstractAffectFeedforward,\n    model::DeviceModel{D, W},\n    devices::V,\n) where {\n    T <: VariableValueParameter,\n    V <: Union{Vector{D}, IS.FlattenIteratorWrapper{D}},\n    W <: AbstractDeviceFormulation,\n} where {D <: PSY.Component}\n    if get_rebuild_model(get_settings(container)) && has_container_key(container, T, D)\n        return\n    end\n    source_key = get_optimization_container_key(ff)\n    _add_parameters!(container, T(), source_key, model, devices)\n    return\nend\n\nfunction add_parameters!(\n    container::OptimizationContainer,\n    ::Type{T},\n    ff::FixValueFeedforward,\n    model::DeviceModel{D, W},\n    devices::V,\n) where {\n    T <: VariableValueParameter,\n    V <: Union{Vector{D}, IS.FlattenIteratorWrapper{D}},\n    W <: AbstractDeviceFormulation,\n} where {D <: PSY.Component}\n    if get_rebuild_model(get_settings(container)) && has_container_key(container, T, D)\n        return\n    end\n    source_key = get_optimization_container_key(ff)\n    _add_parameters!(container, T(), source_key, model, devices)\n    _set_affected_variables!(container, T(), D, ff)\n    return\nend\n\nfunction add_parameters!(\n    container::OptimizationContainer,\n    ::Type{T},\n    ff::FixValueFeedforward,\n    model::ServiceModel{K, W},\n    devices::V,\n) where {\n    T <: VariableValueParameter,\n    V <: Union{Vector{D}, IS.FlattenIteratorWrapper{D}},\n    W <: AbstractServiceFormulation,\n    K <: PSY.Reserve,\n} where {D <: PSY.Component}\n    if get_rebuild_model(get_settings(container)) && has_container_key(container, T, D)\n        return\n    end\n    source_key = get_optimization_container_key(ff)\n    _add_parameters!(container, T(), source_key, model, devices)\n    _set_affected_variables!(container, T(), K, ff)\n    return\nend\n\nfunction _set_affected_variables!(\n    container::OptimizationContainer,\n    ::T,\n    device_type::Type{U},\n    ff::FixValueFeedforward,\n) where {\n    T <: VariableValueParameter,\n    U <: PSY.Component,\n}\n    source_key = get_optimization_container_key(ff)\n    var_type = get_entry_type(source_key)\n    parameter_container = get_parameter(container, T(), U, \"$var_type\")\n    param_attributes = get_attributes(parameter_container)\n    affected_variables = get_affected_values(ff)\n    push!(param_attributes.affected_keys, affected_variables...)\n    return\nend\n\nfunction _set_affected_variables!(\n    container::OptimizationContainer,\n    ::T,\n    device_type::Type{U},\n    ff::FixValueFeedforward,\n) where {\n    T <: VariableValueParameter,\n    U <: PSY.Service,\n}\n    meta = ff.optimization_container_key.meta\n    parameter_container = get_parameter(container, T(), U, meta)\n    param_attributes = get_attributes(parameter_container)\n    affected_variables = get_affected_values(ff)\n    push!(param_attributes.affected_keys, affected_variables...)\n    return\nend\n\nfunction _add_parameters!(\n    container::OptimizationContainer,\n    param::T,\n    devices::U,\n    model::DeviceModel{D, W},\n) where {\n    T <: TimeSeriesParameter,\n    U <: Union{Vector{D}, IS.FlattenIteratorWrapper{D}},\n    W <: AbstractDeviceFormulation,\n} where {D <: PSY.Component}\n    _add_time_series_parameters!(container, param, devices, model)\n    return\nend\n\nfunction _check_dynamic_branch_rating_ts(\n    ts::AbstractArray,\n    ::T,\n    device::PSY.Device,\n    model::DeviceModel{D, W},\n) where {D <: PSY.Component, T <: TimeSeriesParameter, W <: AbstractDeviceFormulation}\n    if !(T <: AbstractDynamicBranchRatingTimeSeriesParameter)\n        return\n    end\n\n    rating = PSY.get_rating(device)\n    if (T <: PostContingencyDynamicBranchRatingTimeSeriesParameter)\n        if !(PSY.get_rating_b(device) === nothing)\n            rating = PSY.get_rating_b(device)\n        else\n            @warn \"Device $(typeof(device)) '$(PSY.get_name(device))' has Parameter $T but it has no static 'rating_b' defined.\"\n        end\n    end\n\n    multiplier = get_multiplier_value(T(), device, W())\n    if !all(x -> x >= rating, multiplier * ts)\n        @warn \"There are values of Parameter $T associated with $(typeof(device)) '$(PSY.get_name(device))' lower than the device static rating $(rating).\"\n    end\n    return\nend\n\n# Extends `size` to tuples, treating them like scalars\n_size_wrapper(elem) = size(elem)\n_size_wrapper(::Tuple) = ()\n\nfunction _add_time_series_parameters!(\n    container::OptimizationContainer,\n    param::T,\n    network_model::NetworkModel{<:AbstractPTDFModel},\n    devices,\n    model::DeviceModel{D, W},\n) where {D <: PSY.ACTransmission, T <: TimeSeriesParameter, W <: AbstractDeviceFormulation}\n    ts_type = get_default_time_series_type(container)\n    if !(ts_type <: Union{PSY.AbstractDeterministic, PSY.StaticTimeSeries})\n        error(\"add_parameters! for TimeSeriesParameter is not compatible with $ts_type\")\n    end\n    time_steps = get_time_steps(container)\n\n    net_reduction_data = network_model.network_reduction\n    reduced_branch_tracker = get_reduced_branch_tracker(network_model)\n    all_branch_maps_by_type = PNM.get_all_branch_maps_by_type(net_reduction_data)\n\n    # TODO: Temporary workaround to get the name where we assume all the names are the same accross devices.\n    ts_name = _get_time_series_name(T(), first(devices), model)\n    model_interval = get_interval(get_settings(container))\n    ts_interval = model_interval\n    device_name_axis, ts_uuid_axis =\n        get_branch_argument_parameter_axes(\n            net_reduction_data,\n            devices,\n            ts_type,\n            ts_name;\n            interval = ts_interval,\n        )\n    if isempty(device_name_axis)\n        @info \"No devices with time series $ts_name found for $D devices. Skipping parameter addition.\"\n        return\n    end\n    # name -> ts_uuid cache built from the axis pair so the per-branch loop below\n    # doesn't re-query IS.get_time_series_uuid for each branch.\n    branch_ts_uuids = Dict{String, String}(zip(device_name_axis, ts_uuid_axis))\n    additional_axes = ()\n    param_container = add_param_container!(\n        container,\n        param,\n        D,\n        ts_type,\n        ts_name,\n        ts_uuid_axis,\n        device_name_axis,\n        additional_axes,\n        time_steps,\n    )\n    set_subsystem!(get_attributes(param_container), get_subsystem(model))\n    param_instance = T()\n    jump_model = get_jump_model(container)\n    parent_param = get_parameter_array_data(param_container)\n    parent_mult = get_multiplier_array_data(param_container)\n    # The param array's first axis is `ts_uuid_axis` (UUID-keyed) while the\n    # multiplier array's first axis is `device_name_axis` (name-keyed); we need\n    # two separate row lookups so parallel branches sharing a UUID still write\n    # their multiplier to the correct (per-branch-name) row.\n    param_lookup = get_parameter_array(param_container).lookup[1]\n    mult_lookup = get_multiplier_array(param_container).lookup[1]\n    for (name, (arc, reduction)) in PNM.get_name_to_arc_map(net_reduction_data, D)\n        reduction_entry = all_branch_maps_by_type[reduction][D][arc]\n        if !PNM.has_time_series(reduction_entry, ts_type, ts_name)\n            continue\n        end\n        device_with_time_series =\n            PNM.get_device_with_time_series(reduction_entry, ts_type, ts_name)\n        ts_uuid = branch_ts_uuids[name]\n        i_param = param_lookup[ts_uuid]\n        i_mult = mult_lookup[name]\n\n        has_entry, tracker_container = search_for_reduced_branch_parameter!(\n            reduced_branch_tracker,\n            arc,\n            T,\n        )\n\n        if has_entry\n            @assert !isempty(tracker_container) name arc reduction\n        else\n            raw_ts_vals = get_time_series_initial_values!(\n                container,\n                ts_type,\n                device_with_time_series,\n                ts_name;\n                interval = ts_interval,\n            )\n            ts_vals =\n                _unwrap_for_param.(Ref(param_instance), raw_ts_vals, Ref(additional_axes))\n            @assert all(_size_wrapper.(ts_vals) .== Ref(length.(additional_axes)))\n        end\n        multiplier = get_multiplier_value(T(), reduction_entry, W())\n        _set_multiplier_at!(parent_mult, Float64(multiplier), i_mult)\n        for t in time_steps\n            if !has_entry\n                # Store raw float in tracker for non-recurrent builds. For recurrent\n                # builds (JuMP parameters), read back the VariableRef that the fast-path\n                # setter just created so that parallel branch types share the same\n                # JuMP parameter.\n                _set_parameter_at!(parent_param, jump_model, ts_vals[t], i_param, t)\n                if built_for_recurrent_solves(container)\n                    tracker_container[t] = parent_param[i_param, t]\n                else\n                    tracker_container[t] = ts_vals[t]\n                end\n            else\n                # Reuse the value (Float64) or VariableRef already stored by the first\n                # branch type that processed this arc.\n                _set_parameter_at!(\n                    parent_param,\n                    jump_model,\n                    tracker_container[t],\n                    i_param,\n                    t,\n                )\n            end\n        end\n        add_component_name!(get_attributes(param_container), name, ts_uuid)\n    end\n    return\nend\n\n# NOTE direct equivalent of _add_parameters! on ObjectiveFunctionParameter\n# PERF: compilation hotspot. Switch to TSC.\nfunction _add_time_series_parameters!(\n    container::OptimizationContainer,\n    param::T,\n    devices,\n    model::DeviceModel{D, W},\n) where {D <: PSY.Component, T <: TimeSeriesParameter, W <: AbstractDeviceFormulation}\n    ts_type = get_default_time_series_type(container)\n    if !(ts_type <: Union{PSY.AbstractDeterministic, PSY.StaticTimeSeries})\n        error(\"add_parameters! for TimeSeriesParameter is not compatible with $ts_type\")\n    end\n\n    time_steps = get_time_steps(container)\n    # TODO: Temporary workaround to get the name where we assume all the names are the same accross devices.\n    ts_name = _get_time_series_name(T(), first(devices), model)\n\n    device_names = String[]\n    devices_with_time_series = D[]\n    initial_values = Dict{String, AbstractArray}()\n    # device name -> ts_uuid cache so the second loop below doesn't re-query IS.\n    device_ts_uuids = Dict{String, String}()\n    model_interval = get_interval(get_settings(container))\n    is_ts_interval = _to_is_interval(model_interval)\n    model_resolution = get_resolution(get_settings(container))\n    is_ts_resolution = _to_is_resolution(model_resolution)\n\n    @debug \"adding\" T D ts_name ts_type _group = LOG_GROUP_OPTIMIZATION_CONTAINER\n\n    for device::D in devices\n        if !PSY.has_time_series(device, ts_type, ts_name)\n            @debug \"Time series $(ts_type):$(ts_name) for $D, $(PSY.get_name(device)) not found. Skipping parameter addition for this device.\"\n            continue\n        end\n        device_name = PSY.get_name(device)\n        push!(device_names, device_name)\n        push!(devices_with_time_series, device)\n        ts_uuid = string(\n            IS.get_time_series_uuid(\n                ts_type,\n                device,\n                ts_name;\n                resolution = is_ts_resolution,\n                interval = is_ts_interval,\n            ),\n        )\n        device_ts_uuids[device_name] = ts_uuid\n        if !(ts_uuid in keys(initial_values))\n            initial_values[ts_uuid] =\n                get_time_series_initial_values!(\n                    container,\n                    ts_type,\n                    device,\n                    ts_name;\n                    interval = model_interval,\n                    resolution = model_resolution,\n                )\n            _check_dynamic_branch_rating_ts(initial_values[ts_uuid], param, device, model)\n        end\n    end\n\n    #=\n    # NOTE this is always the case for \"normal\" time series, but it is currently not enforced in PSY for MBC time series.\n    # TODO decide whether this is an acceptable restriction or whether we need to support multiple time series names\n    # JD: Yes, the restriction are that the names for this has to be unique as they are specified from the model attributes\n    if isempty(active_devices)\n        return\n    end\n    unique_ts_names = unique(ts_names)\n    if length(unique_ts_names) > 1\n        throw(\n            ArgumentError(\n                \"All time series names must be equal for parameter $T within a given device type. Got $unique_ts_names for device type $D\",\n            ),\n        )\n    end\n    ts_name = only(unique_ts_names)\n    =#\n\n    if isempty(device_names)\n        @info \"No devices with time series $ts_name found for $D devices. Skipping parameter addition.\"\n        return\n    end\n\n    additional_axes =\n        calc_additional_axes(container, param, devices_with_time_series, model)\n    param_container = add_param_container!(\n        container,\n        param,\n        D,\n        ts_type,\n        ts_name,\n        collect(keys(initial_values)),\n        device_names,\n        additional_axes,\n        time_steps,\n    )\n    set_subsystem!(get_attributes(param_container), get_subsystem(model))\n\n    jump_model = get_jump_model(container)\n    param_instance = T()\n    parent_param = get_parameter_array_data(param_container)\n    # `param_axs = collect(keys(initial_values))` was passed to `add_param_container!`\n    # above, so `initial_values`'s iteration order matches the container's first axis.\n    for (i, (ts_uuid, raw_ts_vals)) in enumerate(initial_values)\n        ts_vals = _unwrap_for_param.(Ref(param_instance), raw_ts_vals, Ref(additional_axes))\n        @assert all(_size_wrapper.(ts_vals) .== Ref(length.(additional_axes)))\n\n        for step in time_steps\n            _set_parameter_at!(parent_param, jump_model, ts_vals[step], i, step)\n        end\n    end\n\n    parent_mult = get_multiplier_array_data(param_container)\n    # `devices_with_time_series` was built in the same order as `device_names`, which\n    # matches the multiplier array's first axis, so enumeration index `i` is correct.\n    for (i, device) in enumerate(devices_with_time_series)\n        multiplier = get_multiplier_value(T(), device, W())\n        device_name = PSY.get_name(device)\n        _set_multiplier_at!(parent_mult, Float64(multiplier), i)\n        add_component_name!(\n            get_attributes(param_container),\n            device_name,\n            device_ts_uuids[device_name],\n        )\n    end\n    return\nend\n\n# Layer of indirection to deal with the fact that some time series names are stored in the component\n_get_time_series_name(::T, ::PSY.Component, model::DeviceModel) where {T <: ParameterType} =\n    get_time_series_names(model)[T]\n\n_get_time_series_name(::StartupCostParameter, device::PSY.Component, ::DeviceModel) =\n    get_name(PSY.get_start_up(PSY.get_operation_cost(device)))\n\n_get_time_series_name(::ShutdownCostParameter, device::PSY.Component, ::DeviceModel) =\n    get_name(PSY.get_shut_down(PSY.get_operation_cost(device)))\n\n_get_time_series_name(\n    ::IncrementalCostAtMinParameter,\n    device::PSY.Device,\n    ::DeviceModel,\n) =\n    get_name(PSY.get_incremental_initial_input(PSY.get_operation_cost(device)))\n\n_get_time_series_name(\n    ::DecrementalCostAtMinParameter,\n    device::PSY.Device,\n    ::DeviceModel,\n) =\n    get_name(PSY.get_decremental_initial_input(PSY.get_operation_cost(device)))\n\n_get_time_series_name(\n    ::Union{\n        IncrementalPiecewiseLinearSlopeParameter,\n        IncrementalPiecewiseLinearBreakpointParameter,\n    },\n    device::PSY.Device,\n    ::DeviceModel,\n) =\n    get_name(get_output_offer_curves(PSY.get_operation_cost(device)))\n\n_get_time_series_name(\n    ::Union{\n        DecrementalPiecewiseLinearSlopeParameter,\n        DecrementalPiecewiseLinearBreakpointParameter,\n    },\n    device::PSY.Device,\n    ::DeviceModel,\n) =\n    get_name(get_input_offer_curves(PSY.get_operation_cost(device)))\n\n# Layer of indirection to figure out what eltype we expect to find in various time series\n# (we could just read the time series and figure it out dynamically if this becomes too brittle)\n_get_expected_time_series_eltype(::T) where {T <: ParameterType} = Float64\n_get_expected_time_series_eltype(::StartupCostParameter) = NTuple{3, Float64}\n\n# Lookup that defines which variables the ObjectiveFunctionParameter corresponds to\n_param_to_vars(::FuelCostParameter, ::AbstractDeviceFormulation) = (ActivePowerVariable,)\n_param_to_vars(::StartupCostParameter, ::AbstractThermalFormulation) = (StartVariable,)\n_param_to_vars(::StartupCostParameter, ::ThermalMultiStartUnitCommitment) =\n    MULTI_START_VARIABLES\n_param_to_vars(::ShutdownCostParameter, ::AbstractThermalFormulation) = (StopVariable,)\n_param_to_vars(::AbstractCostAtMinParameter, ::AbstractDeviceFormulation) = (OnVariable,)\n_param_to_vars(\n    ::Union{\n        IncrementalPiecewiseLinearSlopeParameter,\n        IncrementalPiecewiseLinearBreakpointParameter,\n    },\n    ::AbstractDeviceFormulation,\n) =\n    (PiecewiseLinearBlockIncrementalOffer,)\n_param_to_vars(\n    ::Union{\n        DecrementalPiecewiseLinearSlopeParameter,\n        DecrementalPiecewiseLinearBreakpointParameter,\n    },\n    ::AbstractDeviceFormulation,\n) =\n    (PiecewiseLinearBlockDecrementalOffer,)\n\n# Layer of indirection to handle possible additional axes. Most parameters have just the two\n# usual axes (device, timestamp), but some have a third (e.g., piecewise tranche)\ncalc_additional_axes(\n    ::OptimizationContainer,\n    ::T,\n    ::U,\n    ::DeviceModel{D, W},\n) where {\n    T <: ParameterType,\n    U <: Union{Vector{D}, IS.FlattenIteratorWrapper{D}},\n    W <: AbstractDeviceFormulation,\n} where {D <: PSY.Component} = ()\n\ncalc_additional_axes(\n    ::OptimizationContainer,\n    ::T,\n    ::U,\n    ::ServiceModel{D, W},\n) where {\n    T <: ParameterType,\n    U <: Union{Vector{D}, IS.FlattenIteratorWrapper{D}},\n    W <: AbstractServiceFormulation,\n} where {D <: PSY.Service} = ()\n\n_get_max_tranches(data::Vector{IS.PiecewiseStepData}) = maximum(length.(data))\n_get_max_tranches(data::TimeSeries.TimeArray) = _get_max_tranches(values(data))\n_get_max_tranches(data::AbstractDict) = maximum(_get_max_tranches.(values(data)))\n\n# Iterate through all periods of a piecewise time series and return the maximum number of tranches\nfunction get_max_tranches(device::PSY.Device, piecewise_ts::IS.TimeSeriesKey)\n    data = PSY.get_data(PSY.get_time_series(device, piecewise_ts))\n    max_tranches = _get_max_tranches(data)\n    return max_tranches\nend\n\n# It's nice for debugging purposes to have meaningful labels on the tranche axis. These\n# labels are never relied upon in the current implementation\nmake_tranche_axis(n_tranches) = \"tranche_\" .* string.(1:n_tranches)\n\n# Find the global maximum number of tranches we'll have to handle and create the parameter with an axis of that length\nfunction calc_additional_axes(\n    ::OptimizationContainer,\n    ::P,\n    devices::U,\n    ::DeviceModel{D, W},\n) where {\n    P <: AbstractPiecewiseLinearSlopeParameter,\n    U <: Union{Vector{D}, IS.FlattenIteratorWrapper{D}},\n    W <: AbstractDeviceFormulation,\n} where {D <: PSY.Component}\n    curves = _get_parameter_field.((P(),), PSY.get_operation_cost.(devices))\n    max_tranches = maximum(get_max_tranches.(devices, curves))\n    return (make_tranche_axis(max_tranches),)\nend\n\nfunction calc_additional_axes(\n    ::OptimizationContainer,\n    ::P,\n    devices::U,\n    ::DeviceModel{D, W},\n) where {\n    P <: AbstractPiecewiseLinearBreakpointParameter,\n    U <: Union{Vector{D}, IS.FlattenIteratorWrapper{D}},\n    W <: AbstractDeviceFormulation,\n} where {D <: PSY.Component}\n    curves = _get_parameter_field.((P(),), PSY.get_operation_cost.(devices))\n    max_tranches = maximum(get_max_tranches.(devices, curves))\n    return (make_tranche_axis(max_tranches + 1),)  # one more breakpoint than tranches\nend\n\n\"\"\"\nGiven a parameter array, get any additional axes, i.e., those that aren't the first\n(component) or the last (time)\n\"\"\"\nlookup_additional_axes(parameter_array) = axes(parameter_array)[2:(end - 1)]\n\n# Layer of indirection to handle the fact that some parameters come from time series that\n# represent multiple things (e.g., both slopes and breakpoints come from the same time\n# series of `FunctionData`). This function is called on every element of the time series\n# with an expected output axes tuple.\n_unwrap_for_param(::ParameterType, ts_elem, expected_axs) = ts_elem\n\n# For piecewise MarketBidCost-like data, the number of tranches can vary over time, so the\n# parameter container is sized for the maximum number of tranches and in smaller cases we\n# have to pad. We do this by creating additional \"degenerate\" tranches at the top end of the\n# curve with dx = 0 such that their dispatch variables are constrained to 0. In theory, the\n# slope shouldn't matter for these degenerate segments. In practice, we'll use slope = 0 so\n# the term can be more trivially dropped from the objective function.\nfunction _unwrap_for_param(\n    ::AbstractPiecewiseLinearSlopeParameter,\n    ts_elem::IS.PiecewiseStepData,\n    expected_axs,\n)\n    max_len = length(only(expected_axs))\n    y_coords = IS.get_y_coords(ts_elem)\n    @assert length(y_coords) <= max_len\n    fill_value = 0.0  # pad with slope = 0 if necessary (see above)\n    padded_y_coords = vcat(y_coords, fill(fill_value, max_len - length(y_coords)))\n    return padded_y_coords\nend\n\nfunction _unwrap_for_param(\n    ::AbstractPiecewiseLinearBreakpointParameter,\n    ts_elem::IS.PiecewiseStepData,\n    expected_axs,\n)\n    max_len = length(only(expected_axs))\n    x_coords = IS.get_x_coords(ts_elem)\n    @assert length(x_coords) <= max_len\n    fill_value = x_coords[end]  # if padding is necessary, repeat the last breakpoint so dx = 0 (see above)\n    padded_x_coords = vcat(x_coords, fill(fill_value, max_len - length(x_coords)))\n    return padded_x_coords\nend\n\n# PERF: compilation hotspot. Switch to TSC.\n# NOTE direct equivalent of _add_time_series_parameters! for TimeSeriesParameter\nfunction _add_parameters!(\n    container::OptimizationContainer,\n    param::T,\n    devices::U,\n    model::DeviceModel{D, W},\n) where {\n    T <: ObjectiveFunctionParameter,\n    U <: Union{Vector{D}, IS.FlattenIteratorWrapper{D}},\n    W <: AbstractDeviceFormulation,\n} where {D <: PSY.Component}\n    ts_type = get_default_time_series_type(container)\n    if !(ts_type <: Union{PSY.AbstractDeterministic, PSY.StaticTimeSeries})\n        error(\n            \"add_parameters! for ObjectiveFunctionParameter is not compatible with $ts_type\",\n        )\n    end\n    time_steps = get_time_steps(container)\n\n    ts_names = String[]\n    device_names = String[]\n    active_devices = D[]\n    for device in devices\n        ts_name = _get_time_series_name(T(), device, model)\n        if PSY.has_time_series(device, ts_type, ts_name)\n            push!(ts_names, ts_name)\n            push!(device_names, PSY.get_name(device))\n            push!(active_devices, device)\n        else\n            @debug \"Skipped time series for $D, $(PSY.get_name(device))\"\n        end\n    end\n    if isempty(active_devices)\n        return\n    end\n    jump_model = get_jump_model(container)\n\n    additional_axes = calc_additional_axes(container, param, active_devices, model)\n    param_container = add_param_container!(\n        container,\n        param,\n        D,\n        _param_to_vars(T(), W()),\n        SOSStatusVariable.NO_VARIABLE,\n        false,\n        _get_expected_time_series_eltype(T()),\n        device_names,\n        additional_axes...,\n        time_steps,\n    )\n\n    model_interval = get_interval(get_settings(container))\n    ts_interval = model_interval\n    param_instance = T()\n    parent_mult = get_multiplier_array_data(param_container)\n    parent_param = get_parameter_array_data(param_container)\n    for (i, (ts_name, device_name, device)) in\n        enumerate(zip(ts_names, device_names, active_devices))\n        raw_ts_vals = get_time_series_initial_values!(\n            container,\n            ts_type,\n            device,\n            ts_name;\n            interval = ts_interval,\n        )\n        ts_vals = _unwrap_for_param.(Ref(param_instance), raw_ts_vals, Ref(additional_axes))\n        @assert all(_size_wrapper.(ts_vals) .== Ref(length.(additional_axes)))\n        # PWL/cost-function path: the parameter values flowing through\n        # `_set_parameter_at!` below are tuples-of-floats from `_unwrap_for_param`;\n        # the multiplier itself is a scalar Float64 (per-device cost weight).\n        _set_multiplier_at!(\n            parent_mult,\n            get_multiplier_value(T(), device, W()),\n            i,\n        )\n        for step in time_steps\n            _set_parameter_at!(parent_param, jump_model, ts_vals[step], i, step)\n        end\n    end\n    return\nend\n\nfunction _add_parameters!(\n    container::OptimizationContainer,\n    ::T,\n    service::U,\n    model::ServiceModel{U, V},\n) where {T <: TimeSeriesParameter, U <: PSY.Service, V <: AbstractServiceFormulation}\n    ts_type = get_default_time_series_type(container)\n    if !(ts_type <: Union{PSY.AbstractDeterministic, PSY.StaticTimeSeries})\n        error(\"add_parameters! for TimeSeriesParameter is not compatible with $ts_type\")\n    end\n    ts_name = get_time_series_names(model)[T]\n    time_steps = get_time_steps(container)\n    name = PSY.get_name(service)\n    model_interval = get_interval(get_settings(container))\n    ts_interval = model_interval\n    ts_uuid = string(\n        IS.get_time_series_uuid(\n            ts_type,\n            service,\n            ts_name;\n            interval = _to_is_interval(ts_interval),\n        ),\n    )\n    @debug \"adding\" T U _group = LOG_GROUP_OPTIMIZATION_CONTAINER\n    additional_axes = calc_additional_axes(container, T(), [service], model)\n    parameter_container = add_param_container!(\n        container,\n        T(),\n        U,\n        ts_type,\n        ts_name,\n        [ts_uuid],\n        [name],\n        additional_axes,\n        time_steps;\n        meta = name,\n    )\n\n    set_subsystem!(get_attributes(parameter_container), get_subsystem(model))\n    jump_model = get_jump_model(container)\n    ts_vector = get_time_series(container, service, T(), name; interval = ts_interval)\n    multiplier = get_multiplier_value(T(), service, V())\n    parent_mult = get_multiplier_array_data(parameter_container)\n    _set_multiplier_at!(parent_mult, Float64(multiplier), 1)\n    parent_param = get_parameter_array_data(parameter_container)\n    for t in time_steps\n        _set_parameter_at!(parent_param, jump_model, ts_vector[t], 1, t)\n    end\n    add_component_name!(get_attributes(parameter_container), name, ts_uuid)\n    return\nend\n\nfunction _add_parameters!(\n    container::OptimizationContainer,\n    ::T,\n    key::VariableKey{U, D},\n    model::DeviceModel{D, W},\n    devices::V,\n) where {\n    T <: VariableValueParameter,\n    U <: VariableType,\n    V <: Union{Vector{D}, IS.FlattenIteratorWrapper{D}},\n    W <: AbstractDeviceFormulation,\n} where {D <: PSY.Component}\n    @debug \"adding\" T D U _group = LOG_GROUP_OPTIMIZATION_CONTAINER\n    names = [PSY.get_name(device) for device in devices]\n    time_steps = get_time_steps(container)\n    parameter_container = add_param_container!(container, T(), D, key, names, time_steps)\n    jump_model = get_jump_model(container)\n    parent_mult = get_multiplier_array_data(parameter_container)\n    parent_param = get_parameter_array_data(parameter_container)\n    for (i, d) in enumerate(devices)\n        name = PSY.get_name(d)\n        _set_multiplier_at!(\n            parent_mult,\n            get_parameter_multiplier(T(), d, W()),\n            i,\n        )\n        if get_variable_warm_start_value(U(), d, W()) === nothing\n            inital_parameter_value = 0.0\n        else\n            inital_parameter_value = get_variable_warm_start_value(U(), d, W())\n        end\n        for t in time_steps\n            _set_parameter_at!(parent_param, jump_model, inital_parameter_value, i, t)\n        end\n    end\n    return\nend\n\nfunction _add_parameters!(\n    container::OptimizationContainer,\n    ::T,\n    key::VariableKey{U, D},\n    model::DeviceModel{D, W},\n    devices::V,\n) where {\n    T <: OnStatusParameter,\n    U <: OnVariable,\n    V <: Union{Vector{D}, IS.FlattenIteratorWrapper{D}},\n    W <: AbstractThermalFormulation,\n} where {D <: PSY.ThermalGen}\n    @debug \"adding\" T D U _group = LOG_GROUP_OPTIMIZATION_CONTAINER\n    names = [PSY.get_name(device) for device in devices if !PSY.get_must_run(device)]\n    time_steps = get_time_steps(container)\n    parameter_container = add_param_container!(container, T(), D, key, names, time_steps)\n    jump_model = get_jump_model(container)\n    parent_mult = get_multiplier_array_data(parameter_container)\n    parent_param = get_parameter_array_data(parameter_container)\n    # Iterate the same filtered view used to construct `names` so enumeration index\n    # `i` lines up with the parameter container's first axis.\n    for (i, d) in enumerate(Iterators.filter(d -> !PSY.get_must_run(d), devices))\n        name = PSY.get_name(d)\n        _set_multiplier_at!(\n            parent_mult,\n            get_parameter_multiplier(T(), d, W()),\n            i,\n        )\n        if get_variable_warm_start_value(U(), d, W()) === nothing\n            inital_parameter_value = 0.0\n        else\n            inital_parameter_value = get_variable_warm_start_value(U(), d, W())\n        end\n        for t in time_steps\n            _set_parameter_at!(parent_param, jump_model, inital_parameter_value, i, t)\n        end\n    end\n    return\nend\n\nfunction _add_parameters!(\n    container::OptimizationContainer,\n    ::T,\n    key::VariableKey{U, D},\n    model::DeviceModel{D, W},\n    devices::V,\n) where {\n    T <: FixValueParameter,\n    U <: VariableType,\n    V <: Union{Vector{D}, IS.FlattenIteratorWrapper{D}},\n    W <: AbstractDeviceFormulation,\n} where {D <: PSY.Component}\n    @debug \"adding\" T D U _group = LOG_GROUP_OPTIMIZATION_CONTAINER\n    names = [PSY.get_name(device) for device in devices]\n    time_steps = get_time_steps(container)\n    parameter_container =\n        add_param_container!(container, T(), D, key, names, time_steps; meta = \"$U\")\n    jump_model = get_jump_model(container)\n    parent_mult = get_multiplier_array_data(parameter_container)\n    parent_param = get_parameter_array_data(parameter_container)\n    for (i, d) in enumerate(devices)\n        name = PSY.get_name(d)\n        _set_multiplier_at!(\n            parent_mult,\n            get_parameter_multiplier(T(), d, W()),\n            i,\n        )\n        if get_variable_warm_start_value(U(), d, W()) === nothing\n            inital_parameter_value = 0.0\n        else\n            inital_parameter_value = get_variable_warm_start_value(U(), d, W())\n        end\n        for t in time_steps\n            _set_parameter_at!(parent_param, jump_model, inital_parameter_value, i, t)\n        end\n    end\n    return\nend\n\nfunction _add_parameters!(\n    container::OptimizationContainer,\n    ::T,\n    key::AuxVarKey{U, D},\n    model::DeviceModel{D, W},\n    devices::V,\n) where {\n    T <: VariableValueParameter,\n    U <: AuxVariableType,\n    V <: Union{Vector{D}, IS.FlattenIteratorWrapper{D}},\n    W <: AbstractDeviceFormulation,\n} where {D <: PSY.Component}\n    @debug \"adding\" T D U _group = LOG_GROUP_OPTIMIZATION_CONTAINER\n    names = [PSY.get_name(device) for device in devices]\n    time_steps = get_time_steps(container)\n    parameter_container = add_param_container!(\n        container,\n        T(),\n        D,\n        key,\n        names,\n        time_steps,\n    )\n    jump_model = get_jump_model(container)\n    parent_mult = get_multiplier_array_data(parameter_container)\n    parent_param = get_parameter_array_data(parameter_container)\n\n    for (i, d) in enumerate(devices)\n        name = PSY.get_name(d)\n        _set_multiplier_at!(\n            parent_mult,\n            get_parameter_multiplier(T(), d, W()),\n            i,\n        )\n        ini_val = get_initial_parameter_value(T(), d, W())\n        for t in time_steps\n            _set_parameter_at!(parent_param, jump_model, ini_val, i, t)\n        end\n    end\n    return\nend\n\nfunction _add_parameters!(\n    container::OptimizationContainer,\n    ::T,\n    devices::V,\n    model::DeviceModel{D, W},\n) where {\n    T <: OnStatusParameter,\n    V <: Union{Vector{D}, IS.FlattenIteratorWrapper{D}},\n    W <: AbstractDeviceFormulation,\n} where {D <: PSY.Component}\n    @debug \"adding\" T D V _group = LOG_GROUP_OPTIMIZATION_CONTAINER\n\n    # We do this to handle cases where the same parameter is also added as a Feedforward.\n    # When the OnStatusParameter is added without a feedforward it takes a Float value.\n    # This is used to handle the special case of compact formulations.\n    !isempty(get_feedforwards(model)) && return\n    names = [PSY.get_name(device) for device in devices]\n    time_steps = get_time_steps(container)\n    parameter_container = add_param_container!(\n        container,\n        T(),\n        D,\n        VariableKey(OnVariable, D),\n        names,\n        time_steps,\n    )\n    jump_model = get_jump_model(container)\n    parent_mult = get_multiplier_array_data(parameter_container)\n    parent_param = get_parameter_array_data(parameter_container)\n\n    for (i, d) in enumerate(devices)\n        name = PSY.get_name(d)\n        _set_multiplier_at!(\n            parent_mult,\n            get_parameter_multiplier(T(), d, W()),\n            i,\n        )\n        ini_val = get_initial_parameter_value(T(), d, W())\n        for t in time_steps\n            _set_parameter_at!(parent_param, jump_model, ini_val, i, t)\n        end\n    end\n    return\nend\n\nfunction _add_parameters!(\n    container::OptimizationContainer,\n    ::T,\n    key::VariableKey{U, S},\n    model::ServiceModel{S, W},\n    devices::V,\n) where {\n    S <: PSY.AbstractReserve,\n    T <: VariableValueParameter,\n    U <: VariableType,\n    V <: Union{Vector{D}, IS.FlattenIteratorWrapper{D}},\n    W <: AbstractReservesFormulation,\n} where {D <: PSY.Component}\n    @debug \"adding\" T D U _group = LOG_GROUP_OPTIMIZATION_CONTAINER\n    contributing_devices = get_contributing_devices(model)\n    names = [PSY.get_name(device) for device in contributing_devices]\n    time_steps = get_time_steps(container)\n    parameter_container = add_param_container!(\n        container,\n        T(),\n        S,\n        key,\n        names,\n        time_steps;\n        meta = get_service_name(model),\n    )\n    jump_model = get_jump_model(container)\n    parent_mult = get_multiplier_array_data(parameter_container)\n    parent_param = get_parameter_array_data(parameter_container)\n    multiplier = get_parameter_multiplier(T(), S, W())\n    ini_val = get_initial_parameter_value(T(), S, W())\n    for (i, d) in enumerate(contributing_devices)\n        name = PSY.get_name(d)\n        _set_multiplier_at!(parent_mult, multiplier, i)\n        for t in time_steps\n            _set_parameter_at!(parent_param, jump_model, ini_val, i, t)\n        end\n    end\n    return\nend\n"
  },
  {
    "path": "src/parameters/update_container_parameter_values.jl",
    "content": "function _update_parameter_values!(\n    ::AbstractArray{T},\n    ::ParameterType,\n    ::NoAttributes,\n    args...,\n) where {T <: Union{Float64, JuMP.VariableRef}} end\n\n######################## Methods to update Parameters from Time Series #####################\nfunction _set_param_value!(\n    param::JuMPVariableTensor,\n    value::Union{T, AbstractVector{T}},\n    name::String,\n    t::Int,\n) where {T <: ValidDataParamEltypes}\n    fix_maybe_broadcast!(param, value, (name, t))\n    return\nend\n\nfunction _set_param_value!(\n    param::DenseAxisArray{T, 2},\n    value::T,\n    name::String,\n    t::Int,\n) where {T <: ValidDataParamEltypes}\n    param[name, t] = value\n    return\nend\n\nfunction _set_param_value!(\n    param::DenseAxisArray{T},\n    value::Union{T, AbstractVector{T}},\n    name::String,\n    t::Int,\n) where {T <: ValidDataParamEltypes}\n    assign_maybe_broadcast!(param, value, (name, t))\n    return\nend\n\nfunction _update_parameter_values!(\n    parameter_array::DenseAxisArray{T},\n    ::W,\n    attributes::TimeSeriesAttributes{U},\n    ::Type{V},\n    model::DecisionModel,\n    ::DatasetContainer{InMemoryDataset},\n) where {\n    T <: Union{JuMP.VariableRef, Float64},\n    U <: PSY.AbstractDeterministic,\n    V <: PSY.Component,\n    W <: ParameterType,\n}\n    initial_forecast_time = get_current_time(model) # Function not well defined for DecisionModels\n    horizon = get_time_steps(get_optimization_container(model))[end]\n    ts_name = get_time_series_name(attributes)\n    model_interval = get_interval(get_settings(model))\n    ts_interval = model_interval\n    subsystem = get_subsystem(attributes)\n    template = get_template(model)\n    if isempty(subsystem)\n        device_model = get_model(template, V)\n    else\n        device_model = get_model(template, V, subsystem)\n    end\n    components = get_available_components(device_model, get_system(model))\n    # Hoist the underlying dense storage and per-component name lookup once so each\n    # write skips DenseAxisArray's String-keyed axis lookup. `additional_axes` is\n    # invariant for the lifetime of this update call.\n    parent_param = parameter_array.data\n    name_lookup = parameter_array.lookup[1]\n    additional_axes = lookup_additional_axes(parameter_array)\n    ts_uuids = Set{String}()\n    for component in components\n        if !PSY.has_time_series(component, U, ts_name)\n            continue\n        end\n        ts_uuid = _get_ts_uuid(attributes, PSY.get_name(component))\n        if !(ts_uuid in ts_uuids)\n            ts_vector = get_time_series_values!(\n                U,\n                model,\n                component,\n                ts_name,\n                initial_forecast_time,\n                horizon;\n                interval = ts_interval,\n            )\n            i_param = name_lookup[ts_uuid]\n            for (t, value) in enumerate(ts_vector)\n                # first two axes of parameter_array are component, time; we care about any additional ones\n                unwrapped_value =\n                    _unwrap_for_param(W(), value, additional_axes)\n                if !all(isfinite.(unwrapped_value))\n                    error(\"The value for the time series $(ts_name) is not finite. \\\n                          Check that the data in the time series is valid.\")\n                end\n                _set_param_value_at!(parent_param, unwrapped_value, i_param, t)\n            end\n            push!(ts_uuids, ts_uuid)\n        end\n    end\n    return\nend\n\n# Time-series parameter update for reduced ACTransmission branches\nfunction _update_parameter_values!(\n    parameter_array::DenseAxisArray{T},\n    ::W,\n    attributes::TimeSeriesAttributes{U},\n    ::Type{V},\n    model::DecisionModel,\n    ::DatasetContainer{InMemoryDataset},\n) where {\n    T <: Union{JuMP.VariableRef, Float64},\n    U <: PSY.AbstractDeterministic,\n    V <: PSY.ACTransmission,\n    W <: TimeSeriesParameter,\n}\n    initial_forecast_time = get_current_time(model)\n    horizon = get_time_steps(get_optimization_container(model))[end]\n    ts_name = get_time_series_name(attributes)\n    model_interval = get_interval(get_settings(model))\n    ts_interval = model_interval\n\n    network_model = get_network_model(get_template(model))\n    net_reduction_data = network_model.network_reduction\n    all_branch_maps_by_type = PNM.get_all_branch_maps_by_type(net_reduction_data)\n\n    if !haskey(net_reduction_data.name_to_arc_map, V)\n        return\n    end\n\n    # Hoist the underlying dense storage and per-component name lookup once so each\n    # write skips DenseAxisArray's String-keyed axis lookup.\n    parent_param = parameter_array.data\n    name_lookup = parameter_array.lookup[1]\n\n    ts_uuids_updated = Set{String}()\n    for (name, (arc, reduction)) in PNM.get_name_to_arc_map(net_reduction_data, V)\n        reduction_entry = all_branch_maps_by_type[reduction][V][arc]\n        if !PNM.has_time_series(reduction_entry, U, ts_name)\n            continue\n        end\n        device_with_time_series =\n            PNM.get_device_with_time_series(reduction_entry, U, ts_name)\n        ts_uuid = _get_ts_uuid(attributes, name)\n        if ts_uuid in ts_uuids_updated\n            continue\n        end\n        ts_vector = get_time_series_values!(\n            U,\n            model,\n            device_with_time_series,\n            ts_name,\n            initial_forecast_time,\n            horizon;\n            interval = ts_interval,\n        )\n        i_param = name_lookup[ts_uuid]\n        for (t, value) in enumerate(ts_vector)\n            if !isfinite(value)\n                error(\n                    \"The value for the time series $(ts_name) is not finite. \\\n                    Check that the data in the time series is valid.\",\n                )\n            end\n            _set_param_value_at!(parent_param, value, i_param, t)\n        end\n        push!(ts_uuids_updated, ts_uuid)\n    end\n    return\nend\n\nfunction _update_parameter_values!(\n    parameter_array::DenseAxisArray{T},\n    ::ParameterType,\n    attributes::TimeSeriesAttributes{U},\n    service::V,\n    model::DecisionModel,\n    ::DatasetContainer{InMemoryDataset},\n) where {\n    T <: Union{JuMP.VariableRef, Float64},\n    U <: PSY.AbstractDeterministic,\n    V <: PSY.Service,\n}\n    initial_forecast_time = get_current_time(model) # Function not well defined for DecisionModels\n    horizon = get_time_steps(get_optimization_container(model))[end]\n    ts_name = get_time_series_name(attributes)\n    model_interval = get_interval(get_settings(model))\n    ts_interval = model_interval\n    ts_uuid = _get_ts_uuid(attributes, PSY.get_name(service))\n    ts_vector = get_time_series_values!(\n        U,\n        model,\n        service,\n        get_time_series_name(attributes),\n        initial_forecast_time,\n        horizon;\n        interval = ts_interval,\n    )\n    # Hoist the underlying dense storage and resolve the row index once so the\n    # per-time-step writes skip DenseAxisArray's String-keyed axis lookup.\n    parent_param = parameter_array.data\n    i_param = parameter_array.lookup[1][ts_uuid]\n    for (t, value) in enumerate(ts_vector)\n        if !isfinite(value)\n            error(\"The value for the time series $(ts_name) is not finite. \\\n                  Check that the data in the time series is valid.\")\n        end\n        _set_param_value_at!(parent_param, value, i_param, t)\n    end\nend\n\nfunction _update_parameter_values!(\n    parameter_array::DenseAxisArray{T},\n    ::ParameterType,\n    attributes::TimeSeriesAttributes{U},\n    ::Type{V},\n    model::EmulationModel,\n    ::DatasetContainer{InMemoryDataset},\n) where {T <: Union{JuMP.VariableRef, Float64}, U <: PSY.SingleTimeSeries, V <: PSY.Device}\n    initial_forecast_time = get_current_time(model)\n    template = get_template(model)\n    device_model = get_model(template, V)\n    components = get_available_components(device_model, get_system(model))\n    ts_name = get_time_series_name(attributes)\n    ts_resolution = get_resolution(get_settings(model))\n    # Hoist the underlying dense storage and per-component name lookup once.\n    parent_param = parameter_array.data\n    name_lookup = parameter_array.lookup[1]\n    ts_uuids = Set{String}()\n    for component in components\n        ts_uuid = _get_ts_uuid(attributes, PSY.get_name(component))\n        if !(ts_uuid in ts_uuids)\n            # Note: This interface reads one single value per component at a time.\n            value = get_time_series_values!(\n                U,\n                model,\n                component,\n                get_time_series_name(attributes),\n                initial_forecast_time;\n                resolution = ts_resolution,\n            )[1]\n            if !isfinite(value)\n                error(\"The value for the time series $(ts_name) is not finite. \\\n                      Check that the data in the time series is valid.\")\n            end\n            _set_param_value_at!(parent_param, value, name_lookup[ts_uuid], 1)\n            push!(ts_uuids, ts_uuid)\n        end\n    end\n    return\nend\n\nfunction _update_parameter_values!(\n    parameter_array::DenseAxisArray{T},\n    ::ParameterType,\n    attributes::TimeSeriesAttributes{U},\n    service::V,\n    model::EmulationModel,\n    ::DatasetContainer{InMemoryDataset},\n) where {T <: Union{JuMP.VariableRef, Float64}, U <: PSY.SingleTimeSeries, V <: PSY.Service}\n    initial_forecast_time = get_current_time(model)\n    ts_name = get_time_series_name(attributes)\n    ts_uuid = _get_ts_uuid(attributes, PSY.get_name(service))\n    ts_resolution = get_resolution(get_settings(model))\n    # Note: This interface reads one single value per component at a time.\n    value = get_time_series_values!(\n        U,\n        model,\n        service,\n        get_time_series_name(attributes),\n        initial_forecast_time;\n        resolution = ts_resolution,\n    )[1]\n    if !isfinite(value)\n        error(\"The value for the time series $(ts_name) is not finite. \\\n            Check that the data in the time series is valid.\")\n    end\n    parent_param = parameter_array.data\n    i_param = parameter_array.lookup[1][ts_uuid]\n    _set_param_value_at!(parent_param, value, i_param, 1)\n\n    return\nend\n\nfunction _update_parameter_values!(\n    parameter_array::DenseAxisArray{T},\n    ::ParameterType,\n    attributes::VariableValueAttributes,\n    ::Type{<:PSY.Device},\n    model::DecisionModel,\n    state::DatasetContainer{InMemoryDataset},\n) where {T <: Union{JuMP.VariableRef, Float64}}\n    current_time = get_current_time(model)\n    state_values = get_dataset_values(state, get_attribute_key(attributes))\n    component_names, time = axes(parameter_array)\n    model_resolution = get_resolution(model)\n    state_data = get_dataset(state, get_attribute_key(attributes))\n    state_timestamps = state_data.timestamps\n    max_state_index = get_num_rows(state_data)\n    if model_resolution < state_data.resolution\n        t_step = 1\n    else\n        t_step = model_resolution ÷ state_data.resolution\n    end\n    state_data_index = find_timestamp_index(state_timestamps, current_time)\n    sim_timestamps = range(current_time; step = model_resolution, length = time[end])\n    # Hoist underlying dense storage and per-axis name lookups so the inner loop\n    # can index by integer pair, skipping DenseAxisArray's String-keyed lookup.\n    parent_param = parameter_array.data\n    parent_state = state_values.data\n    param_lookup = parameter_array.lookup[1]\n    state_lookup = state_values.lookup[1]\n    for t in time\n        timestamp_ix = min(max_state_index, state_data_index + t_step)\n        @debug \"parameter horizon is over the step\" max_state_index > state_data_index + 1\n        if state_timestamps[timestamp_ix] <= sim_timestamps[t]\n            state_data_index = timestamp_ix\n        end\n        for name in component_names\n            i_state = state_lookup[name]\n            i_param = param_lookup[name]\n            state_value = parent_state[i_state, state_data_index]\n            if !isfinite(state_value)\n                error(\n                    \"The value for the system state used in $(encode_key_as_string(get_attribute_key(attributes))) is not a finite value $(state_value) \\\n                     This is commonly caused by referencing a state value at a time when such decision hasn't been made. \\\n                     Consider reviewing your models' horizon and interval definitions\",\n                )\n            end\n            _set_param_value_at!(parent_param, state_value, i_param, t)\n        end\n    end\n    return\nend\n\nfunction _update_parameter_values!(\n    parameter_array::DenseAxisArray{T},\n    ::ParameterType,\n    attributes::VariableValueAttributes,\n    ::PSY.Reserve,\n    model::DecisionModel,\n    state::DatasetContainer{InMemoryDataset},\n) where {T <: Union{JuMP.VariableRef, Float64}}\n    current_time = get_current_time(model)\n    state_values = get_dataset_values(state, get_attribute_key(attributes))\n    component_names, time = axes(parameter_array)\n    model_resolution = get_resolution(model)\n    state_data = get_dataset(state, get_attribute_key(attributes))\n    state_timestamps = state_data.timestamps\n    max_state_index = get_num_rows(state_data)\n    if model_resolution < state_data.resolution\n        t_step = 1\n    else\n        t_step = model_resolution ÷ state_data.resolution\n    end\n    state_data_index = find_timestamp_index(state_timestamps, current_time)\n    sim_timestamps = range(current_time; step = model_resolution, length = time[end])\n    # Hoist underlying dense storage and per-axis name lookups so the inner loop\n    # can index by integer pair, skipping DenseAxisArray's String-keyed lookup.\n    parent_param = parameter_array.data\n    parent_state = state_values.data\n    param_lookup = parameter_array.lookup[1]\n    state_lookup = state_values.lookup[1]\n    for t in time\n        timestamp_ix = min(max_state_index, state_data_index + t_step)\n        @debug \"parameter horizon is over the step\" max_state_index > state_data_index + 1\n        if state_timestamps[timestamp_ix] <= sim_timestamps[t]\n            state_data_index = timestamp_ix\n        end\n        for name in component_names\n            i_state = state_lookup[name]\n            i_param = param_lookup[name]\n            state_value = parent_state[i_state, state_data_index]\n            if !isfinite(state_value)\n                error(\n                    \"The value for the system state used in $(encode_key_as_string(get_attribute_key(attributes))) is not a finite value $(state_value) \\\n                     This is commonly caused by referencing a state value at a time when such decision hasn't been made. \\\n                     Consider reviewing your models' horizon and interval definitions\",\n                )\n            end\n            _set_param_value_at!(parent_param, state_value, i_param, t)\n        end\n    end\n    return\nend\n\nfunction _update_parameter_values!(\n    parameter_array::DenseAxisArray{T},\n    ::ParameterType,\n    attributes::VariableValueAttributes{VariableKey{OnVariable, U}},\n    ::Type{U},\n    model::DecisionModel,\n    state::DatasetContainer{InMemoryDataset},\n) where {T <: Union{JuMP.VariableRef, Float64}, U <: PSY.Device}\n    current_time = get_current_time(model)\n    state_values = get_dataset_values(state, get_attribute_key(attributes))\n    component_names, time = axes(parameter_array)\n    model_resolution = get_resolution(model)\n    state_data = get_dataset(state, get_attribute_key(attributes))\n    state_timestamps = state_data.timestamps\n    max_state_index = get_num_rows(state_data)\n    if model_resolution < state_data.resolution\n        t_step = 1\n    else\n        t_step = model_resolution ÷ state_data.resolution\n    end\n    state_data_index = find_timestamp_index(state_timestamps, current_time)\n\n    sim_timestamps = range(current_time; step = model_resolution, length = time[end])\n    # Hoist underlying dense storage and per-axis name lookups so the inner loop\n    # can index by integer pair, skipping DenseAxisArray's String-keyed lookup.\n    parent_param = parameter_array.data\n    parent_state = state_values.data\n    param_lookup = parameter_array.lookup[1]\n    state_lookup = state_values.lookup[1]\n    for t in time\n        timestamp_ix = min(max_state_index, state_data_index + t_step)\n        @debug \"parameter horizon is over the step\" max_state_index > state_data_index + 1\n        if state_timestamps[timestamp_ix] <= sim_timestamps[t]\n            state_data_index = timestamp_ix\n        end\n        for name in component_names\n            i_state = state_lookup[name]\n            i_param = param_lookup[name]\n            value = round(parent_state[i_state, state_data_index])\n            if !isfinite(value)\n                error(\n                    \"The value for the system state used in $(encode_key_as_string(get_attribute_key(attributes))) is not a finite value $(value) \\\n                     This is commonly caused by referencing a state value at a time when such decision hasn't been made. \\\n                     Consider reviewing your models' horizon and interval definitions\",\n                )\n            end\n            if 0.0 > value || value > 1.0\n                error(\n                    \"The value for the system state used in $(encode_key_as_string(get_attribute_key(attributes))): $(value) is out of the [0, 1] range\",\n                )\n            end\n            _set_param_value_at!(parent_param, value, i_param, t)\n        end\n    end\n    return\nend\n\nfunction _update_parameter_values!(\n    parameter_array::DenseAxisArray{T},\n    ::ParameterType,\n    attributes::VariableValueAttributes,\n    ::Type{<:PSY.Component},\n    model::EmulationModel,\n    state::DatasetContainer{InMemoryDataset},\n) where {T <: Union{JuMP.VariableRef, Float64}}\n    current_time = get_current_time(model)\n    state_values = get_dataset_values(state, get_attribute_key(attributes))\n    component_names, _ = axes(parameter_array)\n    state_data = get_dataset(state, get_attribute_key(attributes))\n    state_timestamps = state_data.timestamps\n    state_data_index = find_timestamp_index(state_timestamps, current_time)\n    # Hoist underlying dense storage and per-axis name lookups.\n    parent_param = parameter_array.data\n    parent_state = state_values.data\n    param_lookup = parameter_array.lookup[1]\n    state_lookup = state_values.lookup[1]\n    for name in component_names\n        i_state = state_lookup[name]\n        i_param = param_lookup[name]\n        _set_param_value_at!(\n            parent_param,\n            parent_state[i_state, state_data_index],\n            i_param,\n            1,\n        )\n    end\n    return\nend\n\nfunction _update_parameter_values!(\n    parameter_array::DenseAxisArray{T},\n    ::ParameterType,\n    attributes::VariableValueAttributes{VariableKey{OnVariable, U}},\n    ::Type{<:PSY.Component},\n    model::EmulationModel,\n    state::DatasetContainer{InMemoryDataset},\n) where {T <: Union{JuMP.VariableRef, Float64}, U <: PSY.Component}\n    current_time = get_current_time(model)\n    state_values = get_dataset_values(state, get_attribute_key(attributes))\n    component_names, _ = axes(parameter_array)\n    state_data = get_dataset(state, get_attribute_key(attributes))\n    state_timestamps = state_data.timestamps\n    state_data_index = find_timestamp_index(state_timestamps, current_time)\n    has_outage = haskey(\n        get_parameters_values(state),\n        ISOPT.ParameterKey{\n            AvailableStatusParameter,\n            U,\n        }(\n            \"\",\n        ),\n    )\n    if has_outage\n        status_values = get_dataset_values(\n            state,\n            ISOPT.ParameterKey{\n                AvailableStatusParameter,\n                U,\n            }(\n                \"\",\n            ),\n        )\n        status_data = get_dataset(\n            state,\n            ISOPT.ParameterKey{\n                AvailableStatusParameter,\n                U,\n            }(\n                \"\",\n            ),\n        )\n        status_timestamps = status_data.timestamps\n        status_data_index = find_timestamp_index(status_timestamps, current_time)\n        parent_status = status_values.data\n        # `_AxisLookup{Dict{String,Int64}}` wraps a `Dict`; reach for `.data`\n        # so we can `haskey` and integer-index without a String-keyed scan.\n        status_lookup_dict = status_values.lookup[1].data\n    end\n    # Hoist underlying dense storage and per-axis name lookups for the inner loop.\n    parent_param = parameter_array.data\n    parent_state = state_values.data\n    param_lookup = parameter_array.lookup[1]\n    state_lookup = state_values.lookup[1]\n    for name in component_names\n        i_state = state_lookup[name]\n        i_param = param_lookup[name]\n        if has_outage && haskey(status_lookup_dict, name) &&\n           parent_status[status_lookup_dict[name], status_data_index] == 0.0 &&\n           round(parent_state[i_state, state_data_index]) == 1.0\n            # Override feed forward based on status parameter\n            value = 0.0\n        else\n            value = round(parent_state[i_state, state_data_index])\n        end\n        if !isfinite(value)\n            error(\n                \"The value for the system state used in $(encode_key_as_string(get_attribute_key(attributes))) is not a finite value $(value) \\\n                 This is commonly caused by referencing a state value at a time when such decision hasn't been made. \\\n                 Consider reviewing your models' horizon and interval definitions\",\n            )\n        end\n        if 0.0 > value || value > 1.0\n            error(\n                \"The value for the system state used in $(encode_key_as_string(get_attribute_key(attributes))): $(value) is out of the [0, 1] range\",\n            )\n        end\n        _set_param_value_at!(parent_param, value, i_param, 1)\n    end\n    return\nend\n\nfunction _update_parameter_values!(\n    ::AbstractArray{T},\n    ::ParameterType,\n    ::VariableValueAttributes,\n    ::Type{<:PSY.Component},\n    ::EmulationModel,\n    ::EmulationModelStore,\n) where {T <: Union{JuMP.VariableRef, Float64}}\n    error(\"The emulation model has parameters that can't be updated from its results\")\n    return\nend\n\nfunction _update_parameter_values!(\n    parameter_array::DenseAxisArray{T},\n    attributes::EventParametersAttributes{W, U},\n    ::Type{V},\n    model::DecisionModel,\n    state::DatasetContainer{InMemoryDataset},\n) where {\n    T <: Union{JuMP.VariableRef, Float64},\n    W <: PSY.Contingency,\n    U <: EventParameter,\n    V <: PSY.Component,\n}\n    current_time = get_current_time(model)\n    # state_values = get_dataset_values(state, get_attribute_key(attributes))\n    state_values =\n        get_dataset_values(state, U(), V)\n    component_names, time = axes(parameter_array)\n    model_resolution = get_resolution(model)\n    state_data = get_dataset(state, U(), V)\n    state_timestamps = state_data.timestamps\n    max_state_index = get_num_rows(state_data)\n    if model_resolution < state_data.resolution\n        t_step = 1\n    else\n        t_step = model_resolution ÷ state_data.resolution\n    end\n    state_data_index = find_timestamp_index(state_timestamps, current_time)\n\n    sim_timestamps = range(current_time; step = model_resolution, length = time[end])\n    # Hoist underlying dense storage and per-axis name lookups for the inner loop.\n    parent_param = parameter_array.data\n    parent_state = state_values.data\n    param_lookup = parameter_array.lookup[1]\n    state_lookup = state_values.lookup[1]\n    for t in time\n        timestamp_ix = min(max_state_index, state_data_index + t_step)\n        @debug \"parameter horizon is over the step\" max_state_index > state_data_index + 1\n        if state_timestamps[timestamp_ix] <= sim_timestamps[t]\n            state_data_index = timestamp_ix\n        end\n        for name in component_names\n            i_state = state_lookup[name]\n            i_param = param_lookup[name]\n            value = parent_state[i_state, state_data_index]\n            if !isfinite(value)\n                error(\n                    \"The value for the system state used in $(encode_key_as_string(get_attribute_key(attributes))) is not a finite value $(value) \\\n                     This is commonly caused by referencing a state value at a time when such decision hasn't been made. \\\n                     Consider reviewing your models' horizon and interval definitions\",\n                )\n            end\n            _set_param_value_at!(parent_param, value, i_param, t)\n        end\n    end\n    return\nend\n\nfunction _update_parameter_values!(\n    parameter_array::DenseAxisArray{T},\n    ::EventParametersAttributes{W, U},\n    ::Type{V},\n    model::EmulationModel,\n    state::DatasetContainer{InMemoryDataset},\n) where {\n    T <: Union{JuMP.VariableRef, Float64},\n    W <: PSY.Contingency,\n    U <: EventParameter,\n    V <: PSY.Component,\n}\n    current_time = get_current_time(model)\n    state_values = get_dataset_values(state, U(), V)\n    component_names, _ = axes(parameter_array)\n    state_data = get_dataset(state, U(), V)\n    state_timestamps = state_data.timestamps\n    state_data_index = find_timestamp_index(state_timestamps, current_time)\n\n    # Hoist underlying dense storage and per-axis name lookups.\n    parent_param = parameter_array.data\n    parent_state = state_values.data\n    param_lookup = parameter_array.lookup[1]\n    state_lookup = state_values.lookup[1]\n    for name in component_names\n        i_state = state_lookup[name]\n        i_param = param_lookup[name]\n        _set_param_value_at!(\n            parent_param,\n            parent_state[i_state, state_data_index],\n            i_param,\n            1,\n        )\n    end\n    return\nend\n\n\"\"\"\nUpdate parameter function an OperationModel\n\"\"\"\nfunction update_container_parameter_values!(\n    optimization_container::OptimizationContainer,\n    model::OperationModel,\n    key::ParameterKey{T, U},\n    input::DatasetContainer{InMemoryDataset},\n) where {T <: ParameterType, U <: PSY.Component}\n    # Enable again for detailed debugging\n    # TimerOutputs.@timeit RUN_SIMULATION_TIMER \"$T $U Parameter Update\" begin\n    # Note: Do not instantite a new key here because it might not match the param keys in the container\n    # if the keys have strings in the meta fields\n    parameter_array = get_parameter_array(optimization_container, key)\n    parameter_attributes = get_parameter_attributes(optimization_container, key)\n    _update_parameter_values!(parameter_array, T(), parameter_attributes, U, model, input)\n    return\nend\n\nfunction update_container_parameter_values!(\n    optimization_container::OptimizationContainer,\n    model::OperationModel,\n    key::ParameterKey{T, U},\n    input::DatasetContainer{InMemoryDataset},\n) where {T <: EventParameter, U <: PSY.Component}\n    # Enable again for detailed debugging\n    # TimerOutputs.@timeit RUN_SIMULATION_TIMER \"$T $U Parameter Update\" begin\n    # Note: Do not instantite a new key here because it might not match the param keys in the container\n    # if the keys have strings in the meta fields\n    parameter_array = get_parameter_array(optimization_container, key)\n    parameter_attributes = get_parameter_attributes(optimization_container, key)\n    _update_parameter_values!(parameter_array, parameter_attributes, U, model, input)\n    return\nend\n\nfunction update_container_parameter_values!(\n    optimization_container::OptimizationContainer,\n    model::OperationModel,\n    key::ParameterKey{T, U},\n    input::DatasetContainer{InMemoryDataset},\n) where {T <: ObjectiveFunctionParameter, U <: PSY.Component}\n    # Note: Do not instantite a new key here because it might not match the param keys in the container\n    # if the keys have strings in the meta fields\n    parameter_array = get_parameter_array(optimization_container, key)\n    # Multiplier is only needed for the objective function since `_update_parameter_values!` also updates the objective function\n    parameter_multiplier = get_parameter_multiplier_array(optimization_container, key)\n    parameter_attributes = get_parameter_attributes(optimization_container, key)\n    _update_parameter_values!(\n        parameter_array,\n        T(),\n        parameter_multiplier,\n        parameter_attributes,\n        U,\n        model,\n        input,\n    )\n    return\nend\n\nfunction update_container_parameter_values!(\n    optimization_container::OptimizationContainer,\n    model::OperationModel,\n    key::ParameterKey{T, U},\n    input::DatasetContainer{InMemoryDataset},\n) where {T <: ObjectiveFunctionParameter, U <: PSY.Service}\n    # Note: Do not instantiate a new key here because it might not match the param keys in the container\n    # if the keys have strings in the meta fields\n    parameter_array = get_parameter_array(optimization_container, key)\n    # Multiplier is only needed for the objective function since `_update_parameter_values!` also updates the objective function\n    parameter_multiplier = get_parameter_multiplier_array(optimization_container, key)\n    parameter_attributes = get_parameter_attributes(optimization_container, key)\n    _update_parameter_values!(\n        parameter_array,\n        T(),\n        parameter_multiplier,\n        parameter_attributes,\n        U,\n        model,\n        input,\n    )\n    return\nend\n\nfunction update_container_parameter_values!(\n    optimization_container::OptimizationContainer,\n    model::OperationModel,\n    key::ParameterKey{FixValueParameter, U},\n    input::DatasetContainer{InMemoryDataset},\n) where {U <: PSY.Component}\n    # Note: Do not instantite a new key here because it might not match the param keys in the container\n    # if the keys have strings in the meta fields\n    parameter_array = get_parameter_array(optimization_container, key)\n    parameter_attributes = get_parameter_attributes(optimization_container, key)\n    _update_parameter_values!(\n        parameter_array,\n        FixValueParameter(),\n        parameter_attributes,\n        U,\n        model,\n        input,\n    )\n    _fix_parameter_value!(optimization_container, parameter_array, parameter_attributes)\n    return\nend\n\nfunction update_container_parameter_values!(\n    optimization_container::OptimizationContainer,\n    model::OperationModel,\n    key::ParameterKey{FixValueParameter, U},\n    input::DatasetContainer{InMemoryDataset},\n) where {U <: PSY.Service}\n    # Note: Do not instantite a new key here because it might not match the param keys in the container\n    # if the keys have strings in the meta fields\n    parameter_array = get_parameter_array(optimization_container, key)\n    parameter_attributes = get_parameter_attributes(optimization_container, key)\n    service = PSY.get_component(U, get_system(model), key.meta)\n    @assert service !== nothing\n    _update_parameter_values!(\n        parameter_array,\n        FixValueParameter(),\n        parameter_attributes,\n        U,\n        model,\n        input,\n    )\n    _fix_parameter_value!(optimization_container, parameter_array, parameter_attributes)\n    return\nend\n\nfunction update_container_parameter_values!(\n    optimization_container::OptimizationContainer,\n    model::OperationModel,\n    key::ParameterKey{T, U},\n    input::DatasetContainer{InMemoryDataset},\n) where {T <: ParameterType, U <: PSY.Service}\n    # Note: Do not instantite a new key here because it might not match the param keys in the container\n    # if the keys have strings in the meta fields\n    parameter_array = get_parameter_array(optimization_container, key)\n    parameter_attributes = get_parameter_attributes(optimization_container, key)\n    service = PSY.get_component(U, get_system(model), key.meta)\n    @assert service !== nothing\n    _update_parameter_values!(\n        parameter_array,\n        T(),\n        parameter_attributes,\n        service,\n        model,\n        input,\n    )\n    return\nend\n\n# This method is included to avoid ambiguities\nfunction update_container_parameter_values!(\n    optimization_container::OptimizationContainer,\n    model::OperationModel,\n    key::ParameterKey{T, U},\n    input::DatasetContainer{InMemoryDataset},\n) where {T <: EventParameter, U <: PSY.Service}\n    return\nend\n"
  },
  {
    "path": "src/parameters/update_cost_parameters.jl",
    "content": "function _update_parameter_values!(\n    parameter_array::DenseAxisArray,\n    ::T,\n    parameter_multiplier::JuMPFloatArray,\n    attributes::CostFunctionAttributes,\n    ::Type{V},\n    model::DecisionModel,\n    ::DatasetContainer{InMemoryDataset},\n) where {T <: ObjectiveFunctionParameter, V <: PSY.Component}\n    initial_forecast_time = get_current_time(model) # Function not well defined for DecisionModels\n    time_steps = get_time_steps(get_optimization_container(model))\n    horizon = time_steps[end]\n    container = get_optimization_container(model)\n    @assert !is_synchronized(container)\n    template = get_template(model)\n    device_model = get_model(template, V)\n    components = get_available_components(device_model, get_system(model))\n    for component in components\n        name = PSY.get_name(component)\n        op_cost = PSY.get_operation_cost(component)\n        # `handle_variable_cost_parameter` is responsible for figuring out whether there is\n        # actually time variance for this particular component and, if so, performing the update\n        handle_variable_cost_parameter(\n            T(),\n            op_cost,\n            component,\n            name,\n            parameter_array,\n            parameter_multiplier,\n            attributes,\n            container,\n            initial_forecast_time,\n            horizon,\n        )\n    end\n    return\nend\n\n# We only support certain time series costs for PSY.OfferCurveCost, nothing to do for all the others\n# We group them this way because we implement them that way: avoids method ambiguity issues\nhandle_variable_cost_parameter(\n    ::Union{StartupCostParameter, ShutdownCostParameter, AbstractCostAtMinParameter},\n    op_cost::PSY.OperationalCost, args...) = @assert !(op_cost isa PSY.OfferCurveCost)\nhandle_variable_cost_parameter(\n    ::AbstractPiecewiseLinearSlopeParameter,\n    op_cost::PSY.OperationalCost, args...) = @assert !(op_cost isa PSY.OfferCurveCost)\n\n# typically used just with 1 arg, _get_parameter_field(T(), operation_cost).\n_get_parameter_field(::StartupCostParameter, args...; kwargs...) =\n    PSY.get_start_up(args...; kwargs...)\n_get_parameter_field(::ShutdownCostParameter, args...; kwargs...) =\n    PSY.get_shut_down(args...; kwargs...)\n_get_parameter_field(::IncrementalCostAtMinParameter, args...; kwargs...) =\n    PSY.get_incremental_initial_input(args...; kwargs...)\n_get_parameter_field(::DecrementalCostAtMinParameter, args...; kwargs...) =\n    PSY.get_decremental_initial_input(args...; kwargs...)\n_get_parameter_field(\n    ::Union{\n        IncrementalPiecewiseLinearSlopeParameter,\n        IncrementalPiecewiseLinearBreakpointParameter,\n    },\n    args...;\n    kwargs...,\n) =\n    get_output_offer_curves(args...; kwargs...)\n_get_parameter_field(\n    ::Union{\n        DecrementalPiecewiseLinearSlopeParameter,\n        DecrementalPiecewiseLinearBreakpointParameter,\n    },\n    args...;\n    kwargs...,\n) =\n    get_input_offer_curves(args...; kwargs...)\n\n_maybe_tuple(::StartupCostParameter, value) = Tuple(value)\n_maybe_tuple(::ShutdownCostParameter, value) = value\n_maybe_tuple(::AbstractCostAtMinParameter, value) = value\n\nfunction handle_variable_cost_parameter(\n    param::Union{StartupCostParameter, ShutdownCostParameter, AbstractCostAtMinParameter},\n    op_cost::PSY.OfferCurveCost,\n    component,\n    name,\n    parameter_array,\n    parameter_multiplier,\n    attributes,\n    container,\n    initial_forecast_time,\n    horizon,\n)\n    is_time_variant(_get_parameter_field(param, op_cost)) || return\n    ts_vector = _get_parameter_field(param, component, op_cost;\n        start_time = initial_forecast_time,\n        len = horizon,\n    )\n    for (t, value) in enumerate(TimeSeries.values(ts_vector))\n        # startup needs Tuple(value), rest just value. (slight type instability)\n        _set_param_value!(parameter_array, _maybe_tuple(param, value), name, t)\n        update_variable_cost!(\n            param,\n            container,\n            parameter_array,\n            parameter_multiplier,\n            attributes,\n            component,\n            t,\n        )\n    end\n    return\nend\n\nfunction handle_variable_cost_parameter(\n    slope_param::T,\n    op_cost::PSY.OfferCurveCost,\n    component,\n    name,\n    parameter_array,\n    parameter_multiplier,\n    attributes,\n    container,\n    initial_forecast_time,\n    horizon,\n) where {T <: AbstractPiecewiseLinearSlopeParameter}\n    is_time_variant(_get_parameter_field(slope_param, op_cost)) || return\n    ts_vector = _get_parameter_field(slope_param,\n        component, op_cost;\n        start_time = initial_forecast_time,\n        len = horizon,\n    )\n    for (t, value::PSY.PiecewiseStepData) in enumerate(TimeSeries.values(ts_vector))\n        unwrapped_value =\n            _unwrap_for_param(T(), value, lookup_additional_axes(parameter_array))\n        _set_param_value!(parameter_array, unwrapped_value, name, t)\n        update_variable_cost!(\n            slope_param,\n            container,\n            value,  # intentionally passing the PiecewiseStepData here, not the unwrapped\n            parameter_multiplier,\n            attributes,\n            component,\n            t,\n        )\n    end\n    return\nend\n\nfunction handle_variable_cost_parameter(\n    ::FuelCostParameter,\n    op_cost::PSY.ThermalGenerationCost,\n    component,\n    name,\n    parameter_array,\n    parameter_multiplier,\n    attributes,\n    container,\n    initial_forecast_time,\n    horizon,\n)\n    fuel_curve = PSY.get_variable(op_cost)\n    # Nothing to update for this component if we don't have a fuel cost time series\n    (fuel_curve isa PSY.FuelCurve && is_time_variant(PSY.get_fuel_cost(fuel_curve))) ||\n        return\n\n    ts_vector = PSY.get_fuel_cost(\n        component;\n        start_time = initial_forecast_time,\n        len = horizon,\n    )\n    fuel_cost_forecast_values = TimeSeries.values(ts_vector)\n    for (t, value) in enumerate(fuel_cost_forecast_values)\n        # TODO: MBC Is this compact power attribute being used?\n        if attributes.uses_compact_power\n            # TODO implement this\n            value, _ = _convert_variable_cost(value)\n        end\n        _set_param_value!(parameter_array, value, name, t)\n        update_variable_cost!(\n            FuelCostParameter(),\n            container,\n            parameter_array,\n            parameter_multiplier,\n            attributes,\n            component,\n            fuel_curve,\n            t,\n        )\n    end\n    return\nend\n\n_linear_block_param(::Type{IncrementalPiecewiseLinearSlopeParameter}) =\n    PiecewiseLinearBlockIncrementalOffer()\n_linear_block_param(::Type{DecrementalPiecewiseLinearSlopeParameter}) =\n    PiecewiseLinearBlockDecrementalOffer()\n\nfunction _update_pwl_cost_expression(\n    ::P,\n    container::OptimizationContainer,\n    ::Type{T},\n    component_name::String,\n    time_period::Int,\n    cost_data::PSY.PiecewiseStepData,\n) where {P <: AbstractPiecewiseLinearSlopeParameter, T <: PSY.Component}\n    pwl_var_container = get_variable(container, _linear_block_param(P), T)\n    resolution = get_resolution(container)\n    dt = Dates.value(resolution) / MILLISECONDS_IN_HOUR\n    gen_cost = JuMP.AffExpr(0.0)\n    slopes = PSY.get_y_coords(cost_data)\n    for i in 1:length(cost_data)\n        JuMP.add_to_expression!(\n            gen_cost,\n            slopes[i] * dt,\n            pwl_var_container[(component_name, i, time_period)],\n        )\n    end\n    return gen_cost\nend\n\n# For multi-start variables, we need to get a subset of the parameter\n_index_into_param(cost_data, ::T) where {T <: Union{StartVariable, MultiStartVariable}} =\n    start_up_cost(cost_data, T())\n_index_into_param(cost_data, ::VariableType) = cost_data\n\nget_update_multiplier(::DecrementalCostAtMinParameter) = -1.0\nget_update_multiplier(::IncrementalCostAtMinParameter) = 1.0\nget_update_multiplier(::ObjectiveFunctionParameter) = 1.0\n\n# Mirrors the per-component decomposition done at build time, so recurrent solves\n# update the same constituent expression that contributed to ProductionCostExpression.\n_constituent_cost_expression(::StartupCostParameter) = StartUpCostExpression\n_constituent_cost_expression(::ShutdownCostParameter) = ShutDownCostExpression\n_constituent_cost_expression(::AbstractCostAtMinParameter) = FixedCostExpression\n\n# General case\nfunction update_variable_cost!(\n    parameter::ObjectiveFunctionParameter,\n    container::OptimizationContainer,\n    parameter_array::DenseAxisArray{T},\n    parameter_multiplier::JuMPFloatArray,\n    attributes::CostFunctionAttributes{T},\n    component::U,\n    time_period::Int,\n) where {T, U <: PSY.Component}\n    component_name = PSY.get_name(component)\n    cost_data = parameter_array[component_name, time_period]\n    mult_ = parameter_multiplier[component_name, time_period]\n    mult2 = get_update_multiplier(parameter)\n    constituent_type = _constituent_cost_expression(parameter)\n    for MyVariableType in get_variable_types(attributes)\n        variable = get_variable(container, MyVariableType(), U)\n        my_cost_data = _index_into_param(cost_data, MyVariableType())\n        iszero(my_cost_data) && continue\n        cost_expr = variable[component_name, time_period] * my_cost_data * mult_ * mult2\n        add_to_objective_variant_expression!(container, cost_expr)\n        set_expression!(\n            container,\n            ProductionCostExpression,\n            cost_expr,\n            component,\n            time_period,\n        )\n        set_expression!(container, constituent_type, cost_expr, component, time_period)\n    end\n    return\nend\n\nget_update_multiplier(::IncrementalPiecewiseLinearSlopeParameter) = 1.0\nget_update_multiplier(::DecrementalPiecewiseLinearSlopeParameter) = -1.0\n\n# Special case for PiecewiseStepData\nfunction update_variable_cost!(\n    slope_param::AbstractPiecewiseLinearSlopeParameter,\n    container::OptimizationContainer,\n    function_data::PSY.PiecewiseStepData,\n    parameter_multiplier::JuMPFloatArray,\n    ::CostFunctionAttributes,\n    component::T,\n    time_period::Int,\n) where {T <: PSY.Component}\n    component_name = PSY.get_name(component)\n    # TODO handle per-tranche multiplier if necessary\n    mult_ = 1.0 # parameter_multiplier[component_name, time_period, 1]\n    mult2 = get_update_multiplier(slope_param)\n    converted_data = get_piecewise_curve_per_system_unit(\n        function_data,\n        PSY.UnitSystem.NATURAL_UNITS,  # PSY's cost_function_timeseries.jl says this will always be natural units\n        get_base_power(container),\n        PSY.get_base_power(component),\n    )\n    gen_cost =\n        _update_pwl_cost_expression(\n            slope_param,\n            container,\n            T,\n            component_name,\n            time_period,\n            converted_data,\n        )\n    add_to_objective_variant_expression!(container, mult2 * mult_ * gen_cost)\n    set_expression!(container, ProductionCostExpression, gen_cost, component, time_period)\n    set_expression!(container, FuelCostExpression, gen_cost, component, time_period)\n    return\nend\n\n# Special case for fuel cost\nfunction update_variable_cost!(\n    ::FuelCostParameter,\n    container::OptimizationContainer,\n    parameter_array::JuMPFloatArray,\n    parameter_multiplier::JuMPFloatArray,\n    ::CostFunctionAttributes{Float64},\n    component::T,\n    fuel_curve::PSY.FuelCurve,\n    time_period::Int,\n) where {T <: PSY.Component}\n    component_name = PSY.get_name(component)\n    fuel_cost = parameter_array[component_name, time_period]\n    if all(iszero.(last.(fuel_cost)))\n        return\n    end\n    mult_ = parameter_multiplier[component_name, time_period]\n    expression = get_expression(container, FuelConsumptionExpression(), T)\n    cost_expr = expression[component_name, time_period] * fuel_cost * mult_\n    add_to_objective_variant_expression!(container, cost_expr)\n    set_expression!(container, ProductionCostExpression, cost_expr, component, time_period)\n    set_expression!(container, FuelCostExpression, cost_expr, component, time_period)\n    return\nend\n"
  },
  {
    "path": "src/parameters/update_parameters.jl",
    "content": "\"\"\"\nUpdate parameter function an OperationModel\n\"\"\"\nfunction update_parameter_values!(\n    model::OperationModel,\n    key::ParameterKey{T, U},\n    simulation_state::SimulationState,\n) where {T <: ParameterType, U <: PSY.Component}\n    # Enable again for detailed debugging\n    # TimerOutputs.@timeit RUN_SIMULATION_TIMER \"$T $U Parameter Update\" begin\n    optimization_container = get_optimization_container(model)\n    input = get_decision_states(simulation_state)\n    update_container_parameter_values!(optimization_container, model, key, input)\n    parameter_attributes = get_parameter_attributes(optimization_container, key)\n    IS.@record :execution ParameterUpdateEvent(\n        T,\n        U,\n        parameter_attributes,\n        get_current_timestamp(model),\n        get_name(model),\n    )\n    #end\n    return\nend\n\nfunction _fix_parameter_value!(\n    container::OptimizationContainer,\n    parameter_array::DenseAxisArray{Float64, 2},\n    parameter_attributes::VariableValueAttributes,\n)\n    affected_variable_keys = parameter_attributes.affected_keys\n    @assert !isempty(affected_variable_keys)\n    # Hoist underlying dense storage for the parameter array once. The variable\n    # array's storage is hoisted per affected key (different arrays per key).\n    parent_param = parameter_array.data\n    component_names, time = axes(parameter_array)\n    param_lookup = parameter_array.lookup[1]\n    for var_key in affected_variable_keys\n        variable = get_variable(container, var_key)\n        parent_var = variable.data\n        var_lookup = variable.lookup[1]\n        for name in component_names\n            i_param = param_lookup[name]\n            i_var = var_lookup[name]\n            for t in time\n                JuMP.fix(\n                    parent_var[i_var, t],\n                    parent_param[i_param, t];\n                    force = true,\n                )\n            end\n        end\n    end\n    return\nend\n\nfunction update_parameter_values!(\n    model::OperationModel,\n    key::ParameterKey{FixValueParameter, T},\n    simulation_state::SimulationState,\n) where {T <: PSY.Service}\n    # Enable again for detailed debugging\n    # TimerOutputs.@timeit RUN_SIMULATION_TIMER \"$T $U Parameter Update\" begin\n    optimization_container = get_optimization_container(model)\n    # Note: Do not instantite a new key here because it might not match the param keys in the container\n    # if the keys have strings in the meta fields\n    parameter_array = get_parameter_array(optimization_container, key)\n    parameter_attributes = get_parameter_attributes(optimization_container, key)\n    service = PSY.get_component(T, get_system(model), key.meta)\n    @assert service !== nothing\n    input = get_decision_states(simulation_state)\n    _update_parameter_values!(\n        parameter_array,\n        FixValueParameter(),\n        parameter_attributes,\n        service,\n        model,\n        input,\n    )\n    _fix_parameter_value!(optimization_container, parameter_array, parameter_attributes)\n    IS.@record :execution ParameterUpdateEvent(\n        FixValueParameter,\n        T,\n        parameter_attributes,\n        get_current_timestamp(model),\n        get_name(model),\n    )\n    #end\n    return\nend\n"
  },
  {
    "path": "src/services_models/agc.jl",
    "content": "#! format: off\nget_variable_multiplier(_, ::Type{<:PSY.AGC}, ::AbstractAGCFormulation) = NaN\n########################## ActivePowerVariable, AGC ###########################\n\n########################## SteadyStateFrequencyDeviation ##################################\nget_variable_binary(::SteadyStateFrequencyDeviation, ::Type{<:PSY.AGC}, ::AbstractAGCFormulation) = false\n\nget_variable_binary(::ActivePowerVariable, ::Type{<:PSY.Area}, ::AbstractAGCFormulation) = false\n########################## SmoothACE, AggregationTopology ###########################\n\nget_variable_binary(::SmoothACE, ::Type{<:PSY.AggregationTopology}, ::AbstractAGCFormulation) = false\nget_variable_binary(::SmoothACE, ::Type{<:PSY.AGC}, ::AbstractAGCFormulation) = false\n\n########################## DeltaActivePowerUpVariable, AGC ###########################\n\nget_variable_binary(::DeltaActivePowerUpVariable, ::Type{<:PSY.AGC}, ::AbstractAGCFormulation) = false\nget_variable_lower_bound(::DeltaActivePowerUpVariable, ::PSY.AGC, ::AbstractAGCFormulation) = 0.0\n\n########################## DeltaActivePowerDownVariable, AGC ###########################\n\nget_variable_binary(::DeltaActivePowerDownVariable, ::Type{<:PSY.AGC}, ::AbstractAGCFormulation) = false\nget_variable_lower_bound(::DeltaActivePowerDownVariable, ::PSY.AGC, ::AbstractAGCFormulation) = 0.0\n\n########################## AdditionalDeltaPowerUpVariable, Area ###########################\n\nget_variable_binary(::AdditionalDeltaActivePowerUpVariable, ::Type{<:PSY.Area}, ::AbstractAGCFormulation) = false\nget_variable_lower_bound(::AdditionalDeltaActivePowerUpVariable, ::PSY.Area, ::AbstractAGCFormulation) = 0.0\n\n########################## AdditionalDeltaPowerDownVariable, Area ###########################\n\nget_variable_binary(::AdditionalDeltaActivePowerDownVariable, ::Type{<:PSY.Area}, ::AbstractAGCFormulation) = false\nget_variable_lower_bound(::AdditionalDeltaActivePowerDownVariable, ::PSY.Area, ::AbstractAGCFormulation) = 0.0\n\n########################## AreaMismatchVariable, AGC ###########################\nget_variable_binary(::AreaMismatchVariable, ::Type{<:PSY.AGC}, ::AbstractAGCFormulation) = false\n\n########################## LiftVariable, Area ###########################\nget_variable_binary(::LiftVariable, ::Type{<:PSY.AGC}, ::AbstractAGCFormulation) = false\nget_variable_lower_bound(::LiftVariable, ::PSY.AGC, ::AbstractAGCFormulation) = 0.0\n\ninitial_condition_default(::AreaControlError, d::PSY.AGC, ::AbstractAGCFormulation) = PSY.get_initial_ace(d)\ninitial_condition_variable(::AreaControlError, d::PSY.AGC, ::AbstractAGCFormulation) = AreaMismatchVariable()\n\nget_variable_multiplier(::SteadyStateFrequencyDeviation, d::PSY.AGC, ::AbstractAGCFormulation) = -10 * PSY.get_bias(d)\n\n#! format: on\n\nfunction get_default_time_series_names(\n    ::Type{PSY.AGC},\n    ::Type{<:AbstractAGCFormulation},\n)\n    return Dict{Type{<:TimeSeriesParameter}, String}()\nend\n\nfunction get_default_attributes(\n    ::Type{PSY.AGC},\n    ::Type{<:AbstractAGCFormulation},\n)\n    return Dict{String, Any}(\"aggregated_service_model\" => false)\nend\n\n\"\"\"\nSteady State deviation of the frequency\n\"\"\"\nfunction add_variables!(\n    container::OptimizationContainer,\n    ::Type{T},\n) where {T <: SteadyStateFrequencyDeviation}\n    time_steps = get_time_steps(container)\n    variable = add_variable_container!(container, T(), PSY.AGC, time_steps)\n    for t in time_steps\n        variable[t] = JuMP.@variable(container.JuMPmodel, base_name = \"ΔF_{$(t)}\")\n    end\nend\n\n########################## Initial Condition ###########################\n\nfunction _get_variable_initial_value(\n    d::PSY.Component,\n    key::InitialConditionKey,\n    ::AbstractAGCFormulation,\n    ::Nothing,\n)\n    return _get_ace_error(d, key)\nend\n\nfunction add_constraints!(\n    container::OptimizationContainer,\n    ::Type{T},\n    ::Type{LiftVariable},\n    agcs::IS.FlattenIteratorWrapper{U},\n    ::ServiceModel{PSY.AGC, V},\n) where {T <: AbsoluteValueConstraint, U <: PSY.AGC, V <: PIDSmoothACE}\n    time_steps = get_time_steps(container)\n    agc_names = PSY.get_name.(agcs)\n    container_lb =\n        add_constraints_container!(container, T(), U, agc_names, time_steps; meta = \"lb\")\n    container_ub =\n        add_constraints_container!(container, T(), U, agc_names, time_steps; meta = \"ub\")\n    mismatch = get_variable(container, AreaMismatchVariable(), U)\n    z = get_variable(container, LiftVariable(), U)\n    jump_model = get_jump_model(container)\n\n    for t in time_steps, a in agc_names\n        container_lb[a, t] = JuMP.@constraint(jump_model, mismatch[a, t] <= z[a, t])\n        container_ub[a, t] = JuMP.@constraint(jump_model, -1 * mismatch[a, t] <= z[a, t])\n    end\n    return\nend\n\n\"\"\"\nExpression for the power deviation given deviation in the frequency. This expression allows\nupdating the response of the frequency depending on commitment decisions\n\"\"\"\nfunction add_constraints!(\n    container::OptimizationContainer,\n    ::Type{T},\n    ::Type{SteadyStateFrequencyDeviation},\n    agcs::IS.FlattenIteratorWrapper{U},\n    ::ServiceModel{PSY.AGC, V},\n    sys::PSY.System,\n) where {T <: FrequencyResponseConstraint, U <: PSY.AGC, V <: PIDSmoothACE}\n    time_steps = get_time_steps(container)\n    agc_names = PSY.get_name.(agcs)\n\n    frequency_response = 0.0\n    for agc in agcs\n        area = PSY.get_area(agc)\n        frequency_response += PSY.get_load_response(area)\n    end\n\n    for g in PSY.get_components(PSY.get_available, PSY.RegulationDevice, sys)\n        d = PSY.get_droop(g)\n        response = 1 / d\n        frequency_response += response\n    end\n\n    IS.@assert_op frequency_response >= 0.0\n\n    # This value is the one updated later in simulation based on the UC result\n    inv_frequency_response = 1 / frequency_response\n\n    area_balance = get_variable(container, ActivePowerVariable(), PSY.Area)\n    frequency = get_variable(container, SteadyStateFrequencyDeviation(), U)\n    R_up = get_variable(container, DeltaActivePowerUpVariable(), U)\n    R_dn = get_variable(container, DeltaActivePowerDownVariable(), U)\n    R_up_emergency =\n        get_variable(container, AdditionalDeltaActivePowerUpVariable(), PSY.Area)\n    R_dn_emergency =\n        get_variable(container, AdditionalDeltaActivePowerUpVariable(), PSY.Area)\n\n    const_container = add_constraints_container!(container, T(), PSY.System, time_steps)\n\n    for t in time_steps\n        system_balance = sum(area_balance.data[:, t])\n        for agc in agcs\n            a = PSY.get_name(agc)\n            area_name = PSY.get_name(PSY.get_area(agc))\n            JuMP.add_to_expression!(system_balance, R_up[a, t])\n            JuMP.add_to_expression!(system_balance, -1.0, R_dn[a, t])\n            JuMP.add_to_expression!(system_balance, R_up_emergency[area_name, t])\n            JuMP.add_to_expression!(system_balance, -1.0, R_dn_emergency[area_name, t])\n        end\n        const_container[t] = JuMP.@constraint(\n            container.JuMPmodel,\n            frequency[t] == -inv_frequency_response * system_balance\n        )\n    end\n    return\nend\n\nfunction add_constraints!(\n    container::OptimizationContainer,\n    ::Type{T},\n    ::Type{SteadyStateFrequencyDeviation},\n    agcs::IS.FlattenIteratorWrapper{U},\n    model::ServiceModel{PSY.AGC, V},\n    sys::PSY.System,\n) where {T <: SACEPIDAreaConstraint, U <: PSY.AGC, V <: PIDSmoothACE}\n    services = get_available_components(model, sys)\n    time_steps = get_time_steps(container)\n    agc_names = PSY.get_name.(services)\n    area_names = [PSY.get_name(PSY.get_area(s)) for s in services]\n    RAW_ACE = get_expression(container, RawACE(), U)\n    SACE = get_variable(container, SmoothACE(), U)\n    SACE_pid = add_constraints_container!(\n        container,\n        SACEPIDAreaConstraint(),\n        U,\n        agc_names,\n        time_steps,\n    )\n\n    jump_model = get_jump_model(container)\n    for (ix, service) in enumerate(services)\n        kp = PSY.get_K_p(service)\n        ki = PSY.get_K_i(service)\n        kd = PSY.get_K_d(service)\n        Δt = convert(Dates.Second, get_resolution(container)).value\n        a = PSY.get_name(service)\n        for t in time_steps\n            if t == 1\n                ACE_ini = get_initial_condition(container, AreaControlError(), PSY.AGC)[ix]\n                ace_exp = get_value(ACE_ini) + kp * ((1 + Δt / (kp / ki)) * (RAW_ACE[a, t]))\n                SACE_pid[a, t] = JuMP.@constraint(jump_model, SACE[a, t] == ace_exp)\n                continue\n            end\n            SACE_pid[a, t] = JuMP.@constraint(\n                jump_model,\n                SACE[a, t] ==\n                SACE[a, t - 1] +\n                kp * (\n                    (1 + Δt / (kp / ki) + (kd / kp) / Δt) * (RAW_ACE[a, t]) +\n                    (-1 - 2 * (kd / kp) / Δt) * (RAW_ACE[a, t - 1])\n                )\n            )\n        end\n    end\n    return\nend\n\nfunction add_constraints!(\n    container::OptimizationContainer,\n    ::Type{T},\n    ::Type{SmoothACE},\n    agcs::IS.FlattenIteratorWrapper{U},\n    ::ServiceModel{PSY.AGC, V},\n    sys::PSY.System,\n) where {T <: BalanceAuxConstraint, U <: PSY.AGC, V <: PIDSmoothACE}\n    time_steps = get_time_steps(container)\n    agc_names = PSY.get_name.(agcs)\n    aux_equation = add_constraints_container!(\n        container,\n        BalanceAuxConstraint(),\n        PSY.System,\n        agc_names,\n        time_steps,\n    )\n    area_mismatch = get_variable(container, AreaMismatchVariable(), PSY.AGC)\n    SACE = get_variable(container, SmoothACE(), PSY.AGC)\n    R_up = get_variable(container, DeltaActivePowerUpVariable(), PSY.AGC)\n    R_dn = get_variable(container, DeltaActivePowerDownVariable(), PSY.AGC)\n    R_up_emergency =\n        get_variable(container, AdditionalDeltaActivePowerUpVariable(), PSY.Area)\n    R_dn_emergency =\n        get_variable(container, AdditionalDeltaActivePowerUpVariable(), PSY.Area)\n\n    for t in time_steps\n        for agc in agcs\n            a = PSY.get_name(agc)\n            area_name = PSY.get_name(PSY.get_area(agc))\n            aux_equation[a, t] = JuMP.@constraint(\n                container.JuMPmodel,\n                -1 * SACE[a, t] ==\n                (R_up[a, t] - R_dn[a, t]) +\n                (R_up_emergency[area_name, t] - R_dn_emergency[area_name, t]) +\n                area_mismatch[a, t]\n            )\n        end\n    end\n    return\nend\n\nfunction objective_function!(\n    container::OptimizationContainer,\n    agcs::IS.FlattenIteratorWrapper{T},\n    ::ServiceModel{<:PSY.AGC, U},\n) where {T <: PSY.AGC, U <: PIDSmoothACE}\n    add_proportional_cost!(container, LiftVariable(), agcs, U())\n    return\nend\n\n# Defined here so we can dispatch on PIDSmoothACE\nfunction add_feedforward_arguments!(\n    container::OptimizationContainer,\n    model::ServiceModel{PSY.AGC, PIDSmoothACE},\n    areas::IS.FlattenIteratorWrapper{PSY.AGC},\n)\n    for ff in get_feedforwards(model)\n        @debug \"arguments\" ff V _group = LOG_GROUP_FEEDFORWARDS_CONSTRUCTION\n        add_feedforward_arguments!(container, model, areas, ff)\n    end\n    return\nend\n\nfunction add_feedforward_constraints!(\n    container::OptimizationContainer,\n    model::ServiceModel{PSY.AGC, PIDSmoothACE},\n    areas::IS.FlattenIteratorWrapper{PSY.AGC},\n)\n    for ff in get_feedforwards(model)\n        @debug \"arguments\" ff V _group = LOG_GROUP_FEEDFORWARDS_CONSTRUCTION\n        add_feedforward_constraints!(container, model, areas, ff)\n    end\n    return\nend\n\nfunction add_proportional_cost!(\n    container::OptimizationContainer,\n    ::U,\n    agcs::IS.FlattenIteratorWrapper{T},\n    ::PIDSmoothACE,\n) where {T <: PSY.AGC, U <: LiftVariable}\n    lift_variable = get_variable(container, U(), T)\n    for index in Iterators.product(axes(lift_variable)...)\n        add_to_objective_invariant_expression!(\n            container,\n            SERVICES_SLACK_COST * lift_variable[index...],\n        )\n    end\n    return\nend\n"
  },
  {
    "path": "src/services_models/reserve_group.jl",
    "content": "function get_default_time_series_names(\n    ::Type{PSY.ConstantReserveGroup{T}},\n    ::Type{GroupReserve}) where {T <: PSY.ReserveDirection}\n    return Dict{String, Any}()\nend\n\nfunction get_default_attributes(\n    ::Type{PSY.ConstantReserveGroup{T}},\n    ::Type{GroupReserve}) where {T <: PSY.ReserveDirection}\n    return Dict{String, Any}()\nend\n\n############################### Reserve Variables` #########################################\n\"\"\"\nThis function checks if the variables for reserves were created\n\"\"\"\nfunction check_activeservice_variables(\n    container::OptimizationContainer,\n    contributing_services::Vector{T},\n) where {T <: PSY.Service}\n    for service in contributing_services\n        get_variable(\n            container,\n            ActivePowerReserveVariable(),\n            typeof(service),\n            PSY.get_name(service),\n        )\n    end\n    return\nend\n\n################################## Reserve Requirement Constraint ##########################\n\"\"\"\nThis function creates the requirement constraint that will be attained by the appropriate services\n\"\"\"\nfunction add_constraints!(\n    container::OptimizationContainer,\n    ::Type{RequirementConstraint},\n    service::SR,\n    contributing_services::Vector{<:PSY.Service},\n    model::ServiceModel{SR, GroupReserve},\n) where {SR <: PSY.ConstantReserveGroup}\n    time_steps = get_time_steps(container)\n    service_name = PSY.get_name(service)\n    add_constraints_container!(\n        container,\n        RequirementConstraint(),\n        SR,\n        [service_name],\n        time_steps;\n        meta = service_name,\n    )\n    constraint = get_constraint(container, RequirementConstraint(), SR, service_name)\n    use_slacks = get_use_slacks(model)\n    reserve_variables = [\n        get_variable(container, ActivePowerReserveVariable(), typeof(r), PSY.get_name(r)) for r in contributing_services\n    ]\n\n    requirement = PSY.get_requirement(service)\n    for t in time_steps\n        resource_expression = JuMP.GenericAffExpr{Float64, JuMP.VariableRef}()\n        for reserve_variable in reserve_variables\n            JuMP.add_to_expression!(resource_expression, sum(@view reserve_variable[:, t]))\n        end\n        if use_slacks\n            resource_expression += slack_vars[t]\n        end\n        constraint[service_name, t] =\n            JuMP.@constraint(container.JuMPmodel, resource_expression >= requirement)\n    end\n\n    return\nend\n"
  },
  {
    "path": "src/services_models/reserves.jl",
    "content": "#! format: off\n############################### Reserve Variables #########################################\n\nget_variable_multiplier(_, ::Type{<:PSY.Reserve}, ::AbstractReservesFormulation) = NaN\n############################### PostContingencyActivePowerReserveDeploymentVariable, Reserve #########################################\nget_variable_binary(::PostContingencyActivePowerReserveDeploymentVariable, ::Type{<:PSY.Reserve}, ::AbstractSecurityConstrainedReservesFormulation) = false\nfunction get_variable_upper_bound(::PostContingencyActivePowerReserveDeploymentVariable, r::PSY.Reserve, d::PSY.Device, ::AbstractSecurityConstrainedReservesFormulation)\n    return  PSY.get_max_active_power(d)\nend\nget_variable_lower_bound(::PostContingencyActivePowerReserveDeploymentVariable, ::PSY.Reserve, ::PSY.Device, _) = 0.0\nget_variable_warm_start_value(::PostContingencyActivePowerReserveDeploymentVariable, d::PSY.Reserve, ::AbstractSecurityConstrainedReservesFormulation) = 0.0\nget_variable_multiplier(::AbstractContingencyVariableType, ::Type{<:PSY.Reserve{PSY.ReserveDown}}, ::AbstractSecurityConstrainedReservesFormulation) = -1.0\nget_variable_multiplier(::AbstractContingencyVariableType, ::Type{<:PSY.Reserve{PSY.ReserveUp}}, ::AbstractSecurityConstrainedReservesFormulation) = 1.0\nget_variable_multiplier(::VariableType, ::Type{<:PSY.Generator}, ::AbstractSecurityConstrainedReservesFormulation) = -1.0\n\n############################### ActivePowerReserveVariable, Reserve #########################################\nget_variable_binary(::ActivePowerReserveVariable, ::Type{<:PSY.Reserve}, ::AbstractReservesFormulation) = false\nfunction get_variable_upper_bound(::ActivePowerReserveVariable, r::PSY.Reserve, d::PSY.Device, ::AbstractReservesFormulation)\n    return PSY.get_max_output_fraction(r) * PSY.get_max_active_power(d)\nend\nget_variable_upper_bound(::ActivePowerReserveVariable, r::PSY.ReserveDemandCurve, d::PSY.Device, ::AbstractReservesFormulation) = PSY.get_max_active_power(d)\nget_variable_lower_bound(::ActivePowerReserveVariable, ::PSY.Reserve, ::PSY.Device, _) = 0.0\n\n############################### ActivePowerReserveVariable, ReserveNonSpinning #########################################\nget_variable_binary(::ActivePowerReserveVariable, ::Type{<:PSY.ReserveNonSpinning}, ::AbstractReservesFormulation) = false\nfunction get_variable_upper_bound(::ActivePowerReserveVariable, r::PSY.ReserveNonSpinning, d::PSY.Device, ::AbstractReservesFormulation)\n    return PSY.get_max_output_fraction(r) * PSY.get_max_active_power(d)\nend\nget_variable_lower_bound(::ActivePowerReserveVariable, ::PSY.ReserveNonSpinning, ::PSY.Device, _) = 0.0\n\n############################### ServiceRequirementVariable, ReserveDemandCurve ################################\n\nget_variable_binary(::ServiceRequirementVariable, ::Type{<:PSY.ReserveDemandCurve}, ::AbstractReservesFormulation) = false\nget_variable_upper_bound(::ServiceRequirementVariable, ::PSY.ReserveDemandCurve, d::PSY.Component, ::AbstractReservesFormulation) = PSY.get_max_active_power(d)\nget_variable_lower_bound(::ServiceRequirementVariable, ::PSY.ReserveDemandCurve, ::PSY.Component, ::AbstractReservesFormulation) = 0.0\n\nget_multiplier_value(::RequirementTimeSeriesParameter, d::PSY.Reserve, ::AbstractReservesFormulation) = PSY.get_requirement(d)\nget_multiplier_value(::RequirementTimeSeriesParameter, d::PSY.ReserveNonSpinning, ::AbstractReservesFormulation) = PSY.get_requirement(d)\n\nget_parameter_multiplier(::VariableValueParameter, d::Type{<:PSY.AbstractReserve}, ::AbstractReservesFormulation) = 1.0\nget_initial_parameter_value(::VariableValueParameter, d::Type{<:PSY.AbstractReserve}, ::AbstractReservesFormulation) = 0.0\n\nobjective_function_multiplier(::ServiceRequirementVariable, ::StepwiseCostReserve) = -1.0\nsos_status(::PSY.ReserveDemandCurve, ::StepwiseCostReserve)=SOSStatusVariable.NO_VARIABLE\nuses_compact_power(::PSY.ReserveDemandCurve, ::StepwiseCostReserve)=false\n#! format: on\n\nfunction get_initial_conditions_service_model(\n    ::OperationModel,\n    ::ServiceModel{T, D},\n) where {T <: PSY.Reserve, D <: AbstractReservesFormulation}\n    return ServiceModel(T, D)\nend\n\nfunction get_initial_conditions_service_model(\n    ::OperationModel,\n    ::ServiceModel{T, D},\n) where {T <: PSY.VariableReserveNonSpinning, D <: AbstractReservesFormulation}\n    return ServiceModel(T, D)\nend\n\nfunction get_default_time_series_names(\n    ::Type{<:PSY.Reserve},\n    ::Type{T},\n) where {T <: Union{RangeReserve, RampReserve}}\n    return Dict{Type{<:TimeSeriesParameter}, String}(\n        RequirementTimeSeriesParameter => \"requirement\",\n    )\nend\n\nfunction get_default_time_series_names(\n    ::Type{<:PSY.ReserveNonSpinning},\n    ::Type{NonSpinningReserve},\n)\n    return Dict{Type{<:TimeSeriesParameter}, String}(\n        RequirementTimeSeriesParameter => \"requirement\",\n    )\nend\n\nfunction get_default_time_series_names(\n    ::Type{T},\n    ::Type{<:AbstractReservesFormulation},\n) where {T <: PSY.Reserve}\n    return Dict{Type{<:TimeSeriesParameter}, String}()\nend\n\nfunction get_default_attributes(\n    ::Type{<:PSY.Reserve},\n    ::Type{<:AbstractReservesFormulation},\n)\n    return Dict{String, Any}()\nend\n\nfunction get_default_attributes(\n    ::Type{<:PSY.ReserveNonSpinning},\n    ::Type{<:AbstractReservesFormulation},\n)\n    return Dict{String, Any}()\nend\n\n\"\"\"\nAdd variables for ServiceRequirementVariable for StepWiseCostReserve\n\"\"\"\nfunction add_variable!(\n    container::OptimizationContainer,\n    variable_type::T,\n    service::D,\n    formulation,\n) where {\n    T <: ServiceRequirementVariable,\n    D <: PSY.ReserveDemandCurve,\n}\n    time_steps = get_time_steps(container)\n    service_name = PSY.get_name(service)\n    variable = add_variable_container!(\n        container,\n        variable_type,\n        D,\n        [service_name],\n        time_steps;\n        meta = service_name,\n    )\n\n    for t in time_steps\n        variable[service_name, t] = JuMP.@variable(\n            get_jump_model(container),\n            base_name = \"$(T)_$(D)_$(service_name)_{$(service_name), $(t)}\",\n            lower_bound = 0.0,\n        )\n    end\n\n    return\nend\n\nfunction _sum_reserve_variables(\n    vars::AbstractArray{<:JuMP.AbstractVariableRef},\n    extra::Int,\n)\n    acc = get_hinted_aff_expr(length(vars) + extra)\n    for v in vars\n        JuMP.add_to_expression!(acc, v)\n    end\n    return acc\nend\n\n################################## Reserve Requirement Constraint ##########################\nfunction add_constraints!(\n    container::OptimizationContainer,\n    T::Type{RequirementConstraint},\n    service::SR,\n    ::U,\n    model::ServiceModel{SR, V},\n) where {\n    SR <: PSY.AbstractReserve,\n    V <: AbstractReservesFormulation,\n    U <: Union{Vector{D}, IS.FlattenIteratorWrapper{D}},\n} where {D <: PSY.Component}\n    parameters = built_for_recurrent_solves(container)\n\n    time_steps = get_time_steps(container)\n    service_name = PSY.get_name(service)\n    # TODO: Add a method for services that handles this better\n    constraint = add_constraints_container!(\n        container,\n        T(),\n        SR,\n        [service_name],\n        time_steps;\n        meta = service_name,\n    )\n    reserve_variable =\n        get_variable(container, ActivePowerReserveVariable(), SR, service_name)\n    use_slacks = get_use_slacks(model)\n\n    ts_vector = get_time_series(\n        container,\n        service,\n        \"requirement\";\n        interval = get_interval(get_settings(container)),\n    )\n\n    use_slacks && (slack_vars = reserve_slacks!(container, service))\n    requirement = PSY.get_requirement(service)\n    jump_model = get_jump_model(container)\n    extra = use_slacks ? 1 : 0\n    if built_for_recurrent_solves(container)\n        param_container =\n            get_parameter(container, RequirementTimeSeriesParameter(), SR, service_name)\n        param = get_parameter_column_refs(param_container, service_name)\n        for t in time_steps\n            resource_expression =\n                _sum_reserve_variables(@view(reserve_variable[:, t]), extra)\n            use_slacks &&\n                JuMP.add_to_expression!(resource_expression, slack_vars[t])\n            constraint[service_name, t] =\n                JuMP.@constraint(jump_model, resource_expression >= param[t] * requirement)\n        end\n    else\n        for t in time_steps\n            resource_expression =\n                _sum_reserve_variables(@view(reserve_variable[:, t]), extra)\n            use_slacks &&\n                JuMP.add_to_expression!(resource_expression, slack_vars[t])\n            constraint[service_name, t] = JuMP.@constraint(\n                jump_model,\n                resource_expression >= ts_vector[t] * requirement\n            )\n        end\n    end\n    return\nend\n\nfunction add_constraints!(\n    container::OptimizationContainer,\n    T::Type{ParticipationFractionConstraint},\n    service::SR,\n    contributing_devices::U,\n    ::ServiceModel{SR, V},\n) where {\n    SR <: PSY.AbstractReserve,\n    V <: AbstractReservesFormulation,\n    U <: Union{Vector{D}, IS.FlattenIteratorWrapper{D}},\n} where {D <: PSY.Device}\n    max_participation_factor = PSY.get_max_participation_factor(service)\n\n    if max_participation_factor >= 1.0\n        return\n    end\n\n    time_steps = get_time_steps(container)\n    service_name = PSY.get_name(service)\n    cons = add_constraints_container!(\n        container,\n        T(),\n        SR,\n        [PSY.get_name(d) for d in contributing_devices],\n        time_steps;\n        meta = service_name,\n    )\n    var_r = get_variable(container, ActivePowerReserveVariable(), SR, service_name)\n    jump_model = get_jump_model(container)\n    requirement = PSY.get_requirement(service)\n    ts_vector = get_time_series(\n        container,\n        service,\n        \"requirement\";\n        interval = get_interval(get_settings(container)),\n    )\n    param_container =\n        get_parameter(container, RequirementTimeSeriesParameter(), SR, service_name)\n    param = get_parameter_column_refs(param_container, service_name)\n    for t in time_steps, d in contributing_devices\n        name = PSY.get_name(d)\n        if built_for_recurrent_solves(container)\n            cons[name, t] =\n                JuMP.@constraint(\n                    jump_model,\n                    var_r[name, t] <= (requirement * max_participation_factor) * param[t]\n                )\n        else\n            cons[name, t] = JuMP.@constraint(\n                jump_model,\n                var_r[name, t] <= (requirement * max_participation_factor) * ts_vector[t]\n            )\n        end\n    end\n\n    return\nend\n\nfunction add_constraints!(\n    container::OptimizationContainer,\n    T::Type{RequirementConstraint},\n    service::SR,\n    ::U,\n    model::ServiceModel{SR, V},\n) where {\n    SR <: PSY.ConstantReserve,\n    V <: AbstractReservesFormulation,\n    U <: Union{Vector{D}, IS.FlattenIteratorWrapper{D}},\n} where {D <: PSY.Component}\n    time_steps = get_time_steps(container)\n    service_name = PSY.get_name(service)\n    # TODO: The constraint addition is still not clean enough\n    constraint = add_constraints_container!(\n        container,\n        T(),\n        SR,\n        [service_name],\n        time_steps;\n        meta = service_name,\n    )\n    reserve_variable =\n        get_variable(container, ActivePowerReserveVariable(), SR, service_name)\n    use_slacks = get_use_slacks(model)\n    use_slacks && (slack_vars = reserve_slacks!(container, service))\n\n    requirement = PSY.get_requirement(service)\n    jump_model = get_jump_model(container)\n    extra = use_slacks ? 1 : 0\n    for t in time_steps\n        resource_expression =\n            _sum_reserve_variables(@view(reserve_variable[:, t]), extra)\n        use_slacks && JuMP.add_to_expression!(resource_expression, slack_vars[t])\n        constraint[service_name, t] =\n            JuMP.@constraint(jump_model, resource_expression >= requirement)\n    end\n\n    return\nend\n\nfunction objective_function!(\n    container::OptimizationContainer,\n    service::SR,\n    ::ServiceModel{SR, T},\n) where {SR <: PSY.AbstractReserve, T <: AbstractReservesFormulation}\n    add_proportional_cost!(container, ActivePowerReserveVariable(), service, T())\n    return\nend\n\nfunction add_constraints!(\n    container::OptimizationContainer,\n    T::Type{RequirementConstraint},\n    service::SR,\n    ::U,\n    ::ServiceModel{SR, StepwiseCostReserve},\n) where {\n    SR <: PSY.ReserveDemandCurve,\n    U <: Union{Vector{D}, IS.FlattenIteratorWrapper{D}},\n} where {D <: PSY.Component}\n    time_steps = get_time_steps(container)\n    service_name = PSY.get_name(service)\n    constraint = add_constraints_container!(\n        container,\n        T(),\n        SR,\n        [service_name],\n        time_steps;\n        meta = service_name,\n    )\n    reserve_variable =\n        get_variable(container, ActivePowerReserveVariable(), SR, service_name)\n    requirement_variable =\n        get_variable(container, ServiceRequirementVariable(), SR, service_name)\n    jump_model = get_jump_model(container)\n    for t in time_steps\n        constraint[service_name, t] = JuMP.@constraint(\n            jump_model,\n            sum(@view reserve_variable[:, t]) >= requirement_variable[service_name, t]\n        )\n    end\n\n    return\nend\n\n_get_ramp_limits(::PSY.Component) = nothing\n_get_ramp_limits(d::PSY.ThermalGen) = PSY.get_ramp_limits(d)\n_get_ramp_limits(d::PSY.HydroGen) = PSY.get_ramp_limits(d)\n\nfunction _get_ramp_constraint_contributing_devices(\n    service::PSY.Reserve,\n    contributing_devices::Union{Vector{D}, IS.FlattenIteratorWrapper{D}},\n) where {D <: PSY.Component}\n    time_frame = PSY.get_time_frame(service)\n    filtered_device = Vector{D}()\n    for d in contributing_devices\n        ramp_limits = _get_ramp_limits(d)\n        if ramp_limits !== nothing\n            p_lims = PSY.get_active_power_limits(d)\n            max_rate = abs(p_lims.min - p_lims.max) / time_frame\n            if (ramp_limits.up >= max_rate) & (ramp_limits.down >= max_rate)\n                @debug \"Generator $(name) has a nonbinding ramp limits. Constraints Skipped\"\n                continue\n            else\n                push!(filtered_device, d)\n            end\n        end\n    end\n    return filtered_device\nend\n\nfunction add_constraints!(\n    container::OptimizationContainer,\n    T::Type{RampConstraint},\n    service::SR,\n    contributing_devices::Union{Vector{D}, IS.FlattenIteratorWrapper{D}},\n    ::ServiceModel{SR, V},\n) where {\n    SR <: PSY.Reserve{PSY.ReserveUp},\n    V <: AbstractReservesFormulation,\n    D <: PSY.Component,\n}\n    ramp_devices = _get_ramp_constraint_contributing_devices(service, contributing_devices)\n    service_name = PSY.get_name(service)\n    if !isempty(ramp_devices)\n        jump_model = get_jump_model(container)\n        time_steps = get_time_steps(container)\n        time_frame = PSY.get_time_frame(service)\n        variable = get_variable(container, ActivePowerReserveVariable(), SR, service_name)\n        device_name_set = [PSY.get_name(d) for d in ramp_devices]\n        con_up = add_constraints_container!(\n            container,\n            T(),\n            SR,\n            device_name_set,\n            time_steps;\n            meta = service_name,\n        )\n        for d in ramp_devices, t in time_steps\n            name = PSY.get_name(d)\n            ramp_limits = PSY.get_ramp_limits(d)\n            con_up[name, t] = JuMP.@constraint(\n                jump_model,\n                variable[name, t] <= ramp_limits.up * time_frame\n            )\n        end\n    else\n        @warn \"Data doesn't contain contributing devices with ramp limits for service $service_name, consider adjusting your formulation\"\n    end\n    return\nend\n\nfunction add_constraints!(\n    container::OptimizationContainer,\n    T::Type{RampConstraint},\n    service::SR,\n    contributing_devices::Union{Vector{D}, IS.FlattenIteratorWrapper{D}},\n    ::ServiceModel{SR, V},\n) where {\n    SR <: PSY.Reserve{PSY.ReserveDown},\n    V <: AbstractReservesFormulation,\n    D <: PSY.Component,\n}\n    ramp_devices = _get_ramp_constraint_contributing_devices(service, contributing_devices)\n    service_name = PSY.get_name(service)\n    if !isempty(ramp_devices)\n        jump_model = get_jump_model(container)\n        time_steps = get_time_steps(container)\n        time_frame = PSY.get_time_frame(service)\n        variable = get_variable(container, ActivePowerReserveVariable(), SR, service_name)\n        device_name_set = [PSY.get_name(d) for d in ramp_devices]\n        con_down = add_constraints_container!(\n            container,\n            T(),\n            SR,\n            device_name_set,\n            time_steps;\n            meta = service_name,\n        )\n        for d in ramp_devices, t in time_steps\n            name = PSY.get_name(d)\n            ramp_limits = PSY.get_ramp_limits(d)\n            con_down[name, t] = JuMP.@constraint(\n                jump_model,\n                variable[name, t] <= ramp_limits.down * time_frame\n            )\n        end\n    else\n        @warn \"Data doesn't contain contributing devices with ramp limits for service $service_name, consider adjusting your formulation\"\n    end\n    return\nend\n\nfunction add_constraints!(\n    container::OptimizationContainer,\n    T::Type{ReservePowerConstraint},\n    service::SR,\n    contributing_devices::U,\n    ::ServiceModel{SR, V},\n) where {\n    SR <: PSY.VariableReserveNonSpinning,\n    V <: AbstractReservesFormulation,\n    U <: Union{Vector{D}, IS.FlattenIteratorWrapper{D}},\n} where {D <: PSY.Component}\n    time_steps = get_time_steps(container)\n    resolution = get_resolution(container)\n    if resolution > Dates.Minute(1)\n        minutes_per_period = Dates.value(Dates.Minute(resolution))\n    else\n        @warn(\"Not all formulations support under 1-minute resolutions. Exercise caution.\")\n        minutes_per_period = Dates.value(Dates.Second(resolution)) / 60\n    end\n    service_name = PSY.get_name(service)\n    cons = add_constraints_container!(\n        container,\n        T(),\n        SR,\n        [PSY.get_name(d) for d in contributing_devices],\n        time_steps;\n        meta = service_name,\n    )\n    var_r = get_variable(container, ActivePowerReserveVariable(), SR, service_name)\n    reserve_response_time = PSY.get_time_frame(service)\n    jump_model = get_jump_model(container)\n    for d in contributing_devices\n        component_type = typeof(d)\n        name = PSY.get_name(d)\n        varstatus = get_variable(container, OnVariable(), component_type)\n        startup_time = PSY.get_time_limits(d).up\n        ramp_limits = _get_ramp_limits(d)\n        if reserve_response_time > startup_time\n            reserve_limit =\n                PSY.get_active_power_limits(d).min +\n                (reserve_response_time - startup_time) * minutes_per_period * ramp_limits.up\n        else\n            reserve_limit = 0.0\n        end\n        for t in time_steps\n            cons[name, t] = JuMP.@constraint(\n                jump_model,\n                var_r[name, t] <= (1 - varstatus[name, t]) * reserve_limit\n            )\n        end\n    end\n    return\nend\n\nfunction objective_function!(\n    container::OptimizationContainer,\n    service::PSY.ReserveDemandCurve{T},\n    ::ServiceModel{PSY.ReserveDemandCurve{T}, SR},\n) where {T <: PSY.ReserveDirection, SR <: StepwiseCostReserve}\n    add_variable_cost!(container, ServiceRequirementVariable(), service, SR())\n    return\nend\n\nfunction add_variable_cost!(\n    container::OptimizationContainer,\n    ::U,\n    service::T,\n    ::V,\n) where {T <: PSY.ReserveDemandCurve, U <: VariableType, V <: StepwiseCostReserve}\n    _add_variable_cost_to_objective!(container, U(), service, V())\n    return\nend\n\nfunction _add_variable_cost_to_objective!(\n    container::OptimizationContainer,\n    ::T,\n    component::PSY.Reserve,\n    ::U,\n) where {T <: VariableType, U <: StepwiseCostReserve}\n    component_name = PSY.get_name(component)\n    @debug \"PWL Variable Cost\" _group = LOG_GROUP_COST_FUNCTIONS component_name\n    # If array is full of tuples with zeros return 0.0\n    time_steps = get_time_steps(container)\n    variable_cost = PSY.get_variable(component)\n    if variable_cost isa Nothing\n        error(\"ReserveDemandCurve $(component.name) does not have cost data.\")\n    elseif typeof(variable_cost) <: PSY.TimeSeriesKey\n        error(\n            \"Timeseries curve for ReserveDemandCurve $(component.name) is not supported yet.\",\n        )\n    end\n\n    pwl_cost_expressions =\n        _add_pwl_term!(container, component, variable_cost, T(), U())\n    for t in time_steps\n        add_to_expression!(\n            container,\n            ProductionCostExpression,\n            pwl_cost_expressions[t],\n            component,\n            t,\n        )\n        add_to_objective_invariant_expression!(container, pwl_cost_expressions[t])\n    end\n    return\nend\n\nfunction add_proportional_cost!(\n    container::OptimizationContainer,\n    ::U,\n    service::T,\n    ::V,\n) where {\n    T <: Union{PSY.Reserve, PSY.ReserveNonSpinning},\n    U <: ActivePowerReserveVariable,\n    V <: AbstractReservesFormulation,\n}\n    base_p = get_base_power(container)\n    reserve_variable = get_variable(container, U(), T, PSY.get_name(service))\n    for index in Iterators.product(axes(reserve_variable)...)\n        add_to_objective_invariant_expression!(\n            container,\n            # possibly decouple\n            DEFAULT_RESERVE_COST / base_p * reserve_variable[index...],\n        )\n    end\n    return\nend\n"
  },
  {
    "path": "src/services_models/service_slacks.jl",
    "content": "function reserve_slacks!(\n    container::OptimizationContainer,\n    service::T,\n) where {T <: Union{PSY.Reserve, PSY.ReserveNonSpinning}}\n    time_steps = get_time_steps(container)\n    variable = add_variable_container!(\n        container,\n        ReserveRequirementSlack(),\n        T,\n        PSY.get_name(service),\n        time_steps,\n    )\n\n    for t in time_steps\n        variable[t] = JuMP.@variable(\n            get_jump_model(container),\n            base_name = \"slack_{$(PSY.get_name(service)), $(t)}\",\n            lower_bound = 0.0\n        )\n        add_to_objective_invariant_expression!(container, variable[t] * SERVICES_SLACK_COST)\n    end\n    return variable\nend\n\nfunction transmission_interface_slacks!(\n    container::OptimizationContainer,\n    service::T,\n) where {T <: PSY.TransmissionInterface}\n    time_steps = get_time_steps(container)\n\n    for variable_type in [InterfaceFlowSlackUp, InterfaceFlowSlackDown]\n        variable = add_variable_container!(\n            container,\n            variable_type(),\n            T,\n            PSY.get_name(service),\n            time_steps,\n        )\n        penalty = PSY.get_violation_penalty(service)\n        name = PSY.get_name(service)\n        for t in time_steps\n            variable[t] = JuMP.@variable(\n                get_jump_model(container),\n                base_name = \"$(T)_$(variable_type)_{$(name), $(t)}\",\n            )\n            JuMP.set_lower_bound(variable[t], 0.0)\n\n            add_to_objective_invariant_expression!(\n                container,\n                variable[t] * penalty,\n            )\n        end\n    end\n\n    return\nend\n"
  },
  {
    "path": "src/services_models/services_constructor.jl",
    "content": "function get_incompatible_devices(devices_template::Dict)\n    incompatible_device_types = Set{DataType}()\n    for model in values(devices_template)\n        formulation = get_formulation(model)\n        if formulation == FixedOutput\n            if !isempty(get_services(model))\n                @info \"$(formulation) for $(get_component_type(model)) is not compatible with the provision of reserve services\"\n            end\n            push!(incompatible_device_types, get_component_type(model))\n        end\n    end\n    return incompatible_device_types\nend\n\nfunction construct_services!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    stage::ArgumentConstructStage,\n    services_template::ServicesModelContainer,\n    devices_template::DevicesModelContainer,\n    network_model::NetworkModel{<:PM.AbstractPowerModel},\n)\n    isempty(services_template) && return\n    incompatible_device_types = get_incompatible_devices(devices_template)\n\n    groupservice = nothing\n\n    for (key, service_model) in services_template\n        if get_formulation(service_model) === GroupReserve  # group service needs to be constructed last\n            groupservice = key\n            continue\n        end\n        isempty(get_contributing_devices(service_model)) && continue\n        construct_service!(\n            container,\n            sys,\n            stage,\n            service_model,\n            devices_template,\n            incompatible_device_types,\n            network_model,\n        )\n    end\n    groupservice === nothing || construct_service!(\n        container,\n        sys,\n        stage,\n        services_template[groupservice],\n        devices_template,\n        incompatible_device_types,\n        network_model,\n    )\n    return\nend\n\nfunction construct_services!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    stage::ModelConstructStage,\n    services_template::ServicesModelContainer,\n    devices_template::DevicesModelContainer,\n    network_model::NetworkModel{<:PM.AbstractPowerModel},\n)\n    isempty(services_template) && return\n    incompatible_device_types = get_incompatible_devices(devices_template)\n\n    groupservice = nothing\n    for (key, service_model) in services_template\n        if get_formulation(service_model) === GroupReserve  # group service needs to be constructed last\n            groupservice = key\n            continue\n        end\n        isempty(get_contributing_devices_map(service_model)) && continue\n        construct_service!(\n            container,\n            sys,\n            stage,\n            service_model,\n            devices_template,\n            incompatible_device_types,\n            network_model,\n        )\n    end\n    groupservice === nothing || construct_service!(\n        container,\n        sys,\n        stage,\n        services_template[groupservice],\n        devices_template,\n        incompatible_device_types,\n        network_model,\n    )\n    return\nend\n\nfunction construct_service!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ArgumentConstructStage,\n    model::ServiceModel{SR, RangeReserve},\n    devices_template::Dict{Symbol, DeviceModel},\n    incompatible_device_types::Set{<:DataType},\n    ::NetworkModel{<:PM.AbstractPowerModel},\n) where {SR <: PSY.Reserve}\n    name = get_service_name(model)\n    service = PSY.get_component(SR, sys, name)\n    !PSY.get_available(service) && return\n    add_parameters!(container, RequirementTimeSeriesParameter, service, model)\n    contributing_devices = get_contributing_devices(model)\n\n    add_variables!(\n        container,\n        ActivePowerReserveVariable,\n        service,\n        contributing_devices,\n        RangeReserve(),\n    )\n    add_to_expression!(container, ActivePowerReserveVariable, model, devices_template)\n    add_feedforward_arguments!(container, model, service)\n    return\nend\n\nfunction construct_service!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ModelConstructStage,\n    model::ServiceModel{SR, RangeReserve},\n    devices_template::Dict{Symbol, DeviceModel},\n    incompatible_device_types::Set{<:DataType},\n    ::NetworkModel{<:PM.AbstractPowerModel},\n) where {SR <: PSY.Reserve}\n    name = get_service_name(model)\n    service = PSY.get_component(SR, sys, name)\n    !PSY.get_available(service) && return\n    contributing_devices = get_contributing_devices(model)\n\n    add_constraints!(container, RequirementConstraint, service, contributing_devices, model)\n    add_constraints!(\n        container,\n        ParticipationFractionConstraint,\n        service,\n        contributing_devices,\n        model,\n    )\n    objective_function!(container, service, model)\n\n    add_feedforward_constraints!(container, model, service)\n\n    add_constraint_dual!(container, sys, model)\n\n    return\nend\n\nfunction construct_service!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ArgumentConstructStage,\n    model::ServiceModel{SR, RangeReserve},\n    devices_template::Dict{Symbol, DeviceModel},\n    incompatible_device_types::Set{<:DataType},\n    ::NetworkModel{<:PM.AbstractPowerModel},\n) where {SR <: PSY.ConstantReserve}\n    name = get_service_name(model)\n    service = PSY.get_component(SR, sys, name)\n    !PSY.get_available(service) && return\n    contributing_devices = get_contributing_devices(model)\n\n    add_variables!(\n        container,\n        ActivePowerReserveVariable,\n        service,\n        contributing_devices,\n        RangeReserve(),\n    )\n    add_to_expression!(container, ActivePowerReserveVariable, model, devices_template)\n    add_feedforward_arguments!(container, model, service)\n    return\nend\n\nfunction construct_service!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ModelConstructStage,\n    model::ServiceModel{SR, RangeReserve},\n    devices_template::Dict{Symbol, DeviceModel},\n    incompatible_device_types::Set{<:DataType},\n    ::NetworkModel{<:PM.AbstractPowerModel},\n) where {SR <: PSY.ConstantReserve}\n    name = get_service_name(model)\n    service = PSY.get_component(SR, sys, name)\n    !PSY.get_available(service) && return\n    contributing_devices = get_contributing_devices(model)\n\n    add_constraints!(container, RequirementConstraint, service, contributing_devices, model)\n    add_constraints!(\n        container,\n        ParticipationFractionConstraint,\n        service,\n        contributing_devices,\n        model,\n    )\n    objective_function!(container, service, model)\n\n    add_feedforward_constraints!(container, model, service)\n\n    add_constraint_dual!(container, sys, model)\n    return\nend\n\nfunction construct_service!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ArgumentConstructStage,\n    model::ServiceModel{SR, StepwiseCostReserve},\n    devices_template::Dict{Symbol, DeviceModel},\n    incompatible_device_types::Set{<:DataType},\n    ::NetworkModel{<:PM.AbstractPowerModel},\n) where {SR <: PSY.Reserve}\n    name = get_service_name(model)\n    service = PSY.get_component(SR, sys, name)\n    !PSY.get_available(service) && return\n    contributing_devices = get_contributing_devices(model)\n    add_variable!(container, ServiceRequirementVariable(), service, StepwiseCostReserve())\n    add_variables!(\n        container,\n        ActivePowerReserveVariable,\n        service,\n        contributing_devices,\n        StepwiseCostReserve(),\n    )\n    add_to_expression!(container, ActivePowerReserveVariable, model, devices_template)\n    add_expressions!(container, ProductionCostExpression, [service], model)\n    return\nend\n\nfunction construct_service!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ModelConstructStage,\n    model::ServiceModel{SR, StepwiseCostReserve},\n    devices_template::Dict{Symbol, DeviceModel},\n    incompatible_device_types::Set{<:DataType},\n    ::NetworkModel{<:PM.AbstractPowerModel},\n) where {SR <: PSY.Reserve}\n    name = get_service_name(model)\n    service = PSY.get_component(SR, sys, name)\n    !PSY.get_available(service) && return\n    contributing_devices = get_contributing_devices(model)\n\n    add_constraints!(container, RequirementConstraint, service, contributing_devices, model)\n\n    objective_function!(container, service, model)\n\n    add_feedforward_constraints!(container, model, service)\n\n    add_constraint_dual!(container, sys, model)\n    return\nend\n\n#=\nfunction construct_service!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ArgumentConstructStage,\n    model::ServiceModel{S, T},\n    devices_template::Dict{Symbol, DeviceModel},\n    ::Set{<:DataType},\n    ::NetworkModel{<:PM.AbstractPowerModel},\n) where {S <: PSY.AGC, T <: AbstractAGCFormulation}\n    services = get_available_components(model, sys)\n    agc_areas = PSY.get_area.(services)\n    areas = PSY.get_components(PSY.Area, sys)\n    if !isempty(setdiff(areas, agc_areas))\n        throw(\n            IS.ConflictingInputsError(\n                \"All area must have an AGC service assigned in order to model the System's Frequency regulation\",\n            ),\n        )\n    end\n\n    add_variables!(container, SteadyStateFrequencyDeviation)\n    add_variables!(container, AreaMismatchVariable, services, T())\n    add_variables!(container, SmoothACE, services, T())\n    add_variables!(container, LiftVariable, services, T())\n    add_variables!(container, ActivePowerVariable, areas, T())\n    add_variables!(container, DeltaActivePowerUpVariable, services, T())\n    add_variables!(container, DeltaActivePowerDownVariable, services, T())\n    add_variables!(container, AdditionalDeltaActivePowerUpVariable, areas, T())\n    add_variables!(container, AdditionalDeltaActivePowerDownVariable, areas, T())\n\n    add_initial_condition!(container, services, T(), AreaControlError())\n\n    add_to_expression!(\n        container,\n        EmergencyUp,\n        AdditionalDeltaActivePowerUpVariable,\n        areas,\n        model,\n    )\n\n    add_to_expression!(\n        container,\n        EmergencyDown,\n        AdditionalDeltaActivePowerDownVariable,\n        areas,\n        model,\n    )\n\n    add_to_expression!(container, RawACE, SteadyStateFrequencyDeviation, services, model)\n\n    add_feedforward_arguments!(container, model, services)\n    return\nend\n\nfunction construct_service!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ModelConstructStage,\n    model::ServiceModel{S, T},\n    devices_template::Dict{Symbol, DeviceModel},\n    ::Set{<:DataType},\n    ::NetworkModel{<:PM.AbstractPowerModel},\n) where {S <: PSY.AGC, T <: AbstractAGCFormulation}\n    areas = PSY.get_components(PSY.Area, sys)\n    services = get_available_components(model, sys)\n\n    add_constraints!(container, AbsoluteValueConstraint, LiftVariable, services, model)\n    add_constraints!(\n        container,\n        FrequencyResponseConstraint,\n        SteadyStateFrequencyDeviation,\n        services,\n        model,\n        sys,\n    )\n    add_constraints!(\n        container,\n        SACEPIDAreaConstraint,\n        SteadyStateFrequencyDeviation,\n        services,\n        model,\n        sys,\n    )\n    add_constraints!(container, BalanceAuxConstraint, SmoothACE, services, model, sys)\n\n    add_feedforward_constraints!(container, model, services)\n\n    add_constraint_dual!(container, sys, model)\n\n    objective_function!(container, services, model)\n    return\nend\n=#\n\n\"\"\"\n    Constructs a service for ConstantReserveGroup.\n\"\"\"\nfunction construct_service!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ArgumentConstructStage,\n    model::ServiceModel{SR, GroupReserve},\n    ::Dict{Symbol, DeviceModel},\n    ::Set{<:DataType},\n    ::NetworkModel{<:PM.AbstractPowerModel},\n) where {SR <: PSY.ConstantReserveGroup}\n    name = get_service_name(model)\n    service = PSY.get_component(SR, sys, name)\n    !PSY.get_available(service) && return\n    contributing_services = PSY.get_contributing_services(service)\n    # check if variables exist\n    check_activeservice_variables(container, contributing_services)\n\n    return\nend\n\nfunction construct_service!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ModelConstructStage,\n    model::ServiceModel{SR, GroupReserve},\n    ::Dict{Symbol, DeviceModel},\n    ::Set{<:DataType},\n    ::NetworkModel{<:PM.AbstractPowerModel},\n) where {SR <: PSY.ConstantReserveGroup}\n    name = get_service_name(model)\n    service = PSY.get_component(SR, sys, name)\n    !PSY.get_available(service) && return\n    contributing_services = PSY.get_contributing_services(service)\n\n    add_constraints!(\n        container,\n        RequirementConstraint,\n        service,\n        contributing_services,\n        model,\n    )\n\n    add_constraint_dual!(container, sys, model)\n    return\nend\n\nfunction construct_service!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ArgumentConstructStage,\n    model::ServiceModel{SR, RampReserve},\n    devices_template::Dict{Symbol, DeviceModel},\n    incompatible_device_types::Set{<:DataType},\n    ::NetworkModel{<:PM.AbstractPowerModel},\n) where {SR <: PSY.Reserve}\n    name = get_service_name(model)\n    service = PSY.get_component(SR, sys, name)\n    !PSY.get_available(service) && return\n    contributing_devices = get_contributing_devices(model)\n    add_parameters!(container, RequirementTimeSeriesParameter, service, model)\n\n    add_variables!(\n        container,\n        ActivePowerReserveVariable,\n        service,\n        contributing_devices,\n        RampReserve(),\n    )\n    add_to_expression!(container, ActivePowerReserveVariable, model, devices_template)\n    add_feedforward_arguments!(container, model, service)\n    return\nend\n\nfunction construct_service!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ModelConstructStage,\n    model::ServiceModel{SR, RampReserve},\n    devices_template::Dict{Symbol, DeviceModel},\n    incompatible_device_types::Set{<:DataType},\n    ::NetworkModel{<:PM.AbstractPowerModel},\n) where {SR <: PSY.Reserve}\n    name = get_service_name(model)\n    service = PSY.get_component(SR, sys, name)\n    !PSY.get_available(service) && return\n    contributing_devices = get_contributing_devices(model)\n\n    add_constraints!(container, RequirementConstraint, service, contributing_devices, model)\n    add_constraints!(container, RampConstraint, service, contributing_devices, model)\n    add_constraints!(\n        container,\n        ParticipationFractionConstraint,\n        service,\n        contributing_devices,\n        model,\n    )\n\n    objective_function!(container, service, model)\n\n    add_feedforward_constraints!(container, model, service)\n\n    add_constraint_dual!(container, sys, model)\n    return\nend\n\nfunction construct_service!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ArgumentConstructStage,\n    model::ServiceModel{SR, NonSpinningReserve},\n    devices_template::Dict{Symbol, DeviceModel},\n    incompatible_device_types::Set{<:DataType},\n    ::NetworkModel{<:PM.AbstractPowerModel},\n) where {SR <: PSY.ReserveNonSpinning}\n    name = get_service_name(model)\n    service = PSY.get_component(SR, sys, name)\n    !PSY.get_available(service) && return\n    contributing_devices = get_contributing_devices(model)\n    add_parameters!(container, RequirementTimeSeriesParameter, service, model)\n\n    add_variables!(\n        container,\n        ActivePowerReserveVariable,\n        service,\n        contributing_devices,\n        NonSpinningReserve(),\n    )\n    add_feedforward_arguments!(container, model, service)\n    return\nend\n\nfunction construct_service!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ModelConstructStage,\n    model::ServiceModel{SR, NonSpinningReserve},\n    devices_template::Dict{Symbol, DeviceModel},\n    incompatible_device_types::Set{<:DataType},\n    ::NetworkModel{<:PM.AbstractPowerModel},\n) where {SR <: PSY.ReserveNonSpinning}\n    name = get_service_name(model)\n    service = PSY.get_component(SR, sys, name)\n    !PSY.get_available(service) && return\n    contributing_devices = get_contributing_devices(model)\n\n    add_constraints!(container, RequirementConstraint, service, contributing_devices, model)\n    add_constraints!(\n        container,\n        ReservePowerConstraint,\n        service,\n        contributing_devices,\n        model,\n    )\n\n    add_constraints!(\n        container,\n        ParticipationFractionConstraint,\n        service,\n        contributing_devices,\n        model,\n    )\n\n    objective_function!(container, service, model)\n\n    add_feedforward_constraints!(container, model, service)\n\n    add_constraint_dual!(container, sys, model)\n    return\nend\n\nfunction construct_service!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ArgumentConstructStage,\n    model::ServiceModel{T, ConstantMaxInterfaceFlow},\n    devices_template::Dict{Symbol, DeviceModel},\n    incompatible_device_types::Set{<:DataType},\n    network_model::NetworkModel{<:PM.AbstractPowerModel},\n) where {T <: PSY.TransmissionInterface}\n    interfaces = get_available_components(model, sys)\n    interface = PSY.get_component(T, sys, get_service_name(model))\n    if get_use_slacks(model)\n        # Adding the slacks can be done in a cleaner fashion\n        @assert PSY.get_available(interface)\n        transmission_interface_slacks!(container, interface)\n    end\n    # Lazy container addition for the expressions.\n    lazy_container_addition!(\n        container,\n        InterfaceTotalFlow(),\n        T,\n        PSY.get_name.(interfaces),\n        get_time_steps(container),\n    )\n\n    add_feedforward_arguments!(container, model, interface)\n    return\nend\n\nfunction construct_service!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ArgumentConstructStage,\n    model::ServiceModel{PSY.TransmissionInterface, ConstantMaxInterfaceFlow},\n    devices_template::Dict{Symbol, DeviceModel},\n    incompatible_device_types::Set{<:DataType},\n    network_model::NetworkModel{AreaBalancePowerModel},\n)\n    interfaces = get_available_components(model, sys)\n    interface = PSY.get_component(PSY.TransmissionInterface, sys, get_service_name(model))\n    if get_use_slacks(model)\n        # Adding the slacks can be done in a cleaner fashion\n        @assert PSY.get_available(interface)\n        transmission_interface_slacks!(container, interface)\n    end\n    # Lazy container addition for the expressions.\n    lazy_container_addition!(\n        container,\n        InterfaceTotalFlow(),\n        PSY.TransmissionInterface,\n        PSY.get_name.(interfaces),\n        get_time_steps(container),\n    )\n    @warn \"AreaBalancePowerModel doesn't model individual line flows and it ignores the flows on AC Transmission Devices\"\n    add_feedforward_arguments!(container, model, interface)\n    return\nend\n\nfunction construct_service!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ModelConstructStage,\n    model::ServiceModel{PSY.TransmissionInterface, ConstantMaxInterfaceFlow},\n    devices_template::Dict{Symbol, DeviceModel},\n    incompatible_device_types::Set{<:DataType},\n    network_model::NetworkModel{<:PM.AbstractActivePowerModel},\n)\n    name = get_service_name(model)\n    service = PSY.get_component(PSY.TransmissionInterface, sys, name)\n    !PSY.get_available(service) && return\n\n    add_to_expression!(\n        container,\n        InterfaceTotalFlow,\n        FlowActivePowerVariable,\n        service,\n        model,\n        network_model,\n    )\n\n    if get_use_slacks(model)\n        add_to_expression!(\n            container,\n            InterfaceTotalFlow,\n            InterfaceFlowSlackUp,\n            service,\n            model,\n        )\n        add_to_expression!(\n            container,\n            InterfaceTotalFlow,\n            InterfaceFlowSlackDown,\n            service,\n            model,\n        )\n    end\n\n    add_constraints!(container, InterfaceFlowLimit, service, model)\n    add_feedforward_constraints!(container, model, service)\n    add_constraint_dual!(container, sys, model)\n    objective_function!(container, service, model)\n    return\nend\n\nfunction construct_service!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ModelConstructStage,\n    model::ServiceModel{PSY.TransmissionInterface, ConstantMaxInterfaceFlow},\n    devices_template::Dict{Symbol, DeviceModel},\n    incompatible_device_types::Set{<:DataType},\n    network_model::NetworkModel{PTDFPowerModel},\n)\n    name = get_service_name(model)\n    service = PSY.get_component(PSY.TransmissionInterface, sys, name)\n    !PSY.get_available(service) && return\n\n    add_to_expression!(\n        container,\n        InterfaceTotalFlow,\n        PTDFBranchFlow,\n        service,\n        model,\n        network_model,\n    )\n\n    if get_use_slacks(model)\n        add_to_expression!(\n            container,\n            InterfaceTotalFlow,\n            InterfaceFlowSlackUp,\n            service,\n            model,\n        )\n        add_to_expression!(\n            container,\n            InterfaceTotalFlow,\n            InterfaceFlowSlackDown,\n            service,\n            model,\n        )\n    end\n\n    add_constraints!(container, InterfaceFlowLimit, service, model)\n    add_feedforward_constraints!(container, model, service)\n    add_constraint_dual!(container, sys, model)\n    objective_function!(container, service, model)\n    return\nend\n\nfunction construct_service!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ModelConstructStage,\n    model::ServiceModel{PSY.TransmissionInterface, ConstantMaxInterfaceFlow},\n    devices_template::Dict{Symbol, DeviceModel},\n    incompatible_device_types::Set{<:DataType},\n    network_model::NetworkModel{AreaPTDFPowerModel},\n)\n    name = get_service_name(model)\n    service = PSY.get_component(PSY.TransmissionInterface, sys, name)\n    !PSY.get_available(service) && return\n\n    # This function makes interfaces for the AC Branches\n    add_to_expression!(\n        container,\n        InterfaceTotalFlow,\n        PTDFBranchFlow,\n        service,\n        model,\n        network_model,\n    )\n\n    # This function makes interfaces for the interchanges\n    add_to_expression!(\n        container,\n        InterfaceTotalFlow,\n        FlowActivePowerVariable,\n        service,\n        model,\n        network_model,\n    )\n\n    if get_use_slacks(model)\n        add_to_expression!(\n            container,\n            InterfaceTotalFlow,\n            InterfaceFlowSlackUp,\n            service,\n            model,\n        )\n        add_to_expression!(\n            container,\n            InterfaceTotalFlow,\n            InterfaceFlowSlackDown,\n            service,\n            model,\n        )\n    end\n\n    add_constraints!(container, InterfaceFlowLimit, service, model)\n    add_feedforward_constraints!(container, model, service)\n    add_constraint_dual!(container, sys, model)\n    objective_function!(container, service, model)\n    return\nend\n\nfunction construct_service!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ModelConstructStage,\n    model::ServiceModel{PSY.TransmissionInterface, VariableMaxInterfaceFlow},\n    devices_template::Dict{Symbol, DeviceModel},\n    incompatible_device_types::Set{<:DataType},\n    network_model::NetworkModel{<:AbstractPTDFModel},\n)\n    name = get_service_name(model)\n    service = PSY.get_component(PSY.TransmissionInterface, sys, name)\n    !PSY.get_available(service) && return\n\n    # This function makes interfaces for the AC Branches\n    add_to_expression!(\n        container,\n        InterfaceTotalFlow,\n        PTDFBranchFlow,\n        service,\n        model,\n        network_model,\n    )\n\n    if get_use_slacks(model)\n        add_to_expression!(\n            container,\n            InterfaceTotalFlow,\n            InterfaceFlowSlackUp,\n            service,\n            model,\n        )\n        add_to_expression!(\n            container,\n            InterfaceTotalFlow,\n            InterfaceFlowSlackDown,\n            service,\n            model,\n        )\n    end\n\n    add_constraints!(container, InterfaceFlowLimit, service, model)\n    add_feedforward_constraints!(container, model, service)\n    add_constraint_dual!(container, sys, model)\n    objective_function!(container, service, model)\n    return\nend\n\nfunction construct_service!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ModelConstructStage,\n    model::ServiceModel{PSY.TransmissionInterface, U},\n    devices_template::Dict{Symbol, DeviceModel},\n    incompatible_device_types::Set{<:DataType},\n    network_model::NetworkModel{T},\n) where {\n    T <: PM.AbstractPowerModel,\n    U <: Union{ConstantMaxInterfaceFlow, VariableMaxInterfaceFlow},\n}\n    error(\"TransmissionInterface models not implemented for PowerModel of type $T\")\n    return\nend\n\nfunction construct_service!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ArgumentConstructStage,\n    model::ServiceModel{PSY.TransmissionInterface, VariableMaxInterfaceFlow},\n    devices_template::Dict{Symbol, DeviceModel},\n    incompatible_device_types::Set{<:DataType},\n    network_model::NetworkModel{<:PM.AbstractPowerModel},\n)\n    interfaces = get_available_components(model, sys)\n    if get_use_slacks(model)\n        # Adding the slacks can be done in a cleaner fashion\n        interface =\n            PSY.get_component(PSY.TransmissionInterface, sys, get_service_name(model))\n        @assert PSY.get_available(interface)\n        transmission_interface_slacks!(container, interface)\n    end\n    # Lazy container addition for the expressions.\n    lazy_container_addition!(\n        container,\n        InterfaceTotalFlow(),\n        PSY.TransmissionInterface,\n        PSY.get_name.(interfaces),\n        get_time_steps(container),\n    )\n    has_ts = PSY.has_time_series.(interfaces)\n    if any(has_ts) && !all(has_ts)\n        error(\n            \"Not all TransmissionInterfaces devices have time series. Check data to complete (or remove) time series.\",\n        )\n    end\n    if all(has_ts)\n        for device in interfaces\n            name = PSY.get_name(device)\n            num_ts = length(unique(PSY.get_name.(PSY.get_time_series_keys(device))))\n            if num_ts < 2\n                error(\n                    \"TransmissionInterface $name has less than two time series. It is required to add both min_flow and max_flow time series.\",\n                )\n            end\n            add_parameters!(container, MinInterfaceFlowLimitParameter, device, model)\n            add_parameters!(container, MaxInterfaceFlowLimitParameter, device, model)\n        end\n    end\n    interface = PSY.get_component(PSY.TransmissionInterface, sys, get_service_name(model))\n    add_feedforward_arguments!(container, model, interface)\n    return\nend\n\nfunction construct_service!(\n    container::OptimizationContainer,\n    sys::PSY.System,\n    ::ModelConstructStage,\n    model::ServiceModel{PSY.TransmissionInterface, U},\n    devices_template::Dict{Symbol, DeviceModel},\n    incompatible_device_types::Set{<:DataType},\n    network_model::NetworkModel{<:PM.AbstractActivePowerModel},\n) where {U <: Union{ConstantMaxInterfaceFlow, VariableMaxInterfaceFlow}}\n    name = get_service_name(model)\n    service = PSY.get_component(PSY.TransmissionInterface, sys, name)\n    !PSY.get_available(service) && return\n\n    add_to_expression!(\n        container,\n        InterfaceTotalFlow,\n        FlowActivePowerVariable,\n        service,\n        model,\n        network_model,\n    )\n\n    if get_use_slacks(model)\n        add_to_expression!(\n            container,\n            InterfaceTotalFlow,\n            InterfaceFlowSlackUp,\n            service,\n            model,\n        )\n        add_to_expression!(\n            container,\n            InterfaceTotalFlow,\n            InterfaceFlowSlackDown,\n            service,\n            model,\n        )\n    end\n\n    add_constraints!(container, InterfaceFlowLimit, service, model)\n    add_feedforward_constraints!(container, model, service)\n    add_constraint_dual!(container, sys, model)\n    objective_function!(container, service, model)\n    return\nend\n"
  },
  {
    "path": "src/services_models/transmission_interface.jl",
    "content": "#! format: off\nget_variable_binary(_, ::Type{PSY.TransmissionInterface}, ::ConstantMaxInterfaceFlow) = false\nget_variable_lower_bound(::InterfaceFlowSlackUp, ::PSY.TransmissionInterface, ::ConstantMaxInterfaceFlow) = 0.0\nget_variable_lower_bound(::InterfaceFlowSlackDown, ::PSY.TransmissionInterface, ::ConstantMaxInterfaceFlow) = 0.0\n\nget_variable_multiplier(::InterfaceFlowSlackUp, ::PSY.TransmissionInterface, ::ConstantMaxInterfaceFlow) = 1.0\nget_variable_multiplier(::InterfaceFlowSlackDown, ::PSY.TransmissionInterface, ::ConstantMaxInterfaceFlow) = -1.0\n\nget_variable_multiplier(::InterfaceFlowSlackUp, ::PSY.TransmissionInterface, ::VariableMaxInterfaceFlow) = 1.0\nget_variable_multiplier(::InterfaceFlowSlackDown, ::PSY.TransmissionInterface, ::VariableMaxInterfaceFlow) = -1.0\n\nget_multiplier_value(::MinInterfaceFlowLimitParameter, d::PSY.TransmissionInterface, ::VariableMaxInterfaceFlow) = PSY.get_min_active_power_flow_limit(d)\nget_multiplier_value(::MaxInterfaceFlowLimitParameter, d::PSY.TransmissionInterface, ::VariableMaxInterfaceFlow) = PSY.get_max_active_power_flow_limit(d)\n\n#! format: On\nfunction get_default_time_series_names(\n    ::Type{PSY.TransmissionInterface},\n    ::Type{ConstantMaxInterfaceFlow},\n)\n    return Dict{Type{<:TimeSeriesParameter}, String}()\nend\n\nfunction get_default_time_series_names(\n    ::Type{PSY.TransmissionInterface},\n    ::Type{VariableMaxInterfaceFlow},\n)\n    return Dict{Type{<:TimeSeriesParameter}, String}(\n        MinInterfaceFlowLimitParameter => \"min_active_power_flow_limit\",\n        MaxInterfaceFlowLimitParameter => \"max_active_power_flow_limit\",\n    )\nend\n\nfunction get_default_attributes(\n    ::Type{<:PSY.TransmissionInterface},\n    ::Type{ConstantMaxInterfaceFlow})\n    return Dict{String, Any}()\nend\n\nfunction get_default_attributes(\n    ::Type{<:PSY.TransmissionInterface},\n    ::Type{VariableMaxInterfaceFlow})\n    return Dict{String, Any}()\nend\n\nfunction get_initial_conditions_service_model(\n    ::OperationModel,\n    ::ServiceModel{T, D},\n) where {T <: PSY.TransmissionInterface, D <: ConstantMaxInterfaceFlow}\n    return ServiceModel(T, D)\nend\n\nfunction add_constraints!(\n    container::OptimizationContainer,\n    ::Type{InterfaceFlowLimit},\n    interface::T,\n    model::ServiceModel{T, ConstantMaxInterfaceFlow},\n) where {T <: PSY.TransmissionInterface}\n    expr = get_expression(container, InterfaceTotalFlow(), T)\n    interfaces, time_steps = axes(expr)\n    constraint_container_ub = lazy_container_addition!(\n        container,\n        InterfaceFlowLimit(),\n        T,\n        interfaces,\n        time_steps;\n        meta = \"ub\",\n    )\n    constraint_container_lb = lazy_container_addition!(\n        container,\n        InterfaceFlowLimit(),\n        T,\n        interfaces,\n        time_steps;\n        meta = \"lb\",\n    )\n    int_name = PSY.get_name(interface)\n    min_flow, max_flow = PSY.get_active_power_flow_limits(interface)\n    for t in time_steps\n        constraint_container_ub[int_name, t] =\n            JuMP.@constraint(get_jump_model(container), expr[int_name, t] <= max_flow)\n        constraint_container_lb[int_name, t] =\n            JuMP.@constraint(get_jump_model(container), expr[int_name, t] >= min_flow)\n    end\n    return\nend\n\nfunction add_constraints!(\n    container::OptimizationContainer,\n    ::Type{InterfaceFlowLimit},\n    interface::T,\n    model::ServiceModel{T, VariableMaxInterfaceFlow},\n) where {T <: PSY.TransmissionInterface}\n    expr = get_expression(container, InterfaceTotalFlow(), T)\n    interfaces, timesteps = axes(expr)\n    constraint_container_ub = lazy_container_addition!(\n        container,\n        InterfaceFlowLimit(),\n        T,\n        interfaces,\n        timesteps;\n        meta = \"ub\",\n    )\n    constraint_container_lb = lazy_container_addition!(\n        container,\n        InterfaceFlowLimit(),\n        T,\n        interfaces,\n        timesteps;\n        meta = \"lb\",\n    )\n    int_name = PSY.get_name(interface)\n    param_container_min =\n        get_parameter(container, MinInterfaceFlowLimitParameter(), PSY.TransmissionInterface, int_name)\n    param_multiplier_min = get_parameter_multiplier_array(\n        container,\n        MinInterfaceFlowLimitParameter(),\n        PSY.TransmissionInterface,\n        int_name,\n    )\n    param_container_max =\n        get_parameter(container, MaxInterfaceFlowLimitParameter(), PSY.TransmissionInterface, int_name)\n    param_multiplier_max = get_parameter_multiplier_array(\n        container,\n        MaxInterfaceFlowLimitParameter(),\n        PSY.TransmissionInterface,\n        int_name,\n    )\n    param_min = get_parameter_column_refs(param_container_min, int_name)\n    param_max = get_parameter_column_refs(param_container_max, int_name)\n    for t in timesteps\n        constraint_container_ub[int_name, t] =\n            JuMP.@constraint(get_jump_model(container), expr[int_name, t] <= param_multiplier_max[int_name, t] * param_max[t])\n        constraint_container_lb[int_name, t] =\n            JuMP.@constraint(get_jump_model(container), expr[int_name, t] >= param_multiplier_min[int_name, t] * param_min[t])\n    end\n    return\nend\n\nfunction objective_function!(\n    container::OptimizationContainer,\n    service::T,\n    model::ServiceModel{T, U},\n) where {T <: PSY.TransmissionInterface, U <: Union{ConstantMaxInterfaceFlow, VariableMaxInterfaceFlow}}\n    # At the moment the interfaces have no costs associated with them\n    return\nend\n"
  },
  {
    "path": "src/simulation/decision_model_simulation_results.jl",
    "content": "struct DecisionModelSimulationResults <: OperationModelSimulationResults\n    variables::ResultsByKeyAndTime\n    duals::ResultsByKeyAndTime\n    parameters::ResultsByKeyAndTime\n    aux_variables::ResultsByKeyAndTime\n    expressions::ResultsByKeyAndTime\n    forecast_horizon::Int\n    container_key_lookup::Dict{String, OptimizationContainerKey}\nend\n\nfunction SimulationProblemResults(\n    ::Type{DecisionModel},\n    store::SimulationStore,\n    model_name::AbstractString,\n    problem_params::ModelStoreParams,\n    sim_params::SimulationStoreParams,\n    path,\n    container_key_lookup;\n    kwargs...,\n)\n    name = Symbol(model_name)\n    return SimulationProblemResults{DecisionModelSimulationResults}(\n        store,\n        model_name,\n        problem_params,\n        sim_params,\n        path,\n        DecisionModelSimulationResults(\n            ResultsByKeyAndTime(\n                list_decision_model_keys(store, name, STORE_CONTAINER_VARIABLES),\n            ),\n            ResultsByKeyAndTime(\n                list_decision_model_keys(store, name, STORE_CONTAINER_DUALS),\n            ),\n            ResultsByKeyAndTime(\n                list_decision_model_keys(store, name, STORE_CONTAINER_PARAMETERS),\n            ),\n            ResultsByKeyAndTime(\n                list_decision_model_keys(store, name, STORE_CONTAINER_AUX_VARIABLES),\n            ),\n            ResultsByKeyAndTime(\n                list_decision_model_keys(store, name, STORE_CONTAINER_EXPRESSIONS),\n            ),\n            get_horizon_count(problem_params),\n            container_key_lookup,\n        );\n        kwargs...,\n    )\nend\n\nfunction _list_containers(res::SimulationProblemResults{DecisionModelSimulationResults})\n    return (getfield(res.values, x).cached_results for x in get_container_fields(res))\nend\n\nfunction Base.empty!(res::SimulationProblemResults{DecisionModelSimulationResults})\n    foreach(empty!, _list_containers(res))\n    empty!(get_results_timestamps(res))\nend\n\nfunction Base.isempty(res::SimulationProblemResults{DecisionModelSimulationResults})\n    all(isempty, _list_containers(res))\nend\n\n# This returns the number of timestamps stored in all containers.\nfunction Base.length(res::SimulationProblemResults{DecisionModelSimulationResults})\n    return mapreduce(length, +, (y for x in _list_containers(res) for y in values(x)))\nend\n\nlist_aux_variable_keys(res::SimulationProblemResults{DecisionModelSimulationResults}) =\n    res.values.aux_variables.result_keys[:]\nlist_dual_keys(res::SimulationProblemResults{DecisionModelSimulationResults}) =\n    res.values.duals.result_keys[:]\nlist_expression_keys(res::SimulationProblemResults{DecisionModelSimulationResults}) =\n    res.values.expressions.result_keys[:]\nlist_parameter_keys(res::SimulationProblemResults{DecisionModelSimulationResults}) =\n    res.values.parameters.result_keys[:]\nlist_variable_keys(res::SimulationProblemResults{DecisionModelSimulationResults}) =\n    res.values.variables.result_keys[:]\n\nget_cached_aux_variables(res::SimulationProblemResults{DecisionModelSimulationResults}) =\n    res.values.aux_variables.cached_results\nget_cached_duals(res::SimulationProblemResults{DecisionModelSimulationResults}) =\n    res.values.duals.cached_results\nget_cached_expressions(res::SimulationProblemResults{DecisionModelSimulationResults}) =\n    res.values.expressions.cached_results\nget_cached_parameters(res::SimulationProblemResults{DecisionModelSimulationResults}) =\n    res.values.parameters.cached_results\nget_cached_variables(res::SimulationProblemResults{DecisionModelSimulationResults}) =\n    res.values.variables.cached_results\n\nget_cached_results(\n    res::SimulationProblemResults{DecisionModelSimulationResults},\n    ::AuxVarKey,\n) = get_cached_aux_variables(res)\nget_cached_results(\n    res::SimulationProblemResults{DecisionModelSimulationResults},\n    ::ConstraintKey,\n) = get_cached_duals(res)\nget_cached_results(\n    res::SimulationProblemResults{DecisionModelSimulationResults},\n    ::ExpressionKey,\n) = get_cached_expressions(res)\nget_cached_results(\n    res::SimulationProblemResults{DecisionModelSimulationResults},\n    ::ParameterKey,\n) = get_cached_parameters(res)\nget_cached_results(\n    res::SimulationProblemResults{DecisionModelSimulationResults},\n    ::VariableKey,\n) = get_cached_variables(res)\n\nfunction get_forecast_horizon(res::SimulationProblemResults{DecisionModelSimulationResults})\n    return res.values.forecast_horizon\nend\n\nfunction _get_store_value(\n    res::SimulationProblemResults{DecisionModelSimulationResults},\n    container_keys::Vector{<:OptimizationContainerKey},\n    timestamps,\n    ::Nothing,\n)\n    simulation_store_path = joinpath(get_execution_path(res), \"data_store\")\n    return open_store(HdfSimulationStore, simulation_store_path, \"r\") do store\n        _get_store_value(res, container_keys, timestamps, store)\n    end\nend\n\nfunction _get_store_value(\n    sim_results::SimulationProblemResults{DecisionModelSimulationResults},\n    container_keys::Vector{<:OptimizationContainerKey},\n    timestamps::Vector{Dates.DateTime},\n    store::SimulationStore,\n)\n    results_by_key = Dict{OptimizationContainerKey, ResultsByTime}()\n    model_name = Symbol(get_model_name(sim_results))\n    for ckey in container_keys\n        n_dims = get_number_of_dimensions(store, DecisionModelIndexType, model_name, ckey)\n        container_type = DenseAxisArray{Float64, n_dims + 1}\n        results_by_key[ckey] = _get_store_value(container_type,\n            sim_results,\n            ckey,\n            timestamps, store)\n    end\n    return results_by_key\nend\n\nfunction _get_store_value(\n    ::Type{T},\n    sim_results::SimulationProblemResults{DecisionModelSimulationResults},\n    key::OptimizationContainerKey,\n    timestamps::Vector{Dates.DateTime},\n    store::SimulationStore,\n) where {T <: DenseAxisArray{Float64, 2}}\n    resolution = get_resolution(sim_results)\n    horizon = get_forecast_horizon(sim_results)\n    base_power = get_model_base_power(sim_results)\n    model_name = Symbol(get_model_name(sim_results))\n    results_by_time = ResultsByTime(\n        key,\n        SortedDict{Dates.DateTime, T}(),\n        resolution,\n        get_column_names(store, DecisionModelIndexType, model_name, key),\n    )\n    array_size::Union{Nothing, Tuple{Int, Int}} = nothing\n    for ts in timestamps\n        array = read_result(DenseAxisArray, store, model_name, key, ts)\n        if isnothing(array_size)\n            array_size = size(array)\n        elseif size(array) != array_size\n            error(\n                \"Arrays for $(encode_key_as_string(key)) at different timestamps have different sizes\",\n            )\n        end\n        if convert_result_to_natural_units(key)\n            array.data .*= base_power\n        end\n        if array_size[2] != horizon\n            @warn \"$(encode_key_as_string(key)) has a different horizon than the \" *\n                  \"problem specification. Can't assign timestamps to the resulting DataFrame.\"\n            results_by_time.resolution = Dates.Period(Dates.Millisecond(0))\n        end\n        results_by_time[ts] = array\n    end\n\n    return results_by_time\nend\n\nfunction _get_store_value(\n    ::Type{T},\n    sim_results::SimulationProblemResults{DecisionModelSimulationResults},\n    key::OptimizationContainerKey,\n    timestamps::Vector{Dates.DateTime},\n    store::SimulationStore,\n) where {T <: DenseAxisArray{Float64, 3}}\n    resolution = get_resolution(sim_results)\n    horizon = get_forecast_horizon(sim_results)\n    base_power = get_model_base_power(sim_results)\n    model_name = Symbol(get_model_name(sim_results))\n    results_by_time = ResultsByTime(\n        key,\n        SortedDict{Dates.DateTime, T}(),\n        resolution,\n        get_column_names(store, DecisionModelIndexType, model_name, key),\n    )\n    array_size::Union{Nothing, Tuple{Int, Int, Int}} = nothing\n    for ts in timestamps\n        array = read_result(DenseAxisArray, store, model_name, key, ts)\n        if isnothing(array_size)\n            array_size = size(array)\n        elseif size(array) != array_size\n            error(\n                \"Arrays for $(encode_key_as_string(key)) at different timestamps have different sizes\",\n            )\n        end\n        if convert_result_to_natural_units(key)\n            array.data .*= base_power\n        end\n        if array_size[3] != horizon\n            @warn \"$(encode_key_as_string(key)) has a different horizon than the \" *\n                  \"problem specification. Can't assign timestamps to the resulting DataFrame.\"\n            results_by_time.resolution = Dates.Period(Dates.Millisecond(0))\n        end\n        results_by_time[ts] = array\n    end\n\n    return results_by_time\nend\n\nfunction _process_timestamps(\n    res::SimulationProblemResults,\n    initial_time::Union{Nothing, Dates.DateTime},\n    count::Union{Nothing, Int},\n)\n    if initial_time === nothing\n        initial_time = first(get_timestamps(res))\n    end\n\n    if initial_time ∉ res.timestamps\n        invalid_timestamps = [initial_time]\n    else\n        if count === nothing\n            requested_range = [v for v in res.timestamps if v >= initial_time]\n        else\n            requested_range =\n                collect(range(initial_time; length = count, step = get_interval(res)))\n        end\n        invalid_timestamps = [v for v in requested_range if v ∉ res.timestamps]\n    end\n    if !isempty(invalid_timestamps)\n        @error \"Timestamps $(invalid_timestamps) not stored\" get_timestamps(res)\n        throw(IS.InvalidValue(\"Timestamps not stored\"))\n    end\n    return requested_range\nend\n\nfunction _read_results(\n    ::Type{DataFrame},\n    res::SimulationProblemResults{DecisionModelSimulationResults},\n    result_keys,\n    timestamps::Vector{Dates.DateTime},\n    store::Union{Nothing, <:SimulationStore};\n    cols::Union{Colon, Vector{String}} = (:),\n    table_format::TableFormat = TableFormat.LONG,\n)\n    vals = _read_results(res, result_keys, timestamps, store)\n    converted_vals = Dict{OptimizationContainerKey, ResultsByTime{DataFrame}}()\n    for (result_key, result_data) in vals\n        inner_converted = SortedDict{Dates.DateTime, DataFrame}()\n        for (date_key, inner_data) in result_data\n            extra = ntuple(_ -> (:), ndims(inner_data) - 1)\n            inner_converted[date_key] =\n                to_results_dataframe(inner_data[cols, extra...], nothing, Val(table_format))\n        end\n        _cols = (cols isa Vector) ? (cols,) : result_data.column_names\n        num_dims = length(_cols)\n        converted_vals[result_key] = ResultsByTime{DataFrame, num_dims}(\n            result_data.key,\n            inner_converted,\n            result_data.resolution,\n            _cols)\n    end\n    return converted_vals\nend\n\nfunction _read_results(\n    res::SimulationProblemResults{DecisionModelSimulationResults},\n    result_keys,\n    timestamps::Vector{Dates.DateTime},\n    store::Union{Nothing, <:SimulationStore},\n)\n    isempty(result_keys) &&\n        return Dict{OptimizationContainerKey, ResultsByTime{DenseAxisArray{Float64, 2}}}()\n\n    _store = try_resolve_store(store, res.store)\n    existing_keys = list_result_keys(res, first(result_keys))\n    ISOPT._validate_keys(existing_keys, result_keys)\n    cached_results = get_cached_results(res, eltype(result_keys))\n    if _are_results_cached(res, result_keys, timestamps, keys(cached_results))\n        @debug \"reading results from SimulationsResults cache\"  # NOTE tests match on this\n        vals = Dict(k => cached_results[k] for k in result_keys)\n        # Cached data may contain more timestamps than we need, remove these if so\n        (timestamps == get_results_timestamps(res)) && return vals\n        filtered_vals = Dict{keytype(vals), valtype(vals)}()\n        for (result_key, result_data) in vals\n            inner_converted = filter((((k, v),) -> k in timestamps), result_data.data)\n            filtered_vals[result_key] = ResultsByTime{valtype(inner_converted), 1}(\n                result_data.key,\n                inner_converted,\n                result_data.resolution,\n                result_data.column_names)\n        end\n        return filtered_vals\n    else\n        @debug \"reading results from data store\"  # NOTE tests match on this\n        vals = _get_store_value(res, result_keys, timestamps, _store)\n    end\n    return vals\nend\n\n\"\"\"\nReturn the values for the requested variable. It keeps requests when performing multiple retrievals.\n\n# Arguments\n\n  - `args`: Can be a string returned from [`list_variable_names`](@ref) or args that can be\n    splatted into a VariableKey.\n  - `initial_time::Dates.DateTime` : initial of the requested results\n  - `count::Int`: Number of results\n  - `store::SimulationStore`: a store that has been opened for reading\n  - `table_format::TableFormat`: Format of the table to be returned. Default is\n    `TableFormat.LONG` where the columns are `DateTime`, `name`, and `value` when the data\n    has two dimensions and `DateTime`, `name`, `name2`, and `value` when the data has three\n    dimensions.\n    Set to it `TableFormat.WIDE` to pivot the names as columns.\n    Note: `TableFormat.WIDE` is not supported when the data has three dimensions.\n\n# Examples\n\n```julia\nread_variable(results, ActivePowerVariable, ThermalStandard)\nread_variable(results, \"ActivePowerVariable__ThermalStandard\")\nread_variable(results, \"ActivePowerVariable__ThermalStandard\", table_format = TableFormat.WIDE)\n```\n\"\"\"\nfunction read_variable(\n    res::SimulationProblemResults{DecisionModelSimulationResults},\n    args...;\n    initial_time::Union{Nothing, Dates.DateTime} = nothing,\n    count::Union{Int, Nothing} = nothing,\n    store = nothing,\n    table_format::TableFormat = TableFormat.LONG,\n)\n    key = _deserialize_key(VariableKey, res, args...)\n    timestamps = _process_timestamps(res, initial_time, count)\n    return make_dataframes(\n        _read_results(res, [key], timestamps, store)[key];\n        table_format = table_format,\n    )\nend\n\n\"\"\"\nReturn the values for the requested dual. It keeps requests when performing multiple retrievals.\n\n# Arguments\n\n  - `args`: Can be a string returned from [`list_dual_names`](@ref) or args that can be\n    splatted into a ConstraintKey.\n  - `initial_time::Dates.DateTime` : initial of the requested results\n  - `count::Int`: Number of results\n  - `store::SimulationStore`: a store that has been opened for reading\n  - `table_format::TableFormat`: Format of the table to be returned. Default is\n    `TableFormat.LONG` where the columns are `DateTime`, `name`, and `value` when the data\n    has two dimensions and `DateTime`, `name`, `name2`, and `value` when the data has three\n    dimensions.\n    Set to it `TableFormat.WIDE` to pivot the names as columns.\n    Note: `TableFormat.WIDE` is not supported when the data has three dimensions.\n\"\"\"\nfunction read_dual(\n    res::SimulationProblemResults{DecisionModelSimulationResults},\n    args...;\n    initial_time::Union{Nothing, Dates.DateTime} = nothing,\n    count::Union{Int, Nothing} = nothing,\n    store = nothing,\n    table_format::TableFormat = TableFormat.LONG,\n)\n    key = _deserialize_key(ConstraintKey, res, args...)\n    timestamps = _process_timestamps(res, initial_time, count)\n    return make_dataframes(\n        _read_results(res, [key], timestamps, store)[key];\n        table_format = table_format,\n    )\nend\n\n\"\"\"\nReturn the values for the requested parameter. It keeps requests when performing multiple retrievals.\n\n# Arguments\n\n  - `args`: Can be a string returned from [`list_parameter_names`](@ref) or args that can be\n    splatted into a ParameterKey.\n  - `initial_time::Dates.DateTime` : initial of the requested results\n  - `count::Int`: Number of results\n  - `table_format::TableFormat`: Format of the table to be returned. Default is\n    `TableFormat.LONG` where the columns are `DateTime`, `name`, and `value` when the data\n    has two dimensions and `DateTime`, `name`, `name2`, and `value` when the data has three\n    dimensions.\n    Set to it `TableFormat.WIDE` to pivot the names as columns.\n    Note: `TableFormat.WIDE` is not supported when the data has three dimensions.\n\"\"\"\nfunction read_parameter(\n    res::SimulationProblemResults{DecisionModelSimulationResults},\n    args...;\n    initial_time::Union{Nothing, Dates.DateTime} = nothing,\n    count::Union{Int, Nothing} = nothing,\n    store = nothing,\n    table_format::TableFormat = TableFormat.LONG,\n)\n    key = _deserialize_key(ParameterKey, res, args...)\n    timestamps = _process_timestamps(res, initial_time, count)\n    return make_dataframes(\n        _read_results(res, [key], timestamps, store)[key];\n        table_format = table_format,\n    )\nend\n\n\"\"\"\nReturn the values for the requested auxillary variables. It keeps requests when performing multiple retrievals.\n\n# Arguments\n\n  - `args`: Can be a string returned from [`list_aux_variable_names`](@ref) or args that can be\n    splatted into a AuxVarKey.\n  - `initial_time::Dates.DateTime` : initial of the requested results\n  - `count::Int`: Number of results\n\"\"\"\nfunction read_aux_variable(\n    res::SimulationProblemResults{DecisionModelSimulationResults},\n    args...;\n    initial_time::Union{Nothing, Dates.DateTime} = nothing,\n    count::Union{Int, Nothing} = nothing,\n    store = nothing,\n    table_format::TableFormat = TableFormat.LONG,\n)\n    key = _deserialize_key(AuxVarKey, res, args...)\n    timestamps = _process_timestamps(res, initial_time, count)\n    return make_dataframes(\n        _read_results(res, [key], timestamps, store)[key];\n        table_format = table_format,\n    )\nend\n\n\"\"\"\nReturn the values for the requested auxillary variables. It keeps requests when performing multiple retrievals.\n\n# Arguments\n\n  - `args`: Can be a string returned from [`list_expression_names`](@ref) or args that can be\n    splatted into a ExpressionKey.\n  - `initial_time::Dates.DateTime` : initial of the requested results\n  - `count::Int`: Number of results\n\"\"\"\nfunction read_expression(\n    res::SimulationProblemResults{DecisionModelSimulationResults},\n    args...;\n    initial_time::Union{Nothing, Dates.DateTime} = nothing,\n    count::Union{Int, Nothing} = nothing,\n    store = nothing,\n    table_format::TableFormat = TableFormat.LONG,\n)\n    key = _deserialize_key(ExpressionKey, res, args...)\n    timestamps = _process_timestamps(res, initial_time, count)\n    return make_dataframes(\n        _read_results(res, [key], timestamps, store)[key];\n        table_format = table_format,\n    )\nend\n\nfunction get_realized_timestamps(\n    res::IS.Results;\n    start_time::Union{Nothing, Dates.DateTime} = nothing,\n    len::Union{Int, Nothing} = nothing,\n)\n    timestamps = get_timestamps(res)\n    resolution = get_resolution(res)\n    intervals = diff(timestamps)\n    if isempty(intervals) && isnothing(resolution)\n        # If Single Interval Step and single time step: use dummy resolution/interval\n        interval = Dates.Millisecond(1)\n        resolution = Dates.Millisecond(1)\n    elseif !isempty(intervals) && isnothing(resolution)\n        # Multiple simulation steps but single time step: Set resolution = interval\n        interval = first(intervals)\n        resolution = interval\n    elseif isempty(intervals) && !isnothing(resolution)\n        # There is multiple time steps but single simulation step: Set interval = resolution\n        interval = resolution\n    else\n        # Both data are available: Use existing resolution and grab first interval\n        interval = first(intervals)\n    end\n    horizon = get_forecast_horizon(res)\n    start_time = isnothing(start_time) ? first(timestamps) : start_time\n    end_time =\n        if isnothing(len)\n            last(timestamps) + interval - resolution\n        else\n            start_time + (len - 1) * resolution\n        end\n\n    requested_range = start_time:resolution:end_time\n    available_range =\n        first(timestamps):resolution:(last(timestamps) + (horizon - 1) * resolution)\n    invalid_timestamps = setdiff(requested_range, available_range)\n\n    if !isempty(invalid_timestamps)\n        msg = \"Requested time does not match available results\"\n        @error msg\n        throw(IS.InvalidValue(msg))\n    end\n\n    return requested_range\nend\n\nfunction get_realized_timestamps(\n    res::SimulationProblemResults;\n    start_time::Union{Nothing, Dates.DateTime} = nothing,\n    len::Union{Int, Nothing} = nothing,\n)\n    timestamps = get_timestamps(res)\n    resolution = get_resolution(res)\n    interval = get_interval(res)\n    horizon = get_forecast_horizon(res)\n    start_time = isnothing(start_time) ? first(timestamps) : start_time\n    end_time =\n        if isnothing(len)\n            last(timestamps) + interval - resolution\n        else\n            start_time + (len - 1) * resolution\n        end\n\n    requested_range = start_time:resolution:end_time\n    available_range =\n        first(timestamps):resolution:(last(timestamps) + (horizon - 1) * resolution)\n    invalid_timestamps = setdiff(requested_range, available_range)\n\n    if !isempty(invalid_timestamps)\n        msg = \"Requested time does not match available results\"\n        @error msg\n        throw(IS.InvalidValue(msg))\n    end\n\n    return requested_range\nend\n\n\"\"\"\nHigh-level function to read a DataFrame of results.\n\n# Arguments\n\n  - `res`: the results to read.\n  - `result_keys::Vector{<:OptimizationContainerKey}`: the keys to read. Output will be a\n    `Dict{OptimizationContainerKey, DataFrame}` with these as the keys\n  - `start_time::Union{Nothing, Dates.DateTime} = nothing`: the time at which the resulting\n    time series should begin; `nothing` indicates the first time in the results\n  - `len::Union{Int, Nothing} = nothing`: the number of steps in the resulting time series;\n    `nothing` indicates up to the end of the results\n  - `cols::Union{Colon, Vector{String}} = (:)`: which columns to fetch; defaults to `:`,\n    i.e., all the columns\n\"\"\"\nfunction read_results_with_keys(\n    res::SimulationProblemResults{DecisionModelSimulationResults},\n    result_keys::Vector{<:OptimizationContainerKey};\n    start_time::Union{Nothing, Dates.DateTime} = nothing,\n    len::Union{Int, Nothing} = nothing,\n    cols::Union{Colon, Vector{String}} = (:),\n    table_format = TableFormat.LONG,\n)\n    meta = RealizedMeta(res; start_time = start_time, len = len)\n    timestamps = _process_timestamps(res, meta.start_time, meta.len)\n    result_values =\n        _read_results(\n            DataFrame,\n            res,\n            result_keys,\n            timestamps,\n            nothing;\n            cols = cols,\n            table_format = table_format,\n        )\n    return get_realization(result_values, meta; table_format = table_format)\nend\n\nfunction _are_results_cached(\n    res::SimulationProblemResults{DecisionModelSimulationResults},\n    output_keys::Vector{<:OptimizationContainerKey},\n    timestamps::Vector{Dates.DateTime},\n    cached_keys,\n)\n    return isempty(setdiff(timestamps, get_results_timestamps(res))) &&\n           isempty(setdiff(output_keys, cached_keys))\nend\n\n\"\"\"\nLoad the simulation results into memory for repeated reads. This is useful when loading\nresults from remote locations over network connections, when reading the same data very many\ntimes, etc. Multiple calls augment the cache according to these rules, where \"variable\"\nmeans \"variable, expression, etc.\":\n  - Requests for an already cached variable at a lesser `count` than already cached do *not*\n    decrease the `count` of the cached variable\n  - Requests for an already cached variable at a greater `count` than already cached *do*\n    increase the `count` of the cached variable\n  - Requests for new variables are fulfilled without evicting existing variables\n\nNote that `count` is global across all variables, so increasing the `count` re-reads already\ncached variables. For each variable, each element must be the name encoded as a string, like\n`\"ActivePowerVariable__ThermalStandard\"` or a Tuple with its constituent types, like\n`(ActivePowerVariable, ThermalStandard)`. To clear the cache, use [`Base.empty!`](@ref).\n\n# Arguments\n\n  - `count::Int`: Number of windows to load.\n  - `initial_time::Dates.DateTime` : Initial time of first window to load. Defaults to\n    first.\n  - `aux_variables::Vector{Union{String, Tuple}}`: Optional list of aux variables to load.\n  - `duals::Vector{Union{String, Tuple}}`: Optional list of duals to load.\n  - `expressions::Vector{Union{String, Tuple}}`: Optional list of expressions to load.\n  - `parameters::Vector{Union{String, Tuple}}`: Optional list of parameters to load.\n  - `variables::Vector{Union{String, Tuple}}`: Optional list of variables to load.\n\"\"\"\nfunction load_results!(\n    res::SimulationProblemResults{DecisionModelSimulationResults},\n    count::Int;\n    initial_time::Union{Dates.DateTime, Nothing} = nothing,\n    variables = Vector{Tuple}(),\n    duals = Vector{Tuple}(),\n    parameters = Vector{Tuple}(),\n    aux_variables = Vector{Tuple}(),\n    expressions = Vector{Tuple}(),\n    store::Union{Nothing, <:SimulationStore} = nothing,\n)\n    initial_time = initial_time === nothing ? first(get_timestamps(res)) : initial_time\n    count = max(count, length(get_results_timestamps(res)))\n    new_timestamps = _process_timestamps(res, initial_time, count)\n\n    for (key_type, new_items) in [\n        (ConstraintKey, duals),\n        (ParameterKey, parameters),\n        (VariableKey, variables),\n        (AuxVarKey, aux_variables),\n        (ExpressionKey, expressions),\n    ]\n        new_keys = key_type[_deserialize_key(key_type, res, x...) for x in new_items]\n        existing_results = get_cached_results(res, key_type)\n        total_keys = union(collect(keys(existing_results)), new_keys)\n        # _read_results checks the cache to eliminate unnecessary re-reads\n        merge!(existing_results, _read_results(res, total_keys, new_timestamps, store))\n    end\n    set_results_timestamps!(res, new_timestamps)\n\n    return nothing\nend\n\nfunction _read_optimizer_stats(\n    res::SimulationProblemResults{DecisionModelSimulationResults},\n    store::SimulationStore,\n)\n    return read_optimizer_stats(store, Symbol(res.problem))\nend\n"
  },
  {
    "path": "src/simulation/emulation_model_simulation_results.jl",
    "content": "struct EmulationModelSimulationResults <: OperationModelSimulationResults\n    variables::Dict{OptimizationContainerKey, DataFrames.DataFrame}\n    duals::Dict{OptimizationContainerKey, DataFrames.DataFrame}\n    parameters::Dict{OptimizationContainerKey, DataFrames.DataFrame}\n    aux_variables::Dict{OptimizationContainerKey, DataFrames.DataFrame}\n    expressions::Dict{OptimizationContainerKey, DataFrames.DataFrame}\n    container_key_lookup::Dict{String, OptimizationContainerKey}\nend\n\nfunction SimulationProblemResults(\n    ::Type{EmulationModel},\n    store::SimulationStore,\n    model_name::AbstractString,\n    problem_params::ModelStoreParams,\n    sim_params::SimulationStoreParams,\n    path,\n    container_key_lookup;\n    kwargs...,\n)\n    return SimulationProblemResults{EmulationModelSimulationResults}(\n        store,\n        model_name,\n        problem_params,\n        sim_params,\n        path,\n        EmulationModelSimulationResults(\n            Dict(\n                x => DataFrames.DataFrame() for\n                x in list_emulation_model_keys(store, STORE_CONTAINER_VARIABLES)\n            ),\n            Dict(\n                x => DataFrames.DataFrame() for\n                x in list_emulation_model_keys(store, STORE_CONTAINER_DUALS)\n            ),\n            Dict(\n                x => DataFrames.DataFrame() for\n                x in list_emulation_model_keys(store, STORE_CONTAINER_PARAMETERS)\n            ),\n            Dict(\n                x => DataFrames.DataFrame() for\n                x in list_emulation_model_keys(store, STORE_CONTAINER_AUX_VARIABLES)\n            ),\n            Dict(\n                x => DataFrames.DataFrame() for\n                x in list_emulation_model_keys(store, STORE_CONTAINER_EXPRESSIONS)\n            ),\n            container_key_lookup,\n        );\n        kwargs...,\n    )\nend\n\nlist_aux_variable_keys(res::SimulationProblemResults{EmulationModelSimulationResults}) =\n    collect(keys(res.values.aux_variables))\nlist_dual_keys(res::SimulationProblemResults{EmulationModelSimulationResults}) =\n    collect(keys(res.values.duals))\nlist_expression_keys(res::SimulationProblemResults{EmulationModelSimulationResults}) =\n    collect(keys(res.values.expressions))\nlist_parameter_keys(res::SimulationProblemResults{EmulationModelSimulationResults}) =\n    collect(keys(res.values.parameters))\nlist_variable_keys(res::SimulationProblemResults{EmulationModelSimulationResults}) =\n    collect(keys(res.values.variables))\n\nget_cached_aux_variables(res::SimulationProblemResults{EmulationModelSimulationResults}) =\n    res.values.aux_variables\nget_cached_duals(res::SimulationProblemResults{EmulationModelSimulationResults}) =\n    res.values.duals\nget_cached_expressions(res::SimulationProblemResults{EmulationModelSimulationResults}) =\n    res.values.expressions\nget_cached_parameters(res::SimulationProblemResults{EmulationModelSimulationResults}) =\n    res.values.parameters\nget_cached_variables(res::SimulationProblemResults{EmulationModelSimulationResults}) =\n    res.values.variables\n\nfunction _list_containers(res::SimulationProblemResults)\n    return (getfield(res.values, x) for x in get_container_fields(res))\nend\n\nfunction Base.empty!(res::SimulationProblemResults{EmulationModelSimulationResults})\n    for container in _list_containers(res)\n        for df in values(container)\n            empty!(df)\n        end\n    end\nend\n\nfunction Base.isempty(res::SimulationProblemResults{EmulationModelSimulationResults})\n    for container in _list_containers(res)\n        for df in values(container)\n            if !isempty(df)\n                return false\n            end\n        end\n    end\n\n    return true\nend\n\nfunction Base.length(res::SimulationProblemResults{EmulationModelSimulationResults})\n    count_not_empty = 0\n    for container in _list_containers(res)\n        for df in values(container)\n            if !isempty(df)\n                count_not_empty += 1\n            end\n        end\n    end\n\n    return count_not_empty\nend\n\nfunction _get_store_value(\n    res::SimulationProblemResults{EmulationModelSimulationResults},\n    container_keys::Vector{<:OptimizationContainerKey},\n    ::Nothing;\n    start_time = nothing,\n    len = nothing,\n    table_format = TableFormat.LONG,\n)\n    simulation_store_path = joinpath(get_execution_path(res), \"data_store\")\n    return open_store(HdfSimulationStore, simulation_store_path, \"r\") do store\n        _get_store_value(\n            res,\n            container_keys,\n            store;\n            start_time = start_time,\n            len = len,\n            table_format = table_format,\n        )\n    end\nend\n\nfunction _get_store_value(\n    res::SimulationProblemResults{EmulationModelSimulationResults},\n    container_keys::Vector{<:OptimizationContainerKey},\n    store::SimulationStore;\n    start_time::Union{Nothing, Dates.DateTime} = nothing,\n    len::Union{Nothing, Int} = nothing,\n    table_format = TableFormat.LONG,\n)\n    base_power = res.base_power\n    results = Dict{OptimizationContainerKey, DataFrames.DataFrame}()\n    for key in container_keys\n        start_time, _len, resolution = _check_offsets(res, key, store, start_time, len)\n        start_index = (start_time - first(res.timestamps)) ÷ resolution + 1\n        array = read_results(store, key; index = start_index, len = _len)\n        if convert_result_to_natural_units(key)\n            array.data .*= base_power\n        end\n        # PERF: this is a double-permutedims with HDF\n        # We could make an optimized version of this that reads Arrays\n        # like decision_model_simulation_results\n        timestamps = range(start_time; length = _len, step = res.resolution)\n        results[key] = to_results_dataframe(array, timestamps, Val(table_format))\n    end\n\n    return results\nend\n\nfunction _check_offsets(\n    res::SimulationProblemResults{EmulationModelSimulationResults},\n    key,\n    store,\n    start_time,\n    len,\n)\n    dataset_size = get_emulation_model_dataset_size(store, key)\n    resolution =\n        (last(res.timestamps) - first(res.timestamps) + res.resolution) ÷ dataset_size\n    if isnothing(start_time)\n        start_time = first(res.timestamps)\n    elseif start_time < first(res.timestamps) || start_time > last(res.timestamps)\n        throw(\n            IS.InvalidValue(\n                \"start_time = $start_time is not in the results range $(res.timestamps)\",\n            ),\n        )\n    elseif (start_time - first(res.timestamps)) % resolution != Dates.Millisecond(0)\n        throw(\n            IS.InvalidValue(\n                \"start_time = $start_time is not a multiple of resolution = $resolution\",\n            ),\n        )\n    end\n\n    if isnothing(len)\n        len = (last(res.timestamps) + resolution - start_time) ÷ resolution\n    elseif start_time + resolution * len > last(res.timestamps) + res.resolution\n        throw(\n            IS.InvalidValue(\n                \"len = $len resolution = $resolution exceeds the results range $(res.timestamps)\",\n            ),\n        )\n    end\n\n    return start_time, len, resolution\nend\n\nfunction _read_results(\n    res::SimulationProblemResults{EmulationModelSimulationResults},\n    result_keys,\n    store;\n    start_time = nothing,\n    len = nothing,\n    table_format = TableFormat.LONG,\n)\n    isempty(result_keys) && return Dict{OptimizationContainerKey, DataFrames.DataFrame}()\n    _store = try_resolve_store(store, res.store)\n    existing_keys = list_result_keys(res, first(result_keys))\n    ISOPT._validate_keys(existing_keys, result_keys)\n    cached_results = Dict(\n        k => v for\n        (k, v) in get_cached_results(res, eltype(result_keys)) if !isempty(v)\n    )\n    if isempty(setdiff(result_keys, keys(cached_results)))\n        @debug \"reading aux_variables from SimulationsResults\"\n        vals = Dict(k => cached_results[k] for k in result_keys)\n        if table_format == TableFormat.WIDE\n            for (k, v) in vals\n                if :name2 in DataFrames.propertynames(v)\n                    error(\n                        \"TableFormat.WIDE is not supported when the data has three dimensions.\",\n                    )\n                end\n            end\n            vals = Dict(\n                k => DataFrames.unstack(v, :DateTime, :name, :value) for (k, v) in vals\n            )\n        end\n    else\n        @debug \"reading aux_variables from data store\"\n        vals =\n            _get_store_value(\n                res,\n                result_keys,\n                _store;\n                start_time = start_time,\n                len = len,\n                table_format = table_format,\n            )\n    end\n    return vals\nend\n\nfunction read_results_with_keys(\n    res::SimulationProblemResults{EmulationModelSimulationResults},\n    result_keys::Vector{<:OptimizationContainerKey};\n    start_time::Union{Nothing, Dates.DateTime} = nothing,\n    len::Union{Nothing, Int} = nothing,\n    table_format = TableFormat.LONG,\n)\n    return _read_results(\n        res,\n        result_keys,\n        nothing;\n        start_time = start_time,\n        len = len,\n        table_format = table_format,\n    )\nend\n\n\"\"\"\nLoad the simulation results into memory for repeated reads. This is useful when loading\nresults from remote locations over network connections.\n\nFor each variable/parameter/dual, etc., each element must be the name encoded as a string,\nlike `\"ActivePowerVariable__ThermalStandard\"`` or a Tuple with its constituent types, like\n`(ActivePowerVariable, ThermalStandard)`.\n\n# Arguments\n\n  - `aux_variables::Vector{Union{String, Tuple}}`: Optional list of aux variables to load.\n  - `duals::Vector{Union{String, Tuple}}`: Optional list of duals to load.\n  - `expressions::Vector{Union{String, Tuple}}`: Optional list of expressions to load.\n  - `parameters::Vector{Union{String, Tuple}}`: Optional list of parameters to load.\n  - `variables::Vector{Union{String, Tuple}}`: Optional list of variables to load.\n\"\"\"\nfunction load_results!(\n    res::SimulationProblemResults{EmulationModelSimulationResults};\n    aux_variables = Vector{Tuple}(),\n    duals = Vector{Tuple}(),\n    expressions = Vector{Tuple}(),\n    parameters = Vector{Tuple}(),\n    variables = Vector{Tuple}(),\n)\n    # TODO: consider extending this to support start_time and len\n    aux_variable_keys = [_deserialize_key(AuxVarKey, res, x...) for x in aux_variables]\n    dual_keys = [_deserialize_key(ConstraintKey, res, x...) for x in duals]\n    expression_keys = [_deserialize_key(ExpressionKey, res, x...) for x in expressions]\n    parameter_keys = [_deserialize_key(ParameterKey, res, x...) for x in parameters]\n    variable_keys = [_deserialize_key(VariableKey, res, x...) for x in variables]\n    function merge_results(store)\n        merge!(get_cached_aux_variables(res), _read_results(res, aux_variable_keys, store))\n        merge!(get_cached_duals(res), _read_results(res, dual_keys, store))\n        merge!(get_cached_expressions(res), _read_results(res, expression_keys, store))\n        merge!(get_cached_parameters(res), _read_results(res, parameter_keys, store))\n        merge!(get_cached_variables(res), _read_results(res, variable_keys, store))\n    end\n\n    if res.store isa InMemorySimulationStore\n        merge_results(res.store)\n    else\n        simulation_store_path = joinpath(res.execution_path, \"data_store\")\n        open_store(HdfSimulationStore, simulation_store_path, \"r\") do store\n            merge_results(store)\n        end\n    end\n\n    return\nend\n\n# TODO: These aren't being written to the store.\nfunction _read_optimizer_stats(\n    res::SimulationProblemResults{EmulationModelSimulationResults},\n    store::SimulationStore,\n)\n    return\nend\n"
  },
  {
    "path": "src/simulation/get_components_interface.jl",
    "content": "# Analogous to `src/get_components_interface.jl` in PowerSystems.jl, see comments there.\n\n# get_components\n\"\"\"\nCalling `get_components` on a `Results` is the same as calling\n[`get_available_components`] on the system attached to the results.\n\"\"\"\nPSY.get_components(\n    ::Type{T},\n    res::IS.Results;\n    subsystem_name = nothing,\n) where {T <: IS.InfrastructureSystemsComponent} =\n    IS.get_components(T, res; subsystem_name = subsystem_name)\n\nPSY.get_components(res::IS.Results, attribute::IS.SupplementalAttribute) =\n    IS.get_components(res, attribute)\n\nPSY.get_components(\n    filter_func::Function,\n    ::Type{T},\n    res::IS.Results;\n    subsystem_name = nothing,\n) where {T <: IS.InfrastructureSystemsComponent} =\n    IS.get_components(filter_func, T, res; subsystem_name = subsystem_name)\n\nPSY.get_components(\n    scope_limiter::Union{Function, Nothing},\n    selector::IS.ComponentSelector,\n    res::IS.Results,\n) =\n    IS.get_components(scope_limiter, selector, res)\n\nPSY.get_components(selector::IS.ComponentSelector, res::IS.Results) =\n    IS.get_components(selector, res)\n\n# get_component\n\"\"\"\nCalling `get_component` on a `Results` is the same as calling\n[`get_available_component`] on the system attached to the results.\n\"\"\"\nPSY.get_component(res::IS.Results, uuid::Base.UUID) = IS.get_component(res, uuid)\nPSY.get_component(res::IS.Results, uuid::String) = IS.get_component(res, uuid)\n\nPSY.get_component(\n    ::Type{T},\n    res::IS.Results,\n    name::AbstractString,\n) where {T <: IS.InfrastructureSystemsComponent} =\n    IS.get_component(T, res, name)\n\nPSY.get_component(\n    scope_limiter::Union{Function, Nothing},\n    selector::IS.SingularComponentSelector,\n    res::IS.Results,\n) =\n    IS.get_component(scope_limiter, selector, res)\n\nPSY.get_component(selector::IS.SingularComponentSelector, res::IS.Results) =\n    IS.get_component(selector, res)\n\n# get_groups\n\"\"\"\nCalling `get_groups` on a `Results` is the same as calling [`get_available_groups`] on\nthe system attached to the results.\n\"\"\"\nPSY.get_groups(\n    scope_limiter::Union{Function, Nothing},\n    selector::IS.ComponentSelector,\n    res::IS.Results,\n) =\n    IS.get_groups(scope_limiter, selector, res)\n\nPSY.get_groups(selector::IS.ComponentSelector, res::IS.Results) =\n    IS.get_groups(selector, res)\n"
  },
  {
    "path": "src/simulation/hdf_simulation_store.jl",
    "content": "const HDF_FILENAME = \"simulation_store.h5\"\nconst HDF_SIMULATION_ROOT_PATH = \"simulation\"\nconst EMULATION_MODEL_PATH = \"$HDF_SIMULATION_ROOT_PATH/emulation_model\"\nconst OPTIMIZER_STATS_PATH = \"optimizer_stats\"\nconst SERIALIZED_KEYS_PATH = \"serialized_keys\"\n\n# This only applies if chunks are enabled, and that will only likely happen if we enable\n# compression.\n# The optimal number of chunks to store in memory will vary widely.\n# The HDF docs recommend keeping chunk byte sizes between 10 KiB - 1 MiB.\n# We want to make it big enough to compress duplicate values.\n# The downside to making this larger is that any read causes the\n# entire chunk to be read.\n# If one variable has 10,000 components and each value is a Float64 then one row would\n# consume 10,000 * 8 = 78 KiB\nDEFAULT_MAX_CHUNK_BYTES = 128 * KiB\n\n\"\"\"\nStores simulation data in an HDF file.\n\"\"\"\nmutable struct HdfSimulationStore <: SimulationStore\n    file::HDF5.File\n    params::SimulationStoreParams\n    # The key order is the problem execution order.\n    dm_data::OrderedDict{Symbol, DatasetContainer{HDF5Dataset}}\n    em_data::DatasetContainer{HDF5Dataset}\n    # The key is the problem name.\n    optimizer_stats_datasets::Dict{Symbol, HDF5.Dataset}\n    optimizer_stats_write_index::Dict{Symbol, Int}\n    cache::OptimizationOutputCaches\nend\n\nget_initial_time(store::HdfSimulationStore) = get_initial_time(store.params)\n\nfunction HdfSimulationStore(file_path::AbstractString, mode::AbstractString)\n    if !(mode in (\"w\", \"r\", \"rw\"))\n        throw(IS.ConflictingInputsError(\"mode can only be 'w', 'r', or 'rw'\"))\n    end\n\n    if !isfile(file_path) && mode in (\"r\", \"rw\")\n        throw(IS.ConflictingInputsError(\"$file_path does not exist\"))\n    end\n\n    if isfile(file_path) && mode == \"w\"\n        throw(IS.ConflictingInputsError(\"$file_path already exists\"))\n    end\n\n    hdf5_mode = mode == \"rw\" ? \"r+\" : mode\n    file = HDF5.h5open(file_path, hdf5_mode)\n    if mode == \"w\"\n        HDF5.create_group(file, HDF_SIMULATION_ROOT_PATH)\n        @debug \"Created store\" file_path\n    end\n\n    store = HdfSimulationStore(\n        file,\n        SimulationStoreParams(),\n        OrderedDict{Symbol, DatasetContainer{HDF5Dataset}}(),\n        DatasetContainer{HDF5Dataset}(),\n        Dict{Symbol, HDF5.Dataset}(),\n        Dict{Symbol, Int}(),\n        OptimizationOutputCaches(),\n    )\n    mode in (\"r\", \"rw\") && _deserialize_attributes!(store)\n\n    finalizer(_check_state, store)\n    return store\nend\n\n\"\"\"\nConstruct and open an HdfSimulationStore.\n\nWhen reading or writing results in a program you should use the method that accepts a\nfunction in order to guarantee that the file handle gets closed.\n\n# Arguments\n\n  - `directory::AbstractString`: Directory containing the store file\n  - `mode::AbstractString`: Mode to use to open the store file\n  - `filename::AbstractString`: Base name of the store file\n\n# Examples\n\n```julia\n# Assumes a simulation has been executed in the './rts' directory with these parameters.\npath = \"./rts\"\nproblem = :ED\nvar_name = :P__ThermalStandard\ntimestamp = DateTime(\"2020-01-01T05:00:00\")\nstore = open_store(HdfSimulationStore, path)\ndf = PowerSimulations.read_result(DataFrame, store, model, :variables, var_name, timestamp)\n```\n\"\"\"\nfunction open_store(\n    ::Type{HdfSimulationStore},\n    directory::AbstractString,\n    mode = \"r\";\n    filename = HDF_FILENAME,\n)\n    return HdfSimulationStore(joinpath(directory, filename), mode)\nend\n\nfunction open_store(\n    func::Function,\n    ::Type{HdfSimulationStore},\n    directory::AbstractString,\n    mode = \"r\";\n    filename = HDF_FILENAME,\n)\n    store = nothing\n    try\n        store = HdfSimulationStore(joinpath(directory, filename), mode)\n        return func(store)\n    finally\n        if store !== nothing\n            close(store)\n        end\n    end\nend\n\nfunction Base.close(store::HdfSimulationStore)\n    flush(store)\n    HDF5.close(store.file)\n    empty!(store.cache)\n    @debug \"Close store file handle\" store.file\nend\n\nfunction Base.isopen(store::HdfSimulationStore)\n    return store.file === nothing ? false : HDF5.isopen(store.file)\nend\n\nfunction Base.flush(store::HdfSimulationStore)\n    for (key, output_cache) in store.cache.data\n        _flush_data!(output_cache, store, key, false)\n        @assert !has_dirty(output_cache) \"$key has dirty cache after flushing\"\n    end\n\n    flush(store.file)\n    @debug \"Flush store\"\n    return\nend\n\nget_params(store::HdfSimulationStore) = store.params\n\nfunction set_cache_flush_rules!(store::HdfSimulationStore, flush_rules::CacheFlushRules)\n    new_cache = OptimizationOutputCaches(flush_rules)\n    for (key, output_cache) in store.cache.data\n        new_cache.data[key] = output_cache\n    end\n    store.cache = new_cache\n    @debug \"Updated store cache rules\" get_min_flush_size(store.cache) get_max_size(\n        store.cache,\n    )\n    return\nend\n\nfunction get_decision_model_params(store::HdfSimulationStore, model_name::Symbol)\n    return get_decision_model_params(get_params(store), model_name)\nend\n\nfunction get_emulation_model_params(store::HdfSimulationStore)\n    return get_emulation_model_params(get_params(store))\nend\n\nfunction get_container_key_lookup(store::HdfSimulationStore)\n    function _get_lookup()\n        root = _get_root(store)\n        buf = IOBuffer(root[SERIALIZED_KEYS_PATH][:])\n        return Serialization.deserialize(buf)\n    end\n    isopen(store) && return _get_lookup()\n\n    store.file = HDF5.h5open(store.file.filename, \"r\")\n    try\n        return _get_lookup()\n    finally\n        HDF5.close(store.file)\n    end\nend\n\n\"\"\"\nReturn the problem names in order of execution.\n\"\"\"\nlist_decision_models(store::HdfSimulationStore) = keys(get_dm_data(store))\n\n\"\"\"\nReturn the fields stored for the `problem` and `container_type` (duals/parameters/variables).\n\"\"\"\nfunction list_decision_model_keys(\n    store::HdfSimulationStore,\n    model::Symbol,\n    container_type::Symbol,\n)\n    container = getfield(get_dm_data(store)[model], container_type)\n    return collect(keys(container))\nend\n\nfunction list_emulation_model_keys(store::HdfSimulationStore, container_type::Symbol)\n    container = getfield(get_em_data(store), container_type)\n    return collect(keys(container))\nend\n\nfunction write_optimizer_stats!(\n    store::HdfSimulationStore,\n    model::OperationModel,\n    ::DecisionModelIndexType,\n)\n    stats = get_optimizer_stats(model)\n    model_name = get_name(model)\n    dataset = _get_dataset(OptimizerStats, store, model_name)\n\n    # Uncomment for performance measures of HDF Store\n    #TimerOutputs.@timeit RUN_SIMULATION_TIMER \"Write optimizer stats\" begin\n    dataset[:, store.optimizer_stats_write_index[model_name]] = to_matrix(stats)\n    #end\n\n    store.optimizer_stats_write_index[model_name] += 1\n    return\nend\n\nfunction write_optimizer_stats!(\n    store::HdfSimulationStore,\n    model::OperationModel,\n    ::EmulationModelIndexType,\n)\n    return\nend\n\n\"\"\"\nRead the optimizer stats for a problem execution.\n\"\"\"\nfunction read_optimizer_stats(\n    store::HdfSimulationStore,\n    simulation_step::Int,\n    model_name::Symbol,\n    execution_index::Int,\n)\n    optimizer_stats_write_index =\n        (simulation_step - 1) *\n        store.params.decision_models_params[model_name].num_executions + execution_index\n    dataset = _get_dataset(OptimizerStats, store, model_name)\n    return OptimizerStats(dataset[:, optimizer_stats_write_index])\nend\n\n\"\"\"\nReturn the optimizer stats for a problem as a DataFrame.\n\"\"\"\nfunction read_optimizer_stats(store::HdfSimulationStore, model_name)\n    dataset = _get_dataset(OptimizerStats, store, model_name)\n    data = permutedims(dataset[:, :])\n    stats = [IS.to_namedtuple(OptimizerStats(data[i, :])) for i in axes(data)[1]]\n    return DataFrames.DataFrame(stats)\nend\n\nfunction initialize_problem_storage!(\n    store::HdfSimulationStore,\n    params::SimulationStoreParams,\n    dm_problem_reqs::Dict{Symbol, SimulationModelStoreRequirements},\n    em_problem_reqs::SimulationModelStoreRequirements,\n    flush_rules::CacheFlushRules,\n)\n    store.params = params\n    root = store.file[HDF_SIMULATION_ROOT_PATH]\n    problems_group = _get_group_or_create(root, \"decision_models\")\n    store.cache = OptimizationOutputCaches(flush_rules)\n    @info \"Initialize store cache\" get_min_flush_size(store.cache) get_max_size(store.cache)\n    initial_time = get_initial_time(store)\n    container_key_lookup = Dict{String, OptimizationContainerKey}()\n    for (problem, problem_params) in store.params.decision_models_params\n        get_dm_data(store)[problem] = DatasetContainer{HDF5Dataset}()\n        problem_group = _get_group_or_create(problems_group, string(problem))\n        for type in STORE_CONTAINERS\n            group = _get_group_or_create(problem_group, string(type))\n            for (key, reqs) in getfield(dm_problem_reqs[problem], type)\n                !should_write_resulting_value(key) && continue\n                name = encode_key_as_string(key)\n                dataset = _create_dataset(group, name, reqs)\n                # Columns can't be stored in attributes because they might be larger than\n                # the max size of 64 KiB.\n                col = _make_column_name(name)\n                if length(reqs[\"columns\"]) == 1\n                    HDF5.write_dataset(group, col, string.(reqs[\"columns\"][1]))\n                else\n                    col_vals = vcat(reqs[\"columns\"]...)\n                    HDF5.write_dataset(group, col, string.(col_vals))\n                end\n                column_dataset = group[col]\n                datasets = getfield(get_dm_data(store)[problem], type)\n                column_lengths = reqs[\"dims\"][2:(end - 1)]\n                datasets[key] = HDF5Dataset{length(column_lengths)}(\n                    dataset,\n                    column_dataset,\n                    column_lengths,\n                    get_resolution(problem_params),\n                    initial_time,\n                )\n                add_output_cache!(\n                    store.cache,\n                    problem,\n                    key,\n                    get_rule(flush_rules, problem, key),\n                )\n                container_key_lookup[encode_key_as_string(key)] = key\n            end\n        end\n\n        num_stats = params.num_steps * params.decision_models_params[problem].num_executions\n        columns = fieldnames(OptimizerStats)\n        num_columns = length(columns)\n        dataset = HDF5.create_dataset(\n            problem_group,\n            OPTIMIZER_STATS_PATH,\n            HDF5.datatype(Float64),\n            HDF5.dataspace((num_columns, num_stats)),\n        )\n        HDF5.attributes(dataset)[\"columns\"] = [string(x) for x in columns]\n        store.optimizer_stats_datasets[problem] = dataset\n        store.optimizer_stats_write_index[problem] = 1\n        @debug \"Initialized optimizer_stats_datasets $problem ($num_columns, $num_stats)\"\n    end\n\n    emulation_group = _get_group_or_create(root, \"emulation_model\")\n    for emulation_params in values(store.params.emulation_model_params)\n        for type in STORE_CONTAINERS\n            group = _get_group_or_create(emulation_group, string(type))\n            for (key, reqs) in getfield(em_problem_reqs, type)\n                name = encode_key_as_string(key)\n                dataset = _create_dataset(group, name, reqs)\n                # Columns can't be stored in attributes because they might be larger than\n                # the max size of 64 KiB.\n                col = _make_column_name(name)\n                if length(reqs[\"columns\"]) == 1\n                    HDF5.write_dataset(group, col, string.(reqs[\"columns\"][1]))\n                else\n                    col_vals = vcat(reqs[\"columns\"]...)\n                    HDF5.write_dataset(group, col, string.(col_vals))\n                end\n                column_dataset = group[col]\n                datasets = getfield(store.em_data, type)\n                column_lengths = reqs[\"dims\"][2:end]\n                datasets[key] = HDF5Dataset{length(column_lengths)}(\n                    dataset,\n                    column_dataset,\n                    column_lengths,\n                    get_resolution(emulation_params),\n                    initial_time,\n                )\n                container_key_lookup[encode_key_as_string(key)] = key\n            end\n        end\n    end\n    buf = IOBuffer()\n    Serialization.serialize(buf, container_key_lookup)\n    seek(buf, 0)\n    root[SERIALIZED_KEYS_PATH] = buf.data\n\n    # This has to run after problem groups are created.\n    _serialize_attributes(store)\n    return\nend\n\nlog_cache_hit_percentages(x::HdfSimulationStore) = log_cache_hit_percentages(x.cache)\n\nfunction _make_dataframe(data::Matrix{Float64}, columns::Tuple{Vector{String}})\n    return DataFrames.DataFrame(data, columns[1]; copycols = false)\nend\n\n\"\"\"\nReturn DataFrame, DenseAxisArray, or Array for a model result at a timestamp.\n\"\"\"\nfunction read_result(\n    ::Type{DataFrames.DataFrame},\n    store::HdfSimulationStore,\n    model_name::Symbol,\n    key::OptimizationContainerKey,\n    index::Union{DecisionModelIndexType, EmulationModelIndexType},\n)\n    data, columns = _read_data_columns(store, model_name, key, index)\n    return _make_dataframe(data, columns)\nend\n\nfunction _make_denseaxisarray(\n    data::Matrix{Float64},\n    columns::Tuple{Vector{String}},\n)\n    return DenseAxisArray(permutedims(data), columns[1], 1:size(data)[1])\nend\n\nfunction _make_denseaxisarray(\n    data::Matrix{Float64},\n    columns::NTuple{2, <:Any},\n)\n    # Handle 2D data with 2 column axes (e.g., from reshaped 3D emulation data)\n    return DenseAxisArray(\n        permutedims(data),\n        columns[1],\n        columns[2],\n    )\nend\n\nfunction _make_denseaxisarray(\n    data::Array{Float64, 3},\n    columns::NTuple{2, <:Any},\n)\n    return DenseAxisArray(\n        permutedims(data, (2, 3, 1)),\n        columns[1],\n        columns[2],\n        1:size(data)[1],\n    )\nend\n\nfunction read_result(\n    ::Type{DenseAxisArray},\n    store::HdfSimulationStore,\n    model_name::Symbol,\n    key::OptimizationContainerKey,\n    index::Union{DecisionModelIndexType, EmulationModelIndexType},\n)\n    if is_cached(store.cache, model_name, key, index)\n        data = read_result(store.cache, model_name, key, index)\n        columns = get_column_names(store, DecisionModelIndexType, model_name, key)\n    else\n        data, columns = _read_result(store, model_name, key, index)\n    end\n    return _make_denseaxisarray(data, columns)\nend\n\nfunction read_result(\n    ::Type{Array},\n    store::HdfSimulationStore,\n    model_name::Symbol,\n    key::OptimizationContainerKey,\n    index::Union{DecisionModelIndexType, EmulationModelIndexType},\n)\n    if is_cached(store.cache, model_name, key, index)\n        data = read_result(store.cache, model_name, key, index)\n    else\n        data, _ = _read_result(store, model_name, key, index)\n    end\n\n    return data\nend\n\nfunction read_results(\n    store::HdfSimulationStore,\n    key::OptimizationContainerKey;\n    index::Union{Nothing, EmulationModelIndexType} = nothing,\n    len::Union{Nothing, Int} = nothing,\n)\n    dataset = _get_em_dataset(store, key)\n    num_dims = ndims(dataset.values)\n    if num_dims == 2\n        if isnothing(index)\n            @assert_op(isnothing(len))\n            data = dataset.values[:, :]\n        elseif isnothing(len)\n            data = dataset.values[index:end, :]\n        else\n            data = dataset.values[index:(index + len - 1), :]\n        end\n        columns = get_column_names(key, dataset)\n        return DenseAxisArray(permutedims(data), columns..., 1:size(data)[1])\n    elseif num_dims == 3\n        if isnothing(index)\n            @assert_op(isnothing(len))\n            data = dataset.values[:, :, :]\n        elseif isnothing(len)\n            data = dataset.values[index:end, :, :]\n        else\n            data = dataset.values[index:(index + len - 1), :, :]\n        end\n        columns = get_column_names(key, dataset)\n        return DenseAxisArray(permutedims(data, (2, 3, 1)), columns..., 1:size(data)[1])\n    else\n        error(\"Unsupported number of dimensions for emulation dataset: $num_dims\")\n    end\nend\n\nfunction get_column_names(\n    store::HdfSimulationStore,\n    ::Type{DecisionModelIndexType},\n    model_name::Symbol,\n    key::OptimizationContainerKey,\n)\n    !isopen(store) && throw(ArgumentError(\"store must be opened prior to reading\"))\n    dataset = _get_dm_dataset(store, model_name, key)\n    return get_column_names(key, dataset)\nend\n\nfunction get_column_names(\n    store::HdfSimulationStore,\n    ::Type{EmulationModelIndexType},\n    key::OptimizationContainerKey,\n)\n    !isopen(store) && throw(ArgumentError(\"store must be opened prior to reading\"))\n    dataset = _get_em_dataset(store, key)\n    return get_column_names(key, dataset)\nend\n\nfunction get_number_of_dimensions(\n    store::HdfSimulationStore,\n    i::Type{DecisionModelIndexType},\n    model_name::Symbol,\n    key::OptimizationContainerKey,\n)\n    return length(get_column_names(store, i, model_name, key))\nend\n\nfunction get_number_of_dimensions(\n    store::HdfSimulationStore,\n    i::Type{EmulationModelIndexType},\n    key::OptimizationContainerKey,\n)\n    return length(get_column_names(store, i, key))\nend\n\nfunction get_emulation_model_dataset_size(\n    store::HdfSimulationStore,\n    key::OptimizationContainerKey,\n)\n    dataset = _get_em_dataset(store, key)\n    return size(dataset.values)[1]\nend\n\nfunction _read_result(\n    store::HdfSimulationStore,\n    ::Symbol,\n    key::OptimizationContainerKey,\n    index::EmulationModelIndexType,\n)\n    !isopen(store) && throw(ArgumentError(\"store must be opened prior to reading\"))\n    model_params = get_emulation_model_params(store)\n\n    if index > model_params.num_executions\n        throw(\n            ArgumentError(\n                \"index = $index cannot be larger than $(model_params.num_executions)\",\n            ),\n        )\n    end\n\n    dataset = _get_em_dataset(store, key)\n    dset = dataset.values\n    # Uncomment for performance checking\n    #TimerOutputs.@timeit RUN_SIMULATION_TIMER \"Read dataset\" begin\n    num_dims = ndims(dset)\n    if num_dims == 2\n        data = dset[index, :]\n    elseif num_dims == 3\n        data = dset[index, :, :]\n    else\n        error(\"Unsupported number of dimensions for emulation dataset: $num_dims\")\n    end\n    #end\n    columns = get_column_names(key, dataset)\n    data = ndims(data) == 1 ? permutedims(data) : data\n    return data, columns\nend\n\nfunction _read_result(\n    store::HdfSimulationStore,\n    model_name::Symbol,\n    key::OptimizationContainerKey,\n    index::DecisionModelIndexType,\n)\n    simulation_step, execution_index = _get_indices(store, model_name, index)\n    return _read_result(store, model_name, key, simulation_step, execution_index)\nend\n\nfunction _read_result(\n    store::HdfSimulationStore,\n    model_name::Symbol,\n    key::OptimizationContainerKey,\n    simulation_step::Int,\n    execution_index::Int,\n)\n    !isopen(store) && throw(ArgumentError(\"store must be opened prior to reading\"))\n\n    model_params = get_decision_model_params(store, model_name)\n    num_executions = model_params.num_executions\n    if execution_index > num_executions\n        throw(\n            ArgumentError(\n                \"execution_index = $execution_index cannot be larger than $num_executions\",\n            ),\n        )\n    end\n\n    dataset = _get_dm_dataset(store, model_name, key)\n    dset = dataset.values\n    row_index = (simulation_step - 1) * num_executions + execution_index\n    columns = get_column_names(key, dataset)\n\n    # Uncomment for performance checking\n    #TimerOutputs.@timeit RUN_SIMULATION_TIMER \"Read dataset\" begin\n    num_dims = ndims(dset)\n    if num_dims == 3\n        data = dset[:, :, row_index]\n    elseif num_dims == 4\n        data = dset[:, :, :, row_index]\n    else\n        error(\"unsupported dims: $num_dims\")\n    end\n    #end\n\n    return data, columns\nend\n\n\"\"\"\nWrite a decision model result for a timestamp to the store.\n\"\"\"\nfunction write_result!(\n    store::HdfSimulationStore,\n    model_name::Symbol,\n    key::OptimizationContainerKey,\n    index::DecisionModelIndexType,\n    ::Dates.DateTime,\n    data::DenseAxisArray{Float64, N, <:NTuple{N, Any}},\n) where {N}\n    output_cache = get_output_cache(store.cache, model_name, key)\n    cur_size = get_size(store.cache)\n    add_result!(output_cache, index, to_matrix(data), is_full(store.cache, cur_size))\n\n    if get_dirty_size(output_cache) >= get_min_flush_size(store.cache)\n        discard = !should_keep_in_cache(output_cache)\n\n        # PERF: A potentially significant performance improvement would be to queue several\n        # flushes and submit them in parallel.\n        size_flushed = _flush_data!(output_cache, store, model_name, key, discard)\n\n        @debug \"flushed data\" LOG_GROUP_SIMULATION_STORE key size_flushed discard cur_size\n    end\n\n    # Disabled because this is currently a noop.\n    #if is_full(store.cache)\n    #    _flush_data!(store.cache, store)\n    #end\n\n    @debug \"write_result\" get_size(store.cache) encode_key_as_string(key)\n    return\nend\n\n\"\"\"\nWrite a decision model result for a timestamp to the store.\n\"\"\"\nfunction write_result!(\n    store::HdfSimulationStore,\n    model_name::Symbol,\n    key::OptimizationContainerKey,\n    index::DecisionModelIndexType,\n    ::Dates.DateTime,\n    data::DenseAxisArray{Float64, 3, <:NTuple{3, Any}},\n)\n    output_cache = get_output_cache(store.cache, model_name, key)\n    cur_size = get_size(store.cache)\n\n    add_result!(\n        output_cache,\n        index,\n        permutedims(data.data, (3, 1, 2)),\n        is_full(store.cache, cur_size),\n    )\n\n    if get_dirty_size(output_cache) >= get_min_flush_size(store.cache)\n        discard = !should_keep_in_cache(output_cache)\n\n        # PERF: A potentially significant performance improvement would be to queue several\n        # flushes and submit them in parallel.\n        size_flushed = _flush_data!(output_cache, store, model_name, key, discard)\n\n        @debug \"flushed data\" LOG_GROUP_SIMULATION_STORE key size_flushed discard cur_size\n    end\n\n    # Disabled because this is currently a noop.\n    #if is_full(store.cache)\n    #    _flush_data!(store.cache, store)\n    #end\n\n    @debug \"write_result\" get_size(store.cache) encode_key_as_string(key)\n    return\nend\n\n\"\"\"\nWrite an emulation model result for an execution index value and the timestamp of the update\n\"\"\"\nfunction write_result!(\n    store::HdfSimulationStore,\n    ::Symbol,\n    key::OptimizationContainerKey,\n    index::EmulationModelIndexType,\n    simulation_time::Dates.DateTime,\n    array::DenseAxisArray{Float64, 2},\n)\n    # TODO: This is a temporary fix.\n    # Not sure why the special case for this dimension size is needed.\n    # It fails with the key = InfrastructureSystems.Optimization.ParameterKey{OnStatusParameter, ThermalStandard}(\"\")\n    # The array size is 5 x 1\n    data = size(array, 2) == 1 ? reshape(array.data, length(array.data)) : array.data\n    dataset = _get_em_dataset(store, key)\n    _write_dataset!(dataset.values, data, index)\n    set_last_recorded_row!(dataset, index)\n    set_update_timestamp!(dataset, simulation_time)\n    return\nend\n\nfunction write_result!(\n    store::HdfSimulationStore,\n    ::Symbol,\n    key::OptimizationContainerKey,\n    index::EmulationModelIndexType,\n    simulation_time::Dates.DateTime,\n    array::DenseAxisArray{Float64, 3},\n)\n    # Handle 3D arrays by reshaping if the last dimension is 1\n    # This mirrors the 2D case above where size(array, 2) == 1 triggers a reshape\n    if size(array, 3) == 1\n        data = reshape(array.data, size(array, 1), size(array, 2))\n    else\n        data = array.data\n    end\n    dataset = _get_em_dataset(store, key)\n    _write_dataset!(dataset.values, data, index)\n    set_last_recorded_row!(dataset, index)\n    set_update_timestamp!(dataset, simulation_time)\n    return\nend\n\nfunction write_result!(\n    store::HdfSimulationStore,\n    ::Symbol,\n    key::OptimizationContainerKey,\n    index::EmulationModelIndexType,\n    simulation_time::Dates.DateTime,\n    array::DenseAxisArray{Float64},\n)\n    dataset = _get_em_dataset(store, key)\n    _write_dataset!(dataset.values, array.data, index)\n    set_last_recorded_row!(dataset, index)\n    set_update_timestamp!(dataset, simulation_time)\n    return\nend\n\nfunction serialize_system!(store::HdfSimulationStore, sys::PSY.System)\n    root = store.file[HDF_SIMULATION_ROOT_PATH]\n    systems_group = _get_group_or_create(root, \"systems\")\n    uuid = string(IS.get_uuid(sys))\n    if haskey(systems_group, uuid)\n        @debug \"System with UUID = $uuid is already stored\" _group =\n            LOG_GROUP_SIMULATION_STORE\n        return\n    end\n\n    json_text = PSY.to_json(sys)\n    systems_group[uuid] = json_text\n    return\nend\n\nfunction write_system_json!(store::HdfSimulationStore, uuid::String, json_text::String)\n    root = store.file[HDF_SIMULATION_ROOT_PATH]\n    systems_group = _get_group_or_create(root, \"systems\")\n    if !haskey(systems_group, uuid)\n        systems_group[uuid] = json_text\n    end\n    return\nend\n\nfunction has_system(store::HdfSimulationStore, uuid::Base.UUID)\n    root = store.file[HDF_SIMULATION_ROOT_PATH]\n    haskey(root, \"systems\") || return false\n    return haskey(root[\"systems\"], string(uuid))\nend\n\nfunction deserialize_system(store::HdfSimulationStore, uuid::Base.UUID)\n    root = store.file[HDF_SIMULATION_ROOT_PATH]\n    uuid_str = string(uuid)\n    if !haskey(root, \"systems\") || !haskey(root[\"systems\"], uuid_str)\n        error(\"No system with UUID $uuid_str is stored\")\n    end\n    json_text = HDF5.read(root[\"systems\"][uuid_str])\n    return PSY.from_json(json_text, PSY.System)\nend\n\nfunction _check_state(store::HdfSimulationStore)\n    if has_dirty(store.cache)\n        error(\"BUG!!! dirty cache is present at shutdown: $(store.file)\")\n    end\nend\n\nfunction _compute_chunk_count(dims, dtype; max_chunk_bytes = DEFAULT_MAX_CHUNK_BYTES)\n    bytes_per_element = sizeof(dtype)\n\n    if length(dims) == 2\n        size_row = dims[1] * bytes_per_element\n    elseif length(dims) == 3\n        size_row = dims[1] * dims[2] * bytes_per_element\n    elseif length(dims) == 4\n        size_row = dims[1] * dims[2] * dims[3] * bytes_per_element\n    else\n        error(\"unsupported dims = $dims\")\n    end\n\n    chunk_count = minimum((trunc(max_chunk_bytes / size_row), dims[end]))\n    if chunk_count == 0\n        error(\n            \"HDF Max Chunk Bytes is smaller than the size of a row. Please increase it. \" *\n            \"max_chunk_bytes=$max_chunk_bytes dims=$dims \" *\n            \"size_row=$size_row\",\n        )\n    end\n\n    chunk_dims = [x for x in dims]\n    chunk_dims[end] = chunk_count\n    return chunk_dims\nend\n\nfunction _create_dataset(group, name, reqs)\n    dataset = HDF5.create_dataset(\n        group,\n        name,\n        HDF5.datatype(Float64),\n        HDF5.dataspace(reqs[\"dims\"]),\n        # We are choosing to optimize read performance in the first implementation.\n        # Compression would slow that down.\n        #chunk = _compute_chunk_count(reqs[\"dims\"], Float64),\n        #shuffle = (),\n        #deflate = 3,\n    )\n    @debug \"Created dataset for\" group name size(dataset)\n    return dataset\nend\n\nfunction _deserialize_attributes!(store::HdfSimulationStore)\n    container_key_lookup = get_container_key_lookup(store)\n    group = store.file[\"simulation\"]\n    initial_time = Dates.DateTime(HDF5.read(HDF5.attributes(group)[\"initial_time\"]))\n    step_resolution =\n        Dates.Millisecond(HDF5.read(HDF5.attributes(group)[\"step_resolution_ms\"]))\n    num_steps = HDF5.read(HDF5.attributes(group)[\"num_steps\"])\n    store.params = SimulationStoreParams(initial_time, step_resolution, num_steps)\n    empty!(get_dm_data(store))\n    for model in HDF5.read(HDF5.attributes(group)[\"problem_order\"])\n        problem_group = store.file[\"simulation/decision_models/$model\"]\n        # Fall back on old key for backwards compatibility\n        horizon_count = HDF5.read(\n            if haskey(HDF5.attributes(problem_group), \"horizon_count\")\n                HDF5.attributes(problem_group)[\"horizon_count\"]\n            else\n                HDF5.attributes(problem_group)[\"horizon\"]\n            end)\n        model_name = Symbol(model)\n        store.params.decision_models_params[model_name] = ModelStoreParams(\n            HDF5.read(HDF5.attributes(problem_group)[\"num_executions\"]),\n            horizon_count,\n            Dates.Millisecond(HDF5.read(HDF5.attributes(problem_group)[\"interval_ms\"])),\n            Dates.Millisecond(HDF5.read(HDF5.attributes(problem_group)[\"resolution_ms\"])),\n            HDF5.read(HDF5.attributes(problem_group)[\"base_power\"]),\n            Base.UUID(HDF5.read(HDF5.attributes(problem_group)[\"system_uuid\"])),\n        )\n        get_dm_data(store)[model_name] = DatasetContainer{HDF5Dataset}()\n        for type in STORE_CONTAINERS\n            group = problem_group[string(type)]\n            for name in keys(group)\n                if !endswith(name, \"columns\")\n                    dataset = group[name]\n                    column_dataset = group[_make_column_name(name)]\n                    resolution =\n                        get_resolution(get_decision_model_params(store, model_name))\n                    column_lengths = size(dataset)[2:(end - 1)]\n                    item = HDF5Dataset{length(column_lengths)}(\n                        dataset,\n                        column_dataset,\n                        column_lengths,\n                        resolution,\n                        initial_time,\n                    )\n                    container_key = container_key_lookup[name]\n                    getfield(get_dm_data(store)[model_name], type)[container_key] = item\n                    add_output_cache!(\n                        store.cache,\n                        model_name,\n                        container_key,\n                        CacheFlushRule(),\n                    )\n                end\n            end\n        end\n\n        store.optimizer_stats_datasets[model_name] = problem_group[OPTIMIZER_STATS_PATH]\n        store.optimizer_stats_write_index[model_name] = 1\n    end\n\n    em_group = _get_emulation_model_path(store)\n    # Fall back on old key for backwards compatibility\n    horizon_count = HDF5.read(\n        if haskey(HDF5.attributes(em_group), \"horizon_count\")\n            HDF5.attributes(em_group)[\"horizon_count\"]\n        else\n            HDF5.attributes(em_group)[\"horizon\"]\n        end)\n    model_name = Symbol(HDF5.read(HDF5.attributes(em_group)[\"name\"]))\n    resolution = Dates.Millisecond(HDF5.read(HDF5.attributes(em_group)[\"resolution_ms\"]))\n    store.params.emulation_model_params[model_name] = ModelStoreParams(\n        HDF5.read(HDF5.attributes(em_group)[\"num_executions\"]),\n        horizon_count,\n        Dates.Millisecond(HDF5.read(HDF5.attributes(em_group)[\"interval_ms\"])),\n        resolution,\n        HDF5.read(HDF5.attributes(em_group)[\"base_power\"]),\n        Base.UUID(HDF5.read(HDF5.attributes(em_group)[\"system_uuid\"])),\n    )\n    for type in STORE_CONTAINERS\n        group = em_group[string(type)]\n        for name in keys(group)\n            if !endswith(name, \"columns\")\n                dataset = group[name]\n                column_dataset = group[_make_column_name(name)]\n                column_lengths = size(dataset)[2:end]\n                item = HDF5Dataset{length(column_lengths)}(\n                    dataset,\n                    column_dataset,\n                    column_lengths,\n                    resolution,\n                    initial_time,\n                )\n                container_key = container_key_lookup[name]\n                getfield(store.em_data, type)[container_key] = item\n                add_output_cache!(store.cache, model_name, container_key, CacheFlushRule())\n            end\n        end\n    end\n    # TODO: optimizer stats are not being written for EM.\n\n    @debug \"deserialized store params and datasets\" store.params\nend\n\nfunction _serialize_attributes(store::HdfSimulationStore)\n    params = store.params\n    group = store.file[\"simulation\"]\n    HDF5.attributes(group)[\"problem_order\"] =\n        [string(k) for k in keys(params.decision_models_params)]\n    HDF5.attributes(group)[\"initial_time\"] = string(params.initial_time)\n    HDF5.attributes(group)[\"step_resolution_ms\"] =\n        Dates.Millisecond(params.step_resolution).value\n    HDF5.attributes(group)[\"num_steps\"] = params.num_steps\n\n    for problem in keys(params.decision_models_params)\n        problem_group = store.file[\"simulation/decision_models/$problem\"]\n        HDF5.attributes(problem_group)[\"num_executions\"] =\n            params.decision_models_params[problem].num_executions\n        HDF5.attributes(problem_group)[\"horizon_count\"] =\n            params.decision_models_params[problem].horizon_count\n        HDF5.attributes(problem_group)[\"resolution_ms\"] =\n            Dates.Millisecond(params.decision_models_params[problem].resolution).value\n        HDF5.attributes(problem_group)[\"interval_ms\"] =\n            Dates.Millisecond(params.decision_models_params[problem].interval).value\n        HDF5.attributes(problem_group)[\"base_power\"] =\n            params.decision_models_params[problem].base_power\n        HDF5.attributes(problem_group)[\"system_uuid\"] =\n            string(params.decision_models_params[problem].system_uuid)\n    end\n\n    if !isempty(params.emulation_model_params)\n        em_params = first(values(params.emulation_model_params))\n        emulation_group = store.file[\"simulation/emulation_model\"]\n        HDF5.attributes(emulation_group)[\"name\"] =\n            string(first(keys(params.emulation_model_params)))\n        HDF5.attributes(emulation_group)[\"num_executions\"] = em_params.num_executions\n        HDF5.attributes(emulation_group)[\"horizon_count\"] = em_params.horizon_count\n        HDF5.attributes(emulation_group)[\"resolution_ms\"] =\n            Dates.Millisecond(em_params.resolution).value\n        HDF5.attributes(emulation_group)[\"interval_ms\"] =\n            Dates.Millisecond(em_params.interval).value\n        HDF5.attributes(emulation_group)[\"base_power\"] = em_params.base_power\n        HDF5.attributes(emulation_group)[\"system_uuid\"] = string(em_params.system_uuid)\n    end\n    return\nend\n\nfunction _flush_data!(\n    cache::OptimizationOutputCache,\n    store::HdfSimulationStore,\n    model_name,\n    key::OptimizationContainerKey,\n    discard,\n)\n    return _flush_data!(cache, store, OptimizationResultCacheKey(model_name, key), discard)\nend\n\nfunction _flush_data!(\n    cache::OptimizationOutputCache,\n    store::HdfSimulationStore,\n    cache_key::OptimizationResultCacheKey,\n    discard::Bool,\n)\n    !has_dirty(cache) && return 0\n    dataset = _get_dm_dataset(store, cache_key)\n    timestamps, data = get_dirty_data_to_flush!(cache)\n    num_results = length(timestamps)\n    @assert_op num_results == size(data)[end]\n    end_index = dataset.write_index + length(timestamps) - 1\n    write_range = (dataset.write_index):end_index\n    # Enable only for development and benchmarking\n    #TimerOutputs.@timeit RUN_SIMULATION_TIMER \"Write $(key.key) array to HDF\" begin\n    _write_dataset!(dataset.values, data, write_range)\n    #end\n\n    discard && discard_results!(cache, timestamps)\n\n    dataset.write_index += num_results\n    size_flushed = cache.size_per_entry * num_results\n\n    @debug \"Flushed cache results to HDF5\" LOG_GROUP_SIMULATION_STORE cache_key size_flushed num_results get_size(\n        store.cache,\n    )\n    return size_flushed\nend\n\nfunction _get_dataset(::Type{OptimizerStats}, store::HdfSimulationStore, model_name)\n    return store.optimizer_stats_datasets[model_name]\nend\n\nfunction _get_dataset(::Type{OptimizerStats}, store::HdfSimulationStore)\n    return store.optimizer_stats_datasets\nend\n\nfunction _get_em_dataset(store::HdfSimulationStore, key::OptimizationContainerKey)\n    return getfield(get_em_data(store), get_store_container_type(key))[key]\nend\n\nfunction _get_dm_dataset(store::HdfSimulationStore, model_name::Symbol)\n    return get_dm_data(store)[model_name]\nend\n\nfunction _get_dm_dataset(\n    store::HdfSimulationStore,\n    model_name::Symbol,\n    key::OptimizationContainerKey,\n)\n    return getfield(get_dm_data(store)[model_name], get_store_container_type(key))[key]\nend\n\nfunction _get_dm_dataset(store::HdfSimulationStore, key::OptimizationResultCacheKey)\n    return _get_dm_dataset(store, key.model, key.key)\nend\n\nfunction _get_group_or_create(parent, group_name)\n    if haskey(parent, group_name)\n        group = parent[group_name]\n    else\n        group = HDF5.create_group(parent, group_name)\n        @debug \"Created group\" group\n    end\n\n    return group\nend\n\n_make_column_name(name) = string(name) * \"__columns\"\n\nfunction _get_indices(store::HdfSimulationStore, model_name::Symbol, timestamp)\n    time_diff = Dates.Millisecond(timestamp - store.params.initial_time)\n    step = time_diff ÷ store.params.step_resolution + 1\n    if step > store.params.num_steps\n        throw(\n            ArgumentError(\"timestamp = $timestamp is beyond the simulation: step = $step\"),\n        )\n    end\n    problem_params = store.params.decision_models_params[model_name]\n    initial_time = store.params.initial_time + (step - 1) * store.params.step_resolution\n    time_diff = timestamp - initial_time\n    if time_diff % problem_params.interval != Dates.Millisecond(0)\n        throw(ArgumentError(\"timestamp = $timestamp is not a valid problem timestamp\"))\n    end\n    execution_index = time_diff ÷ problem_params.interval + 1\n    return step, execution_index\nend\n\n_get_root(store::HdfSimulationStore) = store.file[HDF_SIMULATION_ROOT_PATH]\n_get_emulation_model_path(store::HdfSimulationStore) = store.file[EMULATION_MODEL_PATH]\n\nfunction _read_column_names(::Type{OptimizerStats}, store::HdfSimulationStore)\n    dataset = _get_dataset(OptimizerStats, store)\n    return HDF5.read(HDF5.attributes(dataset), \"columns\")\nend\n\nfunction _read_data_columns(\n    store::HdfSimulationStore,\n    model_name::Symbol,\n    key::OptimizationContainerKey,\n    index::DecisionModelIndexType,\n)\n    if is_cached(store.cache, model_name, key, index)\n        data = read_result(store.cache, model_name, key, index)\n        column_dataset = _get_dm_dataset(store, model_name, key).column_dataset\n        if ndims(column_dataset) == 1\n            columns = (column_dataset[:],)\n        elseif ndims(column_dataset) == 2\n            columns = (column_dataset[:, 1], column_dataset[:, 2])\n        else\n            error(\"Datasets with $(ndims(column_dataset)) columns not supported\")\n        end\n    else\n        data, columns = _read_result(store, model_name, key, index)\n    end\n    return data, columns\nend\n\nfunction _read_data_columns(\n    store::HdfSimulationStore,\n    model_name::Symbol,\n    key::OptimizationContainerKey,\n    index::EmulationModelIndexType,\n)\n    # TODO: Enable once the cache is in use for em_data\n    #if is_cached(store.cache, model_name, key, index)\n    # data = read_result(store.cache, model_name, key, index)\n    #columns = _get_em_dataset(store, model_name, key).column_dataset[:]\n    #else\n    #    data, columns = _read_result(store, model_name, key, index)\n    #end\n\n    return _read_result(store, model_name, key, index)\nend\n\nfunction _read_length(::Type{OptimizerStats}, store::HdfSimulationStore)\n    dataset = _get_dataset(OptimizerStats, store)\n    return HDF5.read(HDF5.attributes(dataset), \"columns\")\nend\n\n# Specific data set writing function that writes decision model data. It dispatches on the index type of the dataset as a range\nfunction _write_dataset!(\n    dataset::HDF5.Dataset,\n    array::Array{Float64, 3},\n    row_range::UnitRange{Int64},\n)\n    dataset[:, :, row_range] = array\n    @debug \"wrote dm dataset\" dataset row_range\n    return\nend\n\nfunction _write_dataset!(\n    dataset::HDF5.Dataset,\n    array::Array{Float64, 4},\n    row_range::UnitRange{Int64},\n)\n    dataset[:, :, :, row_range] = array\n    @debug \"wrote dm dataset\" dataset row_range\n    return\nend\n\n# Specific data set writing function that writes emulation model data. It dispatches on the index type of the dataset\nfunction _write_dataset!(\n    dataset::HDF5.Dataset,\n    array::Vector{Float64},\n    index::EmulationModelIndexType,\n)\n    assign_maybe_broadcast!(dataset, array, (index,))\n    @debug \"wrote em dataset\" dataset index\n    return\nend\n\nfunction _write_dataset!(\n    dataset::HDF5.Dataset,\n    array::Matrix{Float64},\n    index::EmulationModelIndexType,\n)\n    dataset[index, :, :] = array\n    @debug \"wrote em dataset\" dataset index\n    return\nend\n\nfunction _write_dataset!(\n    dataset::HDF5.Dataset,\n    array::Array{Float64, 3},\n    index::EmulationModelIndexType,\n)\n    dataset[index, :, :, :] = array\n    @debug \"wrote em dataset\" dataset index\n    return\nend\n\n# TODO DT: this looked wrong. Was it tested?\nfunction _write_dataset!(\n    dataset::HDF5.Dataset,\n    array::Array{Float64, 4},\n    index::EmulationModelIndexType,\n)\n    dataset[index, :, :, :] = array\n    @debug \"wrote em dataset\" dataset index\n    return\nend\n"
  },
  {
    "path": "src/simulation/in_memory_simulation_store.jl",
    "content": "\"\"\"\nStores simulation data in memory\n\"\"\"\nmutable struct InMemorySimulationStore <: SimulationStore\n    params::SimulationStoreParams\n    dm_data::OrderedDict{Symbol, DecisionModelStore}\n    em_data::EmulationModelStore\n    container_key_lookup::Dict{String, OptimizationContainerKey}\nend\n\nfunction InMemorySimulationStore()\n    return InMemorySimulationStore(\n        SimulationStoreParams(),\n        OrderedDict{Symbol, DecisionModelStore}(),\n        EmulationModelStore(),\n        Dict{String, OptimizationContainerKey}(),\n    )\nend\n\nfunction get_number_of_dimensions(\n    store::InMemorySimulationStore,\n    i::Type{EmulationModelIndexType},\n    key::OptimizationContainerKey,\n)\n    return length(get_column_names(store, i, key))\nend\n\nfunction get_number_of_dimensions(\n    store::InMemorySimulationStore,\n    i::Type{DecisionModelIndexType},\n    model_name::Symbol,\n    key::OptimizationContainerKey,\n)\n    return length(get_column_names(store, i, model_name, key))\nend\n\nfunction open_store(\n    func::Function,\n    ::Type{InMemorySimulationStore},\n    directory::AbstractString,  # Unused. Need to match the interface.\n    mode = nothing,\n    filename = nothing,\n)\n    store = InMemorySimulationStore()\n    return func(store)\nend\n\nfunction Base.empty!(store::InMemorySimulationStore)\n    for val in values(get_dm_data(store))\n        empty!(val)\n    end\n    empty!(get_em_data(store))\n    @debug \"Emptied the store\" _group = LOG_GROUP_SIMULATION_STORE\n    return\nend\n\nBase.isopen(::InMemorySimulationStore) = true\nBase.close(::InMemorySimulationStore) = nothing\nBase.flush(::InMemorySimulationStore) = nothing\nget_params(store::InMemorySimulationStore) = store.params\n\nfunction get_decision_model_params(store::InMemorySimulationStore, model_name::Symbol)\n    return get_params(store).decision_models_params[model_name]\nend\n\nget_container_key_lookup(store::InMemorySimulationStore) = store.container_key_lookup\n\nlist_decision_models(x::InMemorySimulationStore) = collect(keys(x.dm_data))\nlog_cache_hit_percentages(::InMemorySimulationStore) = nothing\n\nfunction list_decision_model_keys(\n    store::InMemorySimulationStore,\n    model_name::Symbol,\n    container_type::Symbol,\n)\n    return ISOPT.list_fields(\n        _get_model_results(store, model_name),\n        container_type,\n    )\nend\n\nfunction list_emulation_model_keys(store::InMemorySimulationStore, container_type::Symbol)\n    return ISOPT.list_fields(store.em_data, container_type)\nend\n\nfunction write_result!(\n    store::InMemorySimulationStore,\n    model_name::Symbol,\n    key::OptimizationContainerKey,\n    index::DecisionModelIndexType,\n    update_timestamp::Dates.DateTime,\n    array,\n)\n    write_result!(\n        get_dm_data(store)[model_name],\n        model_name,\n        key,\n        index,\n        update_timestamp,\n        array,\n    )\n    return\nend\n\nfunction write_result!(\n    store::InMemorySimulationStore,\n    model_name::Symbol,\n    key::OptimizationContainerKey,\n    index::EmulationModelIndexType,\n    update_timestamp::Dates.DateTime,\n    array,\n)\n    write_result!(get_em_data(store), model_name, key, index, update_timestamp, array)\n    return\nend\n\nfunction read_optimizer_stats(store::InMemorySimulationStore, model_name)\n    # TODO EmulationModel: this interface is TBD\n    return read_optimizer_stats(get_dm_data(store)[model_name])\nend\n\nfunction initialize_problem_storage!(\n    store::InMemorySimulationStore,\n    params::SimulationStoreParams,\n    dm_problem_reqs::Dict{Symbol, SimulationModelStoreRequirements},\n    em_problem_reqs::SimulationModelStoreRequirements,\n    ::CacheFlushRules,\n)\n    store.params = params\n    for problem in keys(store.params.decision_models_params)\n        get_dm_data(store)[problem] = DecisionModelStore()\n        for type in STORE_CONTAINERS\n            for (key, _) in getfield(dm_problem_reqs[problem], type)\n                container = getfield(get_dm_data(store)[problem], type)\n                container[key] = OrderedDict{Dates.DateTime, DenseAxisArray{Float64}}()\n                store.container_key_lookup[encode_key_as_string(key)] = key\n                @debug \"Added $type $key in $problem\" _group = LOG_GROUP_SIMULATION_STORE\n            end\n        end\n    end\n\n    for type in STORE_CONTAINERS\n        for (key, reqs) in getfield(em_problem_reqs, type)\n            container = get_data_field(get_em_data(store), type)\n            container[key] = InMemoryDataset(\n                fill!(\n                    DenseAxisArray{Float64}(undef, reqs[\"columns\"]..., 1:reqs[\"dims\"][1]),\n                    NaN,\n                ),\n            )\n            store.container_key_lookup[encode_key_as_string(key)] = key\n            @debug \"Added $type $key in emulation store\" _group = LOG_GROUP_SIMULATION_STORE\n        end\n    end\n\n    return\nend\n\nfunction get_column_names(\n    store::InMemorySimulationStore,\n    ::Type{DecisionModelIndexType},\n    model_name::Symbol,\n    key::OptimizationContainerKey,\n)\n    return get_column_names(get_dm_data(store)[model_name], key)\nend\n\nfunction get_column_names(\n    store::InMemorySimulationStore,\n    ::Type{EmulationModelIndexType},\n    model_name::Symbol,\n    key::OptimizationContainerKey,\n)\n    return get_column_names(get_em_data(store)[model_name], key)\nend\n\nfunction read_result(\n    ::Type{DenseAxisArray},\n    store::InMemorySimulationStore,\n    model_name::Symbol,\n    key::OptimizationContainerKey,\n    index::DecisionModelIndexType,\n)\n    return read_results(get_dm_data(store)[model_name], key; index = index)\nend\n\nfunction read_result(\n    ::Type{Array},\n    store::InMemorySimulationStore,\n    model_name::Symbol,\n    key::OptimizationContainerKey,\n    index::DecisionModelIndexType,\n)\n    return permutedims(\n        read_results(get_dm_data(store)[model_name], key; index = index).data,\n    )\nend\n\nfunction read_result(\n    ::Type{DenseAxisArray},\n    store::InMemorySimulationStore,\n    ::Symbol,\n    key::OptimizationContainerKey,\n    index::EmulationModelIndexType,\n)\n    return read_results(get_em_data(store), key; index = index)\nend\n\nfunction read_results(\n    store::InMemorySimulationStore,\n    key::OptimizationContainerKey;\n    index::EmulationModelIndexType = nothing,\n    len::Int = nothing,\n)\n    return read_results(get_em_data(store), key; index = index, len = len)\nend\n\nfunction get_emulation_model_dataset_size(\n    store::InMemorySimulationStore,\n    key::OptimizationContainerKey,\n)\n    return get_dataset_size(get_em_data(store), key)[2]\nend\n\n# Note that this function is not type-stable.\nfunction _get_model_results(store::InMemorySimulationStore, model_name::Symbol)\n    if model_name in keys(get_dm_data(store))\n        results = get_dm_data(store)\n    else\n        # TODO EmulationModel: this interface is TBD\n        error(\"model name $model_name is not stored\")\n    end\n\n    return results[model_name]\nend\n\nfunction write_optimizer_stats!(\n    store::InMemorySimulationStore,\n    model::DecisionModel,\n    index::DecisionModelIndexType,\n)\n    stats = get_optimizer_stats(model)\n    dm_data = get_dm_data(store)\n    write_optimizer_stats!(dm_data[get_name(model)], stats, index)\n    read_optimizer_stats(dm_data[get_name(model)])\n    return\nend\n\nfunction write_optimizer_stats!(\n    store::InMemorySimulationStore,\n    model::EmulationModel,\n    index::EmulationModelIndexType,\n)\n    stats = get_optimizer_stats(model)\n    em_data = get_em_data(store)\n    write_optimizer_stats!(em_data, stats, index)\n    return\nend\n\nserialize_system!(::InMemorySimulationStore, ::PSY.System) = nothing\nwrite_system_json!(::InMemorySimulationStore, ::String, ::String) = nothing\n"
  },
  {
    "path": "src/simulation/initial_condition_update_simulation.jl",
    "content": "function update_initial_conditions!(\n    model::OperationModel,\n    state::SimulationState,\n    ::InterProblemChronology,\n)\n    for key in keys(get_initial_conditions(model))\n        update_initial_conditions!(model, key, state)\n    end\n    return\nend\n\nfunction update_initial_conditions!(\n    ::OperationModel,\n    ::SimulationState,\n    ::IntraProblemChronology,\n)\n    #for key in keys(get_initial_conditions(model))\n    #    update_initial_conditions!(model, key, state)\n    #end\n    error(\"Not Implemented yet\")\n    return\nend\n\nfunction update_initial_conditions!(\n    ics::T,\n    state::SimulationState,\n    model_resolution::Dates.Millisecond,\n) where {\n    T <: Union{\n        Vector{\n            Union{\n                InitialCondition{InitialTimeDurationOn, Nothing},\n                InitialCondition{InitialTimeDurationOn, Float64},\n            },\n        },\n        Vector{\n            Union{\n                InitialCondition{InitialTimeDurationOn, Nothing},\n                InitialCondition{InitialTimeDurationOn, JuMP.VariableRef},\n            },\n        },\n    },\n}\n    for ic in ics\n        var_val = get_system_state_value(state, TimeDurationOn(), get_component_type(ic))\n        state_resolution = get_data_resolution(\n            get_system_state_data(state, TimeDurationOn(), get_component_type(ic)),\n        )\n        # The state data is stored in the state resolution (i.e. lowest resolution among all models)\n        # so this step scales the data to the model resolution.\n        val = var_val[get_component_name(ic)] / (model_resolution / state_resolution)\n        set_ic_quantity!(ic, val)\n    end\n    return\nend\n\nfunction update_initial_conditions!(\n    ics::T,\n    state::SimulationState,\n    model_resolution::Dates.Millisecond,\n) where {\n    T <: Union{\n        Vector{\n            Union{\n                InitialCondition{InitialTimeDurationOff, Nothing},\n                InitialCondition{InitialTimeDurationOff, Float64},\n            },\n        },\n        Vector{\n            Union{\n                InitialCondition{InitialTimeDurationOff, Nothing},\n                InitialCondition{InitialTimeDurationOff, JuMP.VariableRef},\n            },\n        },\n    },\n}\n    for ic in ics\n        isnothing(get_value(ic)) && continue\n        var_val = get_system_state_value(state, TimeDurationOff(), get_component_type(ic))\n        state_resolution = get_data_resolution(\n            get_system_state_data(state, TimeDurationOff(), get_component_type(ic)),\n        )\n        # The state data is stored in the state resolution (i.e. lowest resolution among all models)\n        # so this step scales the data to the model resolution.\n        val = var_val[get_component_name(ic)] / (model_resolution / state_resolution)\n        set_ic_quantity!(ic, val)\n    end\n    return\nend\n\nfunction update_initial_conditions!(\n    ics::T,\n    state::SimulationState,\n    ::Dates.Millisecond,\n) where {\n    T <: Union{\n        Vector{\n            Union{\n                InitialCondition{DevicePower, Nothing},\n                InitialCondition{DevicePower, Float64},\n            },\n        },\n        Vector{\n            Union{\n                InitialCondition{DevicePower, Nothing},\n                InitialCondition{DevicePower, JuMP.VariableRef},\n            },\n        },\n    },\n}\n    for ic in ics\n        comp_name = get_component_name(ic)\n        comp_type = get_component_type(ic)\n        comp = get_component(ic)\n        if hasmethod(PSY.get_must_run, Tuple{comp_type}) && PSY.get_must_run(comp)\n            status_val = 1.0\n        else\n            status_val = get_system_state_value(state, OnVariable(), comp_type)[comp_name]\n        end\n        var_val = get_system_state_value(state, ActivePowerVariable(), comp_type)[comp_name]\n        if !isapprox(status_val, 0.0; atol = ABSOLUTE_TOLERANCE)\n            min = PSY.get_active_power_limits(comp).min\n            max = PSY.get_active_power_limits(comp).max\n            if var_val <= max && var_val >= min\n                set_ic_quantity!(ic, var_val)\n            elseif isapprox(min - var_val, 0.0; atol = ABSOLUTE_TOLERANCE)\n                set_ic_quantity!(ic, min)\n            elseif isapprox(var_val - max, 0.0; atol = ABSOLUTE_TOLERANCE)\n                set_ic_quantity!(ic, max)\n            else\n                error(\"Variable value $(var_val) for ActivePowerVariable \\\\\n                      Status value $(status_val) for OnVariable \\\\\n                      $(comp_type)-$(comp_name) is out of bounds [$(min), $(max)].\")\n            end\n        else\n            if !isapprox(var_val, 0.0; atol = ABSOLUTE_TOLERANCE)\n                error(\"Status and Power variables don't match for $comp_name. \\\\\n                ActivePowerVariable: $(var_val)\\\\\n                Status value: $(status_val) for OnVariable\")\n            end\n            set_ic_quantity!(ic, 0.0)\n        end\n    end\n    return\nend\n\nfunction update_initial_conditions!(\n    ics::T,\n    state::SimulationState,\n    ::Dates.Millisecond,\n) where {\n    T <: Union{\n        Vector{\n            Union{\n                InitialCondition{DeviceStatus, Nothing},\n                InitialCondition{DeviceStatus, Float64},\n            },\n        },\n        Vector{\n            Union{\n                InitialCondition{DeviceStatus, Nothing},\n                InitialCondition{DeviceStatus, JuMP.VariableRef},\n            },\n        },\n    },\n}\n    for ic in ics\n        isnothing(get_value(ic)) && continue\n        var_val = get_system_state_value(state, OnVariable(), get_component_type(ic))\n        set_ic_quantity!(ic, var_val[get_component_name(ic)])\n    end\n    return\nend\n\nfunction update_initial_conditions!(\n    ics::T,\n    state::SimulationState,\n    ::Dates.Millisecond,\n) where {\n    T <: Union{\n        Vector{\n            Union{\n                InitialCondition{DeviceAboveMinPower, Nothing},\n                InitialCondition{DeviceAboveMinPower, Float64},\n            },\n        },\n        Vector{\n            Union{\n                InitialCondition{DeviceAboveMinPower, Nothing},\n                InitialCondition{DeviceAboveMinPower, JuMP.VariableRef},\n            },\n        },\n    },\n}\n    for ic in ics\n        var_val = get_system_state_value(\n            state,\n            PowerAboveMinimumVariable(),\n            get_component_type(ic),\n        )\n        set_ic_quantity!(ic, var_val[get_component_name(ic)])\n    end\n    return\nend\n\nfunction update_initial_conditions!(\n    ics::T,\n    state::SimulationState,\n    ::Dates.Millisecond,\n) where {\n    T <: Union{\n        Vector{\n            Union{\n                InitialCondition{InitialEnergyLevel, Nothing},\n                InitialCondition{InitialEnergyLevel, Float64},\n            },\n        },\n        Vector{\n            Union{\n                InitialCondition{InitialEnergyLevel, Nothing},\n                InitialCondition{InitialEnergyLevel, JuMP.VariableRef},\n            },\n        },\n    },\n}\n    for ic in ics\n        var_val = get_system_state_value(state, EnergyVariable(), get_component_type(ic))\n        set_ic_quantity!(ic, var_val[get_component_name(ic)])\n    end\n    return\nend\n"
  },
  {
    "path": "src/simulation/optimization_output_cache.jl",
    "content": "\"\"\"\nCache for a single parameter/variable/dual.\nStores arrays chronologically by simulation timestamp.\n\"\"\"\nmutable struct OptimizationOutputCache\n    key::OptimizationResultCacheKey\n    \"Contains both clean and dirty entries. Any key in data that is earlier than the first\n    dirty timestamp must be clean.\"\n    data::OrderedDict{Dates.DateTime, Array}\n    \"Oldest entry is first\"\n    dirty_timestamps::Deque{Dates.DateTime}\n    stats::CacheStats\n    size_per_entry::Int\n    flush_rule::CacheFlushRule\nend\n\nfunction OptimizationOutputCache(key, flush_rule)\n    return OptimizationOutputCache(\n        key,\n        OrderedDict{Dates.DateTime, Array}(),\n        Deque{Dates.DateTime}(),\n        CacheStats(),\n        0,\n        flush_rule,\n    )\nend\n\nBase.length(x::OptimizationOutputCache) = length(x.data)\nget_cache_hit_percentage(x::OptimizationOutputCache) = get_cache_hit_percentage(x.stats)\nget_size(x::OptimizationOutputCache) = length(x) * x.size_per_entry\nhas_clean(x::OptimizationOutputCache) =\n    !isempty(x.data) && !is_dirty(x, first(keys(x.data)))\nhas_dirty(x::OptimizationOutputCache) = !isempty(x.dirty_timestamps)\nshould_keep_in_cache(x::OptimizationOutputCache) = x.flush_rule.keep_in_cache\n\nfunction get_dirty_size(cache::OptimizationOutputCache)\n    return length(cache.dirty_timestamps) * cache.size_per_entry\nend\n\nfunction is_dirty(cache::OptimizationOutputCache, timestamp)\n    isempty(cache.dirty_timestamps) && return false\n    return timestamp >= first(cache.dirty_timestamps)\nend\n\n\"\"\"\n    Base.empty!(cache::OptimizationOutputCache)\n\nEmpty the [`OptimizationOutputCache`](@ref)\n\"\"\"\nfunction Base.empty!(cache::OptimizationOutputCache)\n    @assert isempty(cache.dirty_timestamps) \"dirty cache was still present $(cache.key) $(cache.dirty_timestamps)\"\n    empty!(cache.data)\n    cache.size_per_entry = 0\n    return\nend\n\n\"\"\"\nAdd result to the cache.\n\"\"\"\nfunction add_result!(cache::OptimizationOutputCache,\n    timestamp::Dates.DateTime,\n    array::Array{Float64},\n    system_cache_is_full::Bool)\n    if cache.size_per_entry == 0\n        cache.size_per_entry = length(array) * sizeof(first(array))\n    end\n\n    @debug \"add_result!\" cache.key timestamp get_size(cache)\n    if haskey(cache.data, timestamp)\n        throw(IS.InvalidValue(\"$timestamp is already stored in $(cache.key)\"))\n    end\n\n    # Note that we buffer all writes in cache until we reach the flush size.\n    # The entries using \"should_keep_in_cache\" can grow quite large for read caching.\n    if system_cache_is_full && should_keep_in_cache(cache)\n        if has_clean(cache)\n            popfirst!(cache.data)\n            @debug \"replaced cache entry\" LOG_GROUP_SIMULATION_STORE cache.key length(\n                cache.data,\n            )\n        end\n    end\n\n    _add_result!(cache, timestamp, array)\n    return cache.size_per_entry\nend\n\nfunction _add_result!(\n    cache::OptimizationOutputCache,\n    timestamp::Dates.DateTime,\n    data::Array{Float64},\n)\n    cache.data[timestamp] = data\n    push!(cache.dirty_timestamps, timestamp)\n    return\nend\n\nfunction discard_results!(cache::OptimizationOutputCache, timestamps)\n    for timestamp in timestamps\n        pop!(cache.data, timestamp)\n    end\n\n    @debug \"Removed $(first(timestamps)) - $(last(timestamps)) from cache\" cache.key\n    return\nend\n\n\"\"\"\nReturn all dirty data from the cache. Mark the timestamps as clean.\n\"\"\"\nfunction get_dirty_data_to_flush!(cache::OptimizationOutputCache)\n    timestamps = [x for x in cache.dirty_timestamps]\n    empty!(cache.dirty_timestamps)\n    # Uncomment for performance testing of CacheFlush\n    #TimerOutputs.@timeit RUN_SIMULATION_TIMER \"Concatenate arrays for flush\" begin\n    temp = cache.data[first(timestamps)]\n    sd = collect(size(temp))\n    push!(sd, length(timestamps))\n    arrays = Array{Float64}(undef, sd...)\n    for (ix, x) in enumerate(timestamps)\n        temp_data = cache.data[x]\n        if ndims(temp_data) == 1\n            arrays[:, ix] = temp_data\n        elseif ndims(temp_data) == 2\n            arrays[:, :, ix] = temp_data\n        elseif ndims(temp_data) == 3\n            arrays[:, :, :, ix] = temp_data\n        else\n            error(\"Arrays of dimensions $(ndims(temp_data)) are not supported\")\n        end\n    end\n\n    return timestamps, arrays\nend\n\nfunction has_timestamp(cache::OptimizationOutputCache, timestamp)\n    present = haskey(cache.data, timestamp)\n    if present\n        cache.stats.hits += 1\n    else\n        cache.stats.misses += 1\n    end\n\n    return present\nend\n"
  },
  {
    "path": "src/simulation/optimization_output_caches.jl",
    "content": "\"\"\"\nCache for all model results\n\"\"\"\nstruct OptimizationOutputCaches\n    data::Dict{OptimizationResultCacheKey, OptimizationOutputCache}\n    max_size::Int\n    min_flush_size::Int\nend\n\nfunction OptimizationOutputCaches()\n    return OptimizationOutputCaches(\n        Dict{OptimizationResultCacheKey, OptimizationOutputCache}(),\n        0,\n        0,\n    )\nend\n\nfunction OptimizationOutputCaches(rules::CacheFlushRules)\n    return OptimizationOutputCaches(\n        Dict{OptimizationResultCacheKey, OptimizationOutputCache}(),\n        rules.max_size,\n        rules.min_flush_size,\n    )\nend\n\n\"\"\"\n    Base.empty!(cache::OptimizationOutputCaches)\n\nEmpty the [`OptimizationOutputCaches`](@ref)\n\"\"\"\nfunction Base.empty!(cache::OptimizationOutputCaches)\n    for output_cache in values(cache.data)\n        empty!(output_cache)\n    end\nend\n\n# PERF: incremental improvement if we manually keep track of the current size.\n# Could be prone to bugs if we miss a change.\n# The number of containers is not expected to be more than 100.\n\n#decrement_size!(cache::OptimizationOutputCaches, size) = cache.size -= size\n#increment_size!(cache::OptimizationOutputCaches, size) = cache.size += size\n\nget_max_size(cache::OptimizationOutputCaches) = cache.max_size\nget_min_flush_size(cache::OptimizationOutputCaches) = cache.min_flush_size\nget_size(cache::OptimizationOutputCaches) =\n    reduce(+, (get_size(x) for x in values(cache.data)))\n\n# Leave some buffer because we may slightly exceed the limit.\nis_full(cache::OptimizationOutputCaches, cur_size) = cur_size >= cache.max_size * 0.95\n\nfunction add_output_cache!(cache::OptimizationOutputCaches, model_name, key, flush_rule)\n    cache_key = OptimizationResultCacheKey(model_name, key)\n    cache.data[cache_key] = OptimizationOutputCache(cache_key, flush_rule)\n    @debug \"Added cache container for\" LOG_GROUP_SIMULATION_STORE model_name key flush_rule\n    return\nend\n\n\"\"\"\nReturn true if the cache has data that has not been flushed to storage.\n\"\"\"\nfunction has_dirty(cache::OptimizationOutputCaches)\n    for output_cache in values(cache.data)\n        if has_dirty(output_cache)\n            return true\n        end\n    end\n\n    return false\nend\n\nget_output_cache(cache::OptimizationOutputCaches, key::OptimizationResultCacheKey) =\n    cache.data[key]\n\nfunction get_output_cache(\n    cache::OptimizationOutputCaches,\n    model_name,\n    key::OptimizationContainerKey,\n)\n    cache_key = OptimizationResultCacheKey(model_name, key)\n    return get_output_cache(cache, cache_key)\nend\n\n\"\"\"\nReturn true if the data for `timestamp` is stored in cache.\n\"\"\"\nfunction is_cached(cache::OptimizationOutputCaches, model_name, key, index)\n    cache_key = OptimizationResultCacheKey(model_name, key)\n    return is_cached(cache, cache_key, index)\nend\n\nis_cached(cache::OptimizationOutputCaches, key, timestamp::Dates.DateTime) =\n    has_timestamp(cache.data[key], timestamp::Dates.DateTime)\n\nis_cached(cache::OptimizationOutputCaches, key, ::Int) = false\n\n\"\"\"\nLog the cache hit percentages for all caches.\n\"\"\"\nfunction log_cache_hit_percentages(cache::OptimizationOutputCaches)\n    for key in keys(cache.data)\n        output_cache = cache.data[key]\n        cache_hit_pecentage = get_cache_hit_percentage(output_cache)\n        @debug \"Cache stats\" LOG_GROUP_SIMULATION_STORE key cache_hit_pecentage\n    end\n    return\nend\n\n\"\"\"\nRead the result from cache. Callers must first call [`is_cached`](@ref) to check if the\ntimestamp is present.\n\"\"\"\nfunction read_result(cache::OptimizationOutputCaches, model_name, key, timestamp)\n    cache_key = OptimizationResultCacheKey(model_name, key)\n    return read_result(cache, cache_key, timestamp)\nend\n\nread_result(cache::OptimizationOutputCaches, key, timestamp) =\n    cache.data[key].data[timestamp]\n"
  },
  {
    "path": "src/simulation/realized_meta.jl",
    "content": "struct RealizedMeta\n    start_time::Dates.DateTime\n    resolution::Dates.TimePeriod\n    len::Int\n    start_offset::Int\n    end_offset::Int\n    interval_len::Int\n    realized_timestamps::AbstractVector{Dates.DateTime}\nend\n\nfunction RealizedMeta(\n    res::SimulationProblemResults;\n    start_time::Union{Nothing, Dates.DateTime} = nothing,\n    len::Union{Int, Nothing} = nothing,\n)\n    existing_timestamps = get_timestamps(res)\n    interval = existing_timestamps.step\n    resolution = get_resolution(res)\n    interval_len = Int(interval / resolution)\n    realized_timestamps = get_realized_timestamps(res; start_time = start_time, len = len)\n\n    result_start_time = existing_timestamps[findlast(\n        x -> x .<= first(realized_timestamps),\n        existing_timestamps,\n    )]\n    result_end_time = existing_timestamps[findlast(\n        x -> x .<= last(realized_timestamps),\n        existing_timestamps,\n    )]\n\n    len = length(result_start_time:interval:result_end_time)\n\n    start_offset = length(result_start_time:resolution:first(realized_timestamps))\n    end_offset = length(\n        (last(realized_timestamps) + resolution):resolution:(result_end_time + interval - resolution),\n    )\n\n    return RealizedMeta(\n        result_start_time,\n        resolution,\n        len,\n        start_offset,\n        end_offset,\n        interval_len,\n        realized_timestamps,\n    )\nend\n\nfunction _make_dataframe(\n    results_by_time::ResultsByTime{DataFrame, N},\n    num_timestamps::Int,\n    meta::RealizedMeta,\n    key::OptimizationContainerKey,\n    ::Val{TableFormat.LONG},\n) where {N}\n    @assert !isempty(results_by_time)\n    row_index = 1\n    dfs = DataFrame[]\n    first_cols = names(first(values(results_by_time.data)))\n    for (step, (_, df)) in enumerate(results_by_time)\n        if step > 1 && names(df) != first_cols\n            error(\"Mismatched columns. First df = $(first_cols), other df = $(names(df))\")\n        end\n        first_id = step > 1 ? 1 : meta.start_offset\n        last_id =\n            step == meta.len ? meta.interval_len - meta.end_offset : meta.interval_len\n        if last_id - first_id > DataFrames.nrow(df)\n            error(\n                \"Variable $(encode_key_as_string(key)) has $(DataFrames.nrow(df)) number of steps, that is different than the default problem horizon. \\\n            Can't calculate the realized variables. Use `read_variables` instead and write your own concatenation\",\n            )\n        end\n        offset = (step - 1) * meta.interval_len\n        df2 = @chain df begin\n            @subset(first_id .<= :time_index .<= last_id)\n            @transform(:actual_time_index = :time_index .+ offset)\n            @select(Not(:time_index))\n            @rename(:time_index = :actual_time_index)\n        end\n        push!(dfs, df2)\n        row_index += last_id - first_id + 1\n    end\n\n    combined_df = vcat(dfs...)\n    time_df = DataFrame(;\n        DateTime = meta.realized_timestamps,\n        time_index = (meta.start_offset):(meta.start_offset + length(\n            meta.realized_timestamps,\n        ) - 1),\n    )\n    result_df = @chain begin\n        innerjoin(combined_df, time_df; on = :time_index)\n        @select(:DateTime, Not(:DateTime, :time_index))\n        @orderby(:DateTime)\n    end\n\n    actual_num_timestamps = length(unique(result_df.DateTime))\n    if actual_num_timestamps != num_timestamps\n        error(\n            \"Mismatched number of timestamps. Expected $(num_timestamps), got $actual_num_timestamps\",\n        )\n    end\n\n    return result_df\nend\n\nfunction _make_dataframe(\n    results_by_time::ResultsByTime{DataFrame, N},\n    num_timestamps::Int,\n    meta::RealizedMeta,\n    key::OptimizationContainerKey,\n    ::Val{TableFormat.WIDE},\n) where {N}\n    @assert !isempty(results_by_time)\n    row_index = 1\n    dfs = DataFrame[]\n    first_cols = names(first(values(results_by_time.data)))\n    for (step, (_, df)) in enumerate(results_by_time)\n        if step > 1 && names(df) != first_cols\n            error(\"Mismatched columns. First df = $(first_cols), other df = $(names(df))\")\n        end\n        first_id = step > 1 ? 1 : meta.start_offset\n        last_id =\n            step == meta.len ? meta.interval_len - meta.end_offset : meta.interval_len\n        if last_id - first_id > DataFrames.nrow(df)\n            error(\n                \"Variable $(encode_key_as_string(key)) has $(DataFrames.nrow(df)) number of steps, that is different than the default problem horizon. \\\n            Can't calculate the realized variables. Use `read_variables` instead and write your own concatenation\",\n            )\n        end\n        df2 = df[first_id:last_id, :]\n        push!(dfs, df2)\n        row_index += last_id - first_id + 1\n    end\n\n    df = vcat(dfs...)\n    DataFrames.insertcols!(\n        df,\n        1,\n        :DateTime => meta.realized_timestamps,\n    )\n    DataFrames.select!(df, DataFrames.Not(:time_index))\n    if DataFrames.nrow(df) != num_timestamps\n        error(\n            \"Mismatched number of rows. Expected $(num_timestamps), got $(DataFrames.nrow(df))\",\n        )\n    end\n\n    return df\nend\n\nfunction get_realization(\n    results::Dict{OptimizationContainerKey, ResultsByTime{DataFrame}},\n    meta::RealizedMeta;\n    table_format = TableFormat.LONG,\n)\n    realized_values = Dict{OptimizationContainerKey, DataFrames.DataFrame}()\n    lk = ReentrantLock()\n    num_timestamps = length(meta.realized_timestamps)\n    start = time()\n    Threads.@threads for key in collect(keys(results))\n        results_by_time = results[key]\n        lock(lk) do\n            realized_values[key] = _make_dataframe(\n                results_by_time,\n                num_timestamps,\n                meta,\n                key,\n                Val(table_format),\n            )\n        end\n    end\n\n    duration = time() - start\n    if Threads.nthreads() == 1 && duration > 10.0\n        @info \"Time to read results: $duration seconds. You will likely get faster \" *\n              \"results by starting Julia with multiple threads.\"\n    end\n    return realized_values\nend\n"
  },
  {
    "path": "src/simulation/simulation.jl",
    "content": "\"\"\"\n    Simulation(\n        sequence::SimulationSequence,\n        name::String,\n        steps::Int\n        models::SimulationModels,\n        simulation_folder::String,\n        initial_time::Union{Nothing, Dates.DateTime}\n    )\n\nConstruct the `Simulation` structure to run the sequence of decision and emulation models specified.\n\n# Arguments\n\n  - `sequence::SimulationSequence`: Simulation sequence that specify how the decision and emulation models will be executed.\n  - `name::String`: Name of the Simulation\n  - `steps::Int`: Number of steps on which the sequence of models will be executed\n  - `models::SimulationModels`: List of Decision and Emulation Models\n  - `simulation_folder::String`: Folder on which results will be stored\n  - `initial_time::Union{Nothing, Dates.DateTime} = nothing`: Initial time of which the\n    simulation starts. If nothing it will default to the first timestamp of time series of the system.\n\n# Example\n\n```julia\ntemplate_uc = template_unit_commitment()\ntemplate_ed = template_economic_dispatch()\nmy_decision_model_uc = DecisionModel(template_1, sys_uc, optimizer, name = \"UC\")\nmy_decision_model_ed = DecisionModel(template_ed, sys_ed, optimizer, name = \"ED\")\nmodels = SimulationModels(\n    decision_models = [\n        my_decision_model_uc,\n        my_decision_model_ed\n    ]\n)\n# The following sequence set the commitment variables (`OnVariable`) for `ThermalStandard` units from UC to ED.\nsequence = SimulationSequence(;\n    models = models,\n    feedforwards = Dict(\n        \"ED\" => [\n            SemiContinuousFeedforward(;\n                component_type = ThermalStandard,\n                source = OnVariable,\n                affected_values = [ActivePowerVariable],\n            ),\n        ],\n    ),\n)\n\nsim = Simulation(\n    sequence = sequence,\n    name = \"Sim\",\n    steps = 5,\n    models = models,\n    simulation_folder = mktempdir(cleanup=true),\n)\n```\n\"\"\"\nmutable struct Simulation\n    steps::Int\n    models::SimulationModels\n    initial_time::Union{Nothing, Dates.DateTime}\n    sequence::SimulationSequence\n    simulation_folder::String\n    name::String\n    internal::Union{Nothing, SimulationInternal}\n\n    function Simulation(;\n        sequence::SimulationSequence,\n        name::String,\n        steps::Int,\n        models::SimulationModels,\n        simulation_folder::AbstractString,\n        initial_time = nothing,\n    )\n        for model in get_decision_models(models)\n            if get_sequence_uuid(model) != sequence.uuid\n                model_name = get_name(model)\n                throw(\n                    IS.ConflictingInputsError(\n                        \"The decision model definition for $model_name doesn't correspond to the simulation sequence\",\n                    ),\n                )\n            end\n        end\n        em = get_emulation_model(models)\n        if em !== nothing\n            if get_sequence_uuid(em) != sequence.uuid\n                model_name = get_name(em)\n                throw(\n                    IS.ConflictingInputsError(\n                        \"The emulation model definition for $model_name doesn't correspond to the simulation sequence\",\n                    ),\n                )\n            end\n        end\n        new(steps, models, initial_time, sequence, simulation_folder, name, nothing)\n    end\nend\n\n###################### Simulation Accessor Functions ####################\nfunction get_base_powers(sim::Simulation)\n    base_powers = Dict()\n    for model in get_models(sim)\n        base_powers[get_name(model)] = PSY.get_base_power(get_system(model))\n    end\n    return base_powers\nend\n\nget_initial_time(sim::Simulation) = sim.initial_time\nget_sequence(sim::Simulation) = sim.sequence\nget_steps(sim::Simulation) = sim.steps\nget_current_time(sim::Simulation) = get_current_time(get_simulation_state(sim))\nget_simulation_model(s::Simulation, name) = get_simulation_model(get_models(s), name)\nget_models(sim::Simulation) = sim.models\nget_simulation_dir(sim::Simulation) = dirname(sim.internal.logs_dir)\nget_simulation_files_dir(sim::Simulation) = sim.internal.sim_files_dir\nget_simulation_partitions_dir(sim::Simulation) =\n    joinpath(get_simulation_dir(sim), \"simulation_partitions\")\nget_store_dir(sim::Simulation) = sim.internal.store_dir\nget_simulation_status(sim::Simulation) = sim.internal.status\nget_simulation_build_status(sim::Simulation) = sim.internal.build_status\nget_simulation_state(sim::Simulation) = sim.internal.simulation_state\nset_simulation_store!(sim::Simulation, store) = sim.internal.store = store\nget_simulation_store(sim::Simulation) = sim.internal.store\nget_results_dir(sim::Simulation) = sim.internal.results_dir\nget_models_dir(sim::Simulation) = sim.internal.models_dir\n\nget_interval(sim::Simulation, name::Symbol) = get_interval(sim.sequence, name)\n\nfunction get_simulation_time(sim::Simulation, problem_number::Int)\n    return sim.internal.date_ref[problem_number]\nend\n\nget_ini_cond_chronology(sim::Simulation) = get_sequence(sim).ini_cond_chronology\nget_name(sim::Simulation) = sim.name\nget_simulation_folder(sim::Simulation) = sim.simulation_folder\nget_execution_order(sim::Simulation) = get_sequence(sim).execution_order\nget_current_execution_index(sim::Simulation) = get_sequence(sim).current_execution_index\nget_logs_folder(sim::Simulation) = sim.internal.logs_dir\nget_recorder_folder(sim::Simulation) = sim.internal.recorder_dir\nget_console_level(sim::Simulation) = sim.internal.console_level\nget_file_level(sim::Simulation) = sim.internal.file_level\nget_rng(sim::Simulation) = sim.internal.rng\n\nset_simulation_status!(sim::Simulation, status) = sim.internal.status = status\nset_simulation_build_status!(sim::Simulation, status::SimulationBuildStatus) =\n    sim.internal.build_status = status\n\nfunction set_current_time!(sim::Simulation, val::Dates.DateTime)\n    set_current_time!(get_simulation_state(sim), val)\n    return\nend\n\nfunction _get_simulation_initial_times!(sim::Simulation)\n    model_initial_times = OrderedDict{Int, Vector{Dates.DateTime}}()\n    sim_ini_time = get_initial_time(sim)\n    for (model_number, model) in enumerate(get_models(sim).decision_models)\n        system = get_system(model)\n        model_interval = get_interval(get_settings(model))\n        interval_kwarg =\n            model_interval == UNSET_INTERVAL ? (;) : (; interval = model_interval)\n        model_horizon = get_horizon(model)\n        system_horizon = PSY.get_forecast_horizon(system; interval_kwarg...)\n        system_interval = PSY.get_forecast_interval(system; interval_kwarg...)\n        if model_horizon > system_horizon\n            throw(\n                IS.ConflictingInputsError(\n                    \"$(get_name(model)) model horizon: $(Dates.canonicalize(model_horizon)) and forecast horizon: $(Dates.canonicalize(system_horizon)) are not compatible\",\n                ),\n            )\n        end\n        model_initial_times[model_number] =\n            PSY.get_forecast_initial_times(system; interval_kwarg...)\n        for (ix, element) in enumerate(model_initial_times[model_number][1:(end - 1)])\n            if !(element + system_interval == model_initial_times[model_number][ix + 1])\n                throw(\n                    IS.ConflictingInputsError(\n                        \"The sequence of forecasts in the model's systems are invalid\",\n                    ),\n                )\n            end\n        end\n        if sim_ini_time !== nothing &&\n           !mapreduce(x -> x == sim_ini_time, |, model_initial_times[model_number])\n            throw(\n                IS.ConflictingInputsError(\n                    \"The specified simulation initial_time $sim_ini_time isn't contained in model $(get_name(model)).\nManually provided initial times have to be compatible with the specified interval and horizon in the models.\",\n                ),\n            )\n        end\n    end\n    if get_initial_time(sim) === nothing\n        sim.initial_time = model_initial_times[1][1]\n        @debug(\"Initial Simulation timestamp will be infered from the data. \\\\\n               Initial Simulation timestamp set to $(sim.initial_time)\")\n    end\n    if get_models(sim).emulation_model !== nothing\n        em = get_models(sim).emulation_model\n        system = get_system(em)\n        resolution = get_resolution(em)\n        ini_time, ts_length = get_single_time_series_consistency(system, resolution)\n        em_available_times = range(ini_time; step = resolution, length = ts_length)\n        if get_initial_time(sim) ∉ em_available_times\n            throw(\n                IS.ConflictingInputsError(\n                    \"The simulation initial_time $sim_ini_time isn't contained in the\n                    emulation model $(get_name(em)).\",\n                ),\n            )\n        else\n            model_initial_times[length(model_initial_times) + 1] = [sim.initial_time]\n        end\n    end\n    set_current_time!(sim, sim.initial_time)\n    return model_initial_times\nend\n\nfunction _check_steps(\n    sim::Simulation,\n    model_initial_times::OrderedDict{Int, Vector{Dates.DateTime}},\n)\n    sequence = get_sequence(sim)\n    execution_order = get_execution_order(sequence)\n    for (model_number, model) in enumerate(get_models(sim).decision_models)\n        execution_counts = get_executions(model)\n        # Checks the consistency between two methods of calculating the number of executions\n        total_model_executions = length(findall(x -> x == model_number, execution_order))\n        @assert_op total_model_executions == execution_counts\n\n        forecast_count = length(model_initial_times[model_number])\n        if get_steps(sim) * execution_counts > forecast_count\n            throw(\n                IS.ConflictingInputsError(\n                    \"The number of available time series ($(forecast_count)) is not enough to perform the\ndesired amount of simulation steps ($(sim.steps*execution_counts)).\",\n                ),\n            )\n        end\n    end\n    return\nend\n\nfunction _check_folder(sim::Simulation)\n    folder = get_simulation_folder(sim)\n    !isdir(folder) &&\n        throw(IS.ConflictingInputsError(\"Specified folder = $folder is not valid\"))\n    try\n        mkdir(joinpath(folder, \"fake\"))\n        rm(joinpath(folder, \"fake\"))\n    catch e\n        throw(IS.ConflictingInputsError(\"Specified folder does not have write access [$e]\"))\n    end\nend\n\n# Compare initial conditions for all `InitialConditionType`s with the\n# `requires_reconciliation` trait across `models`, log @info messages for mismatches\nfunction _initial_conditions_reconciliation!(\n    models::Vector{<:OperationModel})\n    model_names = get_name.(models)\n    has_mismatches = false\n    @info \"Reconciling initial conditions across models $(join(model_names, \", \"))\"\n    # all_ic_keys: all the `ICKey`s that appear in any of the models\n    all_ic_keys = union(keys.(get_initial_conditions.(models))...)\n    # all_ic_values: Dict{ICKey, Dict{model_index, Dict{component_name, ic_value}}}\n    all_ic_values = Dict()\n    for ic_key in all_ic_keys\n        if !requires_reconciliation(get_entry_type(ic_key))\n            @debug \"Skipping initial conditions reconciliation for $(get_entry_type(ic_key)) due to false requires_reconciliation\"\n            continue\n        end\n        # ic_vals_per_model: Dict{model_index, Dict{component_name, ic_value}}\n        ic_vals_per_model = Dict()\n        for (i, model) in enumerate(models)\n            ics = PSI.get_initial_conditions(model)\n            haskey(ics, ic_key) || continue\n            # ic_vals_per_component: Dict{component_name, ic_value}\n            ic_vals_per_component =\n                Dict(get_name(get_component(ic)) => get_condition(ic) for ic in ics[ic_key])\n            ic_vals_per_model[i] = ic_vals_per_component\n        end\n\n        # Assert that all models have the same components for current ic_key\n        @assert allequal(Set.(keys.(values(ic_vals_per_model)))) \"For IC key $ic_key, not all models have the same components\"\n\n        # For each component in current ic_key, compare values across models\n        component_names = keys(first(values(ic_vals_per_model)))\n        for component_name in component_names\n            all_values = [result[component_name] for result in values(ic_vals_per_model)]\n            ref_value = first(all_values)\n            if !allequal(isapprox.(all_values, ref_value; atol = ABSOLUTE_TOLERANCE))\n                has_mismatches = true\n                mismatch_msg = \"For IC key $ic_key, mismatch on component $component_name:\"\n                for (model_i, result) in sort(pairs(ic_vals_per_model); by = first)\n                    mismatch_msg *= \"\\n\\t$(model_names[model_i]): $(result[component_name])\"\n                end\n                @info mismatch_msg\n            end\n        end\n        all_ic_values[ic_key] = ic_vals_per_model\n    end\n\n    # TODO now that we have found the initial conditions mismatches, we must fix them\n    if has_mismatches\n        @warn \"Models have initial condition mismatches; reconciliation is not yet implemented\"\n    end\n\n    return all_ic_values\nend\n\nfunction _build_single_model_for_simulation(\n    model::DecisionModel,\n    sim::Simulation,\n    model_number::Int,\n)\n    initial_time = get_initial_time(sim)\n    set_initial_time!(model, initial_time)\n    output_dir = joinpath(get_models_dir(sim), string(get_name(model)))\n    mkpath(output_dir)\n    set_output_dir!(model, output_dir)\n    try\n        # TODO-PJ: Temporary while are able to switch from PJ to POI\n        container = get_optimization_container(model)\n        container.built_for_recurrent_solves = true\n        build_impl!(model)\n        sim.internal.date_ref[model_number] = initial_time\n        set_status!(model, ModelBuildStatus.BUILT)\n        _pre_solve_model_checks(model)\n    catch\n        set_status!(model, ModelBuildStatus.FAILED)\n        @error \"Failed to build $(get_name(model))\"\n        rethrow()\n    end\n    return\nend\n\nfunction _build_decision_models!(sim::Simulation)\n    TimerOutputs.@timeit BUILD_PROBLEMS_TIMER \"Build Decision Problems\" begin\n        decision_models = get_decision_models(get_models(sim))\n        #TODO: Re-enable Threads.@threads with proper implementation of the timer.\n        for model_n in 1:length(decision_models)\n            TimerOutputs.@timeit BUILD_PROBLEMS_TIMER \"Problem $(get_name(decision_models[model_n]))\" begin\n                _build_single_model_for_simulation(decision_models[model_n], sim, model_n)\n            end\n        end\n    end\n    _initial_conditions_reconciliation!(get_decision_models(get_models(sim)))\n    return\nend\n\nfunction _build_emulation_model!(sim::Simulation)\n    model = get_emulation_model(get_models(sim))\n\n    if model === nothing\n        return\n    end\n\n    try\n        initial_time = get_initial_time(sim)\n        set_initial_time!(model, initial_time)\n        output_dir = joinpath(get_models_dir(sim), string(get_name(model)))\n        mkpath(output_dir)\n        set_output_dir!(model, output_dir)\n        TimerOutputs.@timeit BUILD_PROBLEMS_TIMER \"Problem Emulation $(get_name(model))\" begin\n            build_impl!(model)\n        end\n        sim.internal.date_ref[length(sim.internal.date_ref) + 1] = initial_time\n        set_status!(model, ModelBuildStatus.BUILT)\n    catch\n        set_status!(model, ModelBuildStatus.FAILED)\n        rethrow()\n    end\n    return\nend\n\nfunction _initialize_simulation_state!(sim::Simulation)\n    step_resolution = get_step_resolution(get_sequence(sim))\n    simulation_models = get_models(sim)\n    initialize_simulation_state!(\n        get_simulation_state(sim),\n        simulation_models,\n        step_resolution,\n        get_initial_time(sim),\n    )\n    return\nend\n\nfunction _get_model_store_requirements!(\n    rules::CacheFlushRules,\n    model::OperationModel,\n    num_rows::Int,\n)\n    model_name = get_name(model)\n    horizon = get_horizon(model)\n    resolution = get_resolution(model)\n    horizon_count = horizon ÷ resolution\n    reqs = SimulationModelStoreRequirements()\n    container = get_optimization_container(model)\n\n    for (key, array) in get_duals(container)\n        !should_write_resulting_value(key) && continue\n        reqs.duals[key] = _calc_dimensions(array, key, num_rows, horizon_count)\n        add_rule!(rules, model_name, key, true)\n    end\n\n    for (key, param_container) in get_parameters(container)\n        !should_write_resulting_value(key) && continue\n        array = get_multiplier_array(param_container)\n        reqs.parameters[key] = _calc_dimensions(array, key, num_rows, horizon_count)\n        add_rule!(rules, model_name, key, false)\n    end\n\n    for (key, array) in get_variables(container)\n        !should_write_resulting_value(key) && continue\n        reqs.variables[key] = _calc_dimensions(array, key, num_rows, horizon_count)\n        add_rule!(rules, model_name, key, true)\n    end\n\n    for (key, array) in get_aux_variables(container)\n        !should_write_resulting_value(key) && continue\n        reqs.aux_variables[key] = _calc_dimensions(array, key, num_rows, horizon_count)\n        add_rule!(rules, model_name, key, true)\n    end\n\n    for (key, array) in get_expressions(container)\n        !should_write_resulting_value(key) && continue\n        reqs.expressions[key] = _calc_dimensions(array, key, num_rows, horizon_count)\n        add_rule!(rules, model_name, key, false)\n    end\n\n    return reqs\nend\n\nfunction _get_emulation_store_requirements(sim::Simulation)\n    sim_state = get_simulation_state(sim)\n    system_state = get_system_states(sim_state)\n    sim_time = get_steps(sim) * get_step_resolution(get_sequence(sim))\n    reqs = SimulationModelStoreRequirements()\n\n    for (key, state_values) in get_duals_values(system_state)\n        !should_write_resulting_value(key) && continue\n        num_time_rows = sim_time ÷ get_data_resolution(state_values)\n        cols = get_column_names(key, state_values)\n        # Use actual data dimensions (excluding last axis which is time) for proper HDF storage\n        data_dims = size(state_values.values)[1:(end - 1)]\n        reqs.duals[key] =\n            Dict(\"columns\" => cols, \"dims\" => (num_time_rows, data_dims...))\n    end\n\n    for (key, state_values) in get_parameters_values(system_state)\n        !should_write_resulting_value(key) && continue\n        num_time_rows = sim_time ÷ get_data_resolution(state_values)\n        cols = get_column_names(key, state_values)\n        # Use actual data dimensions (excluding last axis which is time) for proper HDF storage\n        data_dims = size(state_values.values)[1:(end - 1)]\n        reqs.parameters[key] =\n            Dict(\"columns\" => cols, \"dims\" => (num_time_rows, data_dims...))\n    end\n\n    for (key, state_values) in get_variables_values(system_state)\n        !should_write_resulting_value(key) && continue\n        num_time_rows = sim_time ÷ get_data_resolution(state_values)\n        cols = get_column_names(key, state_values)\n        # Use actual data dimensions (excluding last axis which is time) for proper HDF storage\n        data_dims = size(state_values.values)[1:(end - 1)]\n        reqs.variables[key] =\n            Dict(\"columns\" => cols, \"dims\" => (num_time_rows, data_dims...))\n    end\n\n    for (key, state_values) in get_aux_variables_values(system_state)\n        !should_write_resulting_value(key) && continue\n        num_time_rows = sim_time ÷ get_data_resolution(state_values)\n        cols = get_column_names(key, state_values)\n        # Use actual data dimensions (excluding last axis which is time) for proper HDF storage\n        data_dims = size(state_values.values)[1:(end - 1)]\n        reqs.aux_variables[key] =\n            Dict(\"columns\" => cols, \"dims\" => (num_time_rows, data_dims...))\n    end\n\n    for (key, state_values) in get_expression_values(system_state)\n        !should_write_resulting_value(key) && continue\n        num_time_rows = sim_time ÷ get_data_resolution(state_values)\n        cols = get_column_names(key, state_values)\n        # Use actual data dimensions (excluding last axis which is time) for proper HDF storage\n        data_dims = size(state_values.values)[1:(end - 1)]\n        reqs.expressions[key] =\n            Dict(\"columns\" => cols, \"dims\" => (num_time_rows, data_dims...))\n    end\n    return reqs\nend\n\nfunction _initialize_problem_storage!(\n    sim::Simulation,\n    cache_size_mib::Int = DEFAULT_SIMULATION_STORE_CACHE_SIZE_MiB,\n    min_cache_flush_size_mib::Int = MIN_CACHE_FLUSH_SIZE_MiB,\n)\n    sequence = get_sequence(sim)\n    executions_by_model = sequence.executions_by_model\n    models = get_models(sim)\n    decision_model_store_params = OrderedDict{Symbol, ModelStoreParams}()\n    dm_model_req = Dict{Symbol, SimulationModelStoreRequirements}()\n    rules = CacheFlushRules(;\n        max_size = cache_size_mib * MiB,\n        min_flush_size = trunc(min_cache_flush_size_mib * MiB),\n    )\n    for model in get_decision_models(models)\n        model_name = get_name(model)\n        decision_model_store_params[model_name] = get_store_params(model)\n        num_executions = executions_by_model[model_name]\n        num_rows = num_executions * get_steps(sim)\n        dm_model_req[model_name] = _get_model_store_requirements!(rules, model, num_rows)\n    end\n\n    em = get_emulation_model(models)\n    if em === nothing\n        base_params = last(collect(values(decision_model_store_params)))\n        resolution = minimum([v.resolution for v in values(decision_model_store_params)])\n        emulation_model_store_params = OrderedDict(\n            :Emulator => ModelStoreParams(\n                get_step_resolution(sequence) ÷ resolution, # Num Executions\n                resolution, # Horizon\n                resolution, # Interval\n                resolution, # Resolution\n                get_base_power(base_params),\n                get_system_uuid(base_params),\n            ),\n        )\n    else\n        emulation_model_store_params =\n            OrderedDict(Symbol(get_name(em)) => get_store_params(em))\n    end\n\n    em_model_req = _get_emulation_store_requirements(sim)\n\n    simulation_store_params = SimulationStoreParams(\n        get_initial_time(sim),\n        get_step_resolution(sequence),\n        get_steps(sim),\n        decision_model_store_params,\n        emulation_model_store_params,\n    )\n    @debug \"initialized problem requirements\" simulation_store_params\n    store = get_simulation_store(sim)\n\n    initialize_problem_storage!(\n        store,\n        simulation_store_params,\n        dm_model_req,\n        em_model_req,\n        rules,\n    )\n    return simulation_store_params\nend\n\nfunction _build!(\n    sim::Simulation;\n    store_systems_in_results = true,\n    setup_simulation_partitions = false,\n    partitions = nothing,\n    index = nothing,\n)\n    set_simulation_build_status!(sim, SimulationBuildStatus.IN_PROGRESS)\n    problem_initial_times = _get_simulation_initial_times!(sim)\n    sequence = get_sequence(sim)\n    step_resolution = get_step_resolution(sequence)\n    simulation_models = get_models(sim)\n\n    if !isnothing(partitions) && !isnothing(index)\n        step_range = get_absolute_step_range(partitions, index)\n        sim.initial_time += step_resolution * (step_range.start - 1)\n        set_current_time!(sim.internal.simulation_state, sim.initial_time)\n        sim.steps = length(step_range)\n        @info \"Set parameters for simulation partition\" index sim.initial_time sim.steps\n    end\n\n    for (ix, model) in enumerate(get_decision_models(simulation_models))\n        problem_interval = get_interval(sequence, model)\n        # Note to devs: Here we are setting the number of operations problem executions we\n        # will see for every step of the simulation. The step of the simulation is determined\n        # by the first decision problem interval\n        if ix == 1\n            set_executions!(model, 1)\n        else\n            if step_resolution % problem_interval != Dates.Millisecond(0)\n                error(\n                    \"The $(get_name(model)) problem interval is not an integer fraction of the simulation step\",\n                )\n            end\n            set_executions!(model, Int(step_resolution / problem_interval))\n        end\n    end\n\n    em = get_emulation_model(simulation_models)\n    if em !== nothing\n        em_resolution = get_resolution(em)\n        set_executions!(em, get_steps(sim) * Int(step_resolution / em_resolution))\n    end\n\n    TimerOutputs.@timeit BUILD_PROBLEMS_TIMER \"Check Steps\" begin\n        _check_steps(sim, problem_initial_times)\n    end\n\n    if store_systems_in_results\n        # Spawn system serialization (JSON conversion) in parallel with model builds.\n        # Systems are read-only during building, so this is safe.\n        serialization_task = Threads.@spawn _serialize_systems_to_json(sim)\n    end\n\n    _build_decision_models!(sim)\n    _build_emulation_model!(sim)\n\n    TimerOutputs.@timeit BUILD_PROBLEMS_TIMER \"Initialize Simulation State\" begin\n        _initialize_simulation_state!(sim)\n    end\n\n    TimerOutputs.@timeit BUILD_PROBLEMS_TIMER \"Initialize Problem Storage\" begin\n        open_store(HdfSimulationStore, get_store_dir(sim), \"w\") do store\n            set_simulation_store!(sim, store)\n            try\n                _initialize_problem_storage!(sim)\n                if store_systems_in_results\n                    # Fetch pre-computed JSON from the parallel task and write to HDF5 store.\n                    serialized = fetch(serialization_task)\n                    for (uuid, json_text) in serialized\n                        write_system_json!(store, uuid, json_text)\n                    end\n                end\n            finally\n                set_simulation_store!(sim, nothing)\n            end\n        end\n    end\n\n    if setup_simulation_partitions\n        TimerOutputs.@timeit BUILD_PROBLEMS_TIMER \"Setup Simulation Partition\" begin\n            _setup_simulation_partitions(sim)\n        end\n    end\n\n    return\nend\n\nfunction _set_simulation_internal!(\n    sim::Simulation,\n    partitions::Union{Nothing, SimulationPartitions},\n    recorders,\n    console_level,\n    file_level,\n)\n    sim.internal = SimulationInternal(\n        sim.steps,\n        get_models(sim),\n        get_simulation_folder(sim),\n        get_name(sim),\n        recorders,\n        console_level,\n        file_level;\n        partitions = partitions,\n    )\n    return\nend\n\nfunction _setup_simulation_partitions(sim::Simulation)\n    mkdir(sim.internal.partitions_dir)\n    filename = joinpath(sim.internal.partitions_dir, \"config.json\")\n    IS.to_json(sim.internal.partitions, filename; pretty = true)\n    for i in 1:get_num_partitions(sim.internal.partitions)\n        mkdir(joinpath(sim.internal.partitions_dir, string(i)))\n    end\nend\n\n\"\"\"\nBuild the Simulation, problems and the related folder structure.\n\n# Arguments\n\n  - `sim::Simulation`: simulation object\n  - `recorders::Vector{Symbol} = []`: recorder names to register\n  - `store_systems_in_results::Bool = true`: stores the systems as JSON in the results HDF5 file\n  - `console_level = Logging.Error`:\n  - `file_level = Logging.Info`:\n\"\"\"\nfunction build!(\n    sim::Simulation;\n    recorders = [],\n    console_level = Logging.Error,\n    file_level = Logging.Info,\n    store_systems_in_results = true,\n    partitions::Union{Nothing, SimulationPartitions} = nothing,\n    index = nothing,\n)\n    TimerOutputs.reset_timer!(BUILD_PROBLEMS_TIMER)\n    TimerOutputs.@timeit BUILD_PROBLEMS_TIMER \"Build Simulation\" begin\n        _check_folder(sim)\n        _set_simulation_internal!(sim, partitions, recorders, console_level, file_level)\n        make_dirs(sim.internal)\n        if !isnothing(partitions) && isnothing(index)\n            setup_simulation_partitions = true\n        else\n            setup_simulation_partitions = false\n        end\n        file_mode = \"w\"\n        logger = configure_logging(sim.internal, file_mode)\n        register_recorders!(sim.internal, file_mode)\n        try\n            Logging.with_logger(logger) do\n                try\n                    _build!(\n                        sim;\n                        store_systems_in_results = store_systems_in_results,\n                        setup_simulation_partitions = setup_simulation_partitions,\n                        partitions = partitions,\n                        index = index,\n                    )\n                    set_simulation_build_status!(sim, SimulationBuildStatus.BUILT)\n                    set_simulation_status!(sim, RunStatus.INITIALIZED)\n                catch e\n                    @error \"Simulation build failed\" exception = (e, catch_backtrace())\n                    set_simulation_build_status!(sim, SimulationBuildStatus.FAILED)\n                    set_simulation_status!(sim, RunStatus.NOT_READY)\n                    rethrow(e)\n                end\n            end\n        finally\n            unregister_recorders!(sim.internal)\n            close(logger)\n        end\n    end\n    @info \"\\n$(BUILD_PROBLEMS_TIMER)\\n\"\n    return get_simulation_build_status(sim)\nend\n\nfunction _apply_warm_start!(model::OperationModel)\n    container = get_optimization_container(model)\n    # If the model was used to retrieve duals from an MILP the logic has to be different and\n    # the results need to be read from the primal cache\n    if isempty(container.primal_values_cache)\n        jump_model = get_jump_model(container)\n        all_vars = JuMP.all_variables(jump_model)\n        all_vars_value = jump_value.(all_vars)\n        JuMP.set_start_value.(all_vars, all_vars_value)\n    else\n        for (var_key, variable_value) in container.primal_values_cache.variables_cache\n            variable = get_variable(container, var_key)\n            JuMP.set_start_value.(variable, variable_value)\n        end\n    end\n    return\nend\n\nfunction _get_next_problem_initial_time(sim::Simulation, model_name::Symbol)\n    current_time = get_current_time(sim)\n    sequence = get_sequence(sim)\n    current_exec_index = sequence.current_execution_index\n    exec_order = get_execution_order(sequence)\n\n    if length(exec_order) > 1 && (current_exec_index + 1 > length(exec_order)) # Moving to the next step\n        next_initial_time = get_simulation_time(sim, exec_order[1])\n    elseif length(exec_order) == 1 ||\n           exec_order[current_exec_index + 1] == exec_order[current_exec_index] # Solving the same problem again\n        current_model_interval = get_interval(sim.sequence, model_name)\n        next_initial_time = current_time + current_model_interval\n    else # Solving another problem next\n        next_initial_time = get_simulation_time(sim, exec_order[current_exec_index + 1])\n    end\n    return next_initial_time\nend\n\nfunction _update_system_state!(sim::Simulation, model_name::Symbol)\n    sim_state = get_simulation_state(sim)\n    system_state = get_system_states(sim_state)\n    decision_state = get_decision_states(sim_state)\n    simulation_time = get_current_time(sim_state)\n    next_stage_initial_time = _get_next_problem_initial_time(sim, model_name)\n\n    for key in get_dataset_keys(decision_state)\n        state_data = get_dataset(decision_state, key)\n        last_update = get_update_timestamp(decision_state, key)\n\n        if last_update > simulation_time\n            error(\"Something went really wrong. Please report this error. \\\\\n            last_update: $(last_update) \\\\\n            simulation_time: $(simulation_time) \\\\\n            key: $(encode_key_as_string(key))\")\n        end\n\n        resolution = get_data_resolution(state_data)\n        update_timestamp = max(next_stage_initial_time - resolution, simulation_time)\n        if update_timestamp < get_update_timestamp(system_state, key)\n            error(\"The update overwrites more recent data with past data\")\n        elseif update_timestamp > get_update_timestamp(system_state, key)\n            update_system_state!(system_state, key, decision_state, update_timestamp)\n        else\n            @assert_op update_timestamp == get_update_timestamp(system_state, key)\n        end\n    end\n\n    IS.@record :execution StateUpdateEvent(simulation_time, model_name, \"SystemState\")\n    return\nend\n\nfunction _update_system_state!(sim::Simulation, model::DecisionModel)\n    _update_system_state!(sim, get_name(model))\n    return\nend\n\nfunction _update_system_state!(sim::Simulation, model::EmulationModel)\n    sim_state = get_simulation_state(sim)\n    simulation_time = get_current_time(sim)\n    system_state = get_system_states(sim_state)\n    store = get_simulation_store(sim)\n    em_model_name = get_name(model)\n    for key in get_container_keys(get_optimization_container(model))\n        !should_write_resulting_value(key) && continue\n        update_system_state!(system_state, key, store, em_model_name, simulation_time)\n    end\n    IS.@record :execution StateUpdateEvent(simulation_time, em_model_name, \"SystemState\")\n    return\nend\n\nfunction _update_simulation_state!(sim::Simulation, model::EmulationModel)\n    # Order of these operations matters. Do not reverse.\n    # This will update the state with the results of the store first and then fill\n    # the remaning values with the decision state.\n    _update_system_state!(sim, model)\n    _update_system_state!(sim, get_name(model))\n    return\nend\n\nfunction _update_simulation_state!(sim::Simulation, model::DecisionModel)\n    #Order matters; update parameters first to ensure event parameters are updated first\n    _update_simulation_state_parameters!(sim, model)\n    _update_simulation_state_others!(sim, model)\n    model_name = get_name(model)\n    simulation_time = get_current_time(sim)\n    IS.@record :execution StateUpdateEvent(simulation_time, model_name, \"DecisionState\")\n    return\nend\n\nfunction _update_simulation_state_parameters!(sim::Simulation, model::DecisionModel)\n    model_name = get_name(model)\n    store = get_simulation_store(sim)\n    simulation_time = get_current_time(sim)\n    state = get_simulation_state(sim)\n    model_params = get_decision_model_params(store, model_name)\n    all_parameter_keys = list_decision_model_keys(store, model_name, :parameters)\n    countdown_parameter_keys = filter(_is_event_countdown_parameter_key, all_parameter_keys)\n    other_parameter_keys = filter(!_is_event_countdown_parameter_key, all_parameter_keys)\n    # Order matters; AvailableStatusChangeCountdownParameter must be updated first if it exists\n    for key in countdown_parameter_keys\n        !has_dataset(get_decision_states(state), key) && continue\n        res = read_result(DenseAxisArray, store, model_name, key, simulation_time)\n        update_decision_state!(state, key, res, simulation_time, model_params)\n    end\n    for key in other_parameter_keys\n        !has_dataset(get_decision_states(state), key) && continue\n        res = read_result(DenseAxisArray, store, model_name, key, simulation_time)\n        update_decision_state!(state, key, res, simulation_time, model_params)\n    end\nend\n\nfunction _is_event_countdown_parameter_key(\n    ::ParameterKey{T, U},\n) where {T <: ParameterType, U <: PSY.Component}\n    return false\nend\n\nfunction _is_event_countdown_parameter_key(\n    ::ParameterKey{AvailableStatusChangeCountdownParameter, U},\n) where {U <: PSY.Component}\n    return true\nend\n\nfunction _update_simulation_state_others!(sim::Simulation, model::DecisionModel)\n    model_name = get_name(model)\n    store = get_simulation_store(sim)\n    simulation_time = get_current_time(sim)\n    state = get_simulation_state(sim)\n    model_params = get_decision_model_params(store, model_name)\n    for field in [:duals, :aux_variables, :variables, :expressions]\n        for key in list_decision_model_keys(store, model_name, field)\n            !has_dataset(get_decision_states(state), key) && continue\n            res = read_result(DenseAxisArray, store, model_name, key, simulation_time)\n            update_decision_state!(state, key, res, simulation_time, model_params)\n        end\n    end\n    return\nend\n\nfunction _write_state_to_store!(store::SimulationStore, sim::Simulation)\n    sim_state = get_simulation_state(sim)\n    system_state = get_system_states(sim_state)\n    model_name = get_last_decision_model(sim_state)\n    em_store = get_em_data(store)\n    simulation_time = get_current_time(sim)\n    sim_ini_time = get_initial_time(sim)\n    state_resolution = get_system_states_resolution(sim_state)\n    for key in get_dataset_keys(system_state)\n        store_update_time = get_last_updated_timestamp(em_store, key)\n        state_update_time = get_update_timestamp(system_state, key)\n        # If the store is outdated w.r.t to the state\n        @assert store_update_time <= simulation_time\n        if store_update_time < state_update_time\n            _update_timestamp = max(store_update_time + state_resolution, sim_ini_time)\n            while _update_timestamp <= state_update_time\n                state_values =\n                    get_decision_state_value(sim_state, key, _update_timestamp)\n                ix = get_last_recorded_row(em_store, key) + 1\n                write_result!(\n                    store,\n                    model_name,\n                    key,\n                    ix,\n                    _update_timestamp,\n                    state_values,\n                )\n                _update_timestamp += state_resolution\n            end\n        end\n    end\n    return\nend\n\n\"\"\"\nDefault problem update function for most problems with no customization\n\"\"\"\nfunction update_model!(model::OperationModel, sim::Simulation)\n    update_model!(model, get_simulation_state(sim), get_ini_cond_chronology(sim))\n    if get_rebuild_model(model)\n        container = get_optimization_container(model)\n        reset_optimization_model!(container)\n        build_impl!(container, get_template(model), get_system(model))\n    end\n\n    return\nend\n\nfunction _execute!(\n    sim::Simulation;\n    cache_size_mib::Union{Nothing, Int} = nothing,\n    min_cache_flush_size_mib::Union{Nothing, Int} = nothing,\n    exports = nothing,\n    enable_progress_bar = progress_meter_enabled(),\n    disable_timer_outputs = false,\n    results_channel = nothing,\n)\n    @assert sim.internal !== nothing\n\n    set_simulation_status!(sim, RunStatus.RUNNING)\n    execution_order = get_execution_order(sim)\n    steps = get_steps(sim)\n    num_executions = steps * length(execution_order)\n    store = get_simulation_store(sim)\n    # For InMemorySimulationStore, initialize storage here since it doesn't persist from build.\n    if store isa InMemorySimulationStore\n        _initialize_problem_storage!(\n            sim,\n            something(cache_size_mib, DEFAULT_SIMULATION_STORE_CACHE_SIZE_MiB),\n            something(min_cache_flush_size_mib, MIN_CACHE_FLUSH_SIZE_MiB),\n        )\n    else\n        # Override cache flush rules from build phase if user passes kwargs at execution time.\n        rules = CacheFlushRules(;\n            max_size = something(cache_size_mib, DEFAULT_SIMULATION_STORE_CACHE_SIZE_MiB) * MiB,\n            min_flush_size = trunc(\n                something(min_cache_flush_size_mib, MIN_CACHE_FLUSH_SIZE_MiB) * MiB,\n            ),\n        )\n        set_cache_flush_rules!(store, rules)\n    end\n    store_params = get_params(store)\n    if exports !== nothing\n        if !(exports isa SimulationResultsExport)\n            exports = SimulationResultsExport(exports, store_params)\n        end\n\n        if exports.path === nothing\n            exports.path = get_results_dir(sim)\n        end\n    end\n    sequence = get_sequence(sim)\n    models = get_models(sim)\n\n    prog_bar = ProgressMeter.Progress(num_executions; enabled = enable_progress_bar)\n    disable_timer_outputs && TimerOutputs.disable_timer!(RUN_SIMULATION_TIMER)\n    store = get_simulation_store(sim)\n    for step in 1:steps\n        IS.@record :simulation_status SimulationStepEvent(\n            get_current_time(sim),\n            step,\n            \"start\",\n        )\n        # This progress print is required to show the progress bar upfront\n        ProgressMeter.update!(\n            prog_bar,\n            1;\n            showvalues = [\n                (:Step, 1),\n                (:Problem, get_name(get_simulation_model(models, 1))),\n                (:(\"Simulation Timestamp\"), get_current_time(sim)),\n            ],\n        )\n        for (ix, model_number) in enumerate(execution_order)\n            model = get_simulation_model(models, model_number)\n            model_name = get_name(model)\n            set_current_time!(sim, sim.internal.date_ref[model_number])\n            sequence.current_execution_index = ix\n            current_time = get_current_time(sim)\n            IS.@record :simulation_status ProblemExecutionEvent(\n                current_time,\n                step,\n                model_name,\n                \"start\",\n            )\n\n            progress_event = SimulationProgressEvent(;\n                model_name = string(model_name),\n                step = step,\n                index = (step - 1) * length(execution_order) + ix,\n                timestamp = get_current_time(sim),\n                wall_time = Dates.now(),\n                exec_time_s = 0.0,\n            )\n            ProgressMeter.update!(\n                prog_bar,\n                progress_event.index;\n                showvalues = [\n                    (:Step, progress_event.step),\n                    (:Problem, model_name),\n                    (:(\"Simulation Timestamp\"), progress_event.timestamp),\n                ],\n            )\n\n            start_time = time()\n            TimerOutputs.@timeit RUN_SIMULATION_TIMER \"Execute $(model_name)\" begin\n                if !is_built(model)\n                    error(\"$(model_name) status is not ModelBuildStatus.BUILT\")\n                end\n\n                # Is first run of first problem? Yes -> don't update problem\n\n                TimerOutputs.@timeit RUN_SIMULATION_TIMER \"Update $(model_name)\" begin\n                    !(step == 1 && ix == 1) && update_model!(model, sim)\n                end\n\n                TimerOutputs.@timeit RUN_SIMULATION_TIMER \"Solve $(model_name)\" begin\n                    status = solve!(step, model, current_time, store; exports = exports)\n                end # Run problem Timer\n\n                TimerOutputs.@timeit RUN_SIMULATION_TIMER \"Update State\" begin\n                    if status == RunStatus.SUCCESSFULLY_FINALIZED\n                        # TODO: _update_simulation_state! can use performance improvements\n                        _update_simulation_state!(sim, model)\n                        if model_number == execution_order[end]\n                            _update_system_state!(sim, model)\n                            _write_state_to_store!(store, sim)\n                            # This function needs to be called last so make sure that the update to the\n                            # state get written AFTER the models run.\n                            apply_simulation_events!(sim)\n                        end\n                    end\n                end\n\n                sim.internal.run_count[step][model_number] += 1\n                sim.internal.date_ref[model_number] += get_interval(sequence, model_name)\n\n                # _apply_warm_start! can only be called once all the operations that read solutions\n                # from the optimization container have been called.\n                # See https://github.com/Sienna-Platform/PowerSimulations.jl/pull/793#discussion_r761545526\n                # for reference\n                if warm_start_enabled(model)\n                    _apply_warm_start!(model)\n                end\n\n                IS.@record :simulation_status ProblemExecutionEvent(\n                    get_current_time(sim),\n                    step,\n                    model_name,\n                    \"done\",\n                )\n            end #execution problem timer\n            progress_event.exec_time_s = time() - start_time\n            if !isnothing(results_channel)\n                put!(results_channel, SimulationIntermediateResult(progress_event))\n            end\n        end # execution order for loop\n\n        IS.@record :simulation_status SimulationStepEvent(\n            get_current_time(sim),\n            step,\n            \"done\",\n        )\n    end # Steps for loop\n    return\nend\n\n\"\"\"\nSolves the simulation model for sequential Simulations.\n\n# Arguments\n\n  - `sim::Simulation=sim`: simulation object created by Simulation()\n\nThe optional keyword argument `exports` controls exporting of results to CSV files as\nthe simulation runs.\n\n# Example\n```julia\nsim = Simulation(\"Test\", 7, problems, \"/Users/folder\")\nexecute!(sim::Simulation; kwargs...)\n```\n\"\"\"\nfunction execute!(sim::Simulation; kwargs...)\n    file_mode = \"a\"\n    logger = configure_logging(sim.internal, file_mode)\n    register_recorders!(sim.internal, file_mode)\n\n    # Undocumented option for test & dev only.\n    in_memory = get(kwargs, :in_memory, false)\n    store_type = in_memory ? InMemorySimulationStore : HdfSimulationStore\n\n    sim_build_status = get_simulation_build_status(sim)\n    sim_run_status = get_simulation_status(sim)\n    if (sim_build_status != SimulationBuildStatus.BUILT) ||\n       (sim_run_status != RunStatus.INITIALIZED)\n        error(\n            \"Simulation build status $sim_build_status, or Simulation run status $sim_run_status, are invalid, you need to rebuild the simulation\",\n        )\n    end\n    file_mode = in_memory ? \"w\" : \"rw\"\n    try\n        Logging.with_logger(logger) do\n            open_store(store_type, get_store_dir(sim), file_mode) do store\n                set_simulation_store!(sim, store)\n                try\n                    TimerOutputs.reset_timer!(RUN_SIMULATION_TIMER)\n                    TimerOutputs.@timeit RUN_SIMULATION_TIMER \"Execute Simulation\" begin\n                        _execute!(\n                            sim;\n                            [k => v for (k, v) in kwargs if k != :in_memory]...,\n                        )\n                    end\n                    @info (\"\\n$(RUN_SIMULATION_TIMER)\\n\")\n                    set_simulation_status!(sim, RunStatus.SUCCESSFULLY_FINALIZED)\n                    log_cache_hit_percentages(store)\n                catch e\n                    set_simulation_status!(sim, RunStatus.FAILED)\n                    @error \"simulation failed\" exception = (e, catch_backtrace())\n                end\n            end\n        end\n    finally\n        _empty_problem_caches!(sim)\n        unregister_recorders!(sim.internal)\n        close(logger)\n    end\n\n    if !in_memory && get_simulation_status(sim) == RunStatus.SUCCESSFULLY_FINALIZED\n        IS.compute_file_hash(get_store_dir(sim), HDF_FILENAME)\n    end\n\n    serialize_status(sim)\n    return get_simulation_status(sim)\nend\n\nfunction _empty_problem_caches!(sim::Simulation)\n    models = get_models(sim)\n    for model in get_decision_models(models)\n        empty_time_series_cache!(model)\n    end\n    return\nend\n\nfunction _serialize_systems_to_json(sim::Simulation)\n    simulation_models = get_models(sim)\n    results = Dict{String, String}()\n    @debug Threads.threadid() \"Serializing systems to JSON in parallel with model building\"\n    for dm in get_decision_models(simulation_models)\n        sys = get_system(dm)\n        uuid = string(IS.get_uuid(sys))\n        if !haskey(results, uuid)\n            results[uuid] = PSY.to_json(sys)\n        end\n    end\n    em = get_emulation_model(simulation_models)\n    if !isnothing(em)\n        sys = get_system(em)\n        uuid = string(IS.get_uuid(sys))\n        if !haskey(results, uuid)\n            results[uuid] = PSY.to_json(sys)\n        end\n    end\n    return results\nend\n\nfunction serialize_status(sim::Simulation)\n    serialize_status(get_simulation_status(sim), get_results_dir(sim))\nend\n\nfunction serialize_status(status::RunStatus, results_dir::AbstractString)\n    data = Dict(\"run_status\" => string(status))\n    filename = joinpath(results_dir, \"status.json\")\n    open(filename, \"w\") do io\n        JSON3.write(io, data)\n    end\n\n    return\nend\n\nfunction deserialize_status(sim::Simulation)\n    return deserialize_status(get_results_dir(sim))\nend\n\nfunction deserialize_status(results_path::AbstractString)\n    filename = joinpath(results_path, \"status.json\")\n    if !isfile(filename)\n        error(\"run status file $filename does not exist\")\n    end\n\n    data = open(filename, \"r\") do io\n        JSON3.read(io, Dict)\n    end\n\n    return get_enum_value(RunStatus, data[\"run_status\"])\nend\n\n# The next two structs allow a parent process to monitor the simulation progress.\n# They may eventually be extended to pass result data back to the parent.\n\nBase.@kwdef mutable struct SimulationProgressEvent\n    model_name::String\n    step::Int\n    index::Int\n    timestamp::Dates.DateTime\n    wall_time::Dates.DateTime\n    exec_time_s::Float64\nend\n\nstruct SimulationIntermediateResult\n    progress_event::SimulationProgressEvent\nend\n"
  },
  {
    "path": "src/simulation/simulation_events.jl",
    "content": "function apply_simulation_events!(simulation::Simulation)\n    sequence = get_sequence(simulation)\n    events = get_events(sequence)\n    simulation_state = get_simulation_state(simulation)\n    for event_model in events\n        extend_event_parameters!(simulation, event_model)\n        if check_condition(simulation_state, event_model)\n            # TODO: Events for other event categories we need to do something else\n            em_model = get_emulation_model(get_models(simulation))\n            sys = get_system(em_model)\n            model_name = get_name(em_model)\n            for (event_uuid, device_type_maps) in\n                event_model.attribute_device_map[model_name]\n                event = PSY.get_supplemental_attribute(sys, event_uuid)\n                apply_affect!(simulation, event_model, event, device_type_maps)\n            end\n        end\n    end\nend\n\nfunction extend_event_parameters!(simulation::Simulation, event_model)\n    sequence = get_sequence(simulation)\n    sim_state = get_simulation_state(simulation)\n    em_model = get_emulation_model(get_models(simulation))\n    model_name = get_name(em_model)\n    for (event_uuid, device_type_maps) in\n        event_model.attribute_device_map[model_name]\n        sim_time = get_current_time(simulation)\n        for (dtype, device_names) in device_type_maps\n            if dtype == PSY.RenewableDispatch\n                continue\n            end\n            em_model = get_emulation_model(get_models(simulation))\n            status_change_countdown_data = get_decision_state_data(\n                sim_state,\n                ParameterKey(AvailableStatusChangeCountdownParameter, dtype),\n            )\n            status_data = get_decision_state_data(\n                sim_state,\n                ParameterKey(AvailableStatusParameter, dtype),\n            )\n            state_timestamps = status_data.timestamps\n            state_data_index = find_timestamp_index(state_timestamps, sim_time)\n            if state_data_index == 1\n                for name in device_names\n                    if status_change_countdown_data.values[name, 1] > 1.0\n                        starting_count = status_change_countdown_data.values[name, 1]\n                        for i in 1:length(status_change_countdown_data.values[name, :])\n                            countdown_val = max(starting_count + 1 - i, 0.0)\n                            if countdown_val == 0.0\n                                status_val = 1.0\n                            else\n                                status_val = 0.0\n                            end\n                            status_change_countdown_data.values[name, i] = countdown_val\n                            status_data.values[name, i] = status_val\n                        end\n                    end\n                end\n            end\n        end\n    end\nend\n\nfunction check_condition(\n    ::SimulationState,\n    ::EventModel{<:PSY.Contingency, ContinuousCondition},\n)\n    return true\nend\n\nfunction check_condition(\n    simulation_state::SimulationState,\n    event_model::EventModel{<:PSY.Contingency, PresetTimeCondition},\n)\n    condition = get_event_condition(event_model)\n    event_times = get_time_stamps(condition)\n    current_time = get_current_time(simulation_state)\n    if current_time in event_times\n        return true\n    else\n        return false\n    end\nend\n\nfunction check_condition(\n    simulation_state::SimulationState,\n    event_model::EventModel{<:PSY.Contingency, StateVariableValueCondition},\n)\n    condition = get_event_condition(event_model)\n    variable_type = get_variable_type(condition)\n    device_type = get_device_type(condition)\n    device_name = get_device_name(condition)\n    event_value = get_value(condition)\n\n    system_value =\n        get_system_state_data(simulation_state, variable_type, device_type).values[\n            device_name,\n            1,\n        ]\n    if isapprox(system_value, event_value; atol = ABSOLUTE_TOLERANCE)\n        return true\n    else\n        return false\n    end\nend\n\nfunction check_condition(\n    simulation_state::SimulationState,\n    event_model::EventModel{<:PSY.Contingency, DiscreteEventCondition},\n)\n    condition = get_event_condition(event_model)\n    f = condition.condition_function\n    if f(simulation_state)\n        return true\n    else\n        return false\n    end\nend\n\nfunction apply_affect!(\n    simulation::Simulation,\n    event_model::EventModel{\n        T,\n        <:AbstractEventCondition,\n    },\n    event::T,\n    device_type_maps::Dict{DataType, Set{String}},\n) where {T <: PSY.Contingency}\n    sim_state = get_simulation_state(simulation)\n    sim_time = get_current_time(simulation)\n    rng = get_rng(simulation)\n    for (dtype, device_names) in device_type_maps\n        if !supports_outages(dtype)\n            continue\n        end\n        em_model = get_emulation_model(get_models(simulation))\n        em_model_store = get_store_params(em_model)\n        # Order required: event parameters must be updated first to indicate a change in other parameters/variables.\n        update_system_state!(\n            sim_state,\n            ParameterKey(AvailableStatusChangeCountdownParameter, dtype),\n            device_names,\n            event,\n            event_model,\n            sim_time,\n            rng,\n        )\n        if haskey(\n            sim_state.system_states.parameters,\n            ParameterKey(ActivePowerOffsetParameter, dtype),\n        )\n            update_system_state!(\n                sim_state,\n                ParameterKey(ActivePowerOffsetParameter, dtype),\n                device_names,\n                event,\n                event_model,\n                sim_time,\n                rng,\n            )\n        end\n        if haskey(\n            sim_state.system_states.parameters,\n            ParameterKey(ReactivePowerOffsetParameter, dtype),\n        )\n            update_system_state!(\n                sim_state,\n                ParameterKey(ReactivePowerOffsetParameter, dtype),\n                device_names,\n                event,\n                event_model,\n                sim_time,\n                rng,\n            )\n        end\n        update_system_state!(\n            sim_state,\n            ParameterKey(AvailableStatusParameter, dtype),\n            device_names,\n            event,\n            event_model,\n            sim_time,\n            rng,\n        )\n        for k in keys(sim_state.system_states.variables)  #Not an OrderedDict\n            if typeof(k).parameters[2] != dtype\n                continue\n            end\n            update_system_state!(\n                sim_state,\n                k,\n                device_names,\n                event,\n                event_model,\n                sim_time,\n                rng,\n            )\n        end\n        for k in keys(sim_state.system_states.aux_variables)  #Not an OrderedDict\n            if typeof(k).parameters[2] != dtype\n                continue\n            end\n            update_system_state!(\n                sim_state,\n                k,\n                device_names,\n                event,\n                event_model,\n                sim_time,\n                rng,\n            )\n        end\n\n        # Order is required here too AvailableStatusChangeCountdownParameter needs to\n        # go first to indicate that there is a change in the other values\n        update_decision_state!(\n            sim_state,\n            ParameterKey(AvailableStatusChangeCountdownParameter, dtype),\n            device_names,\n            event,\n            event_model,\n            sim_time,\n            em_model_store,\n        )\n        if haskey(\n            sim_state.decision_states.parameters,\n            ParameterKey(ActivePowerOffsetParameter, dtype),\n        )\n            update_decision_state!(\n                sim_state,\n                ParameterKey(ActivePowerOffsetParameter, dtype),\n                device_names,\n                event,\n                event_model,\n                sim_time,\n                em_model_store,\n            )\n        end\n        if haskey(\n            sim_state.decision_states.parameters,\n            ParameterKey(ReactivePowerOffsetParameter, dtype),\n        )\n            update_decision_state!(\n                sim_state,\n                ParameterKey(ReactivePowerOffsetParameter, dtype),\n                device_names,\n                event,\n                event_model,\n                sim_time,\n                em_model_store,\n            )\n        end\n        update_decision_state!(\n            sim_state,\n            ParameterKey(AvailableStatusParameter, dtype),\n            device_names,\n            event,\n            event_model,\n            sim_time,\n            em_model_store,\n        )\n\n        for k in keys(sim_state.decision_states.variables)  #Not an OrderedDict\n            if typeof(k).parameters[2] != dtype\n                continue\n            end\n            update_decision_state!(\n                sim_state,\n                k,\n                device_names,\n                event,\n                event_model,\n                sim_time,\n                em_model_store,\n            )\n        end\n        for k in keys(sim_state.decision_states.aux_variables)  #Not an OrderedDict\n            if typeof(k).parameters[2] != dtype\n                continue\n            end\n            update_decision_state!(\n                sim_state,\n                k,\n                device_names,\n                event,\n                event_model,\n                sim_time,\n                em_model_store,\n            )\n        end\n    end\n    IS.@record :execution StateUpdateEvent(sim_time, \"Emulator\", \"SystemState\")\nend\n"
  },
  {
    "path": "src/simulation/simulation_info.jl",
    "content": "mutable struct SimulationInfo\n    number::Union{Nothing, Int}\n    sequence_uuid::Union{Nothing, Base.UUID}\n    run_status::RunStatus\nend\n\nSimulationInfo() = SimulationInfo(nothing, nothing, RunStatus.INITIALIZED)\n\nget_number(si::SimulationInfo) = si.number\nset_number!(si::SimulationInfo, val::Int) = si.number = val\nget_sequence_uuid(si::SimulationInfo) = si.sequence_uuid\nset_sequence_uuid!(si::SimulationInfo, val::Base.UUID) = si.sequence_uuid = val\nget_run_status(si::SimulationInfo) = si.run_status\nset_run_status!(si::SimulationInfo, val::RunStatus) = si.run_status = val\n"
  },
  {
    "path": "src/simulation/simulation_internal.jl",
    "content": "mutable struct SimulationInternal\n    sim_files_dir::String\n    partitions::Union{Nothing, SimulationPartitions}\n    store_dir::String\n    logs_dir::String\n    models_dir::String\n    recorder_dir::String\n    results_dir::String\n    partitions_dir::String\n    run_count::OrderedDict{Int, OrderedDict{Int, Int}}\n    date_ref::OrderedDict{Int, Dates.DateTime}\n    status::RunStatus\n    build_status::SimulationBuildStatus\n    simulation_state::SimulationState\n    store::Union{Nothing, SimulationStore}\n    recorders::Vector{Symbol}\n    console_level::Base.CoreLogging.LogLevel\n    file_level::Base.CoreLogging.LogLevel\n    cache_size_mib::Int\n    min_cache_flush_size_mib::Int\n    rng::AbstractRNG\nend\n\nfunction SimulationInternal(\n    steps::Int,\n    models::SimulationModels,\n    base_dir::String,\n    name::String,\n    recorders,\n    console_level::Logging.LogLevel,\n    file_level::Logging.LogLevel;\n    partitions::Union{Nothing, SimulationPartitions} = nothing,\n    cache_size_mib = 1024,\n    min_cache_flush_size_mib = MIN_CACHE_FLUSH_SIZE_MiB,\n)\n    count_dict = OrderedDict{Int, OrderedDict{Int, Int}}()\n\n    for s in 1:steps\n        count_dict[s] = OrderedDict{Int, Int}()\n        model_count = length(get_decision_models(models))\n        for st in 1:model_count\n            count_dict[s][st] = 0\n        end\n        if get_emulation_model(models) !== nothing\n            count_dict[s][model_count + 1] = 0\n        end\n    end\n\n    simulation_dir = joinpath(base_dir, name)\n    if isdir(simulation_dir)\n        simulation_dir = _get_output_dir_name(base_dir, name)\n    end\n\n    sim_files_dir = joinpath(simulation_dir, \"simulation_files\")\n    store_dir = joinpath(simulation_dir, \"data_store\")\n    logs_dir = joinpath(simulation_dir, \"logs\")\n    models_dir = joinpath(simulation_dir, \"problems\")\n    recorder_dir = joinpath(simulation_dir, \"recorder\")\n    results_dir = joinpath(simulation_dir, RESULTS_DIR)\n    partitions_dir = joinpath(simulation_dir, \"simulation_partitions\")\n\n    unique_recorders = Set(REQUIRED_RECORDERS)\n    foreach(x -> push!(unique_recorders, x), recorders)\n\n    return SimulationInternal(\n        sim_files_dir,\n        partitions,\n        store_dir,\n        logs_dir,\n        models_dir,\n        recorder_dir,\n        results_dir,\n        partitions_dir,\n        count_dict,\n        OrderedDict{Int, Dates.DateTime}(),\n        RunStatus.NOT_READY,\n        SimulationBuildStatus.EMPTY,\n        SimulationState(),\n        nothing,\n        collect(unique_recorders),\n        console_level,\n        file_level,\n        cache_size_mib,\n        min_cache_flush_size_mib,\n        Random.Xoshiro(IS.get_random_seed()),\n    )\nend\n\nfunction make_dirs(internal::SimulationInternal)\n    mkdir(dirname(internal.sim_files_dir))\n    for field in\n        (:sim_files_dir, :store_dir, :logs_dir, :models_dir, :recorder_dir, :results_dir)\n        mkdir(getproperty(internal, field))\n    end\nend\n\nfunction _get_output_dir_name(path, sim_name)\n    index = _get_most_recent_execution(path, sim_name) + 1\n    return joinpath(path, \"$sim_name-$index\")\nend\n\nfunction _get_most_recent_execution(path, sim_name)\n    sim_dirs = readdir(path)\n    if isempty(sim_dirs)\n        fail = true\n    elseif length(sim_dirs) == 1\n        fail = sim_dirs[1] != sim_name\n    else\n        fail = false\n    end\n\n    fail && error(\"No simulation directories with name=$sim_name are in $path\")\n    executions = [1]\n    for path_name in sim_dirs\n        regex = Regex(\"\\\\Q$sim_name\\\\E-(\\\\d+)\\$\")\n        m = match(regex, path_name)\n        if !isnothing(m)\n            push!(executions, parse(Int, m.captures[1]))\n        end\n    end\n\n    return maximum(executions)\nend\n\nfunction configure_logging(internal::SimulationInternal, file_mode)\n    return IS.configure_logging(;\n        console = true,\n        console_stream = stderr,\n        console_level = internal.console_level,\n        file = true,\n        filename = joinpath(internal.logs_dir, SIMULATION_LOG_FILENAME),\n        file_level = internal.file_level,\n        file_mode = file_mode,\n        tracker = nothing,\n        set_global = false,\n    )\nend\n\nfunction register_recorders!(internal::SimulationInternal, file_mode)\n    for name in internal.recorders\n        IS.register_recorder!(name; mode = file_mode, directory = internal.recorder_dir)\n    end\nend\n\nfunction unregister_recorders!(internal::SimulationInternal)\n    for name in internal.recorders\n        IS.unregister_recorder!(name)\n    end\nend\n"
  },
  {
    "path": "src/simulation/simulation_models.jl",
    "content": "\"\"\"\n    SimulationModels(\n        decision_models::Vector{<:DecisionModel},\n        emulation_models::Union{Nothing, EmulationModel}\n    )\n\nStores the OperationProblem definitions to be used in the simulation. When creating the\nSimulationModels, the order in which the models are created determines the order on which\nthe simulation is executed.\n\n# Arguments\n\n  - `decision_models::Vector{<:DecisionModel}`: Vector of decision models.\n  - `emulation_models::Union{Nothing, EmulationModel}`: Optional argument to include\n   an EmulationModel in the Simulation\n\n# Example\n\n```julia\ntemplate_uc = template_unit_commitment()\ntemplate_ed = template_economic_dispatch()\nmy_decision_model_uc = DecisionModel(template_1, sys_uc, optimizer, name = \"UC\")\nmy_decision_model_ed = DecisionModel(template_ed, sys_ed, optimizer, name = \"ED\")\nmodels = SimulationModels(\n    decision_models = [\n        my_decision_model_uc,\n        my_decision_model_ed\n    ]\n)\n```\n\"\"\"\nmutable struct SimulationModels\n    decision_models::Vector{<:DecisionModel}\n    emulation_model::Union{Nothing, EmulationModel}\n\n    function SimulationModels(\n        decision_models::Vector,\n        emulation_model::Union{Nothing, EmulationModel} = nothing,\n    )\n        all_names = [get_name(x) for x in decision_models]\n        emulation_model !== nothing && push!(all_names, get_name(emulation_model))\n        model_count =\n            if emulation_model === nothing\n                length(decision_models)\n            else\n                length(decision_models) + 1\n            end\n        if length(Set(all_names)) != model_count\n            error(\"All model names must be unique: $all_names\")\n        end\n\n        return new(decision_models, emulation_model)\n    end\nend\n\nfunction SimulationModels(\n    decision_models::DecisionModel,\n    emulation_model::Union{Nothing, EmulationModel} = nothing,\n)\n    return SimulationModels([decision_models], emulation_model)\nend\n\nfunction SimulationModels(;\n    decision_models,\n    emulation_model::Union{Nothing, EmulationModel} = nothing,\n)\n    return SimulationModels(decision_models, emulation_model)\nend\n\nfunction get_simulation_model(models::SimulationModels, name::Symbol)\n    for model in models.decision_models\n        if get_name(model) == name\n            return model\n        end\n    end\n    em = models.emulation_model\n    if em !== nothing\n        if get_name(em) == name\n            return em\n        end\n    end\n\n    error(\"Model $name not stored in SimulationModels\")\nend\n\nfunction get_simulation_model(models::SimulationModels, index::Int)\n    n_decision_models = length(get_decision_models(models))\n    if index == n_decision_models + 1\n        return models.emulation_model\n    elseif index <= n_decision_models\n        return get_decision_models(models)[index]\n    else\n        error(\"Model number $index is invalid\")\n    end\nend\n\nget_decision_models(models::SimulationModels) = models.decision_models\nget_emulation_model(models::SimulationModels) = models.emulation_model\n\nfunction determine_horizons!(models::SimulationModels)\n    horizons = OrderedDict{Symbol, Dates.Millisecond}()\n    for model in models.decision_models\n        container = get_optimization_container(model)\n        settings = get_settings(container)\n        horizon = get_horizon(settings)\n        if horizon == UNSET_HORIZON\n            sys = get_system(model)\n            horizon = PSY.get_forecast_horizon(sys)\n            set_horizon!(settings, horizon)\n            horizons[get_name(model)] = horizon\n        else\n            horizons[get_name(model)] = horizon\n        end\n    end\n    em = models.emulation_model\n    if em !== nothing\n        resolution = get_resolution(em)\n        horizons[get_name(em)] = resolution\n    end\n    return horizons\nend\n\nfunction determine_intervals(models::SimulationModels)\n    intervals = OrderedDict{Symbol, Dates.Millisecond}()\n    for model in models.decision_models\n        model_interval = get_interval(get_settings(model))\n        if model_interval != UNSET_INTERVAL\n            interval = model_interval\n        else\n            system = get_system(model)\n            interval = PSY.get_forecast_interval(system)\n        end\n        if interval == Dates.Millisecond(0)\n            throw(IS.InvalidValue(\"Model $(get_name(model)) interval not set correctly\"))\n        end\n        intervals[get_name(model)] = IS.time_period_conversion(interval)\n    end\n    em = models.emulation_model\n    if em !== nothing\n        emulator_interval = get_resolution(em)\n        if emulator_interval == Dates.Millisecond(0)\n            throw(IS.InvalidValue(\"Emulator Resolution not set correctly\"))\n        end\n        intervals[get_name(em)] = IS.time_period_conversion(emulator_interval)\n    end\n    return intervals\nend\n\nfunction determine_resolutions(models::SimulationModels)\n    resolutions = OrderedDict{Symbol, Dates.Millisecond}()\n    for model in models.decision_models\n        resolution = get_resolution(model)\n        if resolution == UNSET_RESOLUTION\n            throw(\n                IS.InvalidValue(\"Resolution of model $(get_name(model)) not set correctly\"),\n            )\n        end\n        resolutions[get_name(model)] = IS.time_period_conversion(resolution)\n    end\n    em = models.emulation_model\n    if em !== nothing\n        emulator_resolution = get_resolution(em)\n        resolutions[get_name(em)] = IS.time_period_conversion(emulator_resolution)\n    end\n    return resolutions\nend\n\nfunction initialize_simulation_internals!(models::SimulationModels, uuid::Base.UUID)\n    for (ix, model) in enumerate(get_decision_models(models))\n        set_simulation_number!(model, ix)\n        set_sequence_uuid!(model, uuid)\n    end\n    em = get_emulation_model(models)\n    if em !== nothing\n        ix = length(get_decision_models(models)) + 1\n        set_simulation_number!(em, ix)\n        set_sequence_uuid!(em, uuid)\n    end\n    return\nend\n\nfunction get_model_names(models::SimulationModels)\n    all_names = get_name.(get_decision_models(models))\n    em = get_emulation_model(models)\n    if em !== nothing\n        push!(all_names, get_name(em))\n    end\n    return all_names\nend\n"
  },
  {
    "path": "src/simulation/simulation_partition_results.jl",
    "content": "const _TEMP_WRITE_POSITION = \"__write_position__\"\n\n\"\"\"\nHandles merging of simulation partitions\n\"\"\"\nstruct SimulationPartitionResults\n    \"Directory of main simulation\"\n    path::String\n    \"User-defined simulation name\"\n    simulation_name::String\n    \"Defines how the simulation is split into partitions\"\n    partitions::SimulationPartitions\n    \"Cache of datasets\"\n    datasets::Dict{String, HDF5.Dataset}\nend\n\nfunction SimulationPartitionResults(path::AbstractString)\n    config_file = joinpath(path, \"simulation_partitions\", \"config.json\")\n    config = open(config_file, \"r\") do io\n        JSON3.read(io, Dict)\n    end\n    partitions = IS.deserialize(SimulationPartitions, config)\n    return SimulationPartitionResults(\n        path,\n        basename(path),\n        partitions,\n        Dict{String, HDF5.Dataset}(),\n    )\nend\n\n\"\"\"\nCombine all partition simulation files.\n\"\"\"\nfunction join_simulation(path::AbstractString)\n    results = SimulationPartitionResults(path)\n    join_simulation(results)\n    return\nend\n\nfunction join_simulation(results::SimulationPartitionResults)\n    status = _check_jobs(results)\n    _merge_store_files!(results)\n    _complete(results, status)\n    return\nend\n\nfunction _partition_path(x::SimulationPartitionResults, i)\n    partition_path = joinpath(x.path, \"simulation_partitions\", string(i))\n    execution_no = _get_most_recent_execution(partition_path, x.simulation_name)\n    if execution_no == 1\n        execution_path = joinpath(partition_path, x.simulation_name)\n    else\n        execution_path = joinpath(partition_path, \"$(x.simulation_name)-$execution_no\")\n    end\n    return execution_path\nend\n\n_store_subpath() = joinpath(\"data_store\", \"simulation_store.h5\")\n_store_path(x::SimulationPartitionResults) = joinpath(x.path, _store_subpath())\n\nfunction _check_jobs(results::SimulationPartitionResults)\n    overall_status = RunStatus.SUCCESSFULLY_FINALIZED\n    for i in 1:get_num_partitions(results.partitions)\n        job_results_path = joinpath(_partition_path(results, 1), \"results\")\n        status = deserialize_status(job_results_path)\n        if status != RunStatus.SUCCESSFULLY_FINALIZED\n            @warn \"partition job index = $i was not successful: $status\"\n            overall_status = status\n        end\n    end\n\n    return overall_status\nend\n\nfunction _merge_store_files!(results::SimulationPartitionResults)\n    HDF5.h5open(_store_path(results), \"r+\") do dst\n        for i in 1:get_num_partitions(results.partitions)\n            HDF5.h5open(joinpath(_partition_path(results, i), _store_subpath()), \"r\") do src\n                _copy_datasets!(results, i, src, dst)\n            end\n        end\n\n        for dataset in values(results.datasets)\n            if occursin(\"decision_models\", HDF5.name(dataset))\n                IS.@assert_op HDF5.attrs(dataset)[_TEMP_WRITE_POSITION] ==\n                              size(dataset)[end] + 1\n            else\n                IS.@assert_op HDF5.attrs(dataset)[_TEMP_WRITE_POSITION] ==\n                              size(dataset)[1] + 1\n            end\n            delete!(HDF5.attrs(dataset), _TEMP_WRITE_POSITION)\n        end\n    end\nend\n\nfunction _copy_datasets!(\n    results::SimulationPartitionResults,\n    index::Int,\n    src::HDF5.File,\n    dst::HDF5.File,\n)\n    output_types = string.(STORE_CONTAINERS)\n\n    function process_dataset(src_dataset, merge_func)\n        if !endswith(HDF5.name(src_dataset), \"__columns\")\n            name = HDF5.name(src_dataset)\n            dst_dataset = dst[name]\n            if !haskey(results.datasets, name)\n                results.datasets[name] = dst_dataset\n                HDF5.attrs(dst_dataset)[_TEMP_WRITE_POSITION] = 1\n            end\n            merge_func(results, index, src_dataset, dst_dataset)\n        end\n    end\n\n    for src_group in src[\"simulation/decision_models\"]\n        for output_type in output_types\n            for src_dataset in src_group[output_type]\n                process_dataset(src_dataset, _merge_dataset_rows!)\n            end\n        end\n        process_dataset(src_group[\"optimizer_stats\"], _merge_dataset_rows!)\n    end\n\n    for output_type in output_types\n        for src_dataset in src[\"simulation/emulation_model/$output_type\"]\n            process_dataset(src_dataset, _merge_dataset_columns!)\n        end\n    end\nend\n\nfunction _merge_dataset_columns!(results::SimulationPartitionResults, index, src, dst)\n    num_columns = size(src)[1]\n    step_range = get_absolute_step_range(results.partitions, index)\n    IS.@assert_op num_columns % length(step_range) == 0\n    num_columns_per_step = num_columns ÷ length(step_range)\n    skip_offset = get_valid_step_offset(results.partitions, index) - 1\n    src_start = 1 + num_columns_per_step * skip_offset\n    len = get_valid_step_length(results.partitions, index) * num_columns_per_step\n    src_end = src_start + len - 1\n\n    IS.@assert_op ndims(src) == ndims(dst)\n    dst_start = HDF5.attrs(dst)[_TEMP_WRITE_POSITION]\n    if ndims(src) == 2\n        IS.@assert_op size(src)[2] == size(dst)[2]\n        dst_end = dst_start + len - 1\n        dst[dst_start:dst_end, :] = src[src_start:src_end, :]\n    else\n        error(\"Unsupported dataset ndims: $(ndims(src))\")\n    end\n\n    HDF5.attrs(dst)[_TEMP_WRITE_POSITION] = dst_end + 1\n    return\nend\n\nfunction _merge_dataset_rows!(results::SimulationPartitionResults, index, src, dst)\n    num_rows = size(src)[end]\n    step_range = get_absolute_step_range(results.partitions, index)\n    IS.@assert_op num_rows % length(step_range) == 0\n    num_rows_per_step = num_rows ÷ length(step_range)\n    skip_offset = get_valid_step_offset(results.partitions, index) - 1\n    src_start = 1 + num_rows_per_step * skip_offset\n    len = get_valid_step_length(results.partitions, index) * num_rows_per_step\n    src_end = src_start + len - 1\n\n    IS.@assert_op ndims(src) == ndims(dst)\n    dst_start = HDF5.attrs(dst)[_TEMP_WRITE_POSITION]\n    if ndims(src) == 2\n        IS.@assert_op size(src)[1] == size(dst)[1]\n        dst_end = dst_start + len - 1\n        dst[:, dst_start:dst_end] = src[:, src_start:src_end]\n    elseif ndims(src) == 3\n        IS.@assert_op size(src)[1] == size(dst)[1]\n        IS.@assert_op size(src)[2] == size(dst)[2]\n        dst_end = dst_start + len - 1\n        IS.@assert_op dst_end <= size(dst)[3]\n        dst[:, :, dst_start:dst_end] = src[:, :, src_start:src_end]\n    else\n        error(\"Unsupported dataset ndims: $(ndims(src))\")\n    end\n\n    HDF5.attrs(dst)[_TEMP_WRITE_POSITION] = dst_end + 1\n    return\nend\n\nfunction _complete(results::SimulationPartitionResults, status)\n    serialize_status(status, joinpath(results.path, \"results\"))\n    store_path = _store_path(results)\n    IS.compute_file_hash(dirname(store_path), basename(store_path))\n    return\nend\n"
  },
  {
    "path": "src/simulation/simulation_partitions.jl",
    "content": "\"\"\"\nDefines how a simulation can be partition into partitions and run in parallel.\n\"\"\"\nstruct SimulationPartitions <: IS.InfrastructureSystemsType\n    \"Number of steps in the simulation\"\n    num_steps::Int\n    \"Number of steps in each partition\"\n    period::Int\n    \"Number of steps that a partition overlaps with the previous partition\"\n    num_overlap_steps::Int\n\n    function SimulationPartitions(num_steps, period, num_overlap_steps = 1)\n        if num_overlap_steps > period\n            error(\n                \"period=$period must be greater than num_overlap_steps=$num_overlap_steps\",\n            )\n        end\n        if period >= num_steps\n            error(\"period=$period must be less than simulation steps=$num_steps\")\n        end\n        return new(num_steps, period, num_overlap_steps)\n    end\nend\n\nfunction SimulationPartitions(; num_steps, period, num_overlap_steps)\n    return SimulationPartitions(num_steps, period, num_overlap_steps)\nend\n\n\"\"\"\nReturn the number of partitions in the simulation.\n\"\"\"\nget_num_partitions(x::SimulationPartitions) = Int(ceil(x.num_steps / x.period))\n\n\"\"\"\nReturn a UnitRange for the steps in the partition with the given index. Includes overlap.\n\"\"\"\nfunction get_absolute_step_range(partitions::SimulationPartitions, index::Int)\n    num_partitions = _check_partition_index(partitions, index)\n    start_index = partitions.period * (index - 1) + 1\n    if index < num_partitions\n        end_index = start_index + partitions.period - 1\n    else\n        end_index = partitions.num_steps\n    end\n\n    if index > 1\n        start_index -= partitions.num_overlap_steps\n    end\n\n    return start_index:end_index\nend\n\n\"\"\"\nReturn the step offset for valid data at the given index.\n\"\"\"\nfunction get_valid_step_offset(partitions::SimulationPartitions, index::Int)\n    _check_partition_index(partitions, index)\n    return index == 1 ? 1 : partitions.num_overlap_steps + 1\nend\n\n\"\"\"\nReturn the length of valid data at the given index.\n\"\"\"\nfunction get_valid_step_length(partitions::SimulationPartitions, index::Int)\n    num_partitions = _check_partition_index(partitions, index)\n    if index < num_partitions\n        return partitions.period\n    end\n\n    remainder = partitions.num_steps % partitions.period\n    return remainder == 0 ? partitions.period : remainder\nend\n\nfunction _check_partition_index(partitions::SimulationPartitions, index::Int)\n    num_partitions = get_num_partitions(partitions)\n    if index <= 0 || index > num_partitions\n        error(\"index=$index=inde must be > 0 and <= $num_partitions\")\n    end\n\n    return num_partitions\nend\n\nfunction IS.serialize(partitions::SimulationPartitions)\n    return IS.serialize_struct(partitions)\nend\n\nfunction process_simulation_partition_cli_args(build_function, execute_function, args...)\n    length(args) < 2 && error(\"Usage: setup|execute|join [options]\")\n    function config_logging(filename)\n        return IS.configure_logging(;\n            console = true,\n            console_stream = stderr,\n            console_level = Logging.Warn,\n            file = true,\n            filename = filename,\n            file_level = Logging.Info,\n            file_mode = \"w\",\n            tracker = nothing,\n            set_global = true,\n        )\n    end\n\n    function throw_if_missing(actual, required, label)\n        diff = setdiff(required, actual)\n        !isempty(diff) && error(\"Missing required options for $label: $diff\")\n    end\n\n    operation = args[1]\n    options = Dict{String, String}()\n    for opt in args[2:end]\n        !startswith(opt, \"--\") && error(\"All options must start with '--': $opt\")\n        fields = split(opt[3:end], \"=\")\n        length(fields) != 2 && error(\"All options must use the format --name=value: $opt\")\n        options[fields[1]] = fields[2]\n    end\n\n    if haskey(options, \"output-dir\")\n        output_dir = options[\"output-dir\"]\n    elseif haskey(ENV, \"JADE_RUNTIME_OUTPUT\")\n        output_dir = joinpath(ENV[\"JADE_RUNTIME_OUTPUT\"], \"job-outputs\")\n    else\n        error(\"output-dir must be specified as a CLI option or environment variable\")\n    end\n\n    if operation == \"setup\"\n        required = Set((\"simulation-name\", \"num-steps\", \"num-period-steps\"))\n        throw_if_missing(keys(options), required, operation)\n        if !haskey(options, \"num-overlap-steps\")\n            options[\"num-overlap-steps\"] = \"0\"\n        end\n\n        num_steps = parse(Int, options[\"num-steps\"])\n        num_period_steps = parse(Int, options[\"num-period-steps\"])\n        num_overlap_steps = parse(Int, options[\"num-overlap-steps\"])\n        partitions = SimulationPartitions(num_steps, num_period_steps, num_overlap_steps)\n        config_logging(joinpath(output_dir, \"setup_partition_simulation.log\"))\n        build_function(output_dir, options[\"simulation-name\"], partitions)\n    elseif operation == \"execute\"\n        throw_if_missing(keys(options), Set((\"simulation-name\", \"index\")), operation)\n        index = parse(Int, options[\"index\"])\n        base_dir = joinpath(output_dir, options[\"simulation-name\"])\n        partition_output_dir = joinpath(base_dir, \"simulation_partitions\", string(index))\n        config_file = joinpath(base_dir, \"simulation_partitions\", \"config.json\")\n        config = open(config_file, \"r\") do io\n            JSON3.read(io, Dict)\n        end\n        partitions = IS.deserialize(SimulationPartitions, config)\n        config_logging(joinpath(partition_output_dir, \"run_partition_simulation.log\"))\n        sim = build_function(\n            partition_output_dir,\n            options[\"simulation-name\"],\n            partitions,\n            index,\n        )\n        execute_function(sim)\n    elseif operation == \"join\"\n        throw_if_missing(keys(options), Set((\"simulation-name\",)), operation)\n        base_dir = joinpath(output_dir, options[\"simulation-name\"])\n        config_file = joinpath(base_dir, \"simulation_partitions\", \"config.json\")\n        config = open(config_file, \"r\") do io\n            JSON3.read(io, Dict)\n        end\n        partitions = IS.deserialize(SimulationPartitions, config)\n        config_logging(joinpath(base_dir, \"logs\", \"join_partitioned_simulation.log\"))\n        join_simulation(base_dir)\n    else\n        error(\"Unsupported operation=$operation\")\n    end\n\n    return\nend\n\n\"\"\"\nRun a partitioned simulation in parallel on a local computer.\n\n# Arguments\n\n  - `build_function`: Function reference that returns a built Simulation.\n  - `execute_function`: Function reference that executes a Simulation.\n  - `script::AbstractString`: Path to script that includes ``build_function`` and ``execute_function``.\n  - `output_dir::AbstractString`: Path for simulation outputs\n  - `name::AbstractString`: Simulation name\n  - `num_steps::Integer`: Total number of steps in the simulation\n  - `period::Integer`: Number of steps in each simulation partition\n  - `num_overlap_steps::Integer`: Number of steps that each partition overlaps with the previous partition\n  - `num_parallel_processes`: Number of partitions to run in parallel. If nothing, use the number of cores.\n  - `exeflags`: Path to Julia project. Forwarded to Distributed.addprocs.\n  - `force`: Overwrite the output directory if it already exists.\n\"\"\"\nfunction run_parallel_simulation(\n    build_function,\n    execute_function;\n    script::AbstractString,\n    output_dir::AbstractString,\n    name::AbstractString,\n    num_steps::Integer,\n    period::Integer,\n    num_overlap_steps::Integer = 1,\n    num_parallel_processes = nothing,\n    exeflags = nothing,\n    force = false,\n)\n    if isnothing(num_parallel_processes)\n        num_parallel_processes = Sys.CPU_THREADS\n    end\n\n    partitions = SimulationPartitions(num_steps, period, num_overlap_steps)\n    num_partitions = get_num_partitions(partitions)\n    if isdir(output_dir)\n        if !force\n            error(\n                \"output_dir=$output_dir already exists. Choose a different name or set force=true.\",\n            )\n        end\n        rm(output_dir; recursive = true)\n    end\n    mkdir(output_dir)\n    @info \"Run parallel simulation\" name script output_dir num_steps num_partitions num_parallel_processes\n\n    args = [\n        \"setup\",\n        \"--simulation-name=$name\",\n        \"--num-steps=$(partitions.num_steps)\",\n        \"--num-period-steps=$(partitions.period)\",\n        \"--num-overlap-steps=$(partitions.num_overlap_steps)\",\n        \"--output-dir=$output_dir\",\n    ]\n    parent_module_name = nameof(parentmodule(build_function))\n    build_func_name = nameof(build_function)\n    execute_func_name = nameof(execute_function)\n    process_simulation_partition_cli_args(build_function, execute_function, args...)\n    jobs = Vector{Dict}(undef, num_partitions)\n    for i in 1:num_partitions\n        args = Dict(\n            \"parent_module\" => parent_module_name,\n            \"build_function\" => build_func_name,\n            \"execute_function\" => execute_func_name,\n            \"args\" => [\n                \"execute\",\n                \"--simulation-name=$name\",\n                \"--index=$i\",\n                \"--output-dir=$output_dir\",\n            ],\n        )\n        jobs[i] = args\n    end\n\n    if isnothing(exeflags)\n        Distributed.addprocs(num_parallel_processes)\n    else\n        Distributed.addprocs(num_parallel_processes; exeflags = exeflags)\n    end\n\n    Distributed.@everywhere include($script)\n    try\n        Distributed.pmap(PowerSimulations._run_parallel_simulation, jobs)\n    finally\n        Distributed.rmprocs(Distributed.workers()...)\n    end\n\n    args = [\"join\", \"--simulation-name=$name\", \"--output-dir=$output_dir\"]\n    process_simulation_partition_cli_args(build_function, execute_function, args...)\nend\n\nfunction _run_parallel_simulation(params)\n    start = time()\n    if params[\"parent_module\"] == :Main\n        parent_module = Main\n    else\n        # TODO: not tested\n        parent_module = Base.root_module(Base.__toplevel__, Symbol(params[\"parent_module\"]))\n    end\n    result = process_simulation_partition_cli_args(\n        getproperty(parent_module, params[\"build_function\"]),\n        getproperty(parent_module, params[\"execute_function\"]),\n        params[\"args\"]...,\n    )\n    duration = time() - start\n    args = params[\"args\"]\n    @info \"Completed partition\" args duration\n    return result\nend\n"
  },
  {
    "path": "src/simulation/simulation_problem_results.jl",
    "content": "abstract type OperationModelSimulationResults end\n# Subtypes need to implement the following methods for SimulationProblemResults{T}\n# - read_results_with_keys\n# - list_aux_variable_keys\n# - list_dual_keys\n# - list_expression_keys\n# - list_parameter_keys\n# - list_variable_keys\n# - load_results!\n\n\"\"\"\nHolds the results of a simulation problem for plotting or exporting.\n\"\"\"\nmutable struct SimulationProblemResults{T} <:\n               IS.Results where {T <: OperationModelSimulationResults}\n    problem::String\n    base_power::Float64\n    execution_path::String\n    results_output_folder::String\n    timestamps::StepRange{Dates.DateTime, Dates.Millisecond}\n    results_timestamps::Vector{Dates.DateTime}\n    values::T\n    system::Union{Nothing, PSY.System}\n    system_uuid::Base.UUID\n    resolution::Dates.TimePeriod\n    store::Union{Nothing, SimulationStore}\nend\n\nfunction SimulationProblemResults{T}(\n    store::SimulationStore,\n    model_name::AbstractString,\n    problem_params::ModelStoreParams,\n    sim_params::SimulationStoreParams,\n    path,\n    vals::T;\n    results_output_path = nothing,\n    system = nothing,\n) where {T <: OperationModelSimulationResults}\n    if results_output_path === nothing\n        results_output_path = joinpath(path, \"results\")\n    end\n\n    time_steps = range(\n        sim_params.initial_time;\n        length = problem_params.num_executions * sim_params.num_steps,\n        step = problem_params.interval,\n    )\n    return SimulationProblemResults{T}(\n        model_name,\n        problem_params.base_power,\n        path,\n        results_output_path,\n        time_steps,\n        Vector{Dates.DateTime}(),\n        vals,\n        system,\n        problem_params.system_uuid,\n        get_resolution(problem_params),\n        store isa HdfSimulationStore ? nothing : store,\n    )\nend\n\nget_model_name(res::SimulationProblemResults) = res.problem\nget_system(res::SimulationProblemResults) = res.system\nget_source_data(res::SimulationProblemResults) = get_system(res)  # Needed for compatibility with the IS.Results interface\nget_resolution(res::SimulationProblemResults) = res.resolution\nget_execution_path(res::SimulationProblemResults) = res.execution_path\nget_model_base_power(res::SimulationProblemResults) = res.base_power\nget_system_uuid(results::PSI.SimulationProblemResults) = results.system_uuid\nIS.get_timestamp(result::SimulationProblemResults) = result.results_timestamps\nget_interval(res::SimulationProblemResults) = res.timestamps.step\nIS.get_base_power(result::SimulationProblemResults) = result.base_power\nget_output_dir(res::SimulationProblemResults) = res.results_output_folder\n\nget_results_timestamps(result::SimulationProblemResults) = result.results_timestamps\nfunction set_results_timestamps!(\n    result::SimulationProblemResults,\n    results_timestamps::Vector{Dates.DateTime},\n)\n    result.results_timestamps = results_timestamps\nend\n\nlist_result_keys(res::SimulationProblemResults, ::AuxVarKey) =\n    list_aux_variable_keys(res)\nlist_result_keys(res::SimulationProblemResults, ::ConstraintKey) =\n    list_dual_keys(res)\nlist_result_keys(res::SimulationProblemResults, ::ExpressionKey) =\n    list_expression_keys(res)\nlist_result_keys(res::SimulationProblemResults, ::ParameterKey) =\n    list_parameter_keys(res)\nlist_result_keys(res::SimulationProblemResults, ::VariableKey) =\n    list_variable_keys(res)\n\nget_cached_results(res::SimulationProblemResults, ::Type{<:AuxVarKey}) =\n    get_cached_aux_variables(res)\nget_cached_results(res::SimulationProblemResults, ::Type{<:ConstraintKey}) =\n    get_cached_duals(res)\nget_cached_results(res::SimulationProblemResults, ::Type{<:ExpressionKey}) =\n    get_cached_expressions(res)\nget_cached_results(res::SimulationProblemResults, ::Type{<:ParameterKey}) =\n    get_cached_parameters(res)\nget_cached_results(res::SimulationProblemResults, ::Type{<:VariableKey}) =\n    get_cached_variables(res)\nget_cached_results(\n    res::SimulationProblemResults,\n    ::Type{<:OptimizationContainerKey} = OptimizationContainerKey,\n) =\n    merge(  # PERF: could be done lazily\n        get_cached_aux_variables(res),\n        get_cached_duals(res),\n        get_cached_expressions(res),\n        get_cached_parameters(res),\n        get_cached_variables(res),\n    )\n\n\"\"\"\nReturn an array of variable names (strings) that are available for reads.\n\"\"\"\nlist_variable_names(res::SimulationProblemResults) =\n    encode_keys_as_strings(list_variable_keys(res))\n\n\"\"\"\nReturn an array of dual names (strings) that are available for reads.\n\"\"\"\nlist_dual_names(res::SimulationProblemResults) =\n    encode_keys_as_strings(list_dual_keys(res))\n\n\"\"\"\nReturn an array of parmater names (strings) that are available for reads.\n\"\"\"\nlist_parameter_names(res::SimulationProblemResults) =\n    encode_keys_as_strings(list_parameter_keys(res))\n\n\"\"\"\nReturn an array of auxillary variable names (strings) that are available for reads.\n\"\"\"\nlist_aux_variable_names(res::SimulationProblemResults) =\n    encode_keys_as_strings(list_aux_variable_keys(res))\n\n\"\"\"\nReturn an array of expression names (strings) that are available for reads.\n\"\"\"\nlist_expression_names(res::SimulationProblemResults) =\n    encode_keys_as_strings(list_expression_keys(res))\n\n\"\"\"\nReturn a reference to a StepRange of available timestamps.\n\"\"\"\nget_timestamps(result::SimulationProblemResults) = result.timestamps\n\n\"\"\"\nReturn the system used for the problem. If the system hasn't already been deserialized or\nset with [`set_system!`](@ref) then deserialize and store it.\n\nIf the simulation was configured to serialize all systems to file then the returned system\nwill include all data. If that was not configured then the returned system will include\nall data except time series data.\n\"\"\"\nfunction get_system!(\n    results::Union{OptimizationProblemResults, SimulationProblemResults};\n    kwargs...,\n)\n    !isnothing(get_system(results)) && return get_system(results)\n\n    file = locate_system_file(results)\n    # This flag should remain unpublished because it should never be needed\n    # by the general audience.\n    if !get(kwargs, :use_system_fallback, false) && isfile(file)\n        system = PSY.System(file; time_series_read_only = true)\n        @info \"De-serialized the system from files.\"\n    else\n        system = get_system_fallback(results)\n    end\n\n    set_system!(results, system)\n    return get_system(results)\nend\n\nget_system_fallback(results::SimulationProblemResults) =\n    _deserialize_system(results, results.store)\nget_system_fallback(results::OptimizationProblemResults) = error(\"Could not locate system\")\n\nlocate_system_file(results::SimulationProblemResults) = joinpath(\n    get_execution_path(results),\n    \"problems\",\n    get_model_name(results),\n    make_system_filename(results.system_uuid),\n)\n\nlocate_system_file(results::OptimizationProblemResults) = joinpath(\n    ISOPT.get_results_dir(results),\n    make_system_filename(ISOPT.get_source_data_uuid(results)),\n)\n\nget_system(results::OptimizationProblemResults) = ISOPT.get_source_data(results)\n\nset_system!(results::OptimizationProblemResults, system) =\n    ISOPT.set_source_data!(results, system)\n\nfunction _deserialize_system(results::SimulationProblemResults, ::Nothing)\n    open_store(\n        HdfSimulationStore,\n        joinpath(get_execution_path(results), \"data_store\"),\n        \"r\",\n    ) do store\n        system = deserialize_system(store, results.system_uuid)\n        @info \"De-serialized the system from the simulation store. The system does \" *\n              \"not include time series data.\"\n        return system\n    end\nend\n\nfunction _deserialize_system(::SimulationProblemResults, ::InMemorySimulationStore)\n    # This should never be necessary because the system is guaranteed to be in memory.\n    error(\"Deserializing a system from the InMemorySimulationStore is not supported.\")\nend\n\n\"\"\"\nSet the system in the results instance.\n\nThrows InvalidValue if the system UUID is incorrect.\n\n# Arguments\n\n  - `results::SimulationProblemResults`: Results object\n  - `system::AbstractString`: Path to the system json file\n\n# Examples\n\n```julia\njulia > set_system!(res, \"my_path/system_data.json\")\n```\n\"\"\"\nfunction set_system!(results::SimulationProblemResults, system::AbstractString)\n    set_system!(results, System(system))\nend\n\nfunction set_system!(results::SimulationProblemResults, system::PSY.System)\n    sys_uuid = IS.get_uuid(system)\n    if sys_uuid != results.system_uuid\n        throw(\n            IS.InvalidValue(\n                \"System mismatch. $sys_uuid does not match the stored value of $(results.system_uuid)\",\n            ),\n        )\n    end\n\n    results.system = system\n    return\nend\n\nfunction _deserialize_key(\n    ::Type{<:OptimizationContainerKey},\n    results::SimulationProblemResults,\n    name::AbstractString,\n)\n    !haskey(results.values.container_key_lookup, name) && error(\"$name is not stored\")\n    return results.values.container_key_lookup[name]\nend\n\nfunction _deserialize_key(\n    ::Type{T},\n    results::SimulationProblemResults,\n    args...,\n) where {T <: OptimizationContainerKey}\n    return ISOPT.make_key(T, args...)\nend\n\nget_container_fields(x::SimulationProblemResults) =\n    (:aux_variables, :duals, :expressions, :parameters, :variables)\n\n\"\"\"\nReturn the final values for the requested variables for each time step for a problem.\n\nDecision problem results are returned in a Dict{String, Dict{DateTime, DataFrame}}.\n\nEmulation problem results are returned in a Dict{String, DataFrame}.\n\nLimit the data sizes returned by specifying `initial_time` and `count` for decision problems\nor `start_time` and `len` for emulation problems.\n\nIf the Julia process is started with multiple threads, the code will read the variables in\nparallel.\n\nSee also [`load_results!`](@ref) to preload data into memory.\n\n# Arguments\n\n  - `variables::Vector{Union{String, Tuple}}`: Variable name as a string or a Tuple with\n    variable type and device type. If not provided then return all variables.\n  - `initial_time::Dates.DateTime`: Initial time of the requested results. Decision problems\n    only.\n  - `count::Int`: Number of results. Decision problems only.\n  - `start_time::Dates.DateTime`: Start time of the requested results. Emulation problems\n    only.\n  - `len::Int`: Number of rows in each DataFrame. Emulation problems only.\n  - `table_format::TableFormat`: Format of the table to be returned. Default is\n    `TableFormat.LONG` where the columns are `DateTime`, `name`, and `value` when the data\n    has two dimensions and `DateTime`, `name`, `name2`, and `value` when the data has three\n    dimensions.\n    Set to it `TableFormat.WIDE` to pivot the names as columns, matching earlier versions\n    of PowerSimulations.jl.\n    Note: `TableFormat.WIDE` is not supported when the data has three dimensions.\n\n# Examples\n\n```julia\njulia> variables_as_strings =\n    [\"ActivePowerVariable__ThermalStandard\", \"ActivePowerVariable__RenewableDispatch\"]\njulia> variables_as_types =\n    [(ActivePowerVariable, ThermalStandard), (ActivePowerVariable, RenewableDispatch)]\njulia> df_long =read_realized_variables(results, variables_as_strings)\njulia> df_long = read_realized_variables(results, variables_as_types)\njulia> df_wide = read_realized_variables(results, variables_as_types, table_format = TableFormat.WIDE)\njulia> using DataFramesMeta\njulia> df_agg_generators = @chain df_long begin\n    @groupby(:DateTime)\n    @combine(:value = sum(:value))\nend\n```\n\"\"\"\nfunction read_realized_variables(res::SimulationProblemResults; kwargs...)\n    return read_realized_variables(res, list_variable_keys(res); kwargs...)\nend\n\nfunction read_realized_variables(\n    res::SimulationProblemResults,\n    variables::Vector{Tuple{DataType, DataType}};\n    kwargs...,\n)\n    return read_realized_variables(\n        res,\n        [VariableKey(x...) for x in variables];\n        kwargs...,\n    )\nend\n\nfunction read_realized_variables(\n    res::SimulationProblemResults,\n    variables::Vector{<:AbstractString};\n    kwargs...,\n)\n    return read_realized_variables(\n        res,\n        [_deserialize_key(VariableKey, res, x) for x in variables];\n        kwargs...,\n    )\nend\n\nfunction read_realized_variables(\n    res::SimulationProblemResults,\n    variables::Vector{<:OptimizationContainerKey};\n    kwargs...,\n)\n    result_values = read_results_with_keys(res, variables; kwargs...)\n    return Dict(encode_key_as_string(k) => v for (k, v) in result_values)\nend\n\n\"\"\"\nReturn the final values for the requested variable for each time step for a problem.\n\nDecision problem results are returned in a Dict{DateTime, DataFrame}.\n\nEmulation problem results are returned in a DataFrame.\n\nLimit the data sizes returned by specifying `initial_time` and `count` for decision problems\nor `start_time` and `len` for emulation problems.\n\nSee also [`load_results!`](@ref) to preload data into memory.\n\n# Arguments\n\n  - `variable::Union{String, Tuple}`: Variable name as a string or a Tuple with\n    variable type and device type.\n  - `initial_time::Dates.DateTime`: Initial time of the requested results. Decision problems\n    only.\n  - `count::Int`: Number of results. Decision problems only.\n  - `start_time::Dates.DateTime`: Start time of the requested results. Emulation problems\n    only.\n  - `len::Int`: Number of rows in each DataFrame. Emulation problems only.\n  - `table_format::TableFormat`: Format of the table to be returned. Default is\n    `TableFormat.LONG` where the columns are `DateTime`, `name`, and `value` when the data\n    has two dimensions and `DateTime`, `name`, `name2`, and `value` when the data has three\n    dimensions.\n    Set to it `TableFormat.WIDE` to pivot the names as columns.\n    Note: `TableFormat.WIDE` is not supported when the data has three dimensions.\n\n# Examples\n\n```julia\njulia > read_realized_variable(results, \"ActivePowerVariable__ThermalStandard\")\njulia > read_realized_variable(results, (ActivePowerVariable, ThermalStandard))\njulia > read_realized_variable(results, (ActivePowerVariable, ThermalStandard), table_format = TableFormat.WIDE)\n```\n\"\"\"\nfunction read_realized_variable(\n    res::SimulationProblemResults,\n    variable::AbstractString;\n    kwargs...,\n)\n    return first(\n        values(\n            read_realized_variables(\n                res,\n                [_deserialize_key(VariableKey, res, variable)];\n                kwargs...,\n            ),\n        ),\n    )\nend\n\nfunction read_realized_variable(res::SimulationProblemResults, variable...; kwargs...)\n    return first(\n        values(read_realized_variables(res, [VariableKey(variable...)]; kwargs...)),\n    )\nend\n\n\"\"\"\nReturn the final values for the requested auxiliary variables for each time step for a problem.\n\nRefer to [`read_realized_aux_variables`](@ref) for help and examples.\n\"\"\"\nfunction read_realized_aux_variables(res::SimulationProblemResults; kwargs...)\n    return read_realized_aux_variables(\n        res,\n        list_aux_variable_keys(res);\n        kwargs...,\n    )\nend\n\nfunction read_realized_aux_variables(\n    res::SimulationProblemResults,\n    aux_variables::Vector{Tuple{DataType, DataType}};\n    kwargs...,\n)\n    return read_realized_aux_variables(\n        res,\n        [AuxVarKey(x...) for x in aux_variables];\n        kwargs...,\n    )\nend\n\nfunction read_realized_aux_variables(\n    res::SimulationProblemResults,\n    aux_variables::Vector{<:AbstractString};\n    kwargs...,\n)\n    return read_realized_aux_variables(\n        res,\n        [_deserialize_key(AuxVarKey, res, x) for x in aux_variables];\n        kwargs...,\n    )\nend\n\nfunction read_realized_aux_variables(\n    res::SimulationProblemResults,\n    aux_variables::Vector{<:OptimizationContainerKey};\n    kwargs...,\n)\n    result_values = read_results_with_keys(res, aux_variables; kwargs...)\n    return Dict(encode_key_as_string(k) => v for (k, v) in result_values)\nend\n\n\"\"\"\nReturn the final values for the requested auxiliary variable for each time step for a problem.\n\nRefer to [`read_realized_variable`](@ref) for help and examples.\n\"\"\"\nfunction read_realized_aux_variable(\n    res::SimulationProblemResults,\n    aux_variable::AbstractString;\n    kwargs...,\n)\n    return first(\n        values(\n            read_realized_aux_variables(\n                res,\n                [_deserialize_key(AuxVarKey, res, aux_variable)];\n                kwargs...,\n            ),\n        ),\n    )\nend\n\nfunction read_realized_aux_variable(\n    res::SimulationProblemResults,\n    aux_variable...;\n    kwargs...,\n)\n    return first(\n        values(\n            read_realized_aux_variables(res, [AuxVarKey(aux_variable...)]; kwargs...),\n        ),\n    )\nend\n\n\"\"\"\nReturn the final values for the requested parameters for each time step for a problem.\n\nRefer to [`read_realized_parameters`](@ref) for help and examples.\n\"\"\"\nfunction read_realized_parameters(res::SimulationProblemResults; kwargs...)\n    return read_realized_parameters(res, list_parameter_keys(res); kwargs...)\nend\n\nfunction read_realized_parameters(\n    res::SimulationProblemResults,\n    parameters::Vector{Tuple{DataType, DataType}};\n    kwargs...,\n)\n    return read_realized_parameters(\n        res,\n        [ParameterKey(x...) for x in parameters];\n        kwargs...,\n    )\nend\n\nfunction read_realized_parameters(\n    res::SimulationProblemResults,\n    parameters::Vector{<:AbstractString};\n    kwargs...,\n)\n    return read_realized_parameters(\n        res,\n        [_deserialize_key(ParameterKey, res, x) for x in parameters];\n        kwargs...,\n    )\nend\n\nfunction read_realized_parameters(\n    res::SimulationProblemResults,\n    parameters::Vector{<:OptimizationContainerKey};\n    kwargs...,\n)\n    result_values = read_results_with_keys(res, parameters; kwargs...)\n    return Dict(encode_key_as_string(k) => v for (k, v) in result_values)\nend\n\n\"\"\"\nReturn the final values for the requested parameter for each time step for a problem.\n\nRefer to [`read_realized_variable`](@ref) for help and examples.\n\"\"\"\nfunction read_realized_parameter(\n    res::SimulationProblemResults,\n    parameter::AbstractString;\n    kwargs...,\n)\n    return first(\n        values(\n            read_realized_parameters(\n                res,\n                [_deserialize_key(ParameterKey, res, parameter)];\n                kwargs...,\n            ),\n        ),\n    )\nend\n\nfunction read_realized_parameter(res::SimulationProblemResults, parameter...; kwargs...)\n    return first(\n        values(read_realized_parameters(res, [ParameterKey(parameter...)]; kwargs...)),\n    )\nend\n\n\"\"\"\nReturn the final values for the requested duals for each time step for a problem.\n\nRefer to [`read_realized_duals`](@ref) for help and examples.\n\"\"\"\nfunction read_realized_duals(res::SimulationProblemResults; kwargs...)\n    return read_realized_duals(res, list_dual_keys(res); kwargs...)\nend\n\nfunction read_realized_duals(\n    res::SimulationProblemResults,\n    duals::Vector{Tuple{DataType, DataType}};\n    kwargs...,\n)\n    return read_realized_duals(res, [ConstraintKey(x...) for x in duals]; kwargs...)\nend\n\nfunction read_realized_duals(\n    res::SimulationProblemResults,\n    duals::Vector{<:AbstractString};\n    kwargs...,\n)\n    return read_realized_duals(\n        res,\n        [_deserialize_key(ConstraintKey, res, x) for x in duals];\n        kwargs...,\n    )\nend\n\nfunction read_realized_duals(\n    res::SimulationProblemResults,\n    duals::Vector{<:OptimizationContainerKey};\n    kwargs...,\n)\n    result_values = read_results_with_keys(res, duals; kwargs...)\n    return Dict(encode_key_as_string(k) => v for (k, v) in result_values)\nend\n\n\"\"\"\nReturn the final values for the requested dual for each time step for a problem.\n\nRefer to [`read_realized_variable`](@ref) for help and examples.\n\"\"\"\nfunction read_realized_dual(res::SimulationProblemResults, dual::AbstractString; kwargs...)\n    return first(\n        values(\n            read_realized_duals(\n                res,\n                [_deserialize_key(ConstraintKey, res, dual)];\n                kwargs...,\n            ),\n        ),\n    )\nend\n\nfunction read_realized_dual(res::SimulationProblemResults, dual...; kwargs...)\n    return first(values(read_realized_duals(res, [ConstraintKey(dual...)]; kwargs...)))\nend\n\n\"\"\"\nReturn the final values for the requested expressions for each time step for a problem.\n\nRefer to [`read_realized_expressions`](@ref) for help and examples.\n\"\"\"\nfunction read_realized_expressions(res::SimulationProblemResults; kwargs...)\n    return read_realized_expressions(res, list_expression_keys(res); kwargs...)\nend\n\nfunction read_realized_expressions(\n    res::SimulationProblemResults,\n    expressions::Vector{Tuple{DataType, DataType}};\n    kwargs...,\n)\n    return read_realized_expressions(\n        res,\n        [ExpressionKey(x...) for x in expressions];\n        kwargs...,\n    )\nend\n\nfunction read_realized_expressions(\n    res::SimulationProblemResults,\n    expressions::Vector{<:AbstractString};\n    kwargs...,\n)\n    return read_realized_expressions(\n        res,\n        [_deserialize_key(ExpressionKey, res, x) for x in expressions];\n        kwargs...,\n    )\nend\n\nfunction read_realized_expressions(\n    res::SimulationProblemResults,\n    expressions::Vector{<:OptimizationContainerKey};\n    kwargs...,\n)\n    result_values = read_results_with_keys(res, expressions; kwargs...)\n    return Dict(encode_key_as_string(k) => v for (k, v) in result_values)\nend\n\n\"\"\"\nReturn the final values for the requested expression for each time step for a problem.\n\nRefer to [`read_realized_variable`](@ref) for help and examples.\n\"\"\"\nfunction read_realized_expression(\n    res::SimulationProblemResults,\n    expression::AbstractString;\n    kwargs...,\n)\n    return first(\n        values(\n            read_realized_expressions(\n                res,\n                [_deserialize_key(ExpressionKey, res, expression)];\n                kwargs...,\n            ),\n        ),\n    )\nend\n\nfunction read_realized_expression(res::SimulationProblemResults, expression...; kwargs...)\n    return first(\n        values(\n            read_realized_expressions(res, [ExpressionKey(expression...)]; kwargs...),\n        ),\n    )\nend\n\n\"\"\"\nReturn the optimizer stats for the problem as a DataFrame.\n\n# Accepted keywords\n\n  - `store::SimulationStore`: a store that has been opened for reading\n\"\"\"\nfunction read_optimizer_stats(res::SimulationProblemResults; store = nothing)\n    _store = isnothing(store) ? res.store : store\n    return _read_optimizer_stats(res, _store)\nend\n\nfunction _read_optimizer_stats(res::SimulationProblemResults, ::Nothing)\n    open_store(\n        HdfSimulationStore,\n        joinpath(get_execution_path(res), \"data_store\"),\n        \"r\",\n    ) do store\n        _read_optimizer_stats(res, store)\n    end\nend\n\n# Chooses the user-passed store or results store for reading values. Either could be\n# something or nothing. If both are nothing, we must open the HDF5 store.\ntry_resolve_store(user::SimulationStore, results::Union{Nothing, SimulationStore}) = user\ntry_resolve_store(user::Nothing, results::SimulationStore) = results\ntry_resolve_store(user::Nothing, results::Nothing) = nothing\n"
  },
  {
    "path": "src/simulation/simulation_results.jl",
    "content": "function check_folder_integrity(folder::String)\n    folder_files = readdir(folder)\n    alien_files = setdiff(folder_files, KNOWN_SIMULATION_PATHS)\n    alien_files = filter(x -> !any(occursin.(IGNORABLE_FILES, x)), alien_files)\n    if isempty(alien_files)\n        return true\n    else\n        @warn \"Unrecognized simulation files: $(sort(alien_files))\"\n    end\n    if \"data_store\" ∉ folder_files\n        error(\"The file path doesn't contain any data_store folder\")\n    end\n    return false\nend\n\nstruct SimulationResults\n    path::String\n    params::SimulationStoreParams\n    decision_problem_results::Dict{\n        String,\n        SimulationProblemResults{DecisionModelSimulationResults},\n    }\n    emulation_problem_results::SimulationProblemResults{EmulationModelSimulationResults}\n    store::Union{Nothing, SimulationStore}\nend\n\nfunction SimulationResults(path::AbstractString, execution = nothing; ignore_status = false)\n    # This method maintains compatibility with the old interface as long as there is only\n    # one simulation name.\n    unique_names = Set{String}()\n    for name in readdir(path)\n        m = match(r\"(.*)-\\d+$\", name)\n        if isnothing(m)\n            push!(unique_names, name)\n        else\n            push!(unique_names, m.captures[1])\n        end\n    end\n\n    if length(unique_names) == 1\n        name = first(unique_names)\n        return SimulationResults(path, name, execution; ignore_status = ignore_status)\n    end\n\n    if \"data_store\" in readdir(path)\n        return SimulationResults(\n            dirname(path),\n            basename(path),\n            execution;\n            ignore_status = ignore_status,\n        )\n    end\n\n    error(\n        \"Found more than one simulation name in $path. Please call the constructor that includes 'name.'\",\n    )\nend\n\n\"\"\"\nConstruct SimulationResults from a simulation output directory.\n\n# Arguments\n\n  - `path::AbstractString`: Simulation output directory\n  - `name::AbstractString`: Simulation name\n  - `execution::AbstractString`: Execution number. Default is the most recent.\n  - `ignore_status::Bool`: If true, return results even if the simulation failed.\n\"\"\"\nfunction SimulationResults(\n    path::AbstractString,\n    name::AbstractString,\n    execution = nothing;\n    ignore_status = false,\n)\n    if isnothing(execution)\n        execution = _get_most_recent_execution(path, name)\n    end\n    if execution == 1\n        execution_path = joinpath(path, name)\n    else\n        execution_path = joinpath(path, \"$name-$execution\")\n    end\n    if !isdir(execution_path)\n        error(\"No valid simulation in $execution_path: execution = $execution\")\n    end\n\n    @info \"Loading simulation results from $execution_path\"\n    status = deserialize_status(joinpath(execution_path, RESULTS_DIR))\n    _check_status(status, ignore_status)\n\n    if !check_folder_integrity(execution_path)\n        @warn \"The results folder $(execution_path) is not consistent with the default folder structure. \" *\n              \"This can lead to errors or unwanted results.\"\n    end\n\n    simulation_store_path = joinpath(execution_path, \"data_store\")\n    check_file_integrity(simulation_store_path)\n\n    return open_store(HdfSimulationStore, simulation_store_path, \"r\") do store\n        decision_problem_results =\n            Dict{String, SimulationProblemResults{DecisionModelSimulationResults}}()\n        sim_params = get_params(store)\n        container_key_lookup = get_container_key_lookup(store)\n        for (name, problem_params) in sim_params.decision_models_params\n            name = string(name)\n            system = if has_system(store, get_system_uuid(problem_params))\n                deserialize_system(store, get_system_uuid(problem_params))\n            else\n                nothing\n            end\n            problem_result = SimulationProblemResults(\n                DecisionModel,\n                store,\n                name,\n                problem_params,\n                sim_params,\n                execution_path,\n                container_key_lookup;\n                system = system,\n            )\n            decision_problem_results[name] = problem_result\n        end\n\n        em_params = get_emulation_model_params(sim_params)\n        em_system = if has_system(store, get_system_uuid(em_params))\n            deserialize_system(store, get_system_uuid(em_params))\n        else\n            nothing\n        end\n        emulation_result = SimulationProblemResults(\n            EmulationModel,\n            store,\n            string(first(keys(sim_params.emulation_model_params))),\n            em_params,\n            sim_params,\n            execution_path,\n            container_key_lookup;\n            system = em_system,\n        )\n\n        return SimulationResults(\n            execution_path,\n            sim_params,\n            decision_problem_results,\n            emulation_result,\n            nothing,\n        )\n    end\nend\n\n\"\"\"\nConstruct SimulationResults from a simulation.\n\"\"\"\nfunction SimulationResults(sim::Simulation; ignore_status = false, kwargs...)\n    _check_status(get_simulation_status(sim), ignore_status)\n    store = get_simulation_store(sim)\n    execution_path = get_simulation_dir(sim)\n    decision_problem_results =\n        Dict{String, SimulationProblemResults{DecisionModelSimulationResults}}()\n    sim_params = get_params(store)\n    models = get_models(sim)\n    container_key_lookup = get_container_key_lookup(store)\n    for (name, problem_params) in sim_params.decision_models_params\n        model = get_simulation_model(models, name)\n        name = string(name)\n        problem_result = SimulationProblemResults(\n            DecisionModel,\n            store,\n            name,\n            problem_params,\n            sim_params,\n            execution_path,\n            container_key_lookup;\n            system = get_system(model),\n        )\n        decision_problem_results[name] = problem_result\n    end\n\n    emulation_model = get_emulation_model(models)\n    emulation_results = SimulationProblemResults(\n        EmulationModel,\n        store,\n        string(first(keys(sim_params.emulation_model_params))),\n        first(values(sim_params.emulation_model_params)),\n        sim_params,\n        execution_path,\n        container_key_lookup;\n        system = isnothing(emulation_model) ? nothing : get_system(emulation_model),\n    )\n\n    return SimulationResults(\n        execution_path,\n        sim_params,\n        decision_problem_results,\n        emulation_results,\n        store,\n    )\nend\n\n\"\"\"\n    Base.empty!(res::SimulationResults)\n\nEmpty the [`SimulationResults`](@ref)\n\"\"\"\nfunction Base.empty!(res::SimulationResults)\n    foreach(empty!, values(res.decision_problem_results))\n    empty!(res.emulation_problem_results)\nend\n\nBase.isempty(res::SimulationResults) = all(isempty, values(res.decision_problem_results))\nBase.length(res::SimulationResults) =\n    mapreduce(length, +, values(res.decision_problem_results))\nget_exports_folder(x::SimulationResults) = joinpath(x.path, \"exports\")\n\n\"\"\"\nReturn SimulationProblemResults corresponding to a SimulationResults\n\n# Arguments\n - `sim_results::PSI.SimulationResults`: the simulation results to read from\n - `problem::String`: the name of the problem (e.g., \"UC\", \"ED\")\n - `populate_system::Bool = true`: whether to set the results' system as if using\n   [`get_system!`](@ref)\n - `populate_units::Union{IS.UnitSystem, String, Nothing} = IS.UnitSystem.NATURAL_UNITS`:\n   the units system with which to populate the results' system, if any (requires\n   `populate_system=true`)\n\"\"\"\nfunction get_decision_problem_results(\n    results::SimulationResults,\n    problem::String;\n    populate_system::Bool = false,\n    populate_units::Union{IS.UnitSystem, String, Nothing} = nothing,\n)\n    if !haskey(results.decision_problem_results, problem)\n        throw(IS.InvalidValue(\"$problem is not stored\"))\n    end\n\n    results = results.decision_problem_results[problem]\n    _populate_system_in_results!(results, populate_system, populate_units)\n\n    return results\nend\n\n\"\"\"\nReturn SimulationProblemResults corresponding to a SimulationResults\n\n# Arguments\n - `sim_results::PSI.SimulationResults`: the simulation results to read from\n - `populate_system::Bool = true`: whether to set the results' system as if using\n   [`get_system!`](@ref)\n - `populate_units::Union{IS.UnitSystem, String, Nothing} = IS.UnitSystem.NATURAL_UNITS`:\n   the units system with which to populate the results' system, if any (requires\n   `populate_system=true`)\n\"\"\"\nfunction get_emulation_problem_results(\n    results::SimulationResults;\n    populate_system::Bool = false,\n    populate_units::Union{IS.UnitSystem, String, Nothing} = nothing,\n)\n    results = results.emulation_problem_results\n    _populate_system_in_results!(results, populate_system, populate_units)\n    return results\nend\n\nfunction _populate_system_in_results!(\n    results::SimulationProblemResults,\n    populate_system::Bool,\n    populate_units::Union{IS.UnitSystem, String, Nothing},\n)\n    if populate_system\n        try\n            get_system!(results)\n        catch e\n            error(\"Can't find the system file or retrieve the system error=$e\")\n        end\n\n        if populate_units !== nothing\n            PSY.set_units_base_system!(PSI.get_system(results), populate_units)\n        else\n            PSY.set_units_base_system!(PSI.get_system(results), IS.UnitSystem.NATURAL_UNITS)\n        end\n\n    else\n        (populate_units === nothing) ||\n            throw(\n                ArgumentError(\n                    \"populate_units=$populate_units is unaccepted when populate_system=$populate_system\",\n                ),\n            )\n    end\n    return\nend\n\n\"\"\"\nReturn the problem names in the simulation.\n\"\"\"\nlist_decision_problems(results::SimulationResults) =\n    collect(keys(results.decision_problem_results))\n\n\"\"\"\nExport results to files in the results directory.\n\n# Arguments\n\n  - `results::SimulationResults`: simulation results\n  - `exports`: SimulationResultsExport or anything that can be passed to its constructor.\n    (such as Dict or path to JSON file)\n\nAn example JSON file demonstrating possible options is below. Note that `start_time`,\n`end_time`, `path`, and `format` are optional.\n\n```\n{\n  \"decision_models\": [\n    {\n      \"name\": \"ED\",\n      \"variables\": [\n        \"P__ThermalStandard\",\n      ],\n      \"parameters\": [\n        \"all\"\n      ]\n    },\n    {\n      \"name\": \"UC\",\n      \"variables\": [\n        \"On__ThermalStandard\"\n      ],\n      \"parameters\": [\n        \"all\"\n      ],\n      \"duals\": [\n        \"all\"\n      ]\n    }\n  ],\n  \"start_time\": \"2020-01-01T04:00:00\",\n  \"end_time\": null,\n  \"path\": null,\n  \"format\": \"csv\"\n}\n\n```\n\"\"\"\nfunction export_results(results::SimulationResults, exports)\n    if results.store isa InMemorySimulationStore\n        export_results(results, exports, results.store)\n    else\n        simulation_store_path = joinpath(results.path, \"data_store\")\n        open_store(HdfSimulationStore, simulation_store_path, \"r\") do store\n            export_results(results, exports, store)\n        end\n    end\n    return\nend\n\nfunction export_results(results::SimulationResults, exports, store::SimulationStore)\n    if !(exports isa SimulationResultsExport)\n        exports = SimulationResultsExport(exports, results.params)\n    end\n\n    file_type = get_export_file_type(exports)\n\n    for problem_results in values(results.decision_problem_results)\n        problem_exports = get_problem_exports(exports, problem_results.problem)\n        path =\n            exports.path === nothing ? problem_results.results_output_folder : exports.path\n        for timestamp in get_timestamps(problem_results)\n            !should_export(exports, timestamp) && continue\n\n            export_path = mkpath(joinpath(path, problem_results.problem, \"variables\"))\n            for name in list_variable_names(problem_results)\n                if should_export_variable(problem_exports, name)\n                    dfs = read_variable(\n                        problem_results,\n                        name;\n                        initial_time = timestamp,\n                        count = 1,\n                        store = store,\n                    )\n                    ISOPT.export_result(\n                        file_type,\n                        export_path,\n                        name,\n                        timestamp,\n                        dfs[timestamp],\n                    )\n                end\n            end\n\n            export_path = mkpath(joinpath(path, problem_results.problem, \"aux_variables\"))\n            for name in list_aux_variable_names(problem_results)\n                if should_export_aux_variable(problem_exports, name)\n                    dfs = read_aux_variable(\n                        problem_results,\n                        name;\n                        initial_time = timestamp,\n                        count = 1,\n                        store = store,\n                    )\n                    ISOPT.export_result(\n                        file_type,\n                        export_path,\n                        name,\n                        timestamp,\n                        dfs[timestamp],\n                    )\n                end\n            end\n\n            export_path = mkpath(joinpath(path, problem_results.problem, \"parameters\"))\n            for name in list_parameter_names(problem_results)\n                if should_export_parameter(problem_exports, name)\n                    dfs = read_parameter(\n                        problem_results,\n                        name;\n                        initial_time = timestamp,\n                        count = 1,\n                        store = store,\n                    )\n                    ISOPT.export_result(\n                        file_type,\n                        export_path,\n                        name,\n                        timestamp,\n                        dfs[timestamp],\n                    )\n                end\n            end\n\n            export_path = mkpath(joinpath(path, problem_results.problem, \"duals\"))\n            for name in list_dual_names(problem_results)\n                if should_export_dual(problem_exports, name)\n                    dfs = read_dual(\n                        problem_results,\n                        name;\n                        initial_time = timestamp,\n                        count = 1,\n                        store = store,\n                    )\n                    ISOPT.export_result(\n                        file_type,\n                        export_path,\n                        name,\n                        timestamp,\n                        dfs[timestamp],\n                    )\n                end\n            end\n        end\n\n        export_path = mkpath(joinpath(path, problem_results.problem, \"expression\"))\n        for name in list_expression_names(problem_results)\n            if should_export_expression(problem_exports, name)\n                dfs = read_expression(\n                    problem_results,\n                    name;\n                    initial_time = timestamp,\n                    count = 1,\n                    store = store,\n                )\n                ISOPT.export_result(\n                    file_type,\n                    export_path,\n                    name,\n                    timestamp,\n                    dfs[timestamp],\n                )\n            end\n        end\n\n        if problem_exports.optimizer_stats\n            export_path = joinpath(path, problem_results.problem, \"optimizer_stats.csv\")\n            df = read_optimizer_stats(problem_results; store = store)\n            ISOPT.export_result(file_type, export_path, df)\n        end\n    end\n    return\nend\n\nfunction _check_status(status::RunStatus, ignore_status)\n    status == RunStatus.SUCCESSFULLY_FINALIZED && return\n\n    if ignore_status\n        @warn \"Simulation was not successful: $status. Results may not be valid.\"\n    else\n        error(\n            \"Simulation was not successful: status = $status. Set ignore_status = true to override.\",\n        )\n    end\n    return\nend\n"
  },
  {
    "path": "src/simulation/simulation_results_export.jl",
    "content": "\nconst _SUPPORTED_FORMATS = (\"csv\",)\n\nmutable struct SimulationResultsExport\n    models::Dict{Symbol, OptimizationProblemResultsExport}\n    start_time::Dates.DateTime\n    end_time::Dates.DateTime\n    path::Union{Nothing, String}\n    format::String\nend\n\nfunction SimulationResultsExport(\n    models::Vector{OptimizationProblemResultsExport},\n    params::SimulationStoreParams;\n    start_time = nothing,\n    end_time = nothing,\n    path = nothing,\n    format = \"csv\",\n)\n    # This end time is outside the bounds of the simulation.\n    sim_end_time = params.initial_time + params.step_resolution * params.num_steps\n\n    if start_time === nothing\n        start_time = params.initial_time\n    elseif start_time < params.initial_time || start_time >= sim_end_time\n        throw(IS.InvalidValue(\"invalid start_time: $start_time\"))\n    end\n\n    if end_time === nothing\n        # Reduce the end_time to be within the simulation.\n        end_time = sim_end_time - Dates.Second(1)\n    elseif end_time < params.initial_time || end_time >= sim_end_time\n        throw(IS.InvalidValue(\"invalid end_time: $end_time\"))\n    end\n\n    if !(format in list_supported_formats(SimulationResultsExport))\n        throw(IS.InvalidValue(\"format = $format is not supported\"))\n    end\n\n    return SimulationResultsExport(\n        Dict(x.name => x for x in models),\n        start_time,\n        end_time,\n        path,\n        format,\n    )\nend\n\nfunction SimulationResultsExport(filename::AbstractString, params::SimulationStoreParams)\n    if splitext(filename)[2] != \".json\"\n        throw(IS.InvalidValue(\"only JSON files are supported: $filename\"))\n    end\n\n    return SimulationResultsExport(read_json(filename), params)\nend\n\nfunction SimulationResultsExport(data::AbstractDict, params::SimulationStoreParams)\n    models = Vector{OptimizationProblemResultsExport}()\n    for model in get(data, \"models\", [])\n        if !haskey(model, \"name\")\n            throw(IS.InvalidValue(\"model data does not define 'name'\"))\n        end\n\n        problem_params = params.decision_models_params[Symbol(model[\"name\"])]\n        duals = Set(\n            deserialize_key(problem_params, x) for\n            x in get(model, \"duals\", Set{ConstraintKey}())\n        )\n        parameters = Set(\n            deserialize_key(problem_params, x) for\n            x in get(model, \"parameters\", Set{ParameterKey}())\n        )\n        variables = Set(\n            deserialize_key(problem_params, x) for\n            x in get(model, \"variables\", Set{VariableKey}())\n        )\n        aux_variables = Set(\n            deserialize_key(problem_params, x) for\n            x in get(model, \"variables\", Set{AuxVarKey}())\n        )\n        problem_export = OptimizationProblemResultsExport(\n            model[\"name\"];\n            duals = duals,\n            parameters = parameters,\n            variables = variables,\n            optimizer_stats = get(model, \"optimizer_stats\", false),\n            store_all_duals = get(model, \"store_all_duals\", false),\n            store_all_parameters = get(model, \"store_all_parameters\", false),\n            store_all_variables = get(model, \"store_all_variables\", false),\n            store_all_aux_variables = get(model, \"store_all_aux_variables\", false),\n        )\n        push!(models, problem_export)\n    end\n\n    start_time = get(data, \"start_time\", nothing)\n    if start_time isa AbstractString\n        start_time = Dates.DateTime(start_time)\n    end\n\n    end_time = get(data, \"end_time\", nothing)\n    if end_time isa AbstractString\n        end_time = Dates.DateTime(end_time)\n    end\n\n    return SimulationResultsExport(\n        models,\n        params;\n        start_time = start_time,\n        end_time = end_time,\n        path = get(data, \"path\", nothing),\n        format = get(data, \"format\", \"csv\"),\n    )\nend\n\nfunction get_problem_exports(x::SimulationResultsExport, model_name)\n    name = Symbol(model_name)\n    if !haskey(x.models, name)\n        throw(IS.InvalidValue(\"model $name is not stored. keys = $(keys(x.models))\"))\n    end\n\n    return x.models[name]\nend\n\nfunction get_export_file_type(exports::SimulationResultsExport)\n    if exports.format == \"csv\"\n        return CSV.File\n    end\n\n    throw(IS.InvalidValue(\"format not supported: $(exports.format)\"))\nend\n\nlist_supported_formats(::Type{SimulationResultsExport}) = (\"csv\",)\n\nfunction should_export(exports::SimulationResultsExport, tstamp::Dates.DateTime)\n    return tstamp >= exports.start_time && tstamp <= exports.end_time\nend\n\nfunction should_export_dual(exports::SimulationResultsExport, tstamp, model, name)\n    return _should_export(exports, tstamp, model, STORE_CONTAINER_DUALS, name)\nend\n\nfunction should_export_parameter(exports::SimulationResultsExport, tstamp, model, name)\n    return _should_export(exports, tstamp, model, STORE_CONTAINER_PARAMETERS, name)\nend\n\nfunction should_export_variable(exports::SimulationResultsExport, tstamp, model, name)\n    return _should_export(exports, tstamp, model, STORE_CONTAINER_VARIABLES, name)\nend\n\nfunction should_export_expression(exports::SimulationResultsExport, tstamp, model, name)\n    return _should_export(exports, tstamp, model, STORE_CONTAINER_EXPRESSIONS, name)\nend\n\nfunction should_export_aux_variable(exports::SimulationResultsExport, tstamp, model, name)\n    return _should_export(exports, tstamp, model, STORE_CONTAINER_AUX_VARIABLES, name)\nend\n\nfunction _should_export(exports::SimulationResultsExport, tstamp, model, field_name, name)\n    if tstamp < exports.start_time || tstamp >= exports.end_time\n        return false\n    end\n\n    problem_exports = get_problem_exports(exports, model)\n    return ISOPT._should_export(problem_exports, field_name, name)\nend\n"
  },
  {
    "path": "src/simulation/simulation_sequence.jl",
    "content": "function check_simulation_chronology(\n    horizons::OrderedDict{Symbol, Dates.Millisecond},\n    intervals::OrderedDict{Symbol, Dates.Millisecond},\n    resolutions::OrderedDict{Symbol, Dates.Millisecond},\n)\n    models = collect(keys(resolutions))\n\n    for (model, horizon_time) in horizons\n        if horizon_time < intervals[model]\n            throw(IS.ConflictingInputsError(\"horizon ($horizon_time) is\n                                shorter than interval ($interval) for $(model)\"))\n        end\n    end\n\n    for i in 2:length(models)\n        upper_level_model = models[i - 1]\n        lower_level_model = models[i]\n        if horizons[lower_level_model] > horizons[upper_level_model]\n            throw(\n                IS.ConflictingInputsError(\n                    \"The lookahead length $(horizons[upper_level_model]) in model $(upper_level_model) is insufficient to syncronize with $(lower_level_model)\",\n                ),\n            )\n        end\n        if intervals[lower_level_model] == Dates.Millisecond(0)\n            throw(\n                IS.ConflictingInputsError(\n                    \"The interval in model $(lower_level_model) is invalid.\",\n                ),\n            )\n        end\n        if (intervals[upper_level_model] % intervals[lower_level_model]) !=\n           Dates.Millisecond(0)\n            throw(\n                IS.ConflictingInputsError(\n                    \"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.\",\n                ),\n            )\n        end\n    end\n    return\nend\n\n\"\"\"\n_calculate_interval_inner_counts(intervals::OrderedDict{String,<:Dates.TimePeriod})\n\nCalculates how many times a problem is executed for every interval of the previous problem\n\"\"\"\nfunction _calculate_interval_inner_counts(intervals::OrderedDict{Symbol, Dates.Millisecond})\n    order = collect(keys(intervals))\n    reverse_order = length(intervals):-1:1\n    interval_run_counts = Vector{Int}(undef, length(intervals))\n    interval_run_counts[1] = 1\n    for k in reverse_order[1:(end - 1)]\n        model_name = order[k]\n        previous_model_name = order[k - 1]\n        problem_interval = intervals[model_name]\n        previous_problem_interval = intervals[previous_model_name]\n        if Dates.Millisecond(previous_problem_interval % problem_interval) !=\n           Dates.Millisecond(0)\n            throw(\n                IS.ConflictingInputsError(\n                    \"The interval configuration provided results in a fractional number of executions of problem $model_name\",\n                ),\n            )\n        end\n        interval_run_counts[k] = previous_problem_interval / problem_interval\n        @debug \"problem $k is executed $(interval_run_counts[k]) time within each interval of problem $(k-1)\"\n    end\n    return interval_run_counts\nend\n\n\"\"\"\nFunction calculates the total number of problem executions in the simulation and allocates the appropiate vector\n\"\"\"\nfunction _allocate_execution_order(interval_run_counts::Vector{Int})\n    total_size_of_vector = 0\n    for k in eachindex(interval_run_counts)\n        mult = 1\n        for i in 1:k\n            mult *= interval_run_counts[i]\n        end\n        total_size_of_vector += mult\n    end\n    return -1 * ones(Int, total_size_of_vector)\nend\n\nfunction _fill_execution_order(\n    execution_order::Vector{Int},\n    interval_run_counts::Vector{Int},\n)\n    function _fill_problem(index::Int, problem::Int)\n        last_problem = problems[end]\n        if problem < last_problem\n            next_problem = problem + 1\n            for i in 1:interval_run_counts[next_problem]\n                index = _fill_problem(index, next_problem)\n            end\n        end\n        execution_order[index] = problem\n        index -= 1\n    end\n\n    index = length(execution_order)\n    problems = sort!(collect(keys(interval_run_counts)))\n    _fill_problem(index, problems[1])\n    return\nend\n\nfunction _get_execution_order_vector(intervals::OrderedDict{Symbol, Dates.Millisecond})\n    length(intervals) == 1 && return [1]\n    interval_run_counts = _calculate_interval_inner_counts(intervals)\n    execution_order_vector = _allocate_execution_order(interval_run_counts)\n    _fill_execution_order(execution_order_vector, interval_run_counts)\n    @assert isempty(findall(x -> x == -1, execution_order_vector))\n    return execution_order_vector\nend\n\nfunction _get_num_executions_by_model(\n    models::SimulationModels,\n    execution_order::Vector{Int},\n)\n    model_names = get_model_names(models)\n    executions_by_model = OrderedDict(x => 0 for x in model_names)\n    for number in execution_order\n        executions_by_model[model_names[number]] += 1\n    end\n    return executions_by_model\nend\n\nfunction _add_feedforward_to_model(\n    sim_model::OperationModel,\n    ff::T,\n    ::Type{U},\n) where {T <: AbstractAffectFeedforward, U <: PSY.Device}\n    device_model = get_model(get_template(sim_model), get_component_type(ff))\n    if device_model === nothing\n        model_name = get_name(sim_model)\n        throw(\n            IS.ConflictingInputsError(\n                \"Device model $(get_component_type(ff)) not found in model $model_name\",\n            ),\n        )\n    end\n    @debug \"attaching $T to $(get_component_type(ff))\"\n    attach_feedforward!(device_model, ff)\n    return\nend\n\nfunction _add_feedforward_to_model(\n    sim_model::OperationModel,\n    ff::T,\n    ::Type{U},\n) where {T <: AbstractAffectFeedforward, U <: PSY.Service}\n    if get_feedforward_meta(ff) != NO_SERVICE_NAME_PROVIDED\n        service_model = get_model(\n            get_template(sim_model),\n            get_component_type(ff),\n            get_feedforward_meta(ff),\n        )\n        if service_model === nothing\n            throw(\n                IS.ConflictingInputsError(\n                    \"Service model $(get_component_type(ff)) not found in model $(get_name(sim_model))\",\n                ),\n            )\n        end\n        @debug \"attaching $T to $(PSI.get_component_type(ff)) $(PSI.get_feedforward_meta(ff))\"\n        attach_feedforward!(service_model, ff)\n    else\n        service_found = false\n        for (key, model) in get_service_models(get_template(sim_model))\n            if key[2] == Symbol(get_component_type(ff))\n                service_found = true\n                @debug \"attaching $T to $(get_component_type(ff))\"\n                attach_feedforward!(model, ff)\n            end\n        end\n    end\n    return\nend\n\nfunction _attach_feedforwards(models::SimulationModels, feedforwards)\n    names = Set(string.(get_model_names(models)))\n    ff_dict = Dict{Symbol, Vector}()\n    for (model_name, model_feedforwards) in feedforwards\n        if model_name ∈ names\n            model_name_symbol = Symbol(model_name)\n            ff_dict[model_name_symbol] = model_feedforwards\n            for ff in model_feedforwards\n                sim_model = get_simulation_model(models, model_name_symbol)\n                _add_feedforward_to_model(sim_model, ff, get_component_type(ff))\n            end\n        else\n            error(\"Model $model_name not present in the SimulationModels\")\n        end\n    end\n    return ff_dict\nend\n\nfunction _add_event_to_model(\n    sim_model::OperationModel,\n    key::EventKey{T, U},\n    event_model::EventModel,\n) where {T <: PSY.Contingency, U <: PSY.Device}\n    device_model = get_model(get_template(sim_model), U)\n    if !haskey(get_events(device_model), key)\n        set_event_model!(device_model, key, event_model)\n    else\n        @debug \"Event Model with key $key already in the device model\"\n    end\n    return\nend\n\nfunction _validate_event_timeseries_data(\n    sys::PSY.System,\n    event::PSY.Contingency,\n    event_model::EventModel,\n)\n    devices_with_attribute = PSY.get_components(sys, event)\n    for (k, v) in event_model.timeseries_mapping\n        if v !== nothing\n            try\n                PSY.get_time_series(\n                    IS.SingleTimeSeries,\n                    event,\n                    v,\n                )\n            catch e\n                devices_with_attribute = PSY.get_components(sys, event)\n                device_names_with_attribute =\n                    [PSY.get_name(d) for d in devices_with_attribute]\n                error(\n                    \"Event $event belonging to devices $device_names_with_attribute missing time series with name $v\",\n                )\n            end\n        end\n        if !haskey(get_empty_timeseries_mapping(typeof(event)), k)\n            error(\n                \"Key $k passed as part of event time series mapping does not correspond to a parameter.\",\n            )\n        end\n        if k == :outage_status && v === nothing\n            error(\n                \"FixedForcedOutage requires a timeseries mapping for :outage_status parameter\",\n            )\n        end\n    end\nend\n\nfunction _add_model_to_event_map!(\n    model::OperationModel,\n    sys::PSY.System,\n    event_models::Vector{T},\n) where {T <: EventModel}\n    model_name = get_name(model)\n    for event_model in event_models\n        event_type = get_event_type(event_model)\n        if isempty(PSY.get_supplemental_attributes(event_type, sys))\n            error(\n                \"There is no data for $event_type in $(model_name). \\\n            Since events are simulation-wide objects, they need to be added to all models.\",\n            )\n            continue\n        end\n        event_model.attribute_device_map[model_name] =\n            Dict{Base.UUID, Dict{DataType, Set{String}}}()\n        event_model.attribute_device_map[model_name]\n        for event in PSY.get_supplemental_attributes(event_type, sys)\n            _validate_event_timeseries_data(sys, event, event_model)\n            event_uuid = PSY.IS.get_uuid(event)\n            @debug \"Attaching $event_uuid to $model_name\"\n            devices_with_attribute = PSY.get_components(sys, event)\n            device_types_with_attribute = Set{DataType}()\n            event_model.attribute_device_map[model_name][event_uuid] =\n                Dict{DataType, Set{String}}()\n            for device in devices_with_attribute\n                dtype = typeof(device)\n                push!(device_types_with_attribute, dtype)\n                name_set = get!(\n                    event_model.attribute_device_map[model_name][event_uuid],\n                    dtype,\n                    Set{String}(),\n                )\n                push!(name_set, PSY.get_name(device))\n            end\n            for device_type in device_types_with_attribute\n                key = EventKey(event_type, device_type)\n                _add_event_to_model(model, key, event_model)\n            end\n        end\n        event_model.attribute_device_map[model_name]\n    end\n    return\nend\n\nfunction _attach_events!(\n    models::SimulationModels,\n    event_models::Vector{T},\n) where {T <: EventModel}\n    for model in get_decision_models(models)\n        sys = get_system(model)\n        _add_model_to_event_map!(model, sys, event_models)\n    end\n\n    em_model = get_emulation_model(models)\n    if !isnothing(em_model)\n        _add_model_to_event_map!(\n            em_model,\n            get_system(em_model),\n            event_models,\n        )\n    end\n\n    return\nend\n\n\"\"\"\n    SimulationSequence(\n        models::SimulationModels,\n        feedforward::Dict{String, Vector{<:AbstractAffectFeedforward}}\n        ini_cond_chronology::InitialConditionChronology\n    )\n\nConstruct the simulation sequence between decision and emulation models.\n\n# Arguments\n\n  - `models::SimulationModels`: Vector of decisions and emulation models.\n  - `feedforward = Dict{String, Vector{<:AbstractAffectFeedforward}}()`: Optional dictionary to specify how information\n    and variables are exchanged between decision and emulation models.\n  - `ini_cond_chronology::InitialConditionChronology =  InterProblemChronology()`: Define\n    information sharing model between stages with [`InterProblemChronology`](@ref)\n\n# Example\n\n```julia\ntemplate_uc = template_unit_commitment()\ntemplate_ed = template_economic_dispatch()\nmy_decision_model_uc = DecisionModel(template_1, sys_uc, optimizer, name = \"UC\")\nmy_decision_model_ed = DecisionModel(template_ed, sys_ed, optimizer, name = \"ED\")\nmodels = SimulationModels(\n    decision_models = [\n        my_decision_model_uc,\n        my_decision_model_ed\n    ]\n)\n# The following sequence set the commitment variables (`OnVariable`) for `ThermalStandard` units from UC to ED.\nsequence = SimulationSequence(;\n    models = models,\n    feedforwards = Dict(\n        \"ED\" => [\n            SemiContinuousFeedforward(;\n                component_type = ThermalStandard,\n                source = OnVariable,\n                affected_values = [ActivePowerVariable],\n            ),\n        ],\n    ),\n)\n```\n\"\"\"\nmutable struct SimulationSequence\n    horizons::OrderedDict{Symbol, Dates.Millisecond}\n    intervals::OrderedDict{Symbol, Dates.Millisecond}\n    feedforwards::Dict{Symbol, Vector{<:AbstractAffectFeedforward}}\n    events::Vector{<:EventModel}\n    ini_cond_chronology::InitialConditionChronology\n    execution_order::Vector{Int}\n    executions_by_model::OrderedDict{Symbol, Int}\n    current_execution_index::Int64\n    uuid::Base.UUID\n\n    function SimulationSequence(;\n        models::SimulationModels,\n        feedforwards = Dict{String, Vector{<:AbstractAffectFeedforward}}(),\n        events = Vector{EventModel}(),\n        ini_cond_chronology = InterProblemChronology(),\n    )\n        # Allow strings or symbols as keys; convert to symbols.\n        intervals = determine_intervals(models)\n        horizons = determine_horizons!(models)\n        resolutions = determine_resolutions(models)\n\n        if length(models.decision_models) > 1\n            check_simulation_chronology(horizons, intervals, resolutions)\n        end\n\n        if length(models.decision_models) == 1\n            # TODO: Not implemented yet\n            # ini_cond_chronology = IntraProblemChronology()\n        end\n\n        execution_order = _get_execution_order_vector(intervals)\n        executions_by_model = _get_num_executions_by_model(models, execution_order)\n        sequence_uuid = IS.make_uuid()\n        initialize_simulation_internals!(models, sequence_uuid)\n        _attach_events!(models, events)\n        new(\n            horizons,\n            intervals,\n            _attach_feedforwards(models, feedforwards),\n            events,\n            ini_cond_chronology,\n            execution_order,\n            executions_by_model,\n            0,\n            sequence_uuid,\n        )\n    end\nend\n\nget_step_resolution(sequence::SimulationSequence) = first(values(sequence.intervals))\n\nfunction get_interval(sequence::SimulationSequence, problem::Symbol)\n    return sequence.intervals[problem]\nend\n\nfunction get_interval(sequence::SimulationSequence, model::DecisionModel)\n    return sequence.intervals[get_name(model)]\nend\n\nget_events(sequence::SimulationSequence) = sequence.events\nget_execution_order(sequence::SimulationSequence) = sequence.execution_order\n"
  },
  {
    "path": "src/simulation/simulation_state.jl",
    "content": "struct SimulationState\n    current_time::Base.RefValue{Dates.DateTime}\n    last_decision_model::Base.RefValue{Symbol}\n    decision_states::DatasetContainer{InMemoryDataset}\n    system_states::DatasetContainer{InMemoryDataset}\nend\n\nfunction SimulationState()\n    return SimulationState(\n        Ref(UNSET_INI_TIME),\n        Ref(:None),\n        DatasetContainer{InMemoryDataset}(),\n        DatasetContainer{InMemoryDataset}(),\n    )\nend\n\nget_current_time(s::SimulationState) = s.current_time[]\nget_last_decision_model(s::SimulationState) = s.last_decision_model[]\nget_decision_states(s::SimulationState) = s.decision_states\nget_system_states(s::SimulationState) = s.system_states\n\n# Not to be used in hot loops\nfunction get_system_states_resolution(s::SimulationState)\n    system_state = get_system_states(s)\n    # All the system states have the same resolution\n    return get_data_resolution(first(values(system_state.variables)))\nend\n\nfunction set_current_time!(s::SimulationState, val::Dates.DateTime)\n    s.current_time[] = val\n    return\nend\n\nfunction set_last_decision_model!(s::SimulationState, val::Symbol)\n    s.last_decision_model[] = val\n    return\nend\n\nconst STATE_TIME_PARAMS = NamedTuple{(:horizon, :resolution), NTuple{2, Dates.Millisecond}}\n\nfunction _get_state_params(models::SimulationModels, simulation_step::Dates.Millisecond)\n    params = OrderedDict{OptimizationContainerKey, STATE_TIME_PARAMS}()\n    for model in get_decision_models(models)\n        container = get_optimization_container(model)\n        model_resolution = get_resolution(model)\n        model_interval = get_interval(model)\n        horizon_length = get_horizon(model)\n        # This is the portion of the Horizon that \"overflows\" into the next step\n        time_residual = horizon_length - model_interval\n        @assert_op time_residual >= zero(Dates.Millisecond)\n        num_runs = simulation_step / model_interval\n        total_time = (num_runs - 1) * model_interval + horizon_length\n        for type in fieldnames(DatasetContainer)\n            field_containers = getfield(container, type)\n            for key in keys(field_containers)\n                !should_write_resulting_value(key) && continue\n                if !haskey(params, key)\n                    params[key] = (\n                        horizon = max(simulation_step + time_residual, total_time),\n                        resolution = model_resolution,\n                    )\n                else\n                    params[key] = (\n                        horizon = max(params[key].horizon, total_time),\n                        resolution = min(params[key].resolution, model_resolution),\n                    )\n                end\n                @debug get_name(model) key params[key]\n            end\n        end\n    end\n    model = get_emulation_model(models)\n    if model !== nothing\n        container = get_optimization_container(model)\n        model_resolution = get_resolution(model)\n        for type in fieldnames(DatasetContainer)\n            field_containers = getfield(container, type)\n            for key in keys(field_containers)\n                !should_write_resulting_value(key) && continue\n                if !haskey(params, key)\n                    @debug \"New parameter $key found in emulator only\"\n                else\n                    params[key] = (\n                        horizon = params[key].horizon,\n                        resolution = min(params[key].resolution, model_resolution),\n                    )\n                end\n                @debug get_name(model) key params[key]\n            end\n        end\n    end\n    return params\nend\n\nfunction _initialize_model_states!(\n    sim_state::SimulationState,\n    model::OperationModel,\n    simulation_initial_time::Dates.DateTime,\n    simulation_step::Dates.Millisecond,\n    params::OrderedDict{OptimizationContainerKey, STATE_TIME_PARAMS},\n)\n    states = get_decision_states(sim_state)\n    container = get_optimization_container(model)\n    for field in fieldnames(DatasetContainer)\n        field_containers = getfield(container, field)\n        field_states = getfield(states, field)\n        for (key, field_container) in field_containers\n            !should_write_resulting_value(key) && continue\n            value_counts = params[key].horizon ÷ params[key].resolution\n            column_names = get_column_names(container, field, field_container, key)\n            # TODO DT: why would we overwrite a key? Is this a bug?\n            if !haskey(field_states, key) || get_num_rows(field_states[key]) < value_counts\n                field_states[key] = InMemoryDataset(\n                    NaN,\n                    simulation_initial_time,\n                    params[key].resolution,\n                    Int(simulation_step / params[key].resolution),\n                    value_counts,\n                    column_names)\n            end\n        end\n    end\n    return\nend\n\nfunction _initialize_system_states!(\n    sim_state::SimulationState,\n    ::Nothing,\n    simulation_initial_time::Dates.DateTime,\n    params::OrderedDict{OptimizationContainerKey, STATE_TIME_PARAMS},\n)\n    decision_states = get_decision_states(sim_state)\n    emulator_states = get_system_states(sim_state)\n    min_res = minimum([v.resolution for v in values(params)])\n    for key in get_dataset_keys(decision_states)\n        cols = get_column_names(key, get_dataset(decision_states, key))\n        set_dataset!(\n            emulator_states,\n            key,\n            make_system_state(\n                simulation_initial_time,\n                min_res,\n                cols,\n            ),\n        )\n    end\n    return\nend\n\nfunction _initialize_system_states!(\n    sim_state::SimulationState,\n    emulation_model::EmulationModel,\n    simulation_initial_time::Dates.DateTime,\n    params::OrderedDict{OptimizationContainerKey, STATE_TIME_PARAMS},\n)\n    decision_states = get_decision_states(sim_state)\n    emulator_states = get_system_states(sim_state)\n    emulation_container = get_optimization_container(emulation_model)\n    min_res = minimum([v.resolution for v in values(params)])\n\n    for field in fieldnames(DatasetContainer)\n        field_containers = getfield(emulation_container, field)\n        for (key, value) in field_containers\n            !should_write_resulting_value(key) && continue\n            if field == :parameters\n                column_names = get_column_names(key, value)\n            else\n                column_names = get_column_names_from_axis_array(key, value)\n            end\n            set_dataset!(\n                emulator_states,\n                key,\n                make_system_state(\n                    simulation_initial_time,\n                    min_res,\n                    column_names,\n                ),\n            )\n        end\n    end\n\n    for key in get_dataset_keys(decision_states)\n        dm_cols = get_column_names(key, get_dataset(decision_states, key))\n        if has_dataset(emulator_states, key)\n            em_cols = get_column_names(key, get_dataset(emulator_states, key))\n            if length(dm_cols) != length(em_cols)\n                error(\n                    \"The number of dimensions between the decision states and emulator states don't match\",\n                )\n            end\n            if !isempty(symdiff(first(dm_cols), first(em_cols)))\n                error(\n                    \"Mismatch in column names for dataset $key: $(symdiff(dm_cols, em_cols)) \\\n                    This issue is common when filters are applied to the decision model but not to the emulator model.\",\n                )\n            end\n            continue\n        end\n\n        set_dataset!(\n            emulator_states,\n            key,\n            make_system_state(\n                simulation_initial_time,\n                min_res,\n                dm_cols,\n            ),\n        )\n    end\n    return\nend\n\nfunction initialize_simulation_state!(\n    sim_state::SimulationState,\n    models::SimulationModels,\n    simulation_step::Dates.Millisecond,\n    simulation_initial_time::Dates.DateTime,\n)\n    params = _get_state_params(models, simulation_step)\n    for model in get_decision_models(models)\n        _initialize_model_states!(\n            sim_state,\n            model,\n            simulation_initial_time,\n            simulation_step,\n            params,\n        )\n    end\n    set_last_decision_model!(sim_state, get_name(last(get_decision_models(models))))\n    em = get_emulation_model(models)\n    _initialize_system_states!(sim_state, em, simulation_initial_time, params)\n    return\nend\n\nfunction update_decision_state!(\n    state::SimulationState,\n    key::ParameterKey{AvailableStatusChangeCountdownParameter, T},\n    store_data::DenseAxisArray{Float64, 2},\n    simulation_time::Dates.DateTime,\n    model_params::ModelStoreParams,\n) where {T <: PSY.Component}\n    state_data = get_decision_state_data(state, key)\n    column_names = get_column_names(key, state_data)[1]\n    model_resolution = get_resolution(model_params)\n    state_resolution = get_data_resolution(state_data)\n    resolution_ratio = model_resolution ÷ state_resolution\n    state_timestamps = state_data.timestamps\n    @assert_op resolution_ratio >= 1\n    if simulation_time > get_end_of_step_timestamp(state_data)\n        state_data_index = 1\n        state_data.timestamps[:] .=\n            range(\n                simulation_time;\n                step = state_resolution,\n                length = get_num_rows(state_data),\n            )\n    else\n        state_data_index = find_timestamp_index(state_timestamps, simulation_time)\n    end\n    offset = resolution_ratio - 1\n    result_time_index = axes(store_data)[2]\n    set_update_timestamp!(state_data, simulation_time)\n    for t in result_time_index\n        state_range = state_data_index:(state_data_index + offset)\n        for name in column_names, (ix, i) in enumerate(state_range)\n            state_data.values[name, i] = maximum([0.0, store_data[name, t] - ix + 1])\n        end\n        set_last_recorded_row!(state_data, state_range[end])\n        state_data_index += resolution_ratio\n    end\n    return\nend\n\nfunction update_decision_state!(\n    state::SimulationState,\n    key::ParameterKey{AvailableStatusParameter, T},\n    store_data::DenseAxisArray{Float64, 2},\n    simulation_time::Dates.DateTime,\n    model_params::ModelStoreParams,\n) where {T <: PSY.Component}\n    state_data = get_decision_state_data(state, key)\n    countdown_data =\n        get_decision_state_data(state, AvailableStatusChangeCountdownParameter(), T)\n    column_names = get_column_names(key, state_data)[1]\n    model_resolution = get_resolution(model_params)\n    state_resolution = get_data_resolution(state_data)\n    resolution_ratio = model_resolution ÷ state_resolution\n    state_timestamps = state_data.timestamps\n    @assert_op resolution_ratio >= 1\n\n    if simulation_time > get_end_of_step_timestamp(state_data)\n        state_data_index = 1\n        state_data.timestamps[:] .=\n            range(\n                simulation_time;\n                step = state_resolution,\n                length = get_num_rows(state_data),\n            )\n    else\n        state_data_index = find_timestamp_index(state_timestamps, simulation_time)\n    end\n\n    offset = resolution_ratio - 1\n    result_time_index = axes(store_data)[2]\n    set_update_timestamp!(state_data, simulation_time)\n    for t in result_time_index\n        state_range = state_data_index:(state_data_index + offset)\n        for name in column_names, i in state_range\n            if countdown_data.values[name, i] > 0.0\n                state_data.values[name, i] = 0.0\n            else\n                state_data.values[name, i] = 1.0\n            end\n        end\n        set_last_recorded_row!(state_data, state_range[end])\n        state_data_index += resolution_ratio\n    end\n    return\nend\n\nfunction update_decision_state!(\n    state::SimulationState,\n    key::ParameterKey{ActivePowerOffsetParameter, T},\n    column_names::Set{String},\n    event::PSY.Outage,\n    event_model::EventModel,\n    simulation_time::Dates.DateTime,\n    model_params::ModelStoreParams,\n) where {T <: PSY.Component}\n    event_occurrence_data =\n        get_decision_state_data(state, AvailableStatusChangeCountdownParameter(), T)\n    activepower_data =\n        get_decision_state_data(state, ActivePowerTimeSeriesParameter(), T)\n    state_data = get_decision_state_data(state, key)\n    model_resolution = get_resolution(model_params)\n    state_resolution = get_data_resolution(state_data)\n    resolution_ratio = model_resolution ÷ state_resolution\n    @assert_op resolution_ratio >= 1\n    state_timestamps = state_data.timestamps\n    state_data_index = find_timestamp_index(state_timestamps, simulation_time)\n    for name in column_names\n        if event_occurrence_data.values[name, state_data_index] == 1.0\n            outage_index = state_data_index + 1     #outage occurs at the following timestep\n            subsequent_outage_occurence_data =\n                Vector(event_occurrence_data.values[name, outage_index:end])\n            n_remaining_indices = findfirst(x -> x == 1.0, subsequent_outage_occurence_data)\n            if n_remaining_indices === nothing\n                n_remaining_indices = length(subsequent_outage_occurence_data)\n            end\n            for ix in outage_index:(state_data_index + n_remaining_indices)\n                # Set the offset parameter to equal the negative of the timeseries parameter\n                state_data.values[name, ix] =\n                    -1.0 * activepower_data.values[name, ix]\n            end\n        end\n    end\n    return\nend\n\nfunction update_decision_state!(\n    state::SimulationState,\n    key::OptimizationContainerKey,\n    store_data::DenseAxisArray{Float64, 2},\n    simulation_time::Dates.DateTime,\n    model_params::ModelStoreParams,\n)\n    state_data = get_decision_state_data(state, key)\n    column_names = get_column_names(key, state_data)[1]\n    model_resolution = get_resolution(model_params)\n    state_resolution = get_data_resolution(state_data)\n    resolution_ratio = model_resolution ÷ state_resolution\n    state_timestamps = state_data.timestamps\n    @assert_op resolution_ratio >= 1\n\n    if simulation_time > get_end_of_step_timestamp(state_data)\n        state_data_index = 1\n        state_data.timestamps[:] .=\n            range(\n                simulation_time;\n                step = state_resolution,\n                length = get_num_rows(state_data),\n            )\n    else\n        state_data_index = find_timestamp_index(state_timestamps, simulation_time)\n    end\n\n    offset = resolution_ratio - 1\n    result_time_index = axes(store_data)[2]\n    set_update_timestamp!(state_data, simulation_time)\n    for t in result_time_index\n        state_range = state_data_index:(state_data_index + offset)\n        for name in column_names, i in state_range\n            # TODO: We could also interpolate here\n            state_data.values[name, i] = store_data[name, t]\n        end\n        set_last_recorded_row!(state_data, state_range[end])\n        state_data_index += resolution_ratio\n    end\n    return\nend\n\nfunction _get_time_to_recover(\n    event::PSY.GeometricDistributionForcedOutage,\n    event_model::EventModel,\n    simulation_time,\n)\n    timeseries_mapping = event_model.timeseries_mapping\n    if timeseries_mapping[:mean_time_to_recovery] === nothing\n        return PSY.get_mean_time_to_recovery(event)\n    else\n        ts_mttr = PSY.get_time_series(\n            IS.SingleTimeSeries,\n            event,\n            timeseries_mapping[:mean_time_to_recovery];\n            start_time = simulation_time,\n            len = 1,\n        )\n        return TimeSeries.values(ts_mttr.data)[1]\n    end\nend\n\nfunction _get_time_to_recover(\n    event::PSY.FixedForcedOutage,\n    event_model::EventModel,\n    simulation_time,\n)\n    timeseries_mapping = event_model.timeseries_mapping\n    ts_outage_status = PSY.get_time_series(\n        IS.SingleTimeSeries,\n        event,\n        timeseries_mapping[:outage_status];\n        start_time = simulation_time,\n    )\n    vals = TimeSeries.values(ts_outage_status.data)\n    if length(vals) < 3 || findfirst(isequal(0.0), vals[3:end]) === nothing\n        return length(vals)\n    else\n        return findfirst(isequal(0.0), vals[3:end])\n    end\nend\n\nfunction update_decision_state!(\n    state::SimulationState,\n    key::OptimizationContainerKey,\n    column_names::Set{String},\n    event::PSY.Outage,\n    event_model::EventModel,\n    simulation_time::Dates.DateTime,\n    model_params::ModelStoreParams,\n)\n    return\nend\n\nfunction update_decision_state!(\n    state::SimulationState,\n    key::ParameterKey{AvailableStatusChangeCountdownParameter, T},\n    column_names::Set{String},\n    event::PSY.Outage,\n    event_model::EventModel,\n    simulation_time::Dates.DateTime,\n    ::ModelStoreParams,\n) where {T <: PSY.Component}\n    event_occurrence_data =\n        get_system_state_data(state, AvailableStatusChangeCountdownParameter(), T)\n    event_occurrence_values = get_last_recorded_value(event_occurrence_data)\n    # This is required since the data for outages (mttr and λ) is always assumed to be on hourly resolution\n    mttr_resolution = Dates.Hour(1)\n    state_data = get_decision_state_data(state, key)\n    state_resolution = get_data_resolution(state_data)\n    resolution_ratio = mttr_resolution ÷ state_resolution\n    state_timestamps = state_data.timestamps\n    @assert_op resolution_ratio >= 1\n    state_data_index = find_timestamp_index(state_timestamps, simulation_time)\n    for name in column_names\n        state_data.values[name, state_data_index] = event_occurrence_values[name, 1]\n        if event_occurrence_values[name, 1] == 1.0\n            mttr_hr = _get_time_to_recover(event, event_model, simulation_time)\n            mttr_state_resolution = mttr_hr * resolution_ratio\n            if !isinteger(mttr_state_resolution)\n                @warn \"MTTR is not an integer after conversion from hours to $state_resolution resolution\n                    MTTR will be rounded up to $(Int(ceil(mttr_state_resolution))) steps of $state_resolution\"\n                mttr_state_resolution = ceil(mttr_state_resolution)\n            end\n            off_time_step_count = Int(mttr_state_resolution)\n            set_update_timestamp!(state_data, simulation_time)\n            for (ix, countdown) in enumerate(off_time_step_count:-1.0:1.0)\n                if state_data_index + ix > length(state_timestamps) #outage extends beyond current state\n                    break\n                end\n                state_data.values[name, state_data_index + ix] = countdown\n            end\n        end\n    end\n    return\nend\n\nfunction update_decision_state!(\n    state::SimulationState,\n    key::ParameterKey{AvailableStatusParameter, T},\n    column_names::Set{String},\n    event::PSY.Outage,\n    event_model::EventModel,\n    simulation_time::Dates.DateTime,\n    model_params::ModelStoreParams,\n) where {T <: PSY.Component}\n    event_occurrence_data =\n        get_decision_state_data(state, AvailableStatusChangeCountdownParameter(), T)\n    state_data = get_decision_state_data(state, key)\n    model_resolution = get_resolution(model_params)\n    state_resolution = get_data_resolution(state_data)\n    resolution_ratio = model_resolution ÷ state_resolution\n    @assert_op resolution_ratio >= 1\n    state_timestamps = state_data.timestamps\n    state_data_index = find_timestamp_index(state_timestamps, simulation_time)\n    for name in column_names\n        if event_occurrence_data.values[name, state_data_index] == 1.0\n            outage_index = state_data_index + 1     #outage occurs at the following timestep\n            subsequent_outage_occurence_data =\n                Vector(event_occurrence_data.values[name, outage_index:end])\n            n_remaining_indices = findfirst(x -> x == 1.0, subsequent_outage_occurence_data)\n            if n_remaining_indices === nothing\n                n_remaining_indices = length(subsequent_outage_occurence_data)\n            end\n            for ix in outage_index:(state_data_index + n_remaining_indices)\n                state_data.values[name, ix] = 0.0\n            end\n        end\n    end\n    return\nend\n\nfunction update_decision_state!(\n    state::SimulationState,\n    key::AuxVarKey{TimeDurationOn, T},\n    column_names::Set{String},\n    event::PSY.Outage,\n    event_model::EventModel,\n    simulation_time::Dates.DateTime,\n    model_params::ModelStoreParams,\n) where {T <: PSY.Component}\n    event_occurrence_data =\n        get_decision_state_data(state, AvailableStatusChangeCountdownParameter(), T)\n    state_data = get_decision_state_data(state, key)\n\n    state_timestamps = state_data.timestamps\n    state_data_index = find_timestamp_index(state_timestamps, simulation_time)\n    event_occurence_index =\n        find_timestamp_index(event_occurrence_data.timestamps, simulation_time)\n    for name in column_names\n        if event_occurrence_data.values[name, event_occurence_index] == 1.0\n            state_data.values[name, (state_data_index + 1):end] .=\n                MISSING_INITIAL_CONDITIONS_TIME_COUNT\n        end\n    end\n    return\nend\n\nfunction update_decision_state!(\n    state::SimulationState,\n    key::AuxVarKey{TimeDurationOff, T},\n    column_names::Set{String},\n    event::PSY.Outage,\n    event_model::EventModel,\n    simulation_time::Dates.DateTime,\n    model_params::ModelStoreParams,\n) where {T <: PSY.Component}\n    event_occurrence_data =\n        get_decision_state_data(state, AvailableStatusChangeCountdownParameter(), T)\n    state_data = get_decision_state_data(state, key)\n\n    state_timestamps = state_data.timestamps\n    state_data_index = find_timestamp_index(state_timestamps, simulation_time)\n    event_occurence_index =\n        find_timestamp_index(event_occurrence_data.timestamps, simulation_time)\n    for name in column_names\n        if event_occurrence_data.values[name, event_occurence_index] == 1.0\n            for (time_off, ix) in\n                enumerate((state_data_index + 1):length(state_data.values[name, :]))\n                state_data.values[name, ix] = time_off\n            end\n        end\n    end\n    return\nend\n\nfunction update_decision_state!(\n    state::SimulationState,\n    key::VariableKey{T, U},\n    column_names::Set{String},\n    event::PSY.Outage,\n    event_model::EventModel,\n    simulation_time::Dates.DateTime,\n    model_params::ModelStoreParams,\n) where {T <: Union{ActivePowerVariable, OnVariable}, U <: PSY.Component}\n    event_occurrence_data =\n        get_decision_state_data(state, AvailableStatusChangeCountdownParameter(), U)\n    state_data = get_decision_state_data(state, key)\n\n    state_timestamps = state_data.timestamps\n    state_data_index = find_timestamp_index(state_timestamps, simulation_time)\n    event_occurence_index =\n        find_timestamp_index(event_occurrence_data.timestamps, simulation_time)\n    for name in column_names\n        if event_occurrence_data.values[name, event_occurence_index] == 1.0\n            state_data.values[name, (state_data_index + 1):end] .= 0.0\n        end\n    end\n    return\nend\n\nfunction update_decision_state!(\n    state::SimulationState,\n    key::OptimizationContainerKey,\n    store_data::DenseAxisArray{Float64, 3},\n    simulation_time::Dates.DateTime,\n    model_params::ModelStoreParams,\n)\n    state_data = get_decision_state_data(state, key)\n    outages = get_column_names(key, state_data)[1]\n\n    model_resolution = get_resolution(model_params)\n    state_resolution = get_data_resolution(state_data)\n    resolution_ratio = model_resolution ÷ state_resolution\n    state_timestamps = state_data.timestamps\n    @assert_op resolution_ratio >= 1\n\n    if simulation_time > get_end_of_step_timestamp(state_data)\n        state_data_index = 1\n        state_data.timestamps[:] .=\n            range(\n                simulation_time;\n                step = state_resolution,\n                length = get_num_rows(state_data),\n            )\n    else\n        state_data_index = find_timestamp_index(state_timestamps, simulation_time)\n    end\n\n    offset = resolution_ratio - 1\n    result_time_index = axes(store_data)[3]\n    set_update_timestamp!(state_data, simulation_time)\n    for t in result_time_index\n        state_range = state_data_index:(state_data_index + offset)\n        for name in axes(store_data)[2], i in state_range\n            #loop pelo -outages, names t\n            for outage in outages\n                # TODO: We could also interpolate here\n                state_data.values[outage, name, i] = store_data[outage, name, t]\n            end\n        end\n        set_last_recorded_row!(state_data, state_range[end])\n        state_data_index += resolution_ratio\n    end\n    return\nend\n\nfunction update_decision_state!(\n    state::SimulationState,\n    key::AuxVarKey{S, T},\n    store_data::DenseAxisArray{Float64, 2},\n    simulation_time::Dates.DateTime,\n    model_params::ModelStoreParams,\n) where {T <: PSY.Component, S <: Union{TimeDurationOff, TimeDurationOn}}\n    state_data = get_decision_state_data(state, key)\n    model_resolution = get_resolution(model_params)\n    state_resolution = get_data_resolution(state_data)\n    resolution_ratio = model_resolution ÷ state_resolution\n    @assert_op resolution_ratio >= 1\n\n    if simulation_time > get_end_of_step_timestamp(state_data)\n        state_data_index = 1\n        state_data.timestamps[:] .=\n            range(\n                simulation_time;\n                step = state_resolution,\n                length = get_num_rows(state_data),\n            )\n    else\n        state_data_index = find_timestamp_index(state_data.timestamps, simulation_time)\n    end\n\n    offset = resolution_ratio - 1\n    result_time_index = axes(store_data)[2]\n    set_update_timestamp!(state_data, simulation_time)\n\n    if resolution_ratio == 1.0\n        increment_per_period = 1.0\n    elseif state_resolution < Dates.Day(365) && state_resolution > Dates.Minute(1)\n        increment_per_period = Dates.value(Dates.Minute(state_resolution))\n    else\n        error(\"Incorrect Problem Resolution specification: $(state_resolution)\")\n    end\n\n    column_names = axes(state_data.values)[1]\n    for t in result_time_index\n        state_range = state_data_index:(state_data_index + offset)\n        @assert_op state_range[end] <= get_num_rows(state_data)\n        for name in column_names, i in state_range\n            if t == 1 && i == 1\n                state_data.values[name, i] = store_data[name, t] * resolution_ratio\n            else\n                state_data.values[name, i] =\n                    if store_data[name, t] > 0\n                        state_data.values[name, i - 1] + increment_per_period\n                    else\n                        0\n                    end\n            end\n        end\n        set_last_recorded_row!(state_data, state_range[end])\n        state_data_index += resolution_ratio\n    end\n\n    return\nend\n\nfunction get_decision_state_data(state::SimulationState, key::OptimizationContainerKey)\n    return get_dataset(get_decision_states(state), key)\nend\n\nfunction get_decision_state_value(state::SimulationState, key::OptimizationContainerKey)\n    return get_dataset_values(get_decision_states(state), key)\nend\n\nfunction get_decision_state_data(\n    state::SimulationState,\n    ::T,\n    ::Type{U},\n) where {T <: VariableType, U <: Union{PSY.Component, PSY.System}}\n    return get_decision_state_data(state, VariableKey(T, U))\nend\n\nfunction get_decision_state_data(\n    state::SimulationState,\n    ::T,\n    ::Type{U},\n) where {T <: AuxVariableType, U <: Union{PSY.Component, PSY.System}}\n    return get_decision_state_data(state, AuxVarKey(T, U))\nend\n\nfunction get_decision_state_data(\n    state::SimulationState,\n    ::T,\n    ::Type{U},\n) where {T <: ConstraintType, U <: Union{PSY.Component, PSY.System}}\n    return get_decision_state_data(state, ConstraintKey(T, U))\nend\n\nfunction get_decision_state_data(\n    state::SimulationState,\n    ::T,\n    ::Type{U},\n) where {T <: ParameterType, U <: Union{PSY.Component, PSY.System}}\n    return get_decision_state_data(state, ParameterKey(T, U))\nend\n\nfunction get_decision_state_value(\n    state::SimulationState,\n    key::OptimizationContainerKey,\n    date::Dates.DateTime,\n)\n    return get_dataset_values(get_decision_states(state), key, date)\nend\n\nfunction get_system_state_data(state::SimulationState, key::OptimizationContainerKey)\n    return get_dataset(get_system_states(state), key)\nend\n\nfunction get_system_state_value(state::SimulationState, key::OptimizationContainerKey)\n    return get_dataset_values(get_system_states(state), key)[:, 1]\nend\n\nfunction update_system_state!(\n    state::DatasetContainer{InMemoryDataset},\n    key::OptimizationContainerKey,\n    store::SimulationStore,\n    model_name::Symbol,\n    simulation_time::Dates.DateTime,\n)\n    em_data = get_em_data(store)\n    ix = get_last_recorded_row(em_data, key)\n    res = read_result(DenseAxisArray, store, model_name, key, ix)\n    dataset = get_dataset(state, key)\n    set_update_timestamp!(dataset, simulation_time)\n    if typeof(store) == HdfSimulationStore\n        set_dataset_values!(state, key, 1, res)\n    else\n        # Handle different dimensionality of results\n        num_dims = ndims(res)\n        if num_dims == 2\n            set_dataset_values!(state, key, 1, res[:, ix])\n        elseif num_dims == 3\n            set_dataset_values!(state, key, 1, res[:, :, ix])\n        else\n            error(\"Unsupported number of dimensions for emulation result: $num_dims\")\n        end\n    end\n    set_last_recorded_row!(dataset, 1)\n    return\nend\n\nfunction update_system_state!(\n    state::SimulationState,\n    key::OptimizationContainerKey,\n    column_names_::Set{String},\n    event::PSY.Outage,\n    event_model::EventModel,\n    simulation_time::Dates.DateTime,\n    rng::AbstractRNG,\n)\n    return\nend\n\nfunction update_system_state!(\n    state::SimulationState,\n    key::ParameterKey{AvailableStatusParameter, T},\n    column_names_::Set{String},\n    event::PSY.Outage,\n    event_model::EventModel,\n    simulation_time::Dates.DateTime,\n    rng::AbstractRNG,\n) where {T <: PSY.Device}\n    available_status_parameter = get_system_state_data(state, key)\n    available_status_parameter_values = get_last_recorded_value(available_status_parameter)\n\n    available_status_change_parameter =\n        get_system_state_data(state, AvailableStatusChangeCountdownParameter(), T)\n    available_status_change_parameter_values =\n        get_last_recorded_value(available_status_change_parameter)\n\n    for name in column_names_\n        current_status = available_status_parameter_values[name]\n        current_status_change = available_status_change_parameter_values[name]\n        if current_status == 1.0 && current_status_change == 1.0\n            available_status_parameter.values[name, 1] = 0.0\n        end\n    end\n    return\nend\n\nfunction _get_outage_occurrence(\n    event::PSY.GeometricDistributionForcedOutage,\n    event_model::EventModel,\n    rng::AbstractRNG,\n    current_time,\n)\n    timeseries_mapping = event_model.timeseries_mapping\n    if timeseries_mapping[:outage_transition_probability] === nothing\n        λ = PSY.get_outage_transition_probability(event)\n    else\n        ts_outage_prob = PSY.get_time_series(\n            IS.SingleTimeSeries,\n            event,\n            timeseries_mapping[:outage_transition_probability];\n            start_time = current_time,\n            len = 1,\n        )\n        λ = TimeSeries.values(ts_outage_prob.data)[1]\n    end\n    outage_occurrence = Float64(rand(rng, Bernoulli(λ)))\n    return outage_occurrence\nend\n\nfunction _get_outage_occurrence(\n    event::PSY.FixedForcedOutage,\n    event_model::EventModel,\n    rng::AbstractRNG,\n    current_time,\n)\n    timeseries_mapping = event_model.timeseries_mapping\n    ts = PSY.get_time_series(\n        IS.SingleTimeSeries,\n        event,\n        timeseries_mapping[:outage_status];\n        start_time = current_time,\n    )\n    val = TimeSeries.values(ts.data)\n    if length(val) == 1\n        return 0\n    end\n    return val[2]\nend\n\nfunction update_system_state!(\n    state::SimulationState,\n    key::ParameterKey{AvailableStatusChangeCountdownParameter, T},\n    column_names_::Set{String},\n    event::PSY.Outage,\n    event_model::EventModel,\n    simulation_time::Dates.DateTime,\n    rng::AbstractRNG,\n) where {T <: PSY.Component}\n    outage_occurrence = _get_outage_occurrence(event, event_model, rng, simulation_time)\n    sym_state = get_system_states(state)\n    system_dataset = get_dataset(sym_state, key)\n\n    # Writes the timestamp of the value used for the update\n    available_status_parameter = get_system_state_data(state, AvailableStatusParameter(), T)\n    available_status_parameter_values = get_last_recorded_value(available_status_parameter)\n\n    available_status_change_parameter = get_system_state_data(state, key)\n    set_update_timestamp!(system_dataset, simulation_time)\n\n    for name in column_names_\n        current_status = available_status_parameter_values[name]\n        if current_status == 1.0 && outage_occurrence == 1.0\n            @warn \"Outage occurred at time $simulation_time for devices $column_names_\"\n            available_status_change_parameter.values[name, 1] = outage_occurrence\n        else\n            available_status_change_parameter.values[name, 1] = 0.0\n        end\n    end\n    return\nend\n\nfunction update_system_state!(\n    state::SimulationState,\n    key::ParameterKey{ActivePowerOffsetParameter, T},\n    column_names_::Set{String},\n    event::PSY.Outage,\n    event_model::EventModel,\n    simulation_time::Dates.DateTime,\n    rng::AbstractRNG,\n) where {T <: PSY.Component}\n    available_status_parameter = get_system_state_data(state, AvailableStatusParameter(), T)\n    available_status_parameter_values = get_last_recorded_value(available_status_parameter)\n\n    available_status_change_parameter =\n        get_system_state_data(state, AvailableStatusChangeCountdownParameter(), T)\n    available_status_change_parameter_values =\n        get_last_recorded_value(available_status_change_parameter)\n\n    active_power_offset_parameter = get_system_state_data(state, key)\n    active_power_offset_parameter_values =\n        get_last_recorded_value(active_power_offset_parameter)\n\n    active_power_timeseries_parameter =\n        get_system_state_data(state, ActivePowerTimeSeriesParameter(), T)\n    active_power_timeseries_parameter_values =\n        get_last_recorded_value(active_power_timeseries_parameter)\n\n    for name in column_names_\n        current_status = available_status_parameter_values[name]\n        current_status_change = available_status_change_parameter_values[name]\n        if current_status == 1.0 && current_status_change == 1.0\n            active_power_offset_parameter.values[name, 1] =\n                -1.0 * active_power_timeseries_parameter_values[name]\n        end\n    end\n    return\nend\n\nfunction update_system_state!(\n    state::SimulationState,\n    key::AuxVarKey{TimeDurationOff, T},\n    column_names::Set{String},\n    event::PSY.Outage,\n    event_model::EventModel,\n    simulation_time::Dates.DateTime,\n    rng::AbstractRNG,\n) where {T <: PSY.Component}\n    sym_state = get_system_states(state)\n    event_occurrence_data =\n        get_system_state_data(state, AvailableStatusChangeCountdownParameter(), T)\n    event_occurrence_values = get_last_recorded_value(event_occurrence_data)\n\n    system_dataset = get_dataset(sym_state, key)\n    current_status_data = get_system_state_data(state, key)\n    current_status_values = get_last_recorded_value(current_status_data)\n    set_update_timestamp!(system_dataset, simulation_time)\n    for name in column_names\n        if event_occurrence_values[name] == 1.0\n            current_status_data.values[name, 1] = 0.0\n        end\n    end\n    return\nend\n\nfunction update_system_state!(\n    state::SimulationState,\n    key::AuxVarKey{TimeDurationOn, T},\n    column_names::Set{String},\n    event::PSY.Outage,\n    event_model::EventModel,\n    simulation_time::Dates.DateTime,\n    rng::AbstractRNG,\n) where {T <: PSY.Component}\n    sym_state = get_system_states(state)\n    event_occurrence_data =\n        get_system_state_data(state, AvailableStatusChangeCountdownParameter(), T)\n    event_occurrence_values = get_last_recorded_value(event_occurrence_data)\n\n    system_dataset = get_dataset(sym_state, key)\n    current_status_data = get_system_state_data(state, key)\n    current_status_values = get_last_recorded_value(current_status_data)\n    set_update_timestamp!(system_dataset, simulation_time)\n    for name in column_names\n        if event_occurrence_values[name] == 1.0\n            current_status_data.values[name, 1] = MISSING_INITIAL_CONDITIONS_TIME_COUNT\n        end\n    end\n    return\nend\n\nfunction update_system_state!(\n    state::SimulationState,\n    key::VariableKey{T, U},\n    column_names::Set{String},\n    ::PSY.Outage,\n    event_model::EventModel,\n    simulation_time::Dates.DateTime,\n    rng::AbstractRNG,\n) where {T <: Union{ActivePowerVariable, OnVariable}, U <: PSY.Component}\n    sym_state = get_system_states(state)\n    event_occurrence_data =\n        get_system_state_data(state, AvailableStatusChangeCountdownParameter(), U)\n    event_occurrence_values = get_last_recorded_value(event_occurrence_data)\n\n    system_dataset = get_dataset(sym_state, key)\n    current_status_data = get_system_state_data(state, key)\n    current_status_values = get_last_recorded_value(current_status_data)\n    set_update_timestamp!(system_dataset, simulation_time)\n    for name in column_names\n        if event_occurrence_values[name] == 1.0\n            current_status_data.values[name, 1] = 0.0\n        end\n    end\n    return\nend\n\nfunction update_system_state!(\n    state::DatasetContainer{InMemoryDataset},\n    key::OptimizationContainerKey,\n    decision_state::DatasetContainer{InMemoryDataset},\n    simulation_time::Dates.DateTime,\n)\n    decision_dataset = get_dataset(decision_state, key)\n    # Gets the timestamp of the value used for the update, which might not match exactly the\n    # simulation time since the value might have not been updated yet\n    ts = get_value_timestamp(decision_dataset, simulation_time)\n    system_dataset = get_dataset(state, key)\n    get_update_timestamp(system_dataset)\n    if ts == get_update_timestamp(system_dataset)\n        # Uncomment for debugging\n        #@warn \"Skipped overwriting data with the same timestamp \\\\\n        #       key: $(encode_key_as_string(key)), $(simulation_time), $ts\"\n        return\n    end\n\n    # Note: This protection is disabled because the rate of update of the emulator\n    # is now higher than the decision rate. If the event happens in the middle of an \"hourly\"\n    # rate decision variable then the whole hour is updated creating a problem.\n\n    # New logic will be needed to maintain the protection.\n    #if get_update_timestamp(system_dataset) > ts\n    #    error(\"Trying to update with past data a future state timestamp \\\\\n    #        key: $(encode_key_as_string(key)), $(simulation_time), $ts\")\n    #end\n\n    # Writes the timestamp of the value used for the update\n    set_update_timestamp!(system_dataset, ts)\n    # Keep coordination between fields. System state is an array of size 1\n    system_dataset.timestamps[1] = ts\n    data_set_value = get_dataset_value(decision_dataset, simulation_time)\n    set_dataset_values!(state, key, 1, data_set_value)\n    # This value shouldn't be other than one and after one execution is no-op.\n    set_last_recorded_row!(system_dataset, 1)\n    return\nend\n\nfunction update_system_state!(\n    state::DatasetContainer{InMemoryDataset},\n    key::AuxVarKey{T, PSY.ThermalStandard},\n    decision_state::DatasetContainer{InMemoryDataset},\n    simulation_time::Dates.DateTime,\n) where {T <: Union{TimeDurationOn, TimeDurationOff}}\n    decision_dataset = get_dataset(decision_state, key)\n    # Gets the timestamp of the value used for the update, which might not match exactly the\n    # simulation time since the value might have not been updated yet\n\n    ts = get_value_timestamp(decision_dataset, simulation_time)\n    system_dataset = get_dataset(state, key)\n    system_state_resolution = get_data_resolution(system_dataset)\n    decision_state_resolution = get_data_resolution(decision_dataset)\n\n    decision_state_value = get_dataset_value(decision_dataset, simulation_time)\n\n    if ts == get_update_timestamp(system_dataset)\n        # Uncomment for debugging\n        #@warn \"Skipped overwriting data with the same timestamp \\\\\n        #       key: $(encode_key_as_string(key)), $(simulation_time), $ts\"\n        return\n    end\n    #if get_update_timestamp(system_dataset) > ts\n    #    error(\"Trying to update with past data a future state timestamp \\\\\n    #        key: $(encode_key_as_string(key)), $(simulation_time), $ts\")\n    #end\n\n    # Writes the timestamp of the value used for the update\n    set_update_timestamp!(system_dataset, ts)\n    # Keep coordination between fields. System state is an array of size 1\n    system_dataset.timestamps[1] = ts\n    time_ratio = (decision_state_resolution / system_state_resolution)\n    # Don't use set_dataset_values!(state, key, 1, decision_state_value).\n    # For the time variables we need to grab the values to avoid mutation of the\n    # dataframe row\n    set_value!(system_dataset, values(decision_state_value) .* time_ratio, 1)\n    # This value shouldn't be other than one and after one execution is no-op.\n    set_last_recorded_row!(system_dataset, 1)\n    return\nend\n\nfunction get_system_state_value(\n    state::SimulationState,\n    ::T,\n    ::Type{U},\n) where {T <: VariableType, U <: Union{PSY.Component, PSY.System}}\n    return get_system_state_value(state, VariableKey(T, U))\nend\n\nfunction get_system_state_value(\n    state::SimulationState,\n    ::T,\n    ::Type{U},\n) where {T <: AuxVariableType, U <: Union{PSY.Component, PSY.System}}\n    return get_system_state_value(state, AuxVarKey(T, U))\nend\n\nfunction get_system_state_value(\n    state::SimulationState,\n    ::T,\n    ::Type{U},\n) where {T <: ConstraintType, U <: Union{PSY.Component, PSY.System}}\n    return get_system_state_value(state, ConstraintKey(T, U))\nend\n\nfunction get_system_state_value(\n    state::SimulationState,\n    ::T,\n    ::Type{U},\n) where {T <: ParameterType, U <: Union{PSY.Component, PSY.System}}\n    return get_system_state_value(state, ParameterKey(T, U))\nend\n\nfunction get_system_state_data(\n    state::SimulationState,\n    ::T,\n    ::Type{U},\n) where {T <: VariableType, U <: Union{PSY.Component, PSY.System}}\n    return get_system_state_data(state, VariableKey(T, U))\nend\n\nfunction get_system_state_data(\n    state::SimulationState,\n    ::T,\n    ::Type{U},\n) where {T <: AuxVariableType, U <: Union{PSY.Component, PSY.System}}\n    return get_system_state_data(state, AuxVarKey(T, U))\nend\n\nfunction get_system_state_data(\n    state::SimulationState,\n    ::T,\n    ::Type{U},\n) where {T <: ConstraintType, U <: Union{PSY.Component, PSY.System}}\n    return get_system_state_data(state, ConstraintKey(T, U))\nend\n\nfunction get_system_state_data(\n    state::SimulationState,\n    ::T,\n    ::Type{U},\n) where {T <: ParameterType, U <: Union{PSY.Component, PSY.System}}\n    return get_system_state_data(state, ParameterKey(T, U))\nend\n"
  },
  {
    "path": "src/simulation/simulation_store_params.jl",
    "content": "struct SimulationStoreParams\n    initial_time::Dates.DateTime\n    step_resolution::Dates.Millisecond\n    num_steps::Int\n    # The key order is the problem execution order.\n    decision_models_params::OrderedDict{Symbol, ModelStoreParams}\n    emulation_model_params::OrderedDict{Symbol, ModelStoreParams}\n\n    function SimulationStoreParams(\n        initial_time::Dates.DateTime,\n        step_resolution::Dates.Period,\n        num_steps::Int,\n        decision_models_params::OrderedDict{Symbol, ModelStoreParams},\n        emulation_model_params::OrderedDict,\n    )\n        new(\n            initial_time,\n            Dates.Millisecond(step_resolution),\n            num_steps,\n            decision_models_params,\n            emulation_model_params,\n        )\n    end\nend\n\nfunction SimulationStoreParams(initial_time, step_resolution, num_steps)\n    return SimulationStoreParams(\n        initial_time,\n        step_resolution,\n        num_steps,\n        OrderedDict{Symbol, ModelStoreParams}(),\n        OrderedDict{Symbol, ModelStoreParams}(),\n    )\nend\n\nfunction SimulationStoreParams()\n    return SimulationStoreParams(\n        Dates.DateTime(\"1970-01-01T00:00:00\"),\n        Dates.Millisecond(0),\n        0,\n        OrderedDict{Symbol, ModelStoreParams}(),\n        OrderedDict{Symbol, ModelStoreParams}(),\n    )\nend\n\nget_initial_time(store_params::SimulationStoreParams) = store_params.initial_time\n\nfunction get_decision_model_params(store_params::SimulationStoreParams, model_name::Symbol)\n    return store_params.decision_models_params[model_name]\nend\n\nfunction get_emulation_model_params(store_params::SimulationStoreParams)\n    # We currently only store one em_model dataset in the store\n    @assert_op length(store_params.emulation_model_params) == 1\n    return first(values(store_params.emulation_model_params))\nend\n"
  },
  {
    "path": "src/simulation/simulation_store_requirements.jl",
    "content": "struct SimulationModelStoreRequirements\n    duals::Dict{ConstraintKey, Dict{String, Any}}\n    parameters::Dict{ParameterKey, Dict{String, Any}}\n    variables::Dict{VariableKey, Dict{String, Any}}\n    aux_variables::Dict{AuxVarKey, Dict{String, Any}}\n    expressions::Dict{ExpressionKey, Dict{String, Any}}\nend\n\nfunction SimulationModelStoreRequirements()\n    return SimulationModelStoreRequirements(\n        Dict{ConstraintKey, Dict{String, Any}}(),\n        Dict{ParameterKey, Dict{String, Any}}(),\n        Dict{VariableKey, Dict{String, Any}}(),\n        Dict{AuxVarKey, Dict{String, Any}}(),\n        Dict{ExpressionKey, Dict{String, Any}}(),\n    )\nend\n"
  },
  {
    "path": "src/utils/dataframes_utils.jl",
    "content": "\nfunction to_matrix(df::DataFrame)\n    return Matrix{Float64}(df)\nend\n\nfunction to_matrix(df_row::DataFrameRow{DataFrame, DataFrames.Index})\n    return reshape(Vector(df_row), 1, size(df_row)[1])\nend\n"
  },
  {
    "path": "src/utils/datetime_utils.jl",
    "content": "\"\"\"\ncalculates the index in the time series corresponding to the data. Assumes that the dates vector is sorted.\n\"\"\"\nfunction find_timestamp_index(\n    dates::Union{Vector{Dates.DateTime}, StepRange{Dates.DateTime, Dates.Millisecond}},\n    date::Dates.DateTime,\n)\n    if date == first(dates)\n        index = 1\n    elseif date == last(dates)\n        index = length(dates)\n    else\n        dates_resolution = dates[2] - dates[1]\n        index = 1 + ((date - first(dates)) ÷ dates_resolution)\n    end\n    # Uncomment for debugging. The method below is fool proof but slower\n    # s_index = findlast(dates .<= date)\n    # IS.@assert_op index == s_index\n    if index < 1 || index > length(dates)\n        error(\"Requested timestamp $date not in the provided dates $dates\")\n    end\n    return index\nend\n"
  },
  {
    "path": "src/utils/file_utils.jl",
    "content": "\"\"\"\nReturn a decoded JSON file.\n\"\"\"\nfunction read_json(filename::AbstractString)\n    open(filename, \"r\") do io\n        JSON3.read(io)\n    end\nend\n\n\"\"\"\nReturn a DataFrame from a CSV file.\n\"\"\"\nfunction read_dataframe(filename::AbstractString)\n    return CSV.read(filename, DataFrames.DataFrame)\nend\n\n\"\"\"\nReturn the key for the given value\n\"\"\"\nfunction find_key_with_value(d, value)\n    for (k, v) in d\n        v == value && return k\n    end\n    error(\"dict does not have value == $value\")\nend\n\nfunction read_file_hashes(path)\n    data = open(joinpath(path, IS.HASH_FILENAME), \"r\") do io\n        JSON3.read(io)\n    end\n\n    return data[\"files\"]\nend\n\n# this ensures that the timestamp is not double shortened\nfunction find_variable_length(es::Dict, e_list::Array)\n    return size(es[Symbol(splitext(e_list[1])[1])], 1)\nend\n\n\"\"\"\n    check_file_integrity(path::String)\n\nChecks 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\n\n# Arguments\n\n  - `path::String`: this is the folder path that contains the results and the check.sha256 file\n\"\"\"\nfunction check_file_integrity(path::String)\n    matched = true\n    for file_info in read_file_hashes(path)\n        filename = file_info[\"filename\"]\n        @info \"checking integrity of $filename\"\n        expected_hash = file_info[\"hash\"]\n        actual_hash = IS.compute_sha256(joinpath(path, filename))\n        if expected_hash != actual_hash\n            @error \"hash mismatch for file\" filename expected_hash actual_hash\n            matched = false\n        end\n    end\n\n    if !matched\n        throw(\n            IS.HashMismatchError(\n                \"The hash value in the written files does not match the read files, results may have been tampered.\",\n            ),\n        )\n    end\nend\n"
  },
  {
    "path": "src/utils/generate_valid_formulations.jl",
    "content": "\"\"\"\nGenerate valid combinations of device_type/formulation and service_type/formulation.\nReturn vectors of dictionaries with Julia types.\n\n# Arguments\n\n  - `sys::Union{Nothing, System}`: If set, only include component types present in the system.\n\"\"\"\nfunction generate_formulation_combinations(sys = nothing)\n    combos = Dict(\n        \"device_formulations\" => generate_device_formulation_combinations(),\n        \"service_formulations\" => generate_service_formulation_combinations(),\n    )\n\n    filter_formulation_combinations!(combos, sys)\n    return combos\nend\n\nfilter_formulation_combinations!(combos, ::Nothing) = nothing\n\nfunction filter_formulation_combinations!(combos, sys::PSY.System)\n    device_types = Set(PSY.get_existing_device_types(sys))\n    service_types =\n        Set((x for x in PSY.get_existing_component_types(sys) if x <: PSY.Service))\n    filter!(x -> x[\"device_type\"] in device_types, combos[\"device_formulations\"])\n    filter!(x -> x[\"service_type\"] in service_types, combos[\"service_formulations\"])\nend\n\n\"\"\"\nGenerate valid combinations of device_type/formulation and service_type/formulation.\nReturn vectors of dictionaries with Julia types encoded as strings.\n\n# Arguments\n\n  - `sys::Union{Nothing, System}`: If set, only include component types present in the system.\n\"\"\"\nfunction serialize_formulation_combinations(sys = nothing)\n    combos = generate_formulation_combinations(sys)\n    for (i, combo) in enumerate(combos[\"device_formulations\"])\n        for key in keys(combo)\n            combos[\"device_formulations\"][i][key] = string(nameof(combo[key]))\n        end\n    end\n    for (i, combo) in enumerate(combos[\"service_formulations\"])\n        for key in keys(combo)\n            combos[\"service_formulations\"][i][key] = string(nameof(combo[key]))\n        end\n    end\n\n    sort!(combos[\"device_formulations\"]; by = x -> x[\"device_type\"])\n    sort!(combos[\"service_formulations\"]; by = x -> x[\"service_type\"])\n    return combos\nend\n\n\"\"\"\nGenerate valid combinations of device_type/formulation and service_type/formulation and write\nthe result to a JSON file.\n\n# Arguments\n\n  - `sys::Union{Nothing, System}`: If set, only include component types present in the system.\n\"\"\"\nfunction write_formulation_combinations(filename::AbstractString, sys = nothing)\n    open(filename, \"w\") do io\n        JSON3.pretty(io, serialize_formulation_combinations(sys))\n    end\n    @info(\" to $filename\")\nend\n\nfunction generate_device_formulation_combinations()\n    combos = []\n    for (d, f) in Iterators.product(\n        IS.get_all_concrete_subtypes(PSY.Device),\n        IS.get_all_concrete_subtypes(AbstractDeviceFormulation),\n    )\n        # DynamicBranches are not supported in PSI but they are still considered <: PSY.Device since in \n        # PSY 1.0 we haven't introduced the notion of AbstractDynamicBranches. \n        if d <: PSY.DynamicBranch\n            continue\n        end\n        if !isempty(methodswith(DeviceModel{d, f}, construct_device!; supertypes = true))\n            push!(combos, Dict{String, Any}(\"device_type\" => d, \"formulation\" => f))\n        end\n    end\n\n    return combos\nend\n\nfunction generate_service_formulation_combinations()\n    combos = []\n    for (d, f) in Iterators.product(\n        IS.get_all_concrete_subtypes(PSY.Service),\n        IS.get_all_concrete_subtypes(AbstractServiceFormulation),\n    )\n        if !isempty(methodswith(ServiceModel{d, f}, construct_service!; supertypes = true))\n            push!(combos, Dict{String, Any}(\"service_type\" => d, \"formulation\" => f))\n        end\n    end\n\n    return combos\nend\n"
  },
  {
    "path": "src/utils/indexing.jl",
    "content": "# Pad `ixs` with `:` for any unindexed middle dimensions of `dest` (Python `...`-style).\n# Fast path for AbstractArrays where `N` is known at compile time → allocation-free `Val(N - K)`.\n@inline function expand_ixs(\n    ixs::NTuple{K, Any},\n    dest::AbstractArray{<:Any, N},\n) where {K, N}\n    K <= N || throw(ArgumentError(\"`ixs` must not index more dimensions than `dest` has\"))\n    K == N && return ixs\n    # Single-element ixs is the leading axis; multi-element preserves first..last with `:` filling the middle.\n    K == 1 && return (only(ixs), ntuple(_ -> Colon(), Val(N - 1))...)\n    return (Base.front(ixs)..., ntuple(_ -> Colon(), Val(N - K))..., last(ixs))\nend\n\n# Fallback for non-AbstractArray containers (e.g. `HDF5.Dataset`) — `ndims` resolved at runtime.\n@inline function expand_ixs(ixs::NTuple{K, Any}, dest) where {K}\n    N = ndims(dest)\n    K <= N || throw(ArgumentError(\"`ixs` must not index more dimensions than `dest` has\"))\n    K == N && return ixs\n    K == 1 && return (only(ixs), ntuple(_ -> Colon(), N - 1)...)\n    return (Base.front(ixs)..., ntuple(_ -> Colon(), N - K)..., last(ixs))\nend\n\n# Concrete fast path: scalar `src` with a fully-specified index tuple goes through `setindex!`.\n@inline function assign_maybe_broadcast!(\n    dest::DenseAxisArray{T, N},\n    src::T,\n    ixs::NTuple{N, Any},\n) where {T, N}\n    dest[ixs...] = src\n    return\nend\n\n# Array `src`: assign a slice of `dest` from `src` (standard assignment handles non-1:n axes).\n# `dest` is left untyped so non-`AbstractArray` containers (e.g. `HDF5.Dataset`) are also accepted.\n@inline function assign_maybe_broadcast!(dest, src::AbstractArray, ixs::Tuple)\n    dest[expand_ixs(ixs, dest)...] = src\n    return\nend\n\n# Scalar/tuple `src`: broadcast the value across the indexed slice of `dest`.\n@inline function assign_maybe_broadcast!(dest, src, ixs::Tuple)\n    expanded = expand_ixs(ixs, dest)\n    @views dest[expanded...] .= Ref(src)\n    return\nend\n\n# Similar to assign_maybe_broadcast! but for fixing JuMP VariableRefs\n@inline function fix_maybe_broadcast!(\n    dest::DenseAxisArray{JuMP.VariableRef, N},\n    src::Float64,\n    ixs::NTuple{N, Any},\n) where {N}\n    JuMP.fix(dest[ixs...], src; force = true)\n    return\nend\n\nfix_expand(dest, src, ixs::Tuple) =\n    fix_parameter_value.(dest[expand_ixs(ixs, dest)...], src)\nfix_maybe_broadcast!(dest, src::AbstractArray, ixs::Tuple) =\n    fix_expand(dest, src, ixs)\nfix_maybe_broadcast!(dest, src, ixs::Tuple) =\n    fix_expand(dest, Ref(src), ixs)\n"
  },
  {
    "path": "src/utils/jump_utils.jl",
    "content": "const IntegerAxis = Union{Vector{Int}, UnitRange{Int}}\n\nfunction get_hinted_aff_expr(size::Int)\n    expr = JuMP.AffExpr(0.0)\n    sizehint!(expr.terms, size)\n    return expr\nend\n\n#Given the changes in syntax in ParameterJuMP and the new format to create anonymous parameters\nfunction add_jump_parameter(jump_model::JuMP.Model, val::Number)\n    param = JuMP.@variable(jump_model, base_name = \"param\")\n    JuMP.fix(param, val; force = true)\n    return param\nend\n\nfunction write_data(base_power::Float64, save_path::String)\n    JSON3.write(joinpath(save_path, \"base_power.json\"), JSON3.json(base_power))\n    return\nend\n\nfunction jump_value(input::JuMP.VariableRef)::Float64\n    if JuMP.is_fixed(input)\n        return JuMP.fix_value(input)\n    elseif JuMP.has_values(input.model)\n        return JuMP.value(input)\n    else\n        return NaN\n    end\nend\n\nfunction jump_value(input::T)::Float64 where {T <: JuMP.AbstractJuMPScalar}\n    return JuMP.value(input)\nend\n\nfunction jump_value(input::JuMP.ConstraintRef)::Float64\n    return JuMP.dual(input)\nend\n\nfunction jump_value(input::JumpSupportedLiterals)\n    return input\nend\n\n# Like jump_value but for certain special cases before optimize! is called\njump_fixed_value(input::Number) = input\njump_fixed_value(input::JuMP.VariableRef) = JuMP.fix_value(input)\njump_fixed_value(input::JuMP.AffExpr) =\n    sum([coeff * jump_fixed_value(param) for (coeff, param) in JuMP.linear_terms(input)]) +\n    JuMP.constant(input)\n\nfunction fix_parameter_value(input::JuMP.VariableRef, value::Float64)\n    JuMP.fix(input, value; force = true)\n    return\nend\n\n\"\"\"\nConvert Vectors, DenseAxisArrays, and SparkAxisArrays to a matrix.\n\n- If the input is a 1d array or DenseAxisArray, the returned matrix will have\n  a number of rows equal to the length of the input and one column.\n- If the input is a 2d DenseAxisArray, the dimensions are transposed, due to the way we\n  store outputs in JuMP.\n\"\"\"\nfunction to_matrix(vec::Vector)\n    data = vec[:]\n    return reshape(data, length(data), 1)\nend\n\nto_matrix(array::Matrix) = array\n\nfunction to_matrix(array::DenseAxisArray{T, 1}) where {T}\n    data = array.data[:]\n    return reshape(data, length(data), 1)\nend\n\nfunction to_matrix(array::DenseAxisArray{T, 2}) where {T}\n    return permutedims(array.data)\nend\n\nfunction to_matrix(array::DenseAxisArray)\n    error(\"Converting type = $(typeof(array)) to a matrix is not supported.\")\nend\n\nfunction _to_matrix(\n    array::SparseAxisArray{T, N, K},\n    columns,\n) where {T, N, K <: NTuple{N, Any}}\n    time_steps = Set{Int}(k[N] for k in keys(array.data))\n    data = Matrix{Float64}(undef, length(time_steps), length(columns))\n    for (ix, col) in enumerate(columns), t in time_steps\n        data[t, ix] = array.data[(col..., t)]\n    end\n    return data\nend\n\nfunction to_matrix(array::SparseAxisArray{T, N, K}) where {T, N, K <: NTuple{N, Any}}\n    # Don't use get_column_names_from_axis_array to avoid additional string conversion\n    # TODO: I don't understand why we have two mechanisms of creating columns.\n    # Why does get_column_names_from_axis_array rely on encode_tuple_to_column?\n    columns = sort!(unique!([k[1:(N - 1)] for k in keys(array.data)]))\n    return _to_matrix(array, columns)\nend\n\n\"\"\"\nReturn column names from the axes of a JuMP DenseAxisArray or SparseAxisArray.\nThe columns are returned as a tuple of vector of strings.\n1d and 2d arrays will return a tuple of length 1.\n3d arrays will return a tuple of length 2.\n\nThere are two variants of this function:\n  - get_column_names_from_axis_array(array)\n  - get_column_names_from_axis_array(::OptimizationContainerKey, array)\n  \nWhen the variant with the key is called:\n  In cases where the array has one dimension, retrieve the column names from the key.\n  In cases where the array has two or more dimensions, retrieve the column names from the\n  axes.\n\"\"\"\n# TODO: the docstring describes what the code does.\n# The behavior of key vs axes seems suspect to me.\nfunction get_column_names_from_axis_array(\n    array::DenseAxisArray{T, 1, <:Tuple{Vector{String}}},\n) where {T}\n    return (axes(array, 1),)\nend\n\nfunction get_column_names_from_axis_array(\n    array::DenseAxisArray{T, 1, <:Tuple{IntegerAxis}},\n) where {T}\n    # This happens because buses are stored by numbers instead of name.\n    return (string.(axes(array, 1)),)\nend\n\nfunction get_column_names_from_axis_array(\n    array::DenseAxisArray{T, 2, <:Tuple{Vector{String}, IntegerAxis}},\n) where {T}\n    return (axes(array, 1),)\nend\n\nfunction get_column_names_from_axis_array(\n    array::DenseAxisArray{T, 2, <:Tuple{IntegerAxis, IntegerAxis}},\n) where {T}\n    return (string.(axes(array, 1)),)\nend\n\nfunction get_column_names_from_axis_array(\n    array::DenseAxisArray{T, 3, <:Tuple{Vector{String}, Vector{String}, IntegerAxis}},\n) where {T}\n    return (axes(array, 1), axes(array, 2))\nend\n\nfunction get_column_names_from_axis_array(\n    array::DenseAxisArray{T, 3, <:Tuple{Vector{String}, IntegerAxis, IntegerAxis}},\n) where {T}\n    return (axes(array, 1), string.(axes(array, 2)))\nend\n\nfunction get_column_names_from_axis_array(\n    key::OptimizationContainerKey,\n    ::DenseAxisArray{T, 1},\n) where {T}\n    return get_column_names_from_key(key)\nend\n\nfunction get_column_names_from_axis_array(::OptimizationContainerKey, array::DenseAxisArray)\n    return get_column_names_from_axis_array(array)\nend\n\nfunction get_column_names_from_axis_array(\n    ::OptimizationContainerKey,\n    array::SparseAxisArray,\n)\n    return get_column_names_from_axis_array(array)\nend\n\nfunction get_column_names_from_axis_array(\n    array::SparseAxisArray{T, N, K},\n) where {T, N, K <: NTuple{N, Any}}\n    return (\n        sort!(\n            collect(Set(encode_tuple_to_column(k[1:(N - 1)]) for k in keys(array.data))),\n        ),\n    )\nend\n\n\"\"\"\nReturn the column names from a key as a tuple of vector of strings.\nOnly useful for 1d DenseAxisArrays.\n\"\"\"\nfunction get_column_names_from_key(key::OptimizationContainerKey)\n    return ([encode_key_as_string(key)],)\nend\n\nfunction encode_tuple_to_column(val::NTuple{N, <:AbstractString}) where {N}\n    return join(val, PSI_NAME_DELIMITER)\nend\n\nfunction encode_tuple_to_column(val::Tuple{String, Int})\n    return join(string.(val), PSI_NAME_DELIMITER)\nend\n\n\"\"\"\nCreate a DataFrame from a JuMP DenseAxisArray or SparseAxisArray.\n\n# Arguments\n\n  - `array`: JuMP DenseAxisArray or SparseAxisArray to convert\n  - `key::OptimizationContainerKey`:\n\"\"\"\nfunction to_dataframe(\n    array::DenseAxisArray{T, 2},\n    key::OptimizationContainerKey,\n) where {T <: JumpSupportedLiterals}\n    return DataFrame(to_matrix(array), get_column_names_from_axis_array(key, array)[1])\nend\n\nfunction to_dataframe(\n    array::DenseAxisArray{T, 2, <:Tuple{Vector{String}, UnitRange{Int}}},\n) where {T <: JumpSupportedLiterals}\n    return DataFrame(to_matrix(array), get_column_names_from_axis_array(array)[1])\nend\n\nfunction to_dataframe(\n    array::DenseAxisArray{T, 1},\n    key::OptimizationContainerKey,\n) where {T <: JumpSupportedLiterals}\n    cols = get_column_names_from_axis_array(key, array)[1]\n    if length(cols) != 1\n        error(\"Expected a single column, got $(length(cols))\")\n    end\n    return DataFrame(Symbol(cols[1]) => array.data)\nend\n\nfunction to_dataframe(array::SparseAxisArray, key::OptimizationContainerKey)\n    return DataFrame(to_matrix(array), get_column_names_from_axis_array(key, array)[1])\nend\n\n\"\"\"\nConvert a DenseAxisArray containing components to a results DataFrame consumable by users.\n\n# Arguments\n- `array: DenseAxisArray`: JuMP DenseAxisArray to convert\n- `timestamps`: Iterable of timestamps for each component or nothing if time is not known.\n  The resulting DataFrame will have the column \"DateTime\" if timestamps is not nothing.\n  Otherwise, it will have the column \"time_index\", representing the index of the time\n  dimension.\n- `::Val{TableFormat}`: Format of the table to create.\n  If it is TableFormat.LONG, the DataFrame will have the column \"name\", and, if\n  the data has three dimensions, \"name2.\"\n  If it is TableFormat.WIDE, the DataFrame will have columns for each component. Wide\n  format does not support arrays with more than two dimensions.\n\"\"\"\nfunction to_results_dataframe(array::DenseAxisArray, timestamps)\n    return to_results_dataframe(array, timestamps, Val(TableFormat.LONG))()\nend\n\nfunction to_results_dataframe(\n    array::DenseAxisArray{Float64, 1, <:Tuple{Vector{String}}},\n    timestamps,\n    ::Val{TableFormat.LONG},\n)\n    return DataFrames.DataFrame(\n        :DateTime => ones(Int, length(axes(array, 1))),\n        :name => axes(array, 1),\n        :value => array.data,\n    )\nend\n\nfunction to_results_dataframe(\n    array::DenseAxisArray{Float64, 2, <:Tuple{Vector{String}, IntegerAxis}},\n    timestamps,\n    ::Val{TableFormat.LONG},\n)\n    num_timestamps = size(array, 2)\n    if length(timestamps) != num_timestamps\n        error(\n            \"The number of timestamps must match the number of rows per component. \" *\n            \"timestamps = $(length(timestamps)) \" *\n            \"num_timestamps = $num_timestamps\",\n        )\n    end\n    num_rows = length(array.data)\n    timestamps_arr = _collect_timestamps(timestamps)\n    time_col = Vector{Dates.DateTime}(undef, num_rows)\n    name_col = Vector{String}(undef, num_rows)\n\n    row_index = 1\n    for name in axes(array, 1)\n        for time_index in axes(array, 2)\n            time_col[row_index] = timestamps_arr[time_index]\n            name_col[row_index] = name\n            row_index += 1\n        end\n    end\n\n    return DataFrame(\n        :DateTime => time_col,\n        :name => name_col,\n        :value => reshape(permutedims(array.data), num_rows),\n    )\nend\n\n_collect_timestamps(timestamps::Vector{Dates.DateTime}) = timestamps\n_collect_timestamps(timestamps) = collect(timestamps)\n\nfunction to_results_dataframe(\n    array::DenseAxisArray{Float64, 2, <:Tuple{Vector{String}, IntegerAxis}},\n    ::Nothing,\n    ::Val{TableFormat.LONG},\n)\n    num_rows = length(array.data)\n    time_col = Vector{Int}(undef, num_rows)\n    name_col = Vector{String}(undef, num_rows)\n\n    row_index = 1\n    for name in axes(array, 1)\n        for time_index in axes(array, 2)\n            time_col[row_index] = time_index\n            name_col[row_index] = name\n            row_index += 1\n        end\n    end\n\n    return DataFrame(\n        :time_index => time_col,\n        :name => name_col,\n        :value => reshape(permutedims(array.data), num_rows),\n    )\nend\n\nfunction to_results_dataframe(\n    array::DenseAxisArray{Float64, 1, <:Tuple{IntegerAxis}},\n    ::Nothing,\n    ::Val{TableFormat.LONG},\n)\n    num_rows = length(array.data)\n    time_col = Vector{Int}(undef, num_rows)\n    name_col = Vector{String}(undef, num_rows)\n    row_index = 1\n    name = \"Result\"\n    for time_index in axes(array, 1)\n        time_col[row_index] = time_index\n        name_col[row_index] = name\n        row_index += 1\n    end\n\n    return DataFrame(\n        :time_index => time_col,\n        :name => name_col,\n        :value => reshape(permutedims(array.data), num_rows),\n    )\nend\n\nfunction to_results_dataframe(\n    array::DenseAxisArray{Float64, 2, <:Tuple{Vector{String}, IntegerAxis}},\n    timestamps,\n    ::Val{TableFormat.WIDE},\n)\n    df = DataFrame(to_matrix(array), axes(array, 1))\n    DataFrames.insertcols!(df, 1, :DateTime => timestamps)\n    return df\nend\n\nfunction to_results_dataframe(\n    array::DenseAxisArray{Float64, 2, <:Tuple{Vector{String}, IntegerAxis}},\n    ::Nothing,\n    ::Val{TableFormat.WIDE},\n)\n    df = DataFrame(to_matrix(array), axes(array, 1))\n    DataFrames.insertcols!(df, 1, :time_index => axes(array, 2))\n    return df\nend\n\nfunction to_results_dataframe(\n    array::DenseAxisArray{\n        Float64,\n        3,\n        <:Tuple{Vector{String}, Vector{String}, UnitRange{Int}},\n    },\n    timestamps,\n    ::Val{TableFormat.LONG},\n)\n    num_timestamps = size(array, 3)\n    if length(timestamps) != num_timestamps\n        error(\n            \"The number of timestamps must match the number of rows per component. \" *\n            \"timestamps = $(length(timestamps)) \" *\n            \"num_timestamps = $num_timestamps\",\n        )\n    end\n    num_rows = length(array.data)\n    timestamps_arr = _collect_timestamps(timestamps)\n    time_col = Vector{Dates.DateTime}(undef, num_rows)\n    name_col = Vector{String}(undef, num_rows)\n    name2_col = Vector{String}(undef, num_rows)\n    vals = Vector{Float64}(undef, num_rows)\n\n    row_index = 1\n    for name in axes(array, 1)\n        for name2 in axes(array, 2)\n            for time_index in axes(array, 3)\n                time_col[row_index] = timestamps_arr[time_index]\n                name_col[row_index] = name\n                name2_col[row_index] = name2\n                vals[row_index] = array[name, name2, time_index]\n                row_index += 1\n            end\n        end\n    end\n\n    return DataFrame(\n        :DateTime => time_col,\n        :name => name_col,\n        :name2 => name2_col,\n        :value => vals,\n    )\nend\n\nfunction to_results_dataframe(\n    array::DenseAxisArray{\n        Float64,\n        3,\n        <:Tuple{Vector{String}, Vector{String}, UnitRange{Int}},\n    },\n    ::Nothing,\n    ::Val{TableFormat.LONG},\n)\n    num_rows = length(array.data)\n    time_col = Vector{Int}(undef, num_rows)\n    name_col = Vector{String}(undef, num_rows)\n    name2_col = Vector{String}(undef, num_rows)\n    vals = Vector{Float64}(undef, num_rows)\n\n    row_index = 1\n    for name in axes(array, 1)\n        for name2 in axes(array, 2)\n            for time_index in axes(array, 3)\n                time_col[row_index] = time_index\n                name_col[row_index] = name\n                name2_col[row_index] = name2\n                vals[row_index] = array[name, name2, time_index]\n                row_index += 1\n            end\n        end\n    end\n\n    return DataFrame(\n        :time_index => time_col,\n        :name => name_col,\n        :name2 => name2_col,\n        :value => vals,\n    )\nend\n\nfunction to_dataframe(array::SparseAxisArray{T, N, K}) where {T, N, K <: NTuple{N, Any}}\n    columns = get_column_names_from_axis_array(array)\n    return DataFrames.DataFrame(_to_matrix(array, columns), columns)\nend\n\n\"\"\"\nReturns the correct container specification for the selected type of JuMP Model\n\"\"\"\nfunction container_spec(::Type{T}, axs...) where {T <: Any}\n    return DenseAxisArray{T}(undef, axs...)\nend\n\n\"\"\"\nReturns the correct container specification for the selected type of JuMP Model\n\"\"\"\nfunction container_spec(::Type{Float64}, axs...)\n    cont = DenseAxisArray{Float64}(undef, axs...)\n    cont.data .= fill(NaN, size(cont.data))\n    return cont\nend\n\n\"\"\"\nReturns the correct container specification for the selected type of JuMP Model\n\"\"\"\nfunction sparse_container_spec(::Type{T}, axs...) where {T <: JuMP.AbstractJuMPScalar}\n    indexes = Base.Iterators.product(axs...)\n    contents = Dict{eltype(indexes), T}(indexes .=> zero(T))\n    return SparseAxisArray(contents)\nend\n\nfunction sparse_container_spec(::Type{T}, axs...) where {T <: JuMP.VariableRef}\n    indexes = Base.Iterators.product(axs...)\n    contents = Dict{eltype(indexes), Union{Nothing, T}}(indexes .=> nothing)\n    return SparseAxisArray(contents)\nend\n\nfunction sparse_container_spec(::Type{T}, axs...) where {T <: JuMP.ConstraintRef}\n    indexes = Base.Iterators.product(axs...)\n    contents = Dict{eltype(indexes), Union{Nothing, T}}(indexes .=> nothing)\n    return SparseAxisArray(contents)\nend\n\nfunction sparse_container_spec(::Type{T}, axs...) where {T <: Number}\n    indexes = Base.Iterators.product(axs...)\n    contents = Dict{eltype(indexes), T}(indexes .=> zero(T))\n    return SparseAxisArray(contents)\nend\n\nfunction remove_undef!(expression_array::AbstractArray)\n    # iteration is deliberately unsupported for CartesianIndex\n    # Makes this code a bit hacky to be able to use isassigned with an array of arbitrary size.\n    for i in CartesianIndices(expression_array.data)\n        if !isassigned(expression_array.data, i.I...)\n            expression_array.data[i] = zero(eltype(expression_array))\n        end\n    end\n\n    return expression_array\nend\n\nremove_undef!(expression_array::SparseAxisArray) = expression_array\n\nfunction _calc_dimensions(\n    array::DenseAxisArray,\n    key::OptimizationContainerKey,\n    num_rows::Int,\n    horizon::Int,\n)\n    ax = axes(array)\n    columns = get_column_names_from_axis_array(key, array)\n    # Two use cases for read:\n    # 1. Read data for one execution for one device.\n    # 2. Read data for one execution for all devices.\n    # This will ensure that data on disk is contiguous in both cases.\n    if length(ax) == 1\n        if length(ax[1]) != horizon\n            @debug \"$(encode_key_as_string(key)) has length $(length(ax[1])). Different than horizon $horizon.\"\n        end\n        dims = (length(ax[1]), 1, num_rows)\n    elseif length(ax) == 2\n        if length(ax[2]) != horizon\n            @debug \"$(encode_key_as_string(key)) has length $(length(ax[1])). Different than horizon $horizon.\"\n        end\n        dims = (length(ax[2]), length(columns[1]), num_rows)\n    elseif length(ax) == 3\n        if length(ax[3]) != horizon\n            @debug \"$(encode_key_as_string(key)) has length $(length(ax[1])). Different than horizon $horizon.\"\n        end\n        dims = (length(ax[3]), length(columns[1]), length(columns[2]), num_rows)\n    else\n        error(\"unsupported data size $(length(ax))\")\n    end\n\n    return Dict(\"columns\" => columns, \"dims\" => dims)\nend\n\nfunction _calc_dimensions(\n    array::SparseAxisArray,\n    key::OptimizationContainerKey,\n    num_rows::Int,\n    horizon::Int,\n)\n    columns = get_column_names_from_axis_array(key, array)\n    dims = (horizon, length.(columns)..., num_rows)\n    return Dict(\"columns\" => columns, \"dims\" => dims)\nend\n\n\"\"\"\nRun this function only when getting detailed solver stats\n\"\"\"\nfunction _summary_to_dict!(optimizer_stats::OptimizerStats, jump_model::JuMP.Model)\n    # JuMP.solution_summary uses a lot of try-catch so it has a performance hit and should be opt-in\n    jump_summary = JuMP.solution_summary(jump_model; verbose = false)\n    # Note we don't grab all the fields from the summary because not all can be encoded as Float for HDF store\n    fields = [\n        :has_values, # Bool\n        :has_duals, # Bool\n        # Candidate solution\n        :objective_bound, # Union{Missing,Float64}\n        :dual_objective_value, # Union{Missing,Float64}\n        # Work counters\n        :relative_gap, # Union{Missing,Int}\n        :barrier_iterations, # Union{Missing,Int}\n        :simplex_iterations, # Union{Missing,Int}\n        :node_count, # Union{Missing,Int}\n    ]\n\n    for field in fields\n        field_value = getfield(jump_summary, field)\n        if ismissing(field_value)\n            setfield!(optimizer_stats, field, missing)\n        else\n            setfield!(optimizer_stats, field, field_value)\n        end\n    end\n    return\nend\n\nfunction supports_milp(jump_model::JuMP.Model)\n    optimizer_backend = JuMP.backend(jump_model)\n    return MOI.supports_constraint(optimizer_backend, MOI.VariableIndex, MOI.ZeroOne)\nend\n\nfunction _get_solver_time(jump_model::JuMP.Model)\n    solver_solve_time = NaN\n\n    try_s =\n        get!(jump_model.ext, :try_supports_solvetime, (trycatch = true, supports = true))\n    if try_s.trycatch\n        try\n            solver_solve_time = MOI.get(jump_model, MOI.SolveTimeSec())\n            jump_model.ext[:try_supports_solvetime] = (trycatch = false, supports = true)\n        catch\n            @debug \"SolveTimeSec() property not supported by the Solver\"\n            jump_model.ext[:try_supports_solvetime] = (trycatch = false, supports = false)\n        end\n    else\n        if try_s.supports\n            solver_solve_time = MOI.get(jump_model, MOI.SolveTimeSec())\n        end\n    end\n\n    return solver_solve_time\nend\n\nfunction write_optimizer_stats!(optimizer_stats::OptimizerStats, jump_model::JuMP.Model)\n    if JuMP.primal_status(jump_model) == MOI.FEASIBLE_POINT::MOI.ResultStatusCode\n        optimizer_stats.objective_value = JuMP.objective_value(jump_model)\n    else\n        optimizer_stats.objective_value = Inf\n    end\n\n    optimizer_stats.termination_status = Int(JuMP.termination_status(jump_model))\n    optimizer_stats.primal_status = Int(JuMP.primal_status(jump_model))\n    optimizer_stats.dual_status = Int(JuMP.dual_status(jump_model))\n    optimizer_stats.result_count = JuMP.result_count(jump_model)\n    optimizer_stats.solve_time = _get_solver_time(jump_model)\n    if optimizer_stats.detailed_stats\n        _summary_to_dict!(optimizer_stats, jump_model)\n    end\n    return\nend\n\n\"\"\"\nExports the JuMP object in MathOptFormat\n\"\"\"\nfunction serialize_jump_optimization_model(jump_model::JuMP.Model, save_path::String)\n    MOF_model = MOPFM(; format = MOI.FileFormats.FORMAT_MOF)\n    MOI.copy_to(MOF_model, JuMP.backend(jump_model))\n    MOI.write_to_file(MOF_model, save_path)\n    return\nend\n\nfunction write_lp_file(jump_model::JuMP.Model, save_path::String)\n    MOF_model = MOPFM(; format = MOI.FileFormats.FORMAT_LP)\n    MOI.copy_to(MOF_model, JuMP.backend(jump_model))\n    MOI.write_to_file(MOF_model, save_path)\n    return\nend\n\n# check_conflict_status functions can't be tested on CI because free solvers don't support IIS\nfunction check_conflict_status(\n    jump_model::JuMP.Model,\n    constraint_container::DenseAxisArray{JuMP.ConstraintRef},\n)\n    conflict_indices = Vector()\n    dims = axes(constraint_container)\n    for index in Iterators.product(dims...)\n        if isassigned(constraint_container, index...) &&\n           MOI.get(\n            jump_model,\n            MOI.ConstraintConflictStatus(),\n            constraint_container[index...],\n        ) != MOI.NOT_IN_CONFLICT\n            push!(conflict_indices, index)\n        end\n    end\n    return conflict_indices\nend\n\nfunction check_conflict_status(\n    jump_model::JuMP.Model,\n    constraint_container::SparseAxisArray{JuMP.ConstraintRef},\n)\n    conflict_indices = Vector()\n    for (index, constraint) in constraint_container\n        if isassigned(constraint_container, index...) &&\n           MOI.get(jump_model, MOI.ConstraintConflictStatus(), constraint) !=\n           MOI.NOT_IN_CONFLICT\n            push!(conflict_indices, index)\n        end\n    end\n    return conflict_indices\nend\n"
  },
  {
    "path": "src/utils/logging.jl",
    "content": "\nLOG_GROUP_COST_FUNCTIONS = :CostFunctionsConstructor\nLOG_GROUP_OPTIMZATION_CONTAINER = :OptimizationContainer\nLOG_GROUP_TYPE_REGISTRATIONS = :TypeRegistrations\nLOG_GROUP_FEEDFORWARDS_CONSTRUCTION = :FeedforwardConstructor\nLOG_GROUP_BRANCH_CONSTRUCTIONS = :BranchConstructor\nLOG_GROUP_SERVICE_CONSTUCTORS = :ServicesConstructor\nLOG_GROUP_MODELS_VALIDATION = :ModelValidation\nLOG_GROUP_OPTIMIZATION_CONTAINER = :OptimizationContainer\nLOG_GROUP_BUILD_INITIAL_CONDITIONS = :InitialConditionsBuild\nLOG_GROUP_NETWORK_CONSTRUCTION = :NetworkConstructor\nLOG_GROUP_MODEL_STORE = :ModelStore\nLOG_GROUP_RESULTS = :Results\nLOG_GROUP_SIMULATION_STORE = :SimulationStore\n"
  },
  {
    "path": "src/utils/powersystems_utils.jl",
    "content": "\"\"\"\nConvert the internal `Dates.Millisecond` interval (where `UNSET_INTERVAL` means\nunset) to the `Union{Nothing, Dates.Period}` form the IS / PSY time-series API\nexpects. Internal hot paths carry a concrete `Dates.Millisecond` to stay\ntype-stable; this helper performs the boundary conversion.\n\"\"\"\n_to_is_interval(interval::Dates.Millisecond) =\n    interval == UNSET_INTERVAL ? nothing : interval\n\n\"\"\"\nConvert the internal `Dates.Millisecond` resolution (where `UNSET_RESOLUTION`\nmeans unset) to the `Union{Nothing, Dates.Period}` form the IS / PSY\ntime-series API expects.\n\"\"\"\n_to_is_resolution(resolution::Dates.Millisecond) =\n    resolution == UNSET_RESOLUTION ? nothing : resolution\n\nfunction get_available_components(\n    model::DeviceModel{T, <:AbstractDeviceFormulation},\n    sys::PSY.System,\n) where {T <: PSY.Component}\n    subsystem = get_subsystem(model)\n    filter_function = get_attribute(model, \"filter_function\")\n    if filter_function === nothing\n        return PSY.get_components(\n            PSY.get_available,\n            T,\n            sys;\n            subsystem_name = subsystem,\n        )\n    else\n        return PSY.get_components(\n            x -> PSY.get_available(x) && filter_function(x),\n            T,\n            sys;\n            subsystem_name = subsystem,\n        )\n    end\nend\n\nfunction get_available_components(\n    model::ServiceModel{T, <:AbstractServiceFormulation},\n    sys::PSY.System,\n) where {T <: PSY.Component}\n    subsystem = get_subsystem(model)\n    filter_function = get_attribute(model, \"filter_function\")\n    if filter_function === nothing\n        return PSY.get_components(\n            PSY.get_available,\n            T,\n            sys;\n            subsystem_name = subsystem,\n        )\n    else\n        return PSY.get_components(\n            x -> PSY.get_available(x) && filter_function(x),\n            T,\n            sys;\n            subsystem_name = subsystem,\n        )\n    end\nend\n\n_filter_function(x::PSY.ACBus) =\n    PSY.get_bustype(x) != PSY.ACBusTypes.ISOLATED && PSY.get_available(x)\n\nfunction get_available_components(\n    model::NetworkModel,\n    ::Type{PSY.ACBus},\n    sys::PSY.System,\n)\n    subsystem = get_subsystem(model)\n    return PSY.get_components(\n        _filter_function,\n        PSY.ACBus,\n        sys;\n        subsystem_name = subsystem,\n    )\nend\n\nfunction get_available_components(\n    model::NetworkModel,\n    ::Type{T},\n    sys::PSY.System,\n) where {T <: PSY.Component}\n    subsystem = get_subsystem(model)\n    return PSY.get_components(\n        T,\n        sys;\n        subsystem_name = subsystem,\n    )\nend\n\n#=\nfunction get_available_components(\n    ::Type{PSY.RegulationDevice{T}},\n    sys::PSY.System,\n) where {T <: PSY.Component}\n    return PSY.get_components(\n        x -> (PSY.get_available(x) && PSY.has_service(x, PSY.AGC)),\n        PSY.RegulationDevice{T},\n        sys,\n    )\nend\n=#\n\nmake_system_filename(sys::PSY.System) = make_system_filename(IS.get_uuid(sys))\nmake_system_filename(sys_uuid::Union{Base.UUID, AbstractString}) = \"system-$(sys_uuid).json\"\n\nfunction check_hvdc_line_limits_consistency(\n    d::Union{PSY.TwoTerminalHVDC, PSY.TModelHVDCLine},\n)\n    from_min = PSY.get_active_power_limits_from(d).min\n    to_min = PSY.get_active_power_limits_to(d).min\n    from_max = PSY.get_active_power_limits_from(d).max\n    to_max = PSY.get_active_power_limits_to(d).max\n\n    if from_max < to_min\n        throw(\n            IS.ConflictingInputsError(\n                \"From Max $(from_max) can't be a smaller value than To Min $(to_min)\",\n            ),\n        )\n    elseif to_max < from_min\n        throw(\n            IS.ConflictingInputsError(\n                \"To Max $(to_max) can't be a smaller value than From Min $(from_min)\",\n            ),\n        )\n    end\n    return\nend\n\nfunction check_hvdc_line_limits_unidirectional(d::PSY.TwoTerminalHVDC)\n    from_min = PSY.get_active_power_limits_from(d).min\n    to_min = PSY.get_active_power_limits_to(d).min\n    from_max = PSY.get_active_power_limits_from(d).max\n    to_max = PSY.get_active_power_limits_to(d).max\n\n    if from_min < 0 || to_min < 0 || from_max < 0 || to_max < 0\n        throw(\n            IS.ConflictingInputsError(\n                \"Changing flow direction on HVDC Line $(PSY.get_name(d)) is not compatible with non-linear network formulations. \\\n                Bi-directional models with losses are only compatible with linear network models like DCPPowerModel.\",\n            ),\n        )\n    end\n    return\nend\n\n##################################################\n########### Cost Function Utilities ##############\n##################################################\n\n\"\"\"\nObtain proportional (marginal or slope) cost data in system base per unit\ndepending on the specified power units\n\"\"\"\nfunction get_proportional_cost_per_system_unit(\n    cost_term::Float64,\n    unit_system::PSY.UnitSystem,\n    system_base_power::Float64,\n    device_base_power::Float64,\n)\n    return _get_proportional_cost_per_system_unit(\n        cost_term,\n        Val{unit_system}(),\n        system_base_power,\n        device_base_power,\n    )\nend\n\nfunction _get_proportional_cost_per_system_unit(\n    cost_term::Float64,\n    ::Val{PSY.UnitSystem.SYSTEM_BASE},\n    system_base_power::Float64,\n    device_base_power::Float64,\n)\n    return cost_term\nend\n\nfunction _get_proportional_cost_per_system_unit(\n    cost_term::Float64,\n    ::Val{PSY.UnitSystem.DEVICE_BASE},\n    system_base_power::Float64,\n    device_base_power::Float64,\n)\n    return cost_term * (system_base_power / device_base_power)\nend\n\nfunction _get_proportional_cost_per_system_unit(\n    cost_term::Float64,\n    ::Val{PSY.UnitSystem.NATURAL_UNITS},\n    system_base_power::Float64,\n    device_base_power::Float64,\n)\n    return cost_term * system_base_power\nend\n\n\"\"\"\nObtain quadratic cost data in system base per unit\ndepending on the specified power units\n\"\"\"\nfunction get_quadratic_cost_per_system_unit(\n    cost_term::Float64,\n    unit_system::PSY.UnitSystem,\n    system_base_power::Float64,\n    device_base_power::Float64,\n)\n    return _get_quadratic_cost_per_system_unit(\n        cost_term,\n        Val{unit_system}(),\n        system_base_power,\n        device_base_power,\n    )\nend\n\nfunction _get_quadratic_cost_per_system_unit(\n    cost_term::Float64,\n    ::Val{PSY.UnitSystem.SYSTEM_BASE}, # SystemBase Unit\n    system_base_power::Float64,\n    device_base_power::Float64,\n)\n    return cost_term\nend\n\nfunction _get_quadratic_cost_per_system_unit(\n    cost_term::Float64,\n    ::Val{PSY.UnitSystem.DEVICE_BASE}, # DeviceBase Unit\n    system_base_power::Float64,\n    device_base_power::Float64,\n)\n    return cost_term * (system_base_power / device_base_power)^2\nend\n\nfunction _get_quadratic_cost_per_system_unit(\n    cost_term::Float64,\n    ::Val{PSY.UnitSystem.NATURAL_UNITS}, # Natural Units\n    system_base_power::Float64,\n    device_base_power::Float64,\n)\n    return cost_term * system_base_power^2\nend\n\n\"\"\"\nObtain the normalized PiecewiseLinear cost data in system base per unit\ndepending on the specified power units.\n\nNote that the costs (y-axis) are always in \\$/h so\nthey do not require transformation\n\"\"\"\nfunction get_piecewise_pointcurve_per_system_unit(\n    cost_component::PSY.PiecewiseLinearData,\n    unit_system::PSY.UnitSystem,\n    system_base_power::Float64,\n    device_base_power::Float64,\n)\n    return _get_piecewise_pointcurve_per_system_unit(\n        cost_component,\n        Val{unit_system}(),\n        system_base_power,\n        device_base_power,\n    )\nend\n\nfunction _get_piecewise_pointcurve_per_system_unit(\n    cost_component::PSY.PiecewiseLinearData,\n    ::Val{PSY.UnitSystem.SYSTEM_BASE},\n    system_base_power::Float64,\n    device_base_power::Float64,\n)\n    return cost_component\nend\n\nfunction _get_piecewise_pointcurve_per_system_unit(\n    cost_component::PSY.PiecewiseLinearData,\n    ::Val{PSY.UnitSystem.DEVICE_BASE},\n    system_base_power::Float64,\n    device_base_power::Float64,\n)\n    points = cost_component.points\n    points_normalized = Vector{NamedTuple{(:x, :y)}}(undef, length(points))\n    for (ix, point) in enumerate(points)\n        points_normalized[ix] =\n            (x = point.x * (device_base_power / system_base_power), y = point.y)\n    end\n    return PSY.PiecewiseLinearData(points_normalized)\nend\n\nfunction _get_piecewise_pointcurve_per_system_unit(\n    cost_component::PSY.PiecewiseLinearData,\n    ::Val{PSY.UnitSystem.NATURAL_UNITS},\n    system_base_power::Float64,\n    device_base_power::Float64,\n)\n    points = cost_component.points\n    points_normalized = Vector{NamedTuple{(:x, :y)}}(undef, length(points))\n    for (ix, point) in enumerate(points)\n        points_normalized[ix] = (x = point.x / system_base_power, y = point.y)\n    end\n    return PSY.PiecewiseLinearData(points_normalized)\nend\n\n\"\"\"\nObtain the normalized PiecewiseStepData in system base per unit depending on the specified\npower units.\n\nNote that the costs (y-axis) are in \\$/MWh, \\$/(sys pu h) or \\$/(device pu h), so they also\nrequire transformation.\n\"\"\"\nfunction get_piecewise_curve_per_system_unit(\n    cost_component::PSY.PiecewiseStepData,\n    unit_system::PSY.UnitSystem,\n    system_base_power::Float64,\n    device_base_power::Float64,\n)\n    return PSY.PiecewiseStepData(\n        get_piecewise_curve_per_system_unit(\n            PSY.get_x_coords(cost_component),\n            PSY.get_y_coords(cost_component),\n            unit_system,\n            system_base_power,\n            device_base_power,\n        )...,\n    )\nend\n\nfunction get_piecewise_curve_per_system_unit(\n    x_coords::AbstractVector,\n    y_coords::AbstractVector,\n    unit_system::PSY.UnitSystem,\n    system_base_power::Float64,\n    device_base_power::Float64,\n)\n    return _get_piecewise_curve_per_system_unit(\n        x_coords,\n        y_coords,\n        Val{unit_system}(),\n        system_base_power,\n        device_base_power,\n    )\nend\n\nfunction _get_piecewise_curve_per_system_unit(\n    x_coords::AbstractVector,\n    y_coords::AbstractVector,\n    ::Val{PSY.UnitSystem.SYSTEM_BASE},\n    system_base_power::Float64,\n    device_base_power::Float64,\n)\n    return x_coords, y_coords\nend\n\nfunction _get_piecewise_curve_per_system_unit(\n    x_coords::AbstractVector,\n    y_coords::AbstractVector,\n    ::Val{PSY.UnitSystem.DEVICE_BASE},\n    system_base_power::Float64,\n    device_base_power::Float64,\n)\n    ratio = device_base_power / system_base_power\n    x_coords_normalized = x_coords .* ratio\n    y_coords_normalized = y_coords ./ ratio\n    return x_coords_normalized, y_coords_normalized\nend\n\nfunction _get_piecewise_curve_per_system_unit(\n    x_coords::AbstractVector,\n    y_coords::AbstractVector,\n    ::Val{PSY.UnitSystem.NATURAL_UNITS},\n    system_base_power::Float64,\n    device_base_power::Float64,\n)\n    x_coords_normalized = x_coords ./ system_base_power\n    y_coords_normalized = y_coords .* system_base_power\n    return x_coords_normalized, y_coords_normalized\nend\n\nis_time_variant(::IS.TimeSeriesKey) = true\nis_time_variant(::Any) = false\n\nfunction create_temporary_cost_function_in_system_per_unit(\n    original_cost_function::PSY.CostCurve,\n    new_data::PSY.PiecewiseLinearData,\n)\n    return PSY.CostCurve(\n        PSY.PiecewisePointCurve(new_data),\n        PSY.UnitSystem.SYSTEM_BASE,\n        PSY.get_vom_cost(original_cost_function),\n    )\nend\n\nfunction create_temporary_cost_function_in_system_per_unit(\n    original_cost_function::PSY.FuelCurve,\n    new_data::PSY.PiecewiseLinearData,\n)\n    return PSY.FuelCurve(\n        PSY.PiecewisePointCurve(new_data),\n        PSY.UnitSystem.SYSTEM_BASE,\n        PSY.get_fuel_cost(original_cost_function),\n        PSY.LinearCurve(0.0),  # setting fuel offtake cost to default value of 0\n        PSY.get_vom_cost(original_cost_function),\n    )\nend\n\n\"\"\"\nReturn the set of distinct forecast intervals present in the system.\n\"\"\"\nfunction get_forecast_intervals(sys::PSY.System)\n    table = PSY.get_forecast_summary_table(sys)\n    return Set(row.interval for row in eachrow(table) if row.interval !== nothing)\nend\n\n\"\"\"\nReturn `(initial_timestamp, length)` for the `SingleTimeSeries` in `sys` whose\nresolution matches `resolution`. Throws `IS.InvalidValue` when no match exists\nor when matching series disagree on either field. Used to validate emulation\nmodel inputs when a system carries SingleTimeSeries at multiple resolutions.\n\"\"\"\nfunction get_single_time_series_consistency(\n    sys::PSY.System,\n    resolution::Dates.Period,\n)\n    table = PSY.get_static_time_series_summary_table(sys)\n    target = Dates.canonicalize(Dates.Millisecond(resolution))\n    filtered =\n        [row for row in eachrow(table) if row.resolution == target]\n    if isempty(filtered)\n        throw(\n            IS.InvalidValue(\n                \"No SingleTimeSeries found at resolution $(target)\",\n            ),\n        )\n    end\n    unique_pairs =\n        unique((row.initial_timestamp, row.time_step_count) for row in filtered)\n    if length(unique_pairs) > 1\n        throw(\n            IS.InvalidValue(\n                \"SingleTimeSeries at resolution $(target) have inconsistent \" *\n                \"initial times and lengths: $(collect(unique_pairs))\",\n            ),\n        )\n    end\n    ini_time_str, ts_length = first(unique_pairs)\n    return (Dates.DateTime(ini_time_str), ts_length)\nend\n\n\"\"\"\nAutomatically transform `SingleTimeSeries` into `DeterministicSingleTimeSeries` for a\ngiven (horizon, interval) when a DecisionModel is built with these settings and the\nsystem contains only static time series. Uses `delete_existing=false` so multiple\nmodels may share the same system with different transforms.\n\nDoes nothing when:\n\n  - The model's `horizon` or `interval` are unset.\n  - The system has no `SingleTimeSeries` to transform.\n  - The system has existing forecast data AND the requested interval is already present in those forecasts.\n\"\"\"\nfunction auto_transform_time_series!(sys::PSY.System, settings::Settings)\n    model_interval = get_interval(settings)\n    model_horizon = get_horizon(settings)\n    if model_interval == UNSET_INTERVAL || model_horizon == UNSET_HORIZON\n        return\n    end\n\n    counts = PSY.get_time_series_counts(sys)\n    if counts.static_time_series_count < 1\n        return\n    end\n    if counts.forecast_count > 0 && model_interval in get_forecast_intervals(sys)\n        return\n    end\n\n    model_resolution = get_resolution(settings)\n    resolution_kwarg =\n        model_resolution == UNSET_RESOLUTION ? (;) : (; resolution = model_resolution)\n\n    @info \"Auto-transforming SingleTimeSeries to DeterministicSingleTimeSeries\" horizon =\n        Dates.canonicalize(model_horizon) interval = Dates.canonicalize(model_interval)\n    PSY.transform_single_time_series!(\n        sys,\n        model_horizon,\n        model_interval;\n        delete_existing = false,\n        resolution_kwarg...,\n    )\n    return\nend\n\nfunction get_deterministic_time_series_type(sys::PSY.System)\n    time_series_types = IS.get_time_series_counts_by_type(sys.data)\n    existing_types = Set(d[\"type\"] for d in time_series_types)\n    if Set([\"Deterministic\", \"DeterministicSingleTimeSeries\"]) ∈ existing_types\n        error(\n            \"The System contains a combination of forecast data and transformed time series data. Currently this is not supported.\",\n        )\n    end\n    if \"Deterministic\" ∈ existing_types\n        return PSY.Deterministic\n    elseif \"DeterministicSingleTimeSeries\" ∈ existing_types\n        return PSY.DeterministicSingleTimeSeries\n    else\n        error(\n            \"The System does not contain any forecast data or transformed time series data.\",\n        )\n    end\nend\n"
  },
  {
    "path": "src/utils/print_pt_v2.jl",
    "content": "function Base.show(io::IO, container::OptimizationContainer)\n    show(io, get_jump_model(container))\nend\n\nfunction Base.show(io::IO, ::MIME\"text/plain\", input::Union{ServiceModel, DeviceModel})\n    _show_method(io, input, :auto)\nend\n\nfunction Base.show(io::IO, ::MIME\"text/html\", input::Union{ServiceModel, DeviceModel})\n    _show_method(io, input, :html; standalone = false, tf = PrettyTables.tf_html_simple)\nend\n\nfunction _show_method(\n    io::IO,\n    model::Union{ServiceModel, DeviceModel},\n    backend::Symbol;\n    kwargs...,\n)\n    println(io)\n    header = [\"Device Type\", \"Formulation\", \"Slacks\"]\n\n    table = Matrix{String}(undef, 1, length(header))\n    table[1, 1] = string(get_component_type(model))\n    table[1, 2] = string(get_formulation(model))\n    table[1, 3] = string(model.use_slacks)\n\n    PrettyTables.pretty_table(\n        io,\n        table;\n        header = header,\n        backend = Val(backend),\n        title = \"Device Model\",\n        alignment = :l,\n        kwargs...,\n    )\n\n    if !isempty(model.attributes)\n        println(io)\n        header = [\"Name\", \"Value\"]\n\n        table = Matrix{String}(undef, length(model.attributes), length(header))\n        for (ix, (k, v)) in enumerate(model.attributes)\n            table[ix, 1] = string(k)\n            table[ix, 2] = string(v)\n        end\n\n        PrettyTables.pretty_table(\n            io,\n            table;\n            header = header,\n            backend = Val(backend),\n            title = \"Attributes\",\n            alignment = :l,\n            kwargs...,\n        )\n    end\n\n    if !isempty(model.time_series_names)\n        println(io)\n        header = [\"Parameter Name\", \"Time Series Name\"]\n\n        table = Matrix{String}(undef, length(model.time_series_names), length(header))\n        for (ix, (k, v)) in enumerate(model.time_series_names)\n            table[ix, 1] = string(k)\n            table[ix, 2] = string(v)\n        end\n\n        PrettyTables.pretty_table(\n            io,\n            table;\n            header = header,\n            backend = Val(backend),\n            title = \"Time Series Names\",\n            alignment = :l,\n            kwargs...,\n        )\n    end\n\n    if !isempty(model.duals)\n        println(io)\n\n        table = string.(model.duals)\n\n        PrettyTables.pretty_table(\n            io,\n            table;\n            show_header = false,\n            backend = Val(backend),\n            title = \"Duals\",\n            alignment = :l,\n            kwargs...,\n        )\n    end\n\n    if !isempty(model.feedforwards)\n        println(io)\n        header = [\"Type\", \"Source\", \"Affected Values\"]\n        table = Matrix{String}(undef, length(model.feedforwards), length(header))\n        for (ix, v) in enumerate(model.feedforwards)\n            table[ix, 1] = string(typeof(v))\n            table[ix, 2] = encode_key_as_string(get_optimization_container_key(v))\n            table[ix, 3] = first(encode_key_as_string.(get_affected_values(v)))\n        end\n        PrettyTables.pretty_table(\n            io,\n            table;\n            header = header,\n            backend = Val(backend),\n            title = \"Feedforwards\",\n            alignment = :l,\n            kwargs...,\n        )\n    else\n        println(io)\n        print(io, \"No FeedForwards Assigned\")\n    end\nend\n\nfunction Base.show(io::IO, ::MIME\"text/plain\", input::NetworkModel)\n    _show_method(io, input, :auto)\nend\n\nfunction Base.show(io::IO, ::MIME\"text/html\", input::NetworkModel)\n    _show_method(io, input, :html; standalone = false, tf = PrettyTables.tf_html_simple)\nend\n\nfunction _show_method(io::IO, network_model::NetworkModel, backend::Symbol; kwargs...)\n    table = [\n        \"Network Model\" string(get_network_formulation(network_model))\n        \"Slacks\" get_use_slacks(network_model)\n        \"PTDF\" !isnothing(get_PTDF_matrix(network_model))\n        \"Duals\" join(string.(get_duals(network_model)), \" \")\n    ]\n\n    PrettyTables.pretty_table(\n        io,\n        table;\n        backend = Val(backend),\n        header = [\"Field\", \"Value\"],\n        title = \"Network Model\",\n        alignment = :l,\n        kwargs...,\n    )\n    return\nend\n\nfunction Base.show(io::IO, ::MIME\"text/plain\", input::OperationModel)\n    _show_method(io, input, :auto)\nend\n\nfunction Base.show(io::IO, ::MIME\"text/html\", input::OperationModel)\n    _show_method(io, input, :html; standalone = false, tf = PrettyTables.tf_html_simple)\nend\n\nfunction _show_method(io::IO, model::OperationModel, backend::Symbol; kwargs...)\n    _show_method(io, model.template, backend; kwargs...)\nend\n\nfunction Base.show(io::IO, ::MIME\"text/plain\", input::ProblemTemplate)\n    _show_method(io, input, :auto)\nend\n\nfunction Base.show(io::IO, ::MIME\"text/html\", input::ProblemTemplate)\n    _show_method(io, input, :html; standalone = false, tf = PrettyTables.tf_html_simple)\nend\n\nfunction _show_method(io::IO, template::ProblemTemplate, backend::Symbol; kwargs...)\n    table = [\n        \"Network Model\" string(get_network_formulation(template.network_model))\n        \"Slacks\" get_use_slacks(template.network_model)\n        \"PTDF\" !isnothing(get_PTDF_matrix(template.network_model))\n        \"Duals\" isempty(get_duals(template.network_model)) ? \"None\" : string.(get_duals(template.network_model))\n        \"HVDC Network Model\" isnothing(get_hvdc_network_model(template.network_model)) ? \"None\" : replace(string(get_hvdc_network_model(template.network_model)), r\"[()]\" => \"\")\n    ]\n\n    PrettyTables.pretty_table(\n        io,\n        table;\n        backend = Val(backend),\n        show_header = false,\n        title = \"Network Model\",\n        alignment = :l,\n        kwargs...,\n    )\n\n    println(io)\n    header = [\"Device Type\", \"Formulation\", \"Slacks\"]\n\n    table = Matrix{String}(undef, length(template.devices), length(header))\n    for (ix, model) in enumerate(values(template.devices))\n        table[ix, 1] = string(get_component_type(model))\n        table[ix, 2] = string(get_formulation(model))\n        table[ix, 3] = string(model.use_slacks)\n    end\n\n    PrettyTables.pretty_table(\n        io,\n        table;\n        backend = Val(backend),\n        header = header,\n        title = \"Device Models\",\n        alignment = :l,\n    )\n\n    if !isempty(template.branches)\n        println(io)\n        header = [\"Branch Type\", \"Formulation\", \"Slacks\"]\n\n        table = Matrix{String}(undef, length(template.branches), length(header))\n        for (ix, model) in enumerate(values(template.branches))\n            table[ix, 1] = string(get_component_type(model))\n            table[ix, 2] = string(get_formulation(model))\n            table[ix, 3] = string(model.use_slacks)\n        end\n\n        PrettyTables.pretty_table(\n            io,\n            table;\n            header = header,\n            backend = Val(backend),\n            title = \"Branch Models\",\n            alignment = :l,\n            kwargs...,\n        )\n    end\n\n    if !isempty(template.services)\n        println(io)\n        if isempty(first(keys(template.services))[1])\n            header = [\"Service Type\", \"Formulation\", \"Slacks\", \"Aggregated Model\"]\n        else\n            header = [\"Name\", \"Service Type\", \"Formulation\", \"Slacks\", \"Aggregated Model\"]\n        end\n\n        table = Matrix{String}(undef, length(template.services), length(header))\n        for (ix, (key, model)) in enumerate(template.services)\n            if isempty(key[1])\n                table[ix, 1] = string(get_component_type(model))\n                table[ix, 2] = string(get_formulation(model))\n                table[ix, 3] = string(model.use_slacks)\n                table[ix, 4] =\n                    string(get(model.attributes, \"aggregated_service_model\", \"false\"))\n            else\n                table[ix, 1] = key[1]\n                table[ix, 2] = string(get_component_type(model))\n                table[ix, 3] = string(get_formulation(model))\n                table[ix, 4] = string(model.use_slacks)\n                table[ix, 5] =\n                    string(get(model.attributes, \"aggregated_service_model\", \"false\"))\n            end\n        end\n\n        PrettyTables.pretty_table(\n            io,\n            table;\n            header = header,\n            backend = Val(backend),\n            title = \"Service Models\",\n            alignment = :l,\n            kwargs...,\n        )\n    end\n    return\nend\n\nfunction Base.show(io::IO, ::MIME\"text/plain\", input::SimulationModels)\n    _show_method(io, input, :auto)\nend\n\nfunction Base.show(io::IO, ::MIME\"text/html\", input::SimulationModels)\n    _show_method(io, input, :html; standalone = false, tf = PrettyTables.tf_html_simple)\nend\n\n_get_model_type(::DecisionModel{T}) where {T <: DecisionProblem} = T\n_get_model_type(::EmulationModel{T}) where {T <: EmulationProblem} = T\n\nfunction _show_method(io::IO, sim_models::SimulationModels, backend::Symbol; kwargs...)\n    println(io)\n    header = [\"Model Name\", \"Model Type\", \"Status\", \"Output Directory\"]\n\n    table = Matrix{Any}(undef, length(sim_models.decision_models), length(header))\n    for (ix, model) in enumerate(sim_models.decision_models)\n        table[ix, 1] = string(get_name(model))\n        table[ix, 2] = IS.strip_module_name(string(_get_model_type(model)))\n        table[ix, 3] = string(get_status(model))\n        table[ix, 4] = get_output_dir(model)\n    end\n\n    PrettyTables.pretty_table(\n        io,\n        table;\n        header = header,\n        backend = Val(backend),\n        title = \"Decision Models\",\n        alignment = :l,\n        kwargs...,\n    )\n\n    if !isnothing(sim_models.emulation_model)\n        println(io)\n        table = Matrix{Any}(undef, 1, length(header))\n        table[1, 1] = string(get_name(sim_models.emulation_model))\n        table[1, 2] =\n            IS.strip_module_name(string(_get_model_type(sim_models.emulation_model)))\n        table[1, 3] = string(get_status(sim_models.emulation_model))\n        table[1, 4] = get_output_dir(sim_models.emulation_model)\n\n        PrettyTables.pretty_table(\n            io,\n            table;\n            header = header,\n            backend = Val(backend),\n            title = \"Emulator Models\",\n            alignment = :l,\n            kwargs...,\n        )\n    else\n        println(io)\n        println(io, \"No Emulator Model Specified\")\n    end\nend\n\nfunction Base.show(io::IO, ::MIME\"text/plain\", input::SimulationSequence)\n    _show_method(io, input, :auto)\nend\n\nfunction Base.show(io::IO, ::MIME\"text/html\", input::SimulationSequence)\n    _show_method(io, input, :html; standalone = false, tf = PrettyTables.tf_html_simple)\nend\n\nfunction _show_method(io::IO, sequence::SimulationSequence, backend::Symbol; kwargs...)\n    println(io)\n    table = [\n        \"Simulation Step Interval\" Dates.Hour(get_step_resolution(sequence))\n        \"Number of Problems\" length(sequence.executions_by_model)\n    ]\n\n    PrettyTables.pretty_table(\n        io,\n        table;\n        backend = Val(backend),\n        show_header = false,\n        title = \"Simulation Sequence\",\n        alignment = :l,\n        kwargs...,\n    )\n\n    println(io)\n    header = [\"Model Name\", \"Horizon\", \"Interval\", \"Executions Per Step\"]\n\n    table = Matrix{Any}(undef, length(sequence.executions_by_model), length(header))\n    for (ix, (model, executions)) in enumerate(sequence.executions_by_model)\n        table[ix, 1] = string(model)\n        table[ix, 2] = Dates.canonicalize(sequence.horizons[model])\n        table[ix, 3] = Dates.canonicalize(sequence.intervals[model])\n        table[ix, 4] = executions\n    end\n\n    PrettyTables.pretty_table(\n        io,\n        table;\n        header = header,\n        backend = Val(backend),\n        title = \"Simulation Problems\",\n        alignment = :l,\n    )\n\n    if !isempty(sequence.feedforwards)\n        println(io)\n        header = [\"Model Name\", \"Feed Forward Type\"]\n        table = Matrix{Any}(undef, length(sequence.feedforwards), length(header))\n        for (ix, (k, ff)) in enumerate(sequence.feedforwards)\n            table[ix, 1] = k\n            table[ix, 2] = join(string.(typeof.(ff)), \" \")\n        end\n        PrettyTables.pretty_table(\n            io,\n            table;\n            header = header,\n            backend = Val(backend),\n            title = \"Feedforwards\",\n            alignment = :l,\n            kwargs...,\n        )\n    end\nend\n\nfunction Base.show(io::IO, ::MIME\"text/plain\", input::Simulation)\n    _show_method(io, input, :auto)\nend\n\nfunction Base.show(io::IO, ::MIME\"text/html\", input::Simulation)\n    _show_method(io, input, :html; standalone = false, tf = PrettyTables.tf_html_simple)\nend\n\nfunction _get_initial_time_for_show(sim::Simulation)\n    ini_time = get_initial_time(sim)\n    if isnothing(ini_time)\n        return \"Unset Initial Time\"\n    else\n        return string(ini_time)\n    end\nend\n\nfunction _get_build_status_for_show(sim::Simulation)\n    internal = sim.internal\n    if isnothing(internal)\n        return \"EMPTY\"\n    else\n        return string(internal.build_status)\n    end\nend\n\nfunction _get_run_status_for_show(sim::Simulation)\n    internal = sim.internal\n    if isnothing(internal)\n        return \"NOT_READY\"\n    else\n        return string(internal.status)\n    end\nend\n\nfunction _show_method(io::IO, sim::Simulation, backend::Symbol; kwargs...)\n    table = [\n        \"Simulation Name\" get_name(sim)\n        \"Build Status\" _get_build_status_for_show(sim)\n        \"Run Status\" _get_run_status_for_show(sim)\n        \"Initial Time\" _get_initial_time_for_show(sim)\n        \"Steps\" get_steps(sim)\n    ]\n\n    PrettyTables.pretty_table(\n        io,\n        table;\n        backend = Val(backend),\n        show_header = false,\n        title = \"Simulation\",\n        alignment = :l,\n        kwargs...,\n    )\n\n    _show_method(io, sim.models, backend; kwargs...)\n    _show_method(io, sim.sequence, backend; kwargs...)\nend\n\nfunction Base.show(io::IO, ::MIME\"text/plain\", input::SimulationResults)\n    _show_method(io, input, :auto)\nend\n\nfunction Base.show(io::IO, ::MIME\"text/html\", input::SimulationResults)\n    _show_method(io, input, :html; standalone = false, tf = PrettyTables.tf_html_simple)\nend\n\nfunction _show_method(io::IO, results::SimulationResults, backend::Symbol; kwargs...)\n    header = [\"Problem Name\", \"Initial Time\", \"Resolution\", \"Last Solution Timestamp\"]\n\n    table = Matrix{Any}(undef, length(results.decision_problem_results), length(header))\n    for (ix, (key, result)) in enumerate(results.decision_problem_results)\n        table[ix, 1] = key\n        table[ix, 2] = first(result.timestamps)\n        table[ix, 3] = Dates.canonicalize(result.resolution)\n        table[ix, 4] = last(result.timestamps)\n    end\n    println(io)\n    PrettyTables.pretty_table(\n        io,\n        table;\n        header = header,\n        backend = Val(backend),\n        title = \"Decision Problem Results\",\n        alignment = :l,\n    )\n\n    println(io)\n    table = [\n        \"Name\" results.emulation_problem_results.problem\n        \"Resolution\" Dates.Minute(results.emulation_problem_results.resolution)\n        \"Number of steps\" length(results.emulation_problem_results.timestamps)\n    ]\n    PrettyTables.pretty_table(\n        io,\n        table;\n        show_header = false,\n        backend = Val(backend),\n        title = \"Emulator Results\",\n        alignment = :l,\n        kwargs...,\n    )\nend\n\nProblemResultsTypes = Union{OptimizationProblemResults, SimulationProblemResults}\nfunction Base.show(io::IO, ::MIME\"text/plain\", input::ProblemResultsTypes)\n    _show_method(io, input, :auto)\nend\n\nfunction Base.show(io::IO, ::MIME\"text/html\", input::ProblemResultsTypes)\n    _show_method(io, input, :html; standalone = false, tf = PrettyTables.tf_html_simple)\nend\n\nfunction _show_method(\n    io::IO,\n    results::T,\n    backend::Symbol;\n    kwargs...,\n) where {T <: ProblemResultsTypes}\n    timestamps = get_timestamps(results)\n\n    if backend == :html\n        println(io, \"<p> Start: $(first(timestamps))</p>\")\n        println(io, \"<p> End: $(last(timestamps))</p>\")\n        println(\n            io,\n            \"<p> Resolution: $(Dates.Minute(ISOPT.get_resolution(results)))</p>\",\n        )\n    else\n        println(io, \"Start: $(first(timestamps))\")\n        println(io, \"End: $(last(timestamps))\")\n        println(io, \"Resolution: $(Dates.Minute(ISOPT.get_resolution(results)))\")\n    end\n\n    values = Dict{String, Vector{String}}(\n        \"Variables\" => list_variable_names(results),\n        \"Auxiliary variables\" => list_aux_variable_names(results),\n        \"Duals\" => list_dual_names(results),\n        \"Expressions\" => list_expression_names(results),\n        \"Parameters\" => list_parameter_names(results),\n    )\n\n    if hasfield(T, :problem)\n        name = results.problem\n    else\n        name = \"PowerSimulations\"\n    end\n\n    for (k, val) in values\n        if !isempty(val)\n            println(io)\n            PrettyTables.pretty_table(\n                io,\n                val;\n                show_header = false,\n                backend = Val(backend),\n                title = \"$name Problem $k Results\",\n                alignment = :l,\n                kwargs...,\n            )\n        end\n    end\nend\n\nfunction Base.show(io::IO, ::MIME\"text/plain\", bounds::ConstraintBounds)\n    println(io, \"ConstraintBounds:\")\n    println(io, \"Constraint Coefficient\")\n    show(io, MIME\"text/plain\"(), bounds.coefficient)\n    println(io, \"Constraint RHS\")\n    show(io, MIME\"text/plain\"(), bounds.rhs)\nend\n\nfunction Base.show(io::IO, ::MIME\"text/plain\", bounds::VariableBounds)\n    println(io, \"VariableBounds:\")\n    show(io, MIME\"text/plain\"(), bounds.bounds)\nend\n\nfunction Base.show(io::IO, ::MIME\"text/plain\", bounds::NumericalBounds)\n    println(io, rpad(\"  Minimum\", 20), \"Maximum\")\n    println(io, rpad(\"  $(bounds.min)\", 20), \"$(bounds.max)\")\nend\n"
  },
  {
    "path": "src/utils/print_pt_v3.jl",
    "content": "function Base.show(io::IO, container::OptimizationContainer)\n    show(io, get_jump_model(container))\nend\n\nfunction Base.show(io::IO, ::MIME\"text/plain\", input::Union{ServiceModel, DeviceModel})\n    _show_method(io, input, :auto)\nend\n\nfunction Base.show(io::IO, ::MIME\"text/html\", input::Union{ServiceModel, DeviceModel})\n    # The tf_html_simple format was eliminated from PrettyTables and it was added to PowerSystems\n    _show_method(io, input, :html; stand_alone = false, table_format = PSY.tf_html_simple)\nend\n\nfunction _show_method(\n    io::IO,\n    model::Union{ServiceModel, DeviceModel},\n    backend::Symbol;\n    kwargs...,\n)\n    println(io)\n    header = [\"Device Type\", \"Formulation\", \"Slacks\"]\n\n    table = Matrix{String}(undef, 1, length(header))\n    table[1, 1] = string(get_component_type(model))\n    table[1, 2] = string(get_formulation(model))\n    table[1, 3] = string(model.use_slacks)\n\n    PrettyTables.pretty_table(\n        io,\n        table;\n        column_labels = header,\n        backend = backend,\n        title = \"Device Model\",\n        alignment = :l,\n        kwargs...,\n    )\n\n    if !isempty(model.attributes)\n        println(io)\n        header = [\"Name\", \"Value\"]\n\n        table = Matrix{String}(undef, length(model.attributes), length(header))\n        for (ix, (k, v)) in enumerate(model.attributes)\n            table[ix, 1] = string(k)\n            table[ix, 2] = string(v)\n        end\n\n        PrettyTables.pretty_table(\n            io,\n            table;\n            column_labels = header,\n            backend = backend,\n            title = \"Attributes\",\n            alignment = :l,\n            kwargs...,\n        )\n    end\n\n    if !isempty(model.time_series_names)\n        println(io)\n        header = [\"Parameter Name\", \"Time Series Name\"]\n\n        table = Matrix{String}(undef, length(model.time_series_names), length(header))\n        for (ix, (k, v)) in enumerate(model.time_series_names)\n            table[ix, 1] = string(k)\n            table[ix, 2] = string(v)\n        end\n\n        PrettyTables.pretty_table(\n            io,\n            table;\n            column_labels = header,\n            backend = backend,\n            title = \"Time Series Names\",\n            alignment = :l,\n            kwargs...,\n        )\n    end\n\n    if !isempty(model.duals)\n        println(io)\n\n        table = string.(model.duals)\n\n        PrettyTables.pretty_table(\n            io,\n            table;\n            show_column_labels = false,\n            backend = backend,\n            title = \"Duals\",\n            alignment = :l,\n            kwargs...,\n        )\n    end\n\n    if !isempty(model.feedforwards)\n        println(io)\n        header = [\"Type\", \"Source\", \"Affected Values\"]\n        table = Matrix{String}(undef, length(model.feedforwards), length(header))\n        for (ix, v) in enumerate(model.feedforwards)\n            table[ix, 1] = string(typeof(v))\n            table[ix, 2] = encode_key_as_string(get_optimization_container_key(v))\n            table[ix, 3] = first(encode_key_as_string.(get_affected_values(v)))\n        end\n        PrettyTables.pretty_table(\n            io,\n            table;\n            column_labels = header,\n            backend = backend,\n            title = \"Feedforwards\",\n            alignment = :l,\n            kwargs...,\n        )\n    else\n        println(io)\n        print(io, \"No FeedForwards Assigned\")\n    end\nend\n\nfunction Base.show(io::IO, ::MIME\"text/plain\", input::NetworkModel)\n    _show_method(io, input, :auto)\nend\n\nfunction Base.show(io::IO, ::MIME\"text/html\", input::NetworkModel)\n    # The tf_html_simple format was eliminated from PrettyTables and it was added to PowerSystems\n    _show_method(io, input, :html; stand_alone = false, table_format = PSY.tf_html_simple)\nend\n\nfunction _show_method(io::IO, network_model::NetworkModel, backend::Symbol; kwargs...)\n    table = [\n        \"Network Model\" string(get_network_formulation(network_model))\n        \"Slacks\" get_use_slacks(network_model)\n        \"PTDF\" !isnothing(get_PTDF_matrix(network_model))\n        \"Duals\" join(string.(get_duals(network_model)), \" \")\n    ]\n\n    PrettyTables.pretty_table(\n        io,\n        table;\n        backend = backend,\n        column_labels = [\"Field\", \"Value\"],\n        title = \"Network Model\",\n        alignment = :l,\n        kwargs...,\n    )\n    return\nend\n\nfunction Base.show(io::IO, ::MIME\"text/plain\", input::OperationModel)\n    _show_method(io, input, :auto)\nend\n\nfunction Base.show(io::IO, ::MIME\"text/html\", input::OperationModel)\n    # The tf_html_simple format was eliminated from PrettyTables and it was added to PowerSystems\n    _show_method(io, input, :html; stand_alone = false, table_format = PSY.tf_html_simple)\nend\n\nfunction _show_method(io::IO, model::OperationModel, backend::Symbol; kwargs...)\n    _show_method(io, model.template, backend; kwargs...)\nend\n\nfunction Base.show(io::IO, ::MIME\"text/plain\", input::ProblemTemplate)\n    _show_method(io, input, :auto)\nend\n\nfunction Base.show(io::IO, ::MIME\"text/html\", input::ProblemTemplate)\n    # The tf_html_simple format was eliminated from PrettyTables and it was added to PowerSystems\n    _show_method(io, input, :html; stand_alone = false, table_format = PSY.tf_html_simple)\nend\n\nfunction _show_method(io::IO, template::ProblemTemplate, backend::Symbol; kwargs...)\n    table = [\n        \"Network Model\" string(get_network_formulation(template.network_model))\n        \"Slacks\" get_use_slacks(template.network_model)\n        \"PTDF\" !isnothing(get_PTDF_matrix(template.network_model))\n        \"Duals\" isempty(get_duals(template.network_model)) ? \"None\" : string.(get_duals(template.network_model))\n        \"HVDC Network Model\" isnothing(get_hvdc_network_model(template.network_model)) ? \"None\" : replace(string(get_hvdc_network_model(template.network_model)), r\"[()]\" => \"\")\n    ]\n\n    PrettyTables.pretty_table(\n        io,\n        table;\n        backend = backend,\n        show_column_labels = false,\n        title = \"Network Model\",\n        alignment = :l,\n        kwargs...,\n    )\n\n    println(io)\n    header = [\"Device Type\", \"Formulation\", \"Slacks\"]\n\n    table = Matrix{String}(undef, length(template.devices), length(header))\n    for (ix, model) in enumerate(values(template.devices))\n        table[ix, 1] = string(get_component_type(model))\n        table[ix, 2] = string(get_formulation(model))\n        table[ix, 3] = string(model.use_slacks)\n    end\n\n    PrettyTables.pretty_table(\n        io,\n        table;\n        backend = backend,\n        column_labels = header,\n        title = \"Device Models\",\n        alignment = :l,\n    )\n\n    if !isempty(template.branches)\n        println(io)\n        header = [\"Branch Type\", \"Formulation\", \"Slacks\"]\n\n        table = Matrix{String}(undef, length(template.branches), length(header))\n        for (ix, model) in enumerate(values(template.branches))\n            table[ix, 1] = string(get_component_type(model))\n            table[ix, 2] = string(get_formulation(model))\n            table[ix, 3] = string(model.use_slacks)\n        end\n\n        PrettyTables.pretty_table(\n            io,\n            table;\n            column_labels = header,\n            backend = backend,\n            title = \"Branch Models\",\n            alignment = :l,\n            kwargs...,\n        )\n    end\n\n    if !isempty(template.services)\n        println(io)\n        if isempty(first(keys(template.services))[1])\n            header = [\"Service Type\", \"Formulation\", \"Slacks\", \"Aggregated Model\"]\n        else\n            header = [\"Name\", \"Service Type\", \"Formulation\", \"Slacks\", \"Aggregated Model\"]\n        end\n\n        table = Matrix{String}(undef, length(template.services), length(header))\n        for (ix, (key, model)) in enumerate(template.services)\n            if isempty(key[1])\n                table[ix, 1] = string(get_component_type(model))\n                table[ix, 2] = string(get_formulation(model))\n                table[ix, 3] = string(model.use_slacks)\n                table[ix, 4] =\n                    string(get(model.attributes, \"aggregated_service_model\", \"false\"))\n            else\n                table[ix, 1] = key[1]\n                table[ix, 2] = string(get_component_type(model))\n                table[ix, 3] = string(get_formulation(model))\n                table[ix, 4] = string(model.use_slacks)\n                table[ix, 5] =\n                    string(get(model.attributes, \"aggregated_service_model\", \"false\"))\n            end\n        end\n\n        PrettyTables.pretty_table(\n            io,\n            table;\n            column_labels = header,\n            backend = backend,\n            title = \"Service Models\",\n            alignment = :l,\n            kwargs...,\n        )\n    end\n    return\nend\n\nfunction Base.show(io::IO, ::MIME\"text/plain\", input::SimulationModels)\n    _show_method(io, input, :auto)\nend\n\nfunction Base.show(io::IO, ::MIME\"text/html\", input::SimulationModels)\n    # The tf_html_simple format was eliminated from PrettyTables and it was added to PowerSystems\n    _show_method(io, input, :html; stand_alone = false, table_format = PSY.tf_html_simple)\nend\n\n_get_model_type(::DecisionModel{T}) where {T <: DecisionProblem} = T\n_get_model_type(::EmulationModel{T}) where {T <: EmulationProblem} = T\n\nfunction _show_method(io::IO, sim_models::SimulationModels, backend::Symbol; kwargs...)\n    println(io)\n    header = [\"Model Name\", \"Model Type\", \"Status\", \"Output Directory\"]\n\n    table = Matrix{Any}(undef, length(sim_models.decision_models), length(header))\n    for (ix, model) in enumerate(sim_models.decision_models)\n        table[ix, 1] = string(get_name(model))\n        table[ix, 2] = IS.strip_module_name(string(_get_model_type(model)))\n        table[ix, 3] = string(get_status(model))\n        table[ix, 4] = get_output_dir(model)\n    end\n\n    PrettyTables.pretty_table(\n        io,\n        table;\n        column_labels = header,\n        backend = backend,\n        title = \"Decision Models\",\n        alignment = :l,\n        kwargs...,\n    )\n\n    if !isnothing(sim_models.emulation_model)\n        println(io)\n        table = Matrix{Any}(undef, 1, length(header))\n        table[1, 1] = string(get_name(sim_models.emulation_model))\n        table[1, 2] =\n            IS.strip_module_name(string(_get_model_type(sim_models.emulation_model)))\n        table[1, 3] = string(get_status(sim_models.emulation_model))\n        table[1, 4] = get_output_dir(sim_models.emulation_model)\n\n        PrettyTables.pretty_table(\n            io,\n            table;\n            column_labels = header,\n            backend = backend,\n            title = \"Emulator Models\",\n            alignment = :l,\n            kwargs...,\n        )\n    else\n        println(io)\n        println(io, \"No Emulator Model Specified\")\n    end\nend\n\nfunction Base.show(io::IO, ::MIME\"text/plain\", input::SimulationSequence)\n    _show_method(io, input, :auto)\nend\n\nfunction Base.show(io::IO, ::MIME\"text/html\", input::SimulationSequence)\n    # The tf_html_simple format was eliminated from PrettyTables and it was added to PowerSystems\n    _show_method(io, input, :html; stand_alone = false, table_format = PSY.tf_html_simple)\nend\n\nfunction _show_method(io::IO, sequence::SimulationSequence, backend::Symbol; kwargs...)\n    println(io)\n    table = [\n        \"Simulation Step Interval\" Dates.Hour(get_step_resolution(sequence))\n        \"Number of Problems\" length(sequence.executions_by_model)\n    ]\n\n    PrettyTables.pretty_table(\n        io,\n        table;\n        backend = backend,\n        show_column_labels = false,\n        title = \"Simulation Sequence\",\n        alignment = :l,\n        kwargs...,\n    )\n\n    println(io)\n    header = [\"Model Name\", \"Horizon\", \"Interval\", \"Executions Per Step\"]\n\n    table = Matrix{Any}(undef, length(sequence.executions_by_model), length(header))\n    for (ix, (model, executions)) in enumerate(sequence.executions_by_model)\n        table[ix, 1] = string(model)\n        table[ix, 2] = Dates.canonicalize(sequence.horizons[model])\n        table[ix, 3] = Dates.canonicalize(sequence.intervals[model])\n        table[ix, 4] = executions\n    end\n\n    PrettyTables.pretty_table(\n        io,\n        table;\n        column_labels = header,\n        backend = backend,\n        title = \"Simulation Problems\",\n        alignment = :l,\n    )\n\n    if !isempty(sequence.feedforwards)\n        println(io)\n        header = [\"Model Name\", \"Feed Forward Type\"]\n        table = Matrix{Any}(undef, length(sequence.feedforwards), length(header))\n        for (ix, (k, ff)) in enumerate(sequence.feedforwards)\n            table[ix, 1] = k\n            table[ix, 2] = join(string.(typeof.(ff)), \" \")\n        end\n        PrettyTables.pretty_table(\n            io,\n            table;\n            column_labels = header,\n            backend = backend,\n            title = \"Feedforwards\",\n            alignment = :l,\n            kwargs...,\n        )\n    end\nend\n\nfunction Base.show(io::IO, ::MIME\"text/plain\", input::Simulation)\n    _show_method(io, input, :auto)\nend\n\nfunction Base.show(io::IO, ::MIME\"text/html\", input::Simulation)\n    # The tf_html_simple format was eliminated from PrettyTables and it was added to PowerSystems\n    _show_method(io, input, :html; stand_alone = false, table_format = PSY.tf_html_simple)\nend\n\nfunction _get_initial_time_for_show(sim::Simulation)\n    ini_time = get_initial_time(sim)\n    if isnothing(ini_time)\n        return \"Unset Initial Time\"\n    else\n        return string(ini_time)\n    end\nend\n\nfunction _get_build_status_for_show(sim::Simulation)\n    internal = sim.internal\n    if isnothing(internal)\n        return \"EMPTY\"\n    else\n        return string(internal.build_status)\n    end\nend\n\nfunction _get_run_status_for_show(sim::Simulation)\n    internal = sim.internal\n    if isnothing(internal)\n        return \"NOT_READY\"\n    else\n        return string(internal.status)\n    end\nend\n\nfunction _show_method(io::IO, sim::Simulation, backend::Symbol; kwargs...)\n    table = [\n        \"Simulation Name\" get_name(sim)\n        \"Build Status\" _get_build_status_for_show(sim)\n        \"Run Status\" _get_run_status_for_show(sim)\n        \"Initial Time\" _get_initial_time_for_show(sim)\n        \"Steps\" get_steps(sim)\n    ]\n\n    PrettyTables.pretty_table(\n        io,\n        table;\n        backend = backend,\n        show_column_labels = false,\n        title = \"Simulation\",\n        alignment = :l,\n        kwargs...,\n    )\n\n    _show_method(io, sim.models, backend; kwargs...)\n    _show_method(io, sim.sequence, backend; kwargs...)\nend\n\nfunction Base.show(io::IO, ::MIME\"text/plain\", input::SimulationResults)\n    _show_method(io, input, :auto)\nend\n\nfunction Base.show(io::IO, ::MIME\"text/html\", input::SimulationResults)\n    # The tf_html_simple format was eliminated from PrettyTables and it was added to PowerSystems\n    _show_method(io, input, :html; stand_alone = false, table_format = PSY.tf_html_simple)\nend\n\nfunction _show_method(io::IO, results::SimulationResults, backend::Symbol; kwargs...)\n    header = [\"Problem Name\", \"Initial Time\", \"Resolution\", \"Last Solution Timestamp\"]\n\n    table = Matrix{Any}(undef, length(results.decision_problem_results), length(header))\n    for (ix, (key, result)) in enumerate(results.decision_problem_results)\n        table[ix, 1] = key\n        table[ix, 2] = first(result.timestamps)\n        table[ix, 3] = Dates.canonicalize(result.resolution)\n        table[ix, 4] = last(result.timestamps)\n    end\n    println(io)\n    PrettyTables.pretty_table(\n        io,\n        table;\n        column_labels = header,\n        backend = backend,\n        title = \"Decision Problem Results\",\n        alignment = :l,\n    )\n\n    println(io)\n    table = [\n        \"Name\" results.emulation_problem_results.problem\n        \"Resolution\" Dates.Minute(results.emulation_problem_results.resolution)\n        \"Number of steps\" length(results.emulation_problem_results.timestamps)\n    ]\n    PrettyTables.pretty_table(\n        io,\n        table;\n        show_column_labels = false,\n        backend = backend,\n        title = \"Emulator Results\",\n        alignment = :l,\n        kwargs...,\n    )\nend\n\nProblemResultsTypes = Union{OptimizationProblemResults, SimulationProblemResults}\nfunction Base.show(io::IO, ::MIME\"text/plain\", input::ProblemResultsTypes)\n    _show_method(io, input, :auto)\nend\n\nfunction Base.show(io::IO, ::MIME\"text/html\", input::ProblemResultsTypes)\n    # The tf_html_simple format was eliminated from PrettyTables and it was added to PowerSystems\n    _show_method(io, input, :html; stand_alone = false, table_format = PSY.tf_html_simple)\nend\n\nfunction _show_method(\n    io::IO,\n    results::T,\n    backend::Symbol;\n    kwargs...,\n) where {T <: ProblemResultsTypes}\n    timestamps = get_timestamps(results)\n\n    if backend == :html\n        println(io, \"<p> Start: $(first(timestamps))</p>\")\n        println(io, \"<p> End: $(last(timestamps))</p>\")\n        println(\n            io,\n            \"<p> Resolution: $(Dates.Minute(ISOPT.get_resolution(results)))</p>\",\n        )\n    else\n        println(io, \"Start: $(first(timestamps))\")\n        println(io, \"End: $(last(timestamps))\")\n        println(io, \"Resolution: $(Dates.Minute(ISOPT.get_resolution(results)))\")\n    end\n\n    values = Dict{String, Vector{String}}(\n        \"Variables\" => list_variable_names(results),\n        \"Auxiliary variables\" => list_aux_variable_names(results),\n        \"Duals\" => list_dual_names(results),\n        \"Expressions\" => list_expression_names(results),\n        \"Parameters\" => list_parameter_names(results),\n    )\n\n    if hasfield(T, :problem)\n        name = results.problem\n    else\n        name = \"PowerSimulations\"\n    end\n\n    for (k, val) in values\n        if !isempty(val)\n            println(io)\n            PrettyTables.pretty_table(\n                io,\n                val;\n                show_column_labels = false,\n                backend = backend,\n                title = \"$name Problem $k Results\",\n                alignment = :l,\n                kwargs...,\n            )\n        end\n    end\nend\n\nfunction Base.show(io::IO, ::MIME\"text/plain\", bounds::ConstraintBounds)\n    println(io, \"ConstraintBounds:\")\n    println(io, \"Constraint Coefficient\")\n    show(io, MIME\"text/plain\"(), bounds.coefficient)\n    println(io, \"Constraint RHS\")\n    show(io, MIME\"text/plain\"(), bounds.rhs)\nend\n\nfunction Base.show(io::IO, ::MIME\"text/plain\", bounds::VariableBounds)\n    println(io, \"VariableBounds:\")\n    show(io, MIME\"text/plain\"(), bounds.bounds)\nend\n\nfunction Base.show(io::IO, ::MIME\"text/plain\", bounds::NumericalBounds)\n    println(io, rpad(\"  Minimum\", 20), \"Maximum\")\n    println(io, rpad(\"  $(bounds.min)\", 20), \"$(bounds.max)\")\nend\n"
  },
  {
    "path": "src/utils/recorder_events.jl",
    "content": "\"\"\"\nAll events subtyped from this need to be recorded under :simulation_status.\n\"\"\"\nabstract type AbstractSimulationStatusEvent <: IS.AbstractRecorderEvent end\n\nstruct SimulationStepEvent <: AbstractSimulationStatusEvent\n    common::IS.RecorderEventCommon\n    simulation_time::Dates.DateTime\n    step::Int\n    status::String\nend\n\nfunction SimulationStepEvent(\n    simulation_time::Dates.DateTime,\n    step::Int,\n    status::AbstractString,\n)\n    return SimulationStepEvent(\n        IS.RecorderEventCommon(\"SimulationStepEvent\"),\n        simulation_time,\n        step,\n        status,\n    )\nend\n\nstruct ProblemExecutionEvent <: AbstractSimulationStatusEvent\n    common::IS.RecorderEventCommon\n    simulation_time::Dates.DateTime\n    step::Int\n    model_name::Symbol\n    status::String\nend\n\nfunction ProblemExecutionEvent(\n    simulation_time::Dates.DateTime,\n    step::Int,\n    model_name::Symbol,\n    status::AbstractString,\n)\n    return ProblemExecutionEvent(\n        IS.RecorderEventCommon(\"ProblemExecutionEvent\"),\n        simulation_time,\n        step,\n        model_name,\n        status,\n    )\nend\n\nstruct InitialConditionUpdateEvent <: IS.AbstractRecorderEvent\n    common::IS.RecorderEventCommon\n    simulation_time::Dates.DateTime\n    initial_condition_type::String\n    component_type::String\n    device_name::String\n    new_value::Float64\n    previous_value::Float64\n    model_name::String\nend\n\nfunction InitialConditionUpdateEvent(\n    simulation_time,\n    ic::InitialCondition,\n    previous_value::Union{Nothing, Float64},\n    model_name::Symbol,\n)\n    return InitialConditionUpdateEvent(\n        IS.RecorderEventCommon(\"InitialConditionUpdateEvent\"),\n        simulation_time,\n        string(get_ic_type(ic)),\n        string(get_component_type(ic)),\n        get_component_name(ic),\n        isnothing(get_condition(ic)) ? 1e8 : get_condition(ic),\n        isnothing(previous_value) ? 1e8 : previous_value,\n        string(model_name),\n    )\nend\n\nstruct ParameterUpdateEvent <: IS.AbstractRecorderEvent\n    common::IS.RecorderEventCommon\n    simulation_time::Dates.DateTime\n    parameter_type::String\n    component_type::String\n    tag::String\n    model_name::String\nend\n\nfunction ParameterUpdateEvent(\n    parameter_type::Type{<:ParameterType},\n    component_type::DataType,\n    tag::String,\n    simulation_time::Dates.DateTime,\n    model_name::Symbol,\n)\n    return ParameterUpdateEvent(\n        IS.RecorderEventCommon(\"ParameterUpdateEvent\"),\n        simulation_time,\n        string(parameter_type),\n        string(component_type),\n        tag,\n        string(model_name),\n    )\nend\n\nfunction ParameterUpdateEvent(\n    parameter_type::Type{<:ParameterType},\n    component_type::DataType,\n    attributes::TimeSeriesAttributes,\n    simulation_time::Dates.DateTime,\n    model_name::Symbol,\n)\n    return ParameterUpdateEvent(\n        parameter_type,\n        component_type,\n        attributes.name,\n        simulation_time,\n        model_name,\n    )\nend\n\nfunction ParameterUpdateEvent(\n    parameter_type::Type{<:ParameterType},\n    component_type::DataType,\n    attributes::EventParametersAttributes,\n    simulation_time::Dates.DateTime,\n    model_name::Symbol,\n)\n    return ParameterUpdateEvent(\n        parameter_type,\n        component_type,\n        \"outage - event\",\n        simulation_time,\n        model_name,\n    )\nend\n\nfunction ParameterUpdateEvent(\n    parameter_type::Type{<:ParameterType},\n    component_type::DataType,\n    attributes::VariableValueAttributes,\n    simulation_time::Dates.DateTime,\n    model_name::Symbol,\n)\n    return ParameterUpdateEvent(\n        parameter_type,\n        component_type,\n        # TODO: Store as string in the attributes to avoid interpolations\n        encode_key_as_string(get_attribute_key(attributes)),\n        simulation_time,\n        model_name,\n    )\nend\n\nfunction ParameterUpdateEvent(\n    parameter_type::Type{<:ParameterType},\n    component_type::DataType,\n    attributes::CostFunctionAttributes,\n    simulation_time::Dates.DateTime,\n    model_name::Symbol,\n)\n    return ParameterUpdateEvent(\n        parameter_type,\n        component_type,\n        # TODO: Store as string in the attributes to avoid interpolations\n        string(get_variable_types(attributes)),\n        simulation_time,\n        model_name,\n    )\nend\n\nstruct StateUpdateEvent <: IS.AbstractRecorderEvent\n    common::IS.RecorderEventCommon\n    simulation_time::Dates.DateTime\n    model_name::String\n    state_type::String\nend\n\nfunction StateUpdateEvent(simulation_time::Dates.DateTime, model_name, state_type::String)\n    return StateUpdateEvent(\n        IS.RecorderEventCommon(\"StateUpdateEvent\"),\n        simulation_time,\n        string(model_name),\n        state_type,\n    )\nend\n\nfunction get_simulation_step_range(filename::AbstractString, step::Int)\n    events = IS.list_recorder_events(SimulationStepEvent, filename, x -> x.step == step)\n    if length(events) != 2\n        throw(\n            ArgumentError(\n                \"$filename does not have two SimulationStepEvents for step = $step\",\n            ),\n        )\n    end\n\n    if events[1].status != \"start\" || events[2].status != \"done\"\n        throw(\n            ArgumentError(\n                \"$filename does not contain start and done events for step = $step\",\n            ),\n        )\n    end\n\n    return (start = events[1].simulation_time, done = events[2].simulation_time)\nend\n\nfunction get_simulation_model_range(filename::AbstractString, step::Int, model::String)\n    events = IS.list_recorder_events(\n        ProblemExecutionEvent,\n        filename,\n        x -> x.step == step && x.model_name == Symbol(model),\n    )\n    if length(events) != 2\n        throw(\n            ArgumentError(\n                \"$filename does not have two ProblemExecutionEvent for step = $step model = $model\",\n            ),\n        )\n    end\n\n    if events[1].status != \"start\" || events[2].status != \"done\"\n        throw(\n            ArgumentError(\n                \"$filename does not contain start and done events for step = $step model = $model\",\n            ),\n        )\n    end\n\n    return (start = events[1].simulation_time, done = events[2].simulation_time)\nend\n\nfunction _filter_by_type_range!(events::Vector{<:IS.AbstractRecorderEvent}, time_range)\n    return filter!(\n        x -> x.simulation_time >= time_range.start && x.simulation_time <= time_range.done,\n        events,\n    )\nend\n\nfunction _get_recorder_filename(output_dir, recorder_name)\n    return joinpath(output_dir, \"recorder\", recorder_name * \".log\")\nend\n\nfunction _get_simulation_status_recorder_filename(output_dir)\n    return _get_recorder_filename(output_dir, \"simulation_status\")\nend\n\nfunction _get_simulation_recorder_filename(output_dir)\n    return _get_recorder_filename(output_dir, \"execution\")\nend\n\n\"\"\"\n    list_simulation_events(\n        ::Type{T},\n        output_dir::AbstractString,\n        filter_func::Union{Nothing, Function} = nothing;\n        step = nothing,\n        model = nothing,\n    ) where {T <: IS.AbstractRecorderEvent}\n\nList simulation events of type T in a simulation output directory.\n\n# Arguments\n\n  - `output_dir::AbstractString`: Simulation output directory\n  - `filter_func::Union{Nothing, Function} = nothing`: Refer to [`show_simulation_events`](@ref).\n  - `step::Int = nothing`: Filter events by step. Required if model is passed.\n  - `model::Int = nothing`: Filter events by model.\n\"\"\"\nfunction list_simulation_events(\n    ::Type{T},\n    output_dir::AbstractString,\n    filter_func::Union{Nothing, Function} = nothing;\n    step = nothing,\n    model_name::Union{String, Nothing} = nothing,\n) where {T <: IS.AbstractRecorderEvent}\n    if model_name !== nothing && step === nothing\n        throw(ArgumentError(\"step is required if model_name is passed\"))\n    end\n\n    recorder_file = _get_simulation_recorder_filename(output_dir)\n    events = IS.list_recorder_events(T, recorder_file, filter_func)\n\n    if step !== nothing\n        recorder_file = _get_simulation_status_recorder_filename(output_dir)\n        step_range = get_simulation_step_range(recorder_file, step)\n        _filter_by_type_range!(events, step_range)\n    end\n\n    if model_name !== nothing\n        recorder_file = _get_simulation_status_recorder_filename(output_dir)\n        model_range = get_simulation_model_range(recorder_file, step, model_name)\n        _filter_by_type_range!(events, model_range)\n    end\n\n    return events\nend\n\nfunction list_simulation_events(\n    ::Type{T},\n    output_dir::AbstractString,\n    filter_func::Union{Nothing, Function} = nothing;\n    kwargs...,\n) where {T <: AbstractSimulationStatusEvent}\n    recorder_file = _get_simulation_status_recorder_filename(output_dir)\n    return IS.list_recorder_events(T, recorder_file, filter_func)\nend\n\n\"\"\"\n    show_simulation_events(\n        ::Type{T},\n        output_dir::AbstractString,\n        filter_func::Union{Nothing,Function} = nothing;\n        step = nothing,\n        model = nothing,\n        wall_time = false,\n        kwargs...,\n    ) where { T <: IS.AbstractRecorderEvent}\n\nShow all simulation events of type T in a simulation output directory.\n\n# Arguments\n\n  - `::Type{T}`: Recorder event type\n  - `output_dir::AbstractString`: Simulation output directory\n  - `filter_func::Union{Nothing, Function} = nothing`: Refer to [`show_recorder_events`](@ref).\n  - `step::Int = nothing`: Filter events by step. Required if model is passed.\n  - `model::Int = nothing`: Filter events by model.\n  - `wall_time = false`: If true, show the wall_time timestamp.\n\"\"\"\nfunction show_simulation_events(\n    ::Type{T},\n    output_dir::AbstractString,\n    filter_func::Union{Nothing, Function} = nothing;\n    step = nothing,\n    model = nothing,\n    wall_time = false,\n    kwargs...,\n) where {T <: IS.AbstractRecorderEvent}\n    show_simulation_events(\n        stdout,\n        T,\n        output_dir,\n        filter_func;\n        step = step,\n        model = model,\n        wall_time = wall_time,\n        kwargs...,\n    )\nend\n\nfunction show_simulation_events(\n    ::Type{T},\n    output_dir::AbstractString,\n    filter_func::Union{Nothing, Function} = nothing;\n    wall_time = false,\n    kwargs...,\n) where {T <: AbstractSimulationStatusEvent}\n    show_simulation_events(\n        stdout,\n        T,\n        output_dir,\n        filter_func;\n        wall_time = wall_time,\n        kwargs...,\n    )\nend\n\nfunction show_simulation_events(\n    io::IO,\n    ::Type{T},\n    output_dir::AbstractString,\n    filter_func::Union{Nothing, Function} = nothing;\n    wall_time = false,\n    kwargs...,\n) where {T <: AbstractSimulationStatusEvent}\n    events = list_simulation_events(T, output_dir, filter_func)\n    show_recorder_events(io, events, filter_func; wall_time = wall_time, kwargs...)\nend\n\nfunction show_simulation_events(\n    io::IO,\n    ::Type{T},\n    output_dir::AbstractString,\n    filter_func::Union{Nothing, Function} = nothing;\n    step = nothing,\n    model_name::Union{String, Nothing} = nothing,\n    wall_time = false,\n    kwargs...,\n) where {T <: IS.AbstractRecorderEvent}\n    events =\n        list_simulation_events(\n            T,\n            output_dir,\n            filter_func;\n            step = step,\n            model_name = model_name,\n        )\n    show_recorder_events(io, events, filter_func; wall_time = wall_time, kwargs...)\nend\n\n\"\"\"\n    show_recorder_events(\n        ::Type{T},\n        filename::AbstractString,\n        filter_func::Union{Nothing, Function} = nothing;\n        wall_time = false,\n        kwargs...,\n    ) where {T <: IS.AbstractRecorderEvent}\n\nShow the events of type T in a recorder file.\n\n# Arguments\n\n  - `::Type{T}`: Recorder event type\n  - `filename::AbstractString`: recorder filename\n  - `filter_func::Union{Nothing, Function} = nothing`: Optional function that accepts an event\n    of type T and returns a Bool. Apply this function to each event and only return events\n    where the result is true.\n  - `wall_time = false`: If true, show the wall_time timestamp.\n\"\"\"\nfunction show_recorder_events(\n    ::Type{T},\n    filename::AbstractString,\n    filter_func::Union{Nothing, Function} = nothing;\n    wall_time = false,\n    kwargs...,\n) where {T <: IS.AbstractRecorderEvent}\n    show_recorder_events(stdout, T, filename, filter_func; wall_time = wall_time, kwargs...)\nend\n\nfunction show_recorder_events(\n    io::IO,\n    ::Type{T},\n    filename::AbstractString,\n    filter_func::Union{Nothing, Function} = nothing;\n    wall_time = false,\n    kwargs...,\n) where {T <: IS.AbstractRecorderEvent}\n    if wall_time\n        IS.show_recorder_events(io, T, filename, filter_func)\n    else\n        # This will not display the first column, 'timestamp'.\n        # Passign filters_col No longer supported in PrettyTables\n        # f_c(data, i) = i > 1\n        IS.show_recorder_events(io, T, filename, filter_func; kwargs...)\n    end\nend\n\nfunction show_recorder_events(\n    io::IO,\n    events::Vector{T},\n    filter_func::Union{Nothing, Function} = nothing;\n    wall_time = false,\n    kwargs...,\n) where {T <: IS.AbstractRecorderEvent}\n    if wall_time\n        IS.show_recorder_events(io, events; kwargs...)\n    else\n        # This will not display the first column, 'timestamp'.\n        # Passign filters_col No longer supported in PrettyTables\n        #f_c(data, i) = i > 1\n        IS.show_recorder_events(io, events; kwargs...)\n    end\nend\n"
  },
  {
    "path": "src/utils/time_series_utils.jl",
    "content": "apply_maybe_across_time_series(fn::Function, ts_data::AbstractVector) =\n    fn.(ts_data)\n\napply_maybe_across_time_series(fn::Function, ts_data::TimeSeries.TimeArray) =\n    fn.(values(ts_data))\n\napply_maybe_across_time_series(fn::Function, ts_data::AbstractDict) =\n    apply_maybe_across_time_series.(Ref(fn), values(ts_data))\n\napply_maybe_across_time_series(fn::Function, ts_data::IS.TimeSeriesData) =\n    apply_maybe_across_time_series(fn, PSY.get_data(ts_data))\n\n\"\"\"\nHelper function to look up a time series if necessary then apply a function (typically a\nvalidation routine in a `do` block) to every element in it\n\"\"\"\napply_maybe_across_time_series(\n    fn::Function,\n    component::PSY.Component,\n    ts_key::IS.TimeSeriesKey,\n) =\n    apply_maybe_across_time_series(fn, PSY.get_time_series(component, ts_key))\n\n# case where the element isn't a time series\napply_maybe_across_time_series(fn::Function, ::PSY.Component, elem) = fn(elem)\n\n# success case\n_validate_eltype_helper(::Type{T}, element::T) where {T} = true\n\n# failure case\n_validate_eltype_helper(_, _) = false\n\n\"\"\"\nValidate that the eltype of the time series, or the field itself if it's not a time series,\nis of the type given\n\"\"\"\n_validate_eltype(\n    ::Type{T},\n    component::PSY.Component,\n    ts_key::IS.TimeSeriesKey,\n    msg = \"\",\n) where {T} =\n    apply_maybe_across_time_series(component, ts_key) do x\n        result = _validate_eltype_helper(T, x)\n        result || throw(\n            ArgumentError(\n                \"Expected element type $T but got $(typeof(x)) in time series $(get_name(ts_key)) for $(get_name(component))\" *\n                msg,\n            ),\n        )\n    end\n\nfunction _validate_eltype(::Type{T}, component::PSY.Component, element, msg = \"\") where {T}\n    component_name = get_name(component)\n    result = _validate_eltype_helper(T, element)\n    result || throw(\n        ArgumentError(\n            \"Expected element type $T but got $(typeof(element)) for $(get_name(component))\" *\n            msg,\n        ),\n    )\nend\n"
  },
  {
    "path": "test/Project.toml",
    "content": "[deps]\nAqua = \"4c88cf16-eb10-579e-8560-4a9242c79595\"\nCSV = \"336ed68f-0bac-5ca0-87d4-7b16caf5d00b\"\nDataFrames = \"a93c6f00-e57d-5684-b7b6-d8193f3e46c0\"\nDataFramesMeta = \"1313f7d8-7da2-5740-9ea0-a2ca25f37964\"\nDataStructures = \"864edb3b-99cc-5e75-8d2d-829cb0a9cfe8\"\nDates = \"ade2ca70-3891-5945-98fb-dc099432e06a\"\nHiGHS = \"87dc4568-4c63-4d18-b0c0-bb2238e4078b\"\nHydroPowerSimulations = \"fc1677e0-6ad7-4515-bf3a-bd6bf20a0b1b\"\nInfrastructureSystems = \"2cd47ed4-ca9b-11e9-27f2-ab636a7671f1\"\nIpopt = \"b6b21f68-93f8-5de0-b562-5493be1d77c9\"\nJSON3 = \"0f8b85d8-7281-11e9-16c2-39a750bddbf1\"\nJuMP = \"4076af6c-e467-56ae-b986-b466b2749572\"\nLinearAlgebra = \"37e2e46d-f89d-539d-b4ee-838fcccc9c8e\"\nLogging = \"56ddb016-857b-54e1-b83d-db4d58db5568\"\nPkg = \"44cfe95a-1eb2-52ea-b672-e2afdf69b78f\"\nPowerFlows = \"94fada2c-fd9a-4e89-8d82-81405f5cb4f6\"\nPowerModels = \"c36e90e8-916a-50a6-bd94-075b64ef4655\"\nPowerNetworkMatrices = \"bed98974-b02a-5e2f-9fe0-a103f5c450dd\"\nPowerSimulations = \"e690365d-45e2-57bb-ac84-44ba829e73c4\"\nPowerSystemCaseBuilder = \"f00506e0-b84f-492a-93c2-c0a9afc4364e\"\nPowerSystems = \"bcd98974-b02a-5e2f-9ee0-a103f5c450dd\"\nRandom = \"9a3f8284-a2c9-5f02-9a11-845980a1fd5c\"\nRevise = \"295af30f-e4ad-537b-8983-00126c2a3abe\"\nSCS = \"c946c3f1-0d1f-5ce8-9dea-7daa1f7e2d13\"\nSerialization = \"9e88b42a-f829-5b0c-bbe9-9e923198166b\"\nStorageSystemsSimulations = \"e2f1a126-19d0-4674-9252-42b2384f8e3c\"\nTest = \"8dfed614-e22c-5e08-85e1-65c5234f0b40\"\nTestSetExtensions = \"98d24dd4-01ad-11ea-1b02-c9a08f80db04\"\nTimeSeries = \"9e3dc215-6440-5c97-bce1-76c03772f85e\"\nTimerOutputs = \"a759f4b9-e2f1-59dc-863e-4aeb61b1ea8f\"\nUUIDs = \"cf7118a7-6976-5b1a-9a39-7adc72f591a4\"\n\n[compat]\nHiGHS = \"1\"\nIpopt = \"=1.4.0\"\njulia = \"^1.6\"\n"
  },
  {
    "path": "test/includes.jl",
    "content": "# SIIP Packages\nusing PowerSimulations\nusing PowerSystems\nusing PowerSystemCaseBuilder\nusing InfrastructureSystems\nusing PowerNetworkMatrices\nusing HydroPowerSimulations\nimport PowerSystemCaseBuilder: PSITestSystems\nusing PowerNetworkMatrices\nusing StorageSystemsSimulations\nusing PowerFlows\nusing DataFramesMeta\n\n# Test Packages\nusing Test\nusing Logging\n\n# Dependencies for testing\nimport PowerModels as PM\nusing DataFrames\nusing DataFramesMeta\nusing Dates\nusing JuMP\nimport JuMP.Containers: DenseAxisArray, SparseAxisArray\nusing TimeSeries\nusing CSV\nimport JSON3\nusing DataStructures\nimport UUIDs\nusing Random\nimport Serialization\nimport LinearAlgebra\n\nimport PowerSystems as PSY\nimport PowerSimulations as PSI\nimport PowerNetworkMatrices as PNM\nimport InfrastructureSystems as IS\n\nconst PFS = PowerFlows\nconst PSB = PowerSystemCaseBuilder\nconst ISOPT = IS.Optimization\n\nconst BASE_DIR = string(dirname(dirname(pathof(PowerSimulations))))\nconst DATA_DIR = joinpath(BASE_DIR, \"test/test_data\")\n\ninclude(\"test_utils/common_operation_model.jl\")\ninclude(\"test_utils/model_checks.jl\")\ninclude(\"test_utils/mock_operation_models.jl\")\ninclude(\"test_utils/solver_definitions.jl\")\ninclude(\"test_utils/operations_problem_templates.jl\")\ninclude(\"test_utils/run_simulation.jl\")\ninclude(\"test_utils/add_components_to_system.jl\")\ninclude(\"test_utils/add_dlr_ts.jl\")\ninclude(\"test_utils/add_market_bid_cost.jl\")\ninclude(\"test_utils/mbc_system_utils.jl\")\ninclude(\"test_utils/mbc_simulation_utils.jl\")\ninclude(\"test_utils/events_simulation_utils.jl\")\ninclude(\"test_utils/iec_simulation_utils.jl\")\n\nENV[\"RUNNING_PSI_TESTS\"] = \"true\"\nENV[\"SIENNA_RANDOM_SEED\"] = 1234  # Set a fixed seed for reproducibility in tests\n"
  },
  {
    "path": "test/performance/performance_test.jl",
    "content": "precompile_time = @timed using PowerSimulations\n\nusing PowerSimulations\nimport PowerSimulations as PSI\nusing PowerSystems\nimport PowerSystems as PSY\nimport InfrastructureSystems as IS\nusing Logging\nusing PowerSystemCaseBuilder\nusing PowerNetworkMatrices\nusing HydroPowerSimulations\nusing HiGHS\nusing Dates\nusing PowerFlows\n\n@info pkgdir(PowerSimulations)\n\nfunction is_running_on_ci()\n    return get(ENV, \"CI\", \"false\") == \"true\" || haskey(ENV, \"GITHUB_ACTIONS\")\nend\n\nopen(\"precompile_time.txt\", \"a\") do io\n    if length(ARGS) == 0 && !is_running_on_ci()\n        push!(ARGS, \"Local Test\")\n    end\n    write(io, \"| $(ARGS[1]) | $(precompile_time.time) |\\n\")\nend\n\nfunction set_device_models!(template::ProblemTemplate, uc::Bool = true)\n    if uc\n        set_device_model!(template, ThermalMultiStart, ThermalStandardUnitCommitment)\n        set_device_model!(template, ThermalStandard, ThermalStandardUnitCommitment)\n        set_device_model!(template, HydroDispatch, FixedOutput)\n    else\n        set_device_model!(template, ThermalMultiStart, ThermalBasicDispatch)\n        set_device_model!(template, ThermalStandard, ThermalBasicDispatch)\n        set_device_model!(template, HydroDispatch, HydroDispatchRunOfRiver)\n    end\n\n    set_device_model!(template, RenewableDispatch, RenewableFullDispatch)\n    set_device_model!(template, PowerLoad, StaticPowerLoad)\n    set_device_model!(template, DeviceModel(Line, StaticBranch))\n    set_device_model!(template, Transformer2W, StaticBranchUnbounded)\n    set_device_model!(template, TapTransformer, StaticBranchUnbounded)\n    set_service_model!(\n        template,\n        ServiceModel(VariableReserve{ReserveUp}, RangeReserve),\n    )\n    set_service_model!(\n        template,\n        ServiceModel(VariableReserve{ReserveDown}, RangeReserve),\n    )\n    return template\nend\n\n# Copy every SingleTimeSeries attached to components of `src_sys` onto the\n# same-named component in `dst_sys`. Components not present in `dst_sys` are\n# skipped silently. Requires that `dst_sys` already has its own storage.\nfunction copy_single_time_series_across_systems!(dst_sys::System, src_sys::System)\n    for src_component in IS.iterate_components_with_time_series(\n        src_sys.data;\n        time_series_type = PSY.SingleTimeSeries,\n    )\n        dst_component =\n            PSY.get_component(typeof(src_component), dst_sys, PSY.get_name(src_component))\n        dst_component === nothing && continue\n        for ts_metadata in IS.get_time_series_metadata(\n            src_component;\n            time_series_type = PSY.SingleTimeSeries,\n        )\n            ts = PSY.get_time_series(\n                PSY.SingleTimeSeries,\n                src_component,\n                PSY.get_name(ts_metadata);\n                resolution = PSY.get_resolution(ts_metadata),\n            )\n            PSY.add_time_series!(dst_sys, dst_component, ts)\n        end\n    end\n    return dst_sys\nend\n\ntry\n    # Build both systems, then merge the 5-minute SingleTimeSeries from the\n    # realization system onto the DA system so a single System carries raw\n    # 1-hour and 5-minute data that can be transformed separately per model.\n    sys_rts_da = build_system(PSISystems, \"modified_RTS_GMLC_DA_sys\")\n    sys_rts_realization = build_system(PSISystems, \"modified_RTS_GMLC_realization_sys\")\n\n    copy_single_time_series_across_systems!(sys_rts_da, sys_rts_realization)\n\n    # Drop the transform that PSB pre-baked so we can attach new per-resolution\n    # transforms and leave both static series intact.\n    PSY.transform_single_time_series!(\n        sys_rts_da,\n        Hour(48),\n        Hour(24);\n        resolution = Hour(1),\n        delete_existing = true,\n    )\n    PSY.transform_single_time_series!(\n        sys_rts_da,\n        Hour(1),\n        Minute(15);\n        resolution = Minute(5),\n        delete_existing = false,\n    )\n\n    for g in get_components(ThermalStandard, sys_rts_da)\n        get_name(g) == \"121_NUCLEAR_1\" && set_must_run!(g, true)\n    end\n\n    for i in 1:2\n        template_uc = ProblemTemplate(\n            NetworkModel(\n                PTDFPowerModel;\n                use_slacks = true,\n                duals = [CopperPlateBalanceConstraint],\n                power_flow_evaluation = DCPowerFlow(),\n            ),\n        )\n        set_device_models!(template_uc)\n\n        template_ed = ProblemTemplate(\n            NetworkModel(\n                PTDFPowerModel;\n                use_slacks = true,\n                duals = [CopperPlateBalanceConstraint],\n                power_flow_evaluation = DCPowerFlow(),\n            ),\n        )\n        set_device_models!(template_ed, false)\n\n        template_em = ProblemTemplate(\n            NetworkModel(\n                PTDFPowerModel;\n                use_slacks = true,\n                duals = [CopperPlateBalanceConstraint],\n            ),\n        )\n        set_device_models!(template_em, false)\n        empty!(template_em.services)\n\n        models = SimulationModels(;\n            decision_models = [\n                DecisionModel(\n                    template_uc,\n                    sys_rts_da;\n                    name = \"UC\",\n                    optimizer = optimizer_with_attributes(HiGHS.Optimizer,\n                        \"mip_rel_gap\" => 0.01),\n                    initialize_model = true,\n                    optimizer_solve_log_print = false,\n                    direct_mode_optimizer = true,\n                    check_numerical_bounds = false,\n                    horizon = Hour(48),\n                    interval = Hour(24),\n                    resolution = Hour(1),\n                ),\n                DecisionModel(\n                    template_ed,\n                    sys_rts_da;\n                    name = \"ED\",\n                    optimizer = optimizer_with_attributes(HiGHS.Optimizer,\n                        \"mip_rel_gap\" => 0.01),\n                    initialize_model = true,\n                    check_numerical_bounds = false,\n                    horizon = Hour(1),\n                    interval = Minute(15),\n                    resolution = Minute(5),\n                ),\n            ],\n            emulation_model = EmulationModel(\n                template_em,\n                sys_rts_da;\n                name = \"PF\",\n                optimizer = optimizer_with_attributes(HiGHS.Optimizer),\n                resolution = Minute(5),\n            ),\n        )\n\n        sequence = SimulationSequence(;\n            models = models,\n            feedforwards = Dict(\n                \"ED\" => [\n                    SemiContinuousFeedforward(;\n                        component_type = ThermalStandard,\n                        source = OnVariable,\n                        affected_values = [ActivePowerVariable],\n                    ),\n                ],\n                \"PF\" => [\n                    SemiContinuousFeedforward(;\n                        component_type = ThermalStandard,\n                        source = OnVariable,\n                        affected_values = [ActivePowerVariable],\n                    ),\n                ],\n            ),\n            ini_cond_chronology = InterProblemChronology(),\n        )\n\n        sim = Simulation(;\n            name = \"single_system_sim\",\n            steps = 3,\n            models = models,\n            sequence = sequence,\n            initial_time = DateTime(\"2020-01-01T00:00:00\"),\n            simulation_folder = mktempdir(; cleanup = true),\n        )\n\n        build_out, time_build, _, _ =\n            @timed build!(sim; console_level = Logging.Error)\n\n        name = i > 1 ? \"Postcompile\" : \"Precompile\"\n        if build_out == PSI.SimulationBuildStatus.BUILT\n            open(\"build_time.txt\", \"a\") do io\n                write(io, \"| $(ARGS[1])-Build Time $name | $(time_build) |\\n\")\n            end\n        else\n            open(\"build_time.txt\", \"a\") do io\n                write(io, \"| $(ARGS[1])- Build Time $name | FAILED TO TEST |\\n\")\n            end\n        end\n\n        solve_out, time_solve, _, _ = @timed execute!(sim; enable_progress_bar = false)\n\n        if solve_out == PSI.RunStatus.SUCCESSFULLY_FINALIZED\n            open(\"solve_time.txt\", \"a\") do io\n                write(io, \"| $(ARGS[1])-Solve Time $name | $(time_solve) |\\n\")\n            end\n        else\n            open(\"solve_time.txt\", \"a\") do io\n                write(io, \"| $(ARGS[1])- Solve Time $name | FAILED TO TEST |\\n\")\n            end\n        end\n    end\ncatch e\n    rethrow(e)\n    open(\"build_time.txt\", \"a\") do io\n        write(io, \"| $(ARGS[1])- Build Time | FAILED TO TEST |\\n\")\n    end\nend\n\nif !is_running_on_ci()\n    for file in [\"precompile_time.txt\", \"build_time.txt\", \"solve_time.txt\"]\n        name = replace(file, \"_\" => \" \")[begin:(end - 4)]\n        println(\"$name:\")\n        for line in eachline(open(file))\n            println(\"\\t\", line)\n        end\n    end\nend\n"
  },
  {
    "path": "test/run_partitioned_simulation.jl",
    "content": "using PowerSimulations\nusing PowerSystems\nusing PowerSystemCaseBuilder\nusing InfrastructureSystems\nusing PowerNetworkMatrices\nusing Logging\nusing Test\n\nimport PowerModels as PM\nusing DataFrames\nusing Dates\nusing JuMP\nusing TimeSeries\nusing CSV\nusing DataFrames\nusing DataStructures\nimport UUIDs\nusing Random\nimport Serialization\n\nimport PowerSystems as PSY\nimport PowerSimulations as PSI\nimport InfrastructureSystems as IS\n\nconst PSB = PowerSystemCaseBuilder\n\nconst BASE_DIR = string(dirname(dirname(pathof(PowerSimulations))))\nconst DATA_DIR = joinpath(BASE_DIR, \"test/test_data\")\n\ninclude(joinpath(BASE_DIR, \"test/test_utils/solver_definitions.jl\"))\n\n# avoid redefinition of functions and constants when running on CI\nif get(ENV, \"CI\", nothing) != \"true\"\n    include(joinpath(BASE_DIR, \"test/test_utils/common_operation_model.jl\"))\n    include(joinpath(BASE_DIR, \"test/test_utils/model_checks.jl\"))\n    include(joinpath(BASE_DIR, \"test/test_utils/mock_operation_models.jl\"))\n    include(joinpath(BASE_DIR, \"test/test_utils/operations_problem_templates.jl\"))\nend\n\nfunction build_simulation(\n    output_dir::AbstractString,\n    simulation_name::AbstractString,\n    partitions::Union{Nothing, SimulationPartitions} = nothing,\n    index::Union{Nothing, Integer} = nothing;\n    initial_time = nothing,\n    num_steps = nothing,\n    HiGHS_optimizer = HiGHS_optimizer,\n)\n    if isnothing(partitions) && isnothing(num_steps)\n        error(\"num_steps must be set if partitions is nothing\")\n    end\n    if !isnothing(partitions) && !isnothing(num_steps)\n        error(\"num_steps and partitions cannot both be set\")\n    end\n    c_sys5_pjm_da = PSB.build_system(PSISystems, \"c_sys5_pjm\")\n    PSY.transform_single_time_series!(c_sys5_pjm_da, Hour(48), Hour(24))\n    c_sys5_pjm_rt = PSB.build_system(PSISystems, \"c_sys5_pjm_rt\")\n    PSY.transform_single_time_series!(c_sys5_pjm_rt, Hour(1), Hour(1))\n\n    for sys in [c_sys5_pjm_da, c_sys5_pjm_rt]\n        th = get_component(ThermalStandard, sys, \"Park City\")\n        set_active_power_limits!(th, (min = 0.1, max = 1.7))\n        set_status!(th, false)\n        set_active_power!(th, 0.0)\n        c = get_operation_cost(th)\n        PSY.set_start_up!(c, 1500.0)\n        PSY.set_shut_down!(c, 75.0)\n        set_time_at_status!(th, 1)\n\n        th = get_component(ThermalStandard, sys, \"Alta\")\n        set_time_limits!(th, (up = 5, down = 1))\n        set_active_power_limits!(th, (min = 0.05, max = 0.4))\n        set_active_power!(th, 0.05)\n        c = get_operation_cost(th)\n        PSY.set_start_up!(c, 400.0)\n        PSY.set_shut_down!(c, 200.0)\n        set_time_at_status!(th, 2)\n\n        th = get_component(ThermalStandard, sys, \"Brighton\")\n        set_active_power_limits!(th, (min = 2.0, max = 6.0))\n        c = get_operation_cost(th)\n        set_active_power!(th, 4.88041)\n        PSY.set_start_up!(c, 5000.0)\n        PSY.set_shut_down!(c, 3000.0)\n\n        th = get_component(ThermalStandard, sys, \"Sundance\")\n        set_active_power_limits!(th, (min = 1.0, max = 2.0))\n        set_time_limits!(th, (up = 5, down = 1))\n        set_active_power!(th, 2.0)\n        c = get_operation_cost(th)\n        PSY.set_start_up!(c, 4000.0)\n        PSY.set_shut_down!(c, 2000.0)\n        set_time_at_status!(th, 1)\n\n        th = get_component(ThermalStandard, sys, \"Solitude\")\n        set_active_power_limits!(th, (min = 1.0, max = 5.2))\n        set_ramp_limits!(th, (up = 0.0052, down = 0.0052))\n        set_active_power!(th, 2.0)\n        c = get_operation_cost(th)\n        PSY.set_start_up!(c, 3000.0)\n        PSY.set_shut_down!(c, 1500.0)\n        PSY.set_must_run!(th, true)\n        set_status!(th, true)\n    end\n\n    to_json(\n        c_sys5_pjm_da,\n        joinpath(output_dir, \"PSI-5-BUS-UC-ED/c_sys5_pjm_da.json\");\n        force = true,\n    )\n    to_json(\n        c_sys5_pjm_rt,\n        joinpath(output_dir, \"PSI-5-BUS-UC-ED/c_sys5_pjm_rt.json\");\n        force = true,\n    )\n\n    template_uc = template_unit_commitment()\n    set_network_model!(\n        template_uc,\n        NetworkModel(\n            PTDFPowerModel;\n        ),\n    )\n\n    set_device_model!(template_uc, ThermalStandard, ThermalStandardUnitCommitment)\n    template_ed = deepcopy(template_uc)\n    # template_ed.network_model.use_slacks = true\n    set_device_model!(template_ed, ThermalStandard, ThermalBasicDispatch)\n\n    models = SimulationModels(;\n        decision_models = [\n            DecisionModel(\n                template_uc,\n                c_sys5_pjm_da;\n                optimizer = HiGHS_optimizer,\n                name = \"UC\",\n                initialize_model = true,\n            ),\n            DecisionModel(\n                template_ed,\n                c_sys5_pjm_rt;\n                optimizer = HiGHS_optimizer,\n                name = \"ED\",\n                calculate_conflict = true,\n                initialize_model = true,\n            ),\n        ],\n    )\n    sequence = SimulationSequence(;\n        models = models,\n        feedforwards = Dict(\n            \"ED\" => [\n                SemiContinuousFeedforward(;\n                    component_type = ThermalStandard,\n                    source = OnVariable,\n                    affected_values = [ActivePowerVariable],\n                ),\n            ],\n        ),\n        ini_cond_chronology = InterProblemChronology(),\n    )\n\n    sim = Simulation(;\n        name = simulation_name,\n        steps = isnothing(partitions) ? num_steps : partitions.num_steps,\n        models = models,\n        sequence = sequence,\n        simulation_folder = output_dir,\n        initial_time = initial_time,\n    )\n\n    status =\n        build!(sim; partitions = partitions, index = index)\n    if status != PSI.SimulationBuildStatus.BUILT\n        error(\"Failed to build simulation: status=$status\")\n    end\n\n    return sim\nend\n\nfunction execute_simulation(sim, args...; kwargs...)\n    return execute!(sim)\nend\n"
  },
  {
    "path": "test/runtests.jl",
    "content": "include(\"includes.jl\")\n\n# Code Quality Tests\nimport Aqua\nAqua.test_undefined_exports(PowerSimulations)\nAqua.test_ambiguities(PowerSimulations)\nAqua.test_stale_deps(PowerSimulations)\nAqua.find_persistent_tasks_deps(PowerSimulations)\nAqua.test_persistent_tasks(PowerSimulations)\nAqua.test_unbound_args(PowerSimulations)\n\nconst LOG_FILE = \"power-simulations-test.log\"\n\nconst DISABLED_TEST_FILES = [  # Can generate with ls -1 test | grep \"test_.*.jl\"\n# \"test_basic_model_structs.jl\",\n# \"test_device_branch_constructors.jl\",\n# \"test_device_hvdc.jl\",\n# \"test_device_lcc.jl\",\n# \"test_device_load_constructors.jl\",\n# \"test_device_renewable_generation_constructors.jl\",\n# \"test_device_source_constructors.jl\",\n# \"test_device_thermal_generation_constructors.jl\",\n# \"test_events.jl\",\n# \"test_formulation_combinations.jl\",\n# \"test_import_export_cost.jl\",\n# \"test_initialization_problem.jl\",\n# \"test_jump_utils.jl\",\n# \"test_market_bid_cost.jl\",\n# \"test_mbc_sanity_check.jl\",\n# \"test_model_decision.jl\",\n# \"test_model_emulation.jl\",\n# \"test_network_constructors.jl\",\n# \"test_network_constructors_with_dlr.jl\",\n# \"test_power_flow_in_the_loop.jl\",\n# \"test_print.jl\",\n# \"test_problem_template.jl\",\n# \"test_recorder_events.jl\",\n# \"test_services_constructor.jl\",\n# \"test_simulation_build.jl\",\n# \"test_simulation_execute.jl\",\n# \"test_simulation_models.jl\",\n# \"test_simulation_partitions.jl\",\n# \"test_simulation_results_export.jl\",\n# \"test_simulation_results.jl\",\n# \"test_simulation_sequence.jl\",\n# \"test_simulation_store.jl\",\n# \"test_utils.jl\",\n]\n\nLOG_LEVELS = Dict(\n    \"Debug\" => Logging.Debug,\n    \"Info\" => Logging.Info,\n    \"Warn\" => Logging.Warn,\n    \"Error\" => Logging.Error,\n)\n\nfunction get_logging_level(env_name::String, default)\n    level = get(ENV, env_name, default)\n    log_level = get(LOG_LEVELS, level, nothing)\n    if log_level === nothing\n        error(\"Invalid log level $level: Supported levels: $(values(LOG_LEVELS))\")\n    end\n\n    return log_level\nend\n\n\"\"\"\nIncludes the given test files, given as a list without their \".jl\" extensions.\nIf none are given it will scan the directory of the calling file and include all\nthe julia files.\n\"\"\"\nmacro includetests(testarg...)\n    if length(testarg) == 0\n        tests = []\n    elseif length(testarg) == 1\n        tests = testarg[1]\n    else\n        error(\"@includetests takes zero or one argument\")\n    end\n\n    quote\n        tests = $tests\n        rootfile = @__FILE__\n        if length(tests) == 0\n            tests = readdir(dirname(rootfile))\n            tests = filter(\n                f ->\n                    startswith(f, \"test_\") && endswith(f, \".jl\") && f != basename(rootfile),\n                tests,\n            )\n        else\n            tests = map(f -> string(f, \".jl\"), tests)\n        end\n        println()\n        if !isempty(DISABLED_TEST_FILES)\n            @warn(\"Some tests are disabled $DISABLED_TEST_FILES\")\n        end\n        for test in tests\n            test ∈ DISABLED_TEST_FILES && continue\n            print(splitext(test)[1], \": \")\n            include(test)\n            println()\n        end\n    end\nend\n\nfunction get_logging_level_from_env(env_name::String, default)\n    level = get(ENV, env_name, default)\n    return IS.get_logging_level(level)\nend\n\nfunction run_tests()\n    logging_config_filename = get(ENV, \"SIIP_LOGGING_CONFIG\", nothing)\n    if logging_config_filename !== nothing\n        config = IS.LoggingConfiguration(logging_config_filename)\n    else\n        config = IS.LoggingConfiguration(;\n            filename = LOG_FILE,\n            file_level = Logging.Info,\n            console_level = Logging.Error,\n        )\n    end\n    console_logger = ConsoleLogger(config.console_stream, config.console_level)\n\n    IS.open_file_logger(LOG_FILE, config.file_level) do file_logger\n        levels = (Logging.Info, Logging.Warn, Logging.Error)\n        multi_logger =\n            IS.MultiLogger([console_logger, file_logger], IS.LogEventTracker(levels))\n        global_logger(multi_logger)\n\n        if !isempty(config.group_levels)\n            IS.set_group_levels!(multi_logger, config.group_levels)\n        end\n\n        @time @testset \"Begin PowerSimulations tests\" begin\n            @includetests ARGS\n        end\n\n        @test length(IS.get_log_events(multi_logger.tracker, Logging.Error)) == 0\n\n        @info IS.report_log_summary(multi_logger)\n    end\nend\n\nlogger = global_logger()\n\ntry\n    run_tests()\nfinally\n    # Guarantee that the global logger is reset.\n    global_logger(logger)\n    nothing\nend\n"
  },
  {
    "path": "test/test_basic_model_structs.jl",
    "content": "@testset \"DeviceModel Tests\" begin\n    @test_throws ArgumentError DeviceModel(ThermalGen, ThermalStandardUnitCommitment)\n    @test_throws ArgumentError DeviceModel(ThermalStandard, PSI.AbstractThermalFormulation)\n    @test_throws ArgumentError NetworkModel(PM.AbstractPowerModel)\nend\n\n@testset \"NetworkModel Tests\" begin\n    @test_throws ArgumentError NetworkModel(PM.AbstractPowerModel)\n    @test NetworkModel(\n        PTDFPowerModel;\n        use_slacks = true,\n        power_flow_evaluation = [DCPowerFlow(), PSSEExportPowerFlow(:v33, \"exports\")],\n    ) isa NetworkModel\n    @test NetworkModel(\n        PTDFPowerModel;\n        use_slacks = true,\n        power_flow_evaluation = ACPowerFlow(;\n            exporter =\n            PSSEExportPowerFlow(\n                :v33,\n                \"exports\";\n                name = \"my_export_name\",\n                write_comments = true,\n                overwrite = true,\n            ),\n        ),\n    ) isa NetworkModel\nend\n\n@testset \"validate_template dispatch Tests\" begin\n    struct CustomDecisionProblem <: PSI.DecisionProblem end\n    struct CustomEmulationProblem <: PSI.EmulationProblem end\n\n    sys = PSB.build_system(PSITestSystems, \"c_sys5\")\n    template = ProblemTemplate(CopperPlatePowerModel)\n\n    # DecisionModel has no inner constructor, so use the default field constructor\n    decision_model = DecisionModel{CustomDecisionProblem}(\n        :test,\n        template,\n        sys,\n        nothing,\n        PSI.SimulationInfo(),\n        PSI.DecisionModelStore(),\n        Dict{String, Any}(),\n    )\n    @test_throws ErrorException PSI.validate_template(decision_model)\n\n    # EmulationModel has an inner constructor; build with settings then test\n    settings = PSI.Settings(sys)\n    emulation_model = EmulationModel{CustomEmulationProblem}(\n        deepcopy(template),\n        sys,\n        settings,\n        nothing,\n    )\n    @test_throws ErrorException PSI.validate_template(emulation_model)\nend\n\n@testset \"Feedforward Struct Tests\" begin\n    ffs = [\n        UpperBoundFeedforward(;\n            component_type = RenewableDispatch,\n            source = ActivePowerVariable,\n            affected_values = [ActivePowerVariable],\n            add_slacks = true,\n        ),\n        LowerBoundFeedforward(;\n            component_type = RenewableDispatch,\n            source = ActivePowerVariable,\n            affected_values = [ActivePowerVariable],\n            add_slacks = true,\n        ),\n        SemiContinuousFeedforward(;\n            component_type = ThermalMultiStart,\n            source = OnVariable,\n            affected_values = [ActivePowerVariable, ReactivePowerVariable],\n        ),\n    ]\n\n    for ff in ffs\n        for av in PSI.get_affected_values(ff)\n            @test isa(av, PSI.VariableKey)\n        end\n    end\n\n    ff = FixValueFeedforward(;\n        component_type = HydroDispatch,\n        source = OnVariable,\n        affected_values = [OnStatusParameter],\n    )\n\n    for av in PSI.get_affected_values(ff)\n        @test isa(av, PSI.ParameterKey)\n    end\n\n    @test_throws ErrorException UpperBoundFeedforward(\n        component_type = RenewableDispatch,\n        source = ActivePowerVariable,\n        affected_values = [OnStatusParameter],\n        add_slacks = true,\n    )\n\n    @test_throws ErrorException LowerBoundFeedforward(\n        component_type = RenewableDispatch,\n        source = ActivePowerVariable,\n        affected_values = [OnStatusParameter],\n        add_slacks = true,\n    )\n\n    @test_throws ErrorException SemiContinuousFeedforward(\n        component_type = ThermalMultiStart,\n        source = OnVariable,\n        affected_values = [ActivePowerVariable, OnStatusParameter],\n    )\nend\n"
  },
  {
    "path": "test/test_data/results_export.json",
    "content": "{\n  \"models\": [\n    {\n      \"name\": \"ED\",\n      \"variables\": [\n        \"ActivePowerVariable__ThermalStandard\"\n      ],\n      \"store_all_parameters\": true,\n      \"optimizer_stats\": true\n    },\n    {\n      \"name\": \"UC\",\n      \"variables\": [\n        \"OnVariable__ThermalStandard\"\n      ],\n      \"store_all_duals\": true,\n      \"store_all_parameters\": true,\n      \"store_all_aux_variables\": true,\n      \"optimizer_stats\": true\n    }\n  ],\n  \"start_time\": \"2020-01-01T04:00:00\",\n  \"end_time\": null,\n  \"path\": \"export_path\",\n  \"format\": \"csv\"\n}\n"
  },
  {
    "path": "test/test_device_branch_constructors.jl",
    "content": "@testset \"DC Power Flow Models Monitored Line Flow Constraints and Static Unbounded\" begin\n    system = PSB.build_system(PSITestSystems, \"c_sys5_ml\")\n    limits = PSY.get_flow_limits(PSY.get_component(MonitoredLine, system, \"1\"))\n    for model in [DCPPowerModel, PTDFPowerModel]\n        template = get_thermal_dispatch_template_network(\n            NetworkModel(model; PTDF_matrix = PTDF(system)),\n        )\n        model_m = DecisionModel(template, system; optimizer = HiGHS_optimizer)\n        @test build!(model_m; output_dir = mktempdir(; cleanup = true)) ==\n              PSI.ModelBuildStatus.BUILT\n        @test check_variable_bounded(model_m, FlowActivePowerVariable, MonitoredLine)\n\n        @test solve!(model_m) == PSI.RunStatus.SUCCESSFULLY_FINALIZED\n        @test check_flow_variable_values(\n            model_m,\n            FlowActivePowerVariable,\n            MonitoredLine,\n            \"1\",\n            limits.from_to,\n        )\n    end\nend\n\n@testset \"DC Power Flow Models MonitoredLine Asymmetric Flow Limits\" begin\n    system = PSB.build_system(PSITestSystems, \"c_sys5_ml\")\n    ml = PSY.get_component(MonitoredLine, system, \"1\")\n    rating = PSY.get_rating(ml)\n    # Set asymmetric flow limits where from_to < to_from (and both < rating)\n    asymmetric_limits = (from_to = rating * 0.5, to_from = rating * 0.8)\n    PSY.set_flow_limits!(ml, asymmetric_limits)\n    expected_limit = min(rating, asymmetric_limits.from_to, asymmetric_limits.to_from)\n    @test expected_limit == asymmetric_limits.from_to\n\n    # Directly verify get_min_max_limits returns the minimum of the two flow limits and rating\n    minmax = PSI.get_min_max_limits(ml, FlowRateConstraint, StaticBranch)\n    @test minmax.max ≈ expected_limit atol = 1e-6\n    @test minmax.min ≈ -expected_limit atol = 1e-6\n\n    for net_model in [DCPPowerModel, PTDFPowerModel]\n        template = get_thermal_dispatch_template_network(\n            NetworkModel(net_model; PTDF_matrix = PTDF(system)),\n        )\n        set_device_model!(template, DeviceModel(MonitoredLine, StaticBranch))\n        model_m = DecisionModel(template, system; optimizer = HiGHS_optimizer)\n        @test build!(model_m; output_dir = mktempdir(; cleanup = true)) ==\n              PSI.ModelBuildStatus.BUILT\n        @test solve!(model_m) == PSI.RunStatus.SUCCESSFULLY_FINALIZED\n        @test check_flow_variable_values(\n            model_m,\n            FlowActivePowerVariable,\n            MonitoredLine,\n            \"1\",\n            expected_limit,\n        )\n    end\nend\n\n@testset \"AC Power Flow Monitored Line Flow Constraints\" begin\n    system = PSB.build_system(PSITestSystems, \"c_sys5_ml\")\n    limits = PSY.get_flow_limits(PSY.get_component(MonitoredLine, system, \"1\"))\n    template = get_thermal_dispatch_template_network(ACPPowerModel)\n    model_m = DecisionModel(template, system; optimizer = ipopt_optimizer)\n    @test build!(model_m; output_dir = mktempdir(; cleanup = true)) ==\n          PSI.ModelBuildStatus.BUILT\n\n    @test check_variable_bounded(model_m, FlowActivePowerFromToVariable, MonitoredLine)\n    @test check_variable_unbounded(model_m, FlowReactivePowerFromToVariable, MonitoredLine)\n\n    @test solve!(model_m) == PSI.RunStatus.SUCCESSFULLY_FINALIZED\n    @test check_flow_variable_values(\n        model_m,\n        FlowActivePowerFromToVariable,\n        FlowReactivePowerFromToVariable,\n        MonitoredLine,\n        \"1\",\n        0.0,\n        limits.from_to,\n    )\nend\n\n@testset \"DC Power Flow Models Monitored Line Flow Constraints and Static with inequalities\" begin\n    system = PSB.build_system(PSITestSystems, \"c_sys5_ml\")\n    set_rating!(PSY.get_component(Line, system, \"2\"), 1.5)\n    for model in [DCPPowerModel, PTDFPowerModel]\n        template = get_thermal_dispatch_template_network(\n            NetworkModel(model; PTDF_matrix = PTDF(system)),\n        )\n        set_device_model!(template, DeviceModel(Line, StaticBranch))\n        set_device_model!(template, DeviceModel(MonitoredLine, StaticBranchUnbounded))\n        model_m = DecisionModel(template, system; optimizer = HiGHS_optimizer)\n        @test build!(model_m; output_dir = mktempdir(; cleanup = true)) ==\n              PSI.ModelBuildStatus.BUILT\n\n        @test solve!(model_m) == PSI.RunStatus.SUCCESSFULLY_FINALIZED\n        @test check_flow_variable_values(model_m, FlowActivePowerVariable, Line, \"2\", 1.5)\n    end\nend\n\n@testset \"DC Power Flow Models Monitored Line Flow Constraints and Static with Bounds\" begin\n    system = PSB.build_system(PSITestSystems, \"c_sys5_ml\")\n    set_rating!(PSY.get_component(Line, system, \"2\"), 1.5)\n    for model in [DCPPowerModel, PTDFPowerModel]\n        template = get_thermal_dispatch_template_network(NetworkModel(model))\n        set_device_model!(template, DeviceModel(Line, StaticBranchBounds))\n        set_device_model!(template, DeviceModel(MonitoredLine, StaticBranchUnbounded))\n        model_m = DecisionModel(template, system; optimizer = HiGHS_optimizer)\n        @test build!(model_m; output_dir = mktempdir(; cleanup = true)) ==\n              PSI.ModelBuildStatus.BUILT\n\n        @test check_variable_bounded(model_m, FlowActivePowerVariable, Line)\n\n        @test solve!(model_m) == PSI.RunStatus.SUCCESSFULLY_FINALIZED\n        @test check_flow_variable_values(model_m, FlowActivePowerVariable, Line, \"2\", 1.5)\n    end\n\n    # Test the addition of slacks\n    template = get_thermal_dispatch_template_network(NetworkModel(PTDFPowerModel))\n    set_device_model!(template, DeviceModel(Line, StaticBranchBounds; use_slacks = true))\n    set_device_model!(\n        template,\n        DeviceModel(MonitoredLine, StaticBranchBounds; use_slacks = true),\n    )\n    model_m = DecisionModel(template, system; optimizer = HiGHS_optimizer)\n    @test build!(model_m; output_dir = mktempdir(; cleanup = true)) ==\n          PSI.ModelBuildStatus.BUILT\n\n    @test check_variable_bounded(model_m, FlowActivePowerVariable, Line)\n    @test check_variable_bounded(model_m, FlowActivePowerVariable, MonitoredLine)\n    @test !check_variable_bounded(model_m, FlowActivePowerSlackLowerBound, Line)\n    @test !check_variable_bounded(model_m, FlowActivePowerSlackUpperBound, Line)\n    @test !check_variable_bounded(model_m, FlowActivePowerSlackLowerBound, MonitoredLine)\n    @test !check_variable_bounded(model_m, FlowActivePowerSlackUpperBound, MonitoredLine)\n\n    @test solve!(model_m) == PSI.RunStatus.SUCCESSFULLY_FINALIZED\nend\n\n@testset \"DC Power Flow Models for TwoTerminalGenericHVDCLine  with with Line Flow Constraints, TapTransformer & Transformer2W Unbounded\" begin\n    ratelimit_constraint_keys = [\n        PSI.ConstraintKey(FlowRateConstraint, Transformer2W, \"ub\"),\n        PSI.ConstraintKey(FlowRateConstraint, Transformer2W, \"lb\"),\n        PSI.ConstraintKey(FlowRateConstraint, TapTransformer, \"ub\"),\n        PSI.ConstraintKey(FlowRateConstraint, TapTransformer, \"lb\"),\n    ]\n\n    system = PSB.build_system(PSITestSystems, \"c_sys14_dc\")\n    hvdc_line = PSY.get_component(TwoTerminalGenericHVDCLine, system, \"DCLine3\")\n    limits_from = PSY.get_active_power_limits_from(hvdc_line)\n    limits_to = PSY.get_active_power_limits_to(hvdc_line)\n    limits_min = min(limits_from.min, limits_to.min)\n    limits_max = min(limits_from.max, limits_to.max)\n\n    tap_transformer = PSY.get_component(TapTransformer, system, \"Trans3\")\n    rate_limit = PSY.get_rating(tap_transformer)\n\n    transformer = PSY.get_component(Transformer2W, system, \"Trans4\")\n    rate_limit2w = PSY.get_rating(tap_transformer)\n\n    for model in [DCPPowerModel, PTDFPowerModel]\n        template = get_template_dispatch_with_network(\n            NetworkModel(model),\n        )\n        set_device_model!(template, TwoTerminalGenericHVDCLine, HVDCTwoTerminalLossless)\n        set_device_model!(template, DeviceModel(Transformer2W, StaticBranch))\n        set_device_model!(template, DeviceModel(TapTransformer, StaticBranch))\n        model_m = DecisionModel(template, system; optimizer = ipopt_optimizer)\n        @test build!(model_m; output_dir = mktempdir(; cleanup = true)) ==\n              PSI.ModelBuildStatus.BUILT\n\n        psi_constraint_test(model_m, ratelimit_constraint_keys)\n\n        @test solve!(model_m) == PSI.RunStatus.SUCCESSFULLY_FINALIZED\n\n        @test check_flow_variable_values(\n            model_m,\n            FlowActivePowerVariable,\n            TwoTerminalGenericHVDCLine,\n            \"DCLine3\",\n            limits_min,\n            limits_max,\n        )\n        @test check_flow_variable_values(\n            model_m,\n            FlowActivePowerVariable,\n            TapTransformer,\n            \"Trans3\",\n            -rate_limit,\n            rate_limit,\n        )\n        @test check_flow_variable_values(\n            model_m,\n            FlowActivePowerVariable,\n            Transformer2W,\n            \"Trans4\",\n            -rate_limit2w,\n            rate_limit2w,\n        )\n    end\nend\n\n@testset \"DC Power Flow Models for Unbounded TwoTerminalGenericHVDCLine , and StaticBranchBounds for TapTransformer & Transformer2W\" begin\n    system = PSB.build_system(PSITestSystems, \"c_sys14_dc\")\n    hvdc_line = PSY.get_component(TwoTerminalGenericHVDCLine, system, \"DCLine3\")\n    limits_from = PSY.get_active_power_limits_from(hvdc_line)\n    limits_to = PSY.get_active_power_limits_to(hvdc_line)\n    limits_min = min(limits_from.min, limits_to.min)\n    limits_max = min(limits_from.max, limits_to.max)\n\n    tap_transformer = PSY.get_component(TapTransformer, system, \"Trans3\")\n    rate_limit = PSY.get_rating(tap_transformer)\n\n    transformer = PSY.get_component(Transformer2W, system, \"Trans4\")\n    rate_limit2w = PSY.get_rating(tap_transformer)\n\n    for model in [DCPPowerModel, PTDFPowerModel]\n        template = get_template_dispatch_with_network(\n            NetworkModel(model; PTDF_matrix = PTDF(system)),\n        )\n        set_device_model!(\n            template,\n            DeviceModel(TwoTerminalGenericHVDCLine, HVDCTwoTerminalUnbounded),\n        )\n        set_device_model!(template, DeviceModel(TapTransformer, StaticBranchBounds))\n        set_device_model!(template, DeviceModel(Transformer2W, StaticBranchBounds))\n        model_m = DecisionModel(template, system; optimizer = ipopt_optimizer)\n        @test build!(model_m; output_dir = mktempdir(; cleanup = true)) ==\n              PSI.ModelBuildStatus.BUILT\n\n        @test check_variable_unbounded(\n            model_m,\n            FlowActivePowerVariable,\n            TwoTerminalGenericHVDCLine,\n        )\n        @test check_variable_bounded(model_m, FlowActivePowerVariable, TapTransformer)\n        @test check_variable_bounded(model_m, FlowActivePowerVariable, TapTransformer)\n\n        @test solve!(model_m) == PSI.RunStatus.SUCCESSFULLY_FINALIZED\n\n        @test check_flow_variable_values(\n            model_m,\n            FlowActivePowerVariable,\n            TwoTerminalGenericHVDCLine,\n            \"DCLine3\",\n            limits_min,\n            limits_max,\n        )\n        @test check_flow_variable_values(\n            model_m,\n            FlowActivePowerVariable,\n            TapTransformer,\n            \"Trans3\",\n            -rate_limit,\n            rate_limit,\n        )\n        @test check_flow_variable_values(\n            model_m,\n            FlowActivePowerVariable,\n            Transformer2W,\n            \"Trans4\",\n            -rate_limit2w,\n            rate_limit2w,\n        )\n    end\nend\n\n@testset \"HVDCTwoTerminalLossless values check between network models\" begin\n    # Test to compare lossless models with lossless formulation\n    sys_5 = build_system(PSITestSystems, \"c_sys5_uc\")\n\n    line = get_component(Line, sys_5, \"1\")\n    remove_component!(sys_5, line)\n\n    hvdc = TwoTerminalGenericHVDCLine(;\n        name = get_name(line),\n        available = true,\n        active_power_flow = 0.0,\n        # Force the flow in the opposite direction for testing purposes\n        active_power_limits_from = (min = -0.5, max = -0.5),\n        active_power_limits_to = (min = -3.0, max = 2.0),\n        reactive_power_limits_from = (min = -1.0, max = 1.0),\n        reactive_power_limits_to = (min = -1.0, max = 1.0),\n        arc = get_arc(line),\n        loss = LinearCurve(0.0),\n    )\n\n    add_component!(sys_5, hvdc)\n\n    template_uc = ProblemTemplate(\n        NetworkModel(PTDFPowerModel),\n    )\n\n    set_device_model!(template_uc, ThermalStandard, ThermalStandardUnitCommitment)\n    set_device_model!(template_uc, RenewableDispatch, FixedOutput)\n    set_device_model!(template_uc, PowerLoad, StaticPowerLoad)\n    set_device_model!(template_uc, DeviceModel(Line, StaticBranch))\n    set_device_model!(\n        template_uc,\n        DeviceModel(TwoTerminalGenericHVDCLine, HVDCTwoTerminalLossless),\n    )\n\n    model = DecisionModel(\n        template_uc,\n        sys_5;\n        name = \"UC\",\n        optimizer = HiGHS_optimizer,\n    )\n    build!(model; output_dir = mktempdir())\n\n    solve!(model)\n\n    ptdf_vars =\n        read_variables(OptimizationProblemResults(model); table_format = TableFormat.WIDE)\n    ptdf_values = ptdf_vars[\"FlowActivePowerVariable__TwoTerminalGenericHVDCLine\"]\n    ptdf_objective = PSI.get_optimization_container(model).optimizer_stats.objective_value\n\n    set_network_model!(template_uc, NetworkModel(DCPPowerModel))\n\n    model = DecisionModel(\n        template_uc,\n        sys_5;\n        name = \"UC\",\n        optimizer = HiGHS_optimizer,\n    )\n\n    solve!(model; output_dir = mktempdir())\n    dcp_vars =\n        read_variables(OptimizationProblemResults(model); table_format = TableFormat.WIDE)\n    dcp_values = dcp_vars[\"FlowActivePowerVariable__TwoTerminalGenericHVDCLine\"]\n    dcp_objective =\n        PSI.get_optimization_container(model).optimizer_stats.objective_value\n\n    @test isapprox(dcp_objective, ptdf_objective; atol = 0.1)\n    # Resulting solution is in the 4e5 order of magnitude\n    @test all(isapprox.(ptdf_values[!, \"1\"], dcp_values[!, \"1\"]; atol = 10))\nend\n\n@testset \"HVDCDispatch Model Tests\" begin\n    # Test to compare lossless models with lossless formulation\n    sys_5 = build_system(PSITestSystems, \"c_sys5_uc\")\n    # Revert to previous rating before data change to prevent different optimal solutions for the lossless model and lossless formulation:\n    PSY.set_rating!(PSY.get_component(PSY.Line, sys_5, \"6\"), 2.0)\n\n    line = get_component(Line, sys_5, \"1\")\n    remove_component!(sys_5, line)\n\n    hvdc = TwoTerminalGenericHVDCLine(;\n        name = get_name(line),\n        available = true,\n        active_power_flow = 0.0,\n        # Force the flow in the opposite direction for testing purposes\n        active_power_limits_from = (min = -2.0, max = 2.0),\n        active_power_limits_to = (min = -2.0, max = 2.0),\n        reactive_power_limits_from = (min = -1.0, max = 1.0),\n        reactive_power_limits_to = (min = -1.0, max = 1.0),\n        arc = get_arc(line),\n        loss = LinearCurve(0.0),\n    )\n\n    add_component!(sys_5, hvdc)\n    for net_model in [DCPPowerModel, PTDFPowerModel]\n        @testset \"$net_model\" begin\n            PSY.set_loss!(hvdc, PSY.LinearCurve(0.0))\n            template_uc = ProblemTemplate(\n                NetworkModel(net_model; use_slacks = true),\n            )\n\n            set_device_model!(template_uc, ThermalStandard, ThermalBasicUnitCommitment)\n            set_device_model!(template_uc, RenewableDispatch, FixedOutput)\n            set_device_model!(template_uc, PowerLoad, StaticPowerLoad)\n            set_device_model!(template_uc, DeviceModel(Line, StaticBranchBounds))\n            set_device_model!(\n                template_uc,\n                DeviceModel(TwoTerminalGenericHVDCLine, HVDCTwoTerminalLossless),\n            )\n\n            model_ref = DecisionModel(\n                template_uc,\n                sys_5;\n                name = \"UC\",\n                optimizer = HiGHS_optimizer,\n                store_variable_names = true,\n            )\n\n            solve!(model_ref; output_dir = mktempdir())\n            ref_vars = read_variables(\n                OptimizationProblemResults(model_ref);\n                table_format = TableFormat.WIDE,\n            )\n            ref_values = ref_vars[\"FlowActivePowerVariable__Line\"]\n            hvdc_ref_values =\n                ref_vars[\"FlowActivePowerVariable__TwoTerminalGenericHVDCLine\"]\n            ref_objective = model_ref.internal.container.optimizer_stats.objective_value\n            ref_total_gen = sum(\n                sum.(\n                    eachrow(\n                        DataFrames.select(\n                            ref_vars[\"ActivePowerVariable__ThermalStandard\"],\n                            Not(:DateTime),\n                        ),\n                    )\n                ),\n            )\n            set_device_model!(\n                template_uc,\n                DeviceModel(TwoTerminalGenericHVDCLine, HVDCTwoTerminalDispatch),\n            )\n\n            model = DecisionModel(\n                template_uc,\n                sys_5;\n                name = \"UC\",\n                optimizer = HiGHS_optimizer,\n            )\n\n            solve!(model; output_dir = mktempdir())\n            no_loss_vars = read_variables(\n                OptimizationProblemResults(model);\n                table_format = TableFormat.WIDE,\n            )\n            no_loss_values = no_loss_vars[\"FlowActivePowerVariable__Line\"]\n            hvdc_ft_no_loss_values =\n                no_loss_vars[\"FlowActivePowerFromToVariable__TwoTerminalGenericHVDCLine\"]\n            hvdc_tf_no_loss_values =\n                no_loss_vars[\"FlowActivePowerToFromVariable__TwoTerminalGenericHVDCLine\"]\n            no_loss_objective =\n                PSI.get_optimization_container(model).optimizer_stats.objective_value\n            no_loss_total_gen = sum(\n                sum.(\n                    eachrow(\n                        DataFrames.select(\n                            no_loss_vars[\"ActivePowerVariable__ThermalStandard\"],\n                            Not(:DateTime),\n                        ),\n                    ),\n                ),\n            )\n\n            @test isapprox(no_loss_objective, ref_objective; atol = 0.1)\n\n            for col in names(ref_values)\n                test_result =\n                    all(isapprox.(ref_values[!, col], no_loss_values[!, col]; atol = 0.1))\n                @test test_result\n                test_result || break\n            end\n\n            @test all(\n                isapprox.(\n                    hvdc_ft_no_loss_values[!, \"1\"],\n                    -hvdc_tf_no_loss_values[!, \"1\"];\n                    atol = 1e-3,\n                ),\n            )\n\n            @test isapprox(no_loss_total_gen, ref_total_gen; atol = 0.1)\n\n            PSY.set_loss!(hvdc, PSY.LinearCurve(0.005, 0.1))\n\n            model_wl = DecisionModel(\n                template_uc,\n                sys_5;\n                name = \"UC\",\n                optimizer = HiGHS_optimizer,\n            )\n\n            solve!(model_wl; output_dir = mktempdir())\n            dispatch_vars = read_variables(\n                OptimizationProblemResults(model_wl);\n                table_format = TableFormat.WIDE,\n            )\n            dispatch_values_ft =\n                dispatch_vars[\"FlowActivePowerFromToVariable__TwoTerminalGenericHVDCLine\"]\n            dispatch_values_tf =\n                dispatch_vars[\"FlowActivePowerToFromVariable__TwoTerminalGenericHVDCLine\"]\n            wl_total_gen = sum(\n                sum.(\n                    eachrow(\n                        DataFrames.select(\n                            dispatch_vars[\"ActivePowerVariable__ThermalStandard\"],\n                            Not(:DateTime),\n                        ),\n                    ),\n                ),\n            )\n            dispatch_objective = model_wl.internal.container.optimizer_stats.objective_value\n\n            # Note: for this test data the system does better by allowing more losses so\n            # the total cost is lower.\n            @test wl_total_gen > no_loss_total_gen\n\n            for col in names(dispatch_values_tf)\n                test_result = all(dispatch_values_tf[!, col] .<= dispatch_values_ft[!, col])\n                @test test_result\n                test_result || break\n            end\n        end\n    end\nend\n\n@testset \"DC Power Flow Models for TwoTerminalGenericHVDCLine  Dispatch and TapTransformer & Transformer2W Unbounded\" begin\n    ratelimit_constraint_keys = [\n        PSI.ConstraintKey(FlowRateConstraint, Transformer2W, \"ub\"),\n        PSI.ConstraintKey(FlowRateConstraint, Line, \"ub\"),\n        PSI.ConstraintKey(FlowRateConstraint, Line, \"lb\"),\n        PSI.ConstraintKey(FlowRateConstraint, TapTransformer, \"ub\"),\n        PSI.ConstraintKey(FlowRateConstraint, Transformer2W, \"lb\"),\n        PSI.ConstraintKey(FlowRateConstraint, TapTransformer, \"lb\"),\n        PSI.ConstraintKey(FlowRateConstraint, TwoTerminalGenericHVDCLine, \"ub\"),\n        PSI.ConstraintKey(FlowRateConstraint, TwoTerminalGenericHVDCLine, \"lb\"),\n    ]\n\n    system = PSB.build_system(PSITestSystems, \"c_sys14_dc\")\n\n    hvdc_line = PSY.get_component(TwoTerminalGenericHVDCLine, system, \"DCLine3\")\n    limits_from = PSY.get_active_power_limits_from(hvdc_line)\n    limits_to = PSY.get_active_power_limits_to(hvdc_line)\n    limits_min = min(limits_from.min, limits_to.min)\n    limits_max = min(limits_from.max, limits_to.max)\n\n    tap_transformer = PSY.get_component(TapTransformer, system, \"Trans3\")\n    rate_limit = PSY.get_rating(tap_transformer)\n\n    transformer = PSY.get_component(Transformer2W, system, \"Trans4\")\n    rate_limit2w = PSY.get_rating(tap_transformer)\n\n    template = get_template_dispatch_with_network(\n        NetworkModel(PTDFPowerModel),\n    )\n    set_device_model!(template, DeviceModel(TapTransformer, StaticBranch))\n    set_device_model!(template, DeviceModel(Transformer2W, StaticBranch))\n    set_device_model!(\n        template,\n        DeviceModel(TwoTerminalGenericHVDCLine, HVDCTwoTerminalLossless),\n    )\n    model_m = DecisionModel(template, system; optimizer = HiGHS_optimizer)\n    @test build!(model_m; output_dir = mktempdir(; cleanup = true)) ==\n          PSI.ModelBuildStatus.BUILT\n\n    psi_constraint_test(model_m, ratelimit_constraint_keys)\n\n    @test solve!(model_m) == PSI.RunStatus.SUCCESSFULLY_FINALIZED\n\n    @test check_flow_variable_values(\n        model_m,\n        FlowActivePowerVariable,\n        TwoTerminalGenericHVDCLine,\n        \"DCLine3\",\n        limits_max,\n    )\n    @test check_flow_variable_values(\n        model_m,\n        FlowActivePowerVariable,\n        TapTransformer,\n        \"Trans3\",\n        rate_limit,\n    )\n    @test check_flow_variable_values(\n        model_m,\n        FlowActivePowerVariable,\n        Transformer2W,\n        \"Trans4\",\n        rate_limit2w,\n    )\nend\n\n@testset \"DC Power Flow Models for PhaseShiftingTransformer and Line\" begin\n    system = build_system(PSITestSystems, \"c_sys5_uc\")\n\n    line = get_component(Line, system, \"1\")\n    remove_component!(system, line)\n\n    ps = PhaseShiftingTransformer(;\n        name = get_name(line),\n        available = true,\n        active_power_flow = 0.0,\n        reactive_power_flow = 0.0,\n        r = get_r(line),\n        x = get_r(line),\n        primary_shunt = 0.0,\n        tap = 1.0,\n        α = 0.0,\n        rating = get_rating(line),\n        arc = get_arc(line),\n        base_power = get_base_power(system),\n    )\n\n    add_component!(system, ps)\n\n    template = get_template_dispatch_with_network(\n        NetworkModel(PTDFPowerModel; PTDF_matrix = PTDF(system)),\n    )\n    set_device_model!(template, DeviceModel(PhaseShiftingTransformer, PhaseAngleControl))\n    model_m = DecisionModel(template, system; optimizer = HiGHS_optimizer)\n    @test build!(model_m; output_dir = mktempdir(; cleanup = true)) ==\n          PSI.ModelBuildStatus.BUILT\n\n    @test check_variable_unbounded(\n        model_m,\n        FlowActivePowerVariable,\n        PhaseShiftingTransformer,\n    )\n\n    @test solve!(model_m) == PSI.RunStatus.SUCCESSFULLY_FINALIZED\n\n    @test check_flow_variable_values(\n        model_m,\n        FlowActivePowerVariable,\n        PhaseShiftingTransformer,\n        \"1\",\n        get_rating(ps),\n    )\n\n    @test check_flow_variable_values(\n        model_m,\n        PhaseShifterAngle,\n        PhaseShiftingTransformer,\n        \"1\",\n        -π / 2,\n        π / 2,\n    )\nend\n\n@testset \"AC Power Flow Models for TwoTerminalGenericHVDCLine  Flow Constraints and TapTransformer & Transformer2W Unbounded\" begin\n    ratelimit_constraint_keys = [\n        PSI.ConstraintKey(FlowRateConstraintFromTo, Transformer2W),\n        PSI.ConstraintKey(FlowRateConstraintToFrom, Transformer2W),\n        PSI.ConstraintKey(FlowRateConstraintFromTo, TapTransformer),\n        PSI.ConstraintKey(FlowRateConstraintToFrom, TapTransformer),\n        PSI.ConstraintKey(FlowRateConstraint, TwoTerminalGenericHVDCLine, \"ub\"),\n        PSI.ConstraintKey(FlowRateConstraint, TwoTerminalGenericHVDCLine, \"lb\"),\n    ]\n\n    system = PSB.build_system(PSITestSystems, \"c_sys14_dc\")\n\n    hvdc_line = PSY.get_component(TwoTerminalGenericHVDCLine, system, \"DCLine3\")\n    limits_from = PSY.get_active_power_limits_from(hvdc_line)\n    limits_to = PSY.get_active_power_limits_to(hvdc_line)\n    limits_min = min(limits_from.min, limits_to.min)\n    limits_max = min(limits_from.max, limits_to.max)\n\n    tap_transformer = PSY.get_component(TapTransformer, system, \"Trans3\")\n    rate_limit = PSY.get_rating(tap_transformer)\n\n    transformer = PSY.get_component(Transformer2W, system, \"Trans4\")\n    rate_limit2w = PSY.get_rating(tap_transformer)\n\n    template = get_template_dispatch_with_network(ACPPowerModel)\n    set_device_model!(template, TapTransformer, StaticBranchBounds)\n    set_device_model!(template, Transformer2W, StaticBranchBounds)\n    set_device_model!(\n        template,\n        DeviceModel(TwoTerminalGenericHVDCLine, HVDCTwoTerminalLossless),\n    )\n    model_m = DecisionModel(template, system; optimizer = ipopt_optimizer)\n    @test build!(model_m; output_dir = mktempdir(; cleanup = true)) ==\n          PSI.ModelBuildStatus.BUILT\n    @test check_variable_bounded(model_m, FlowActivePowerFromToVariable, TapTransformer)\n    @test check_variable_unbounded(model_m, FlowReactivePowerFromToVariable, TapTransformer)\n    @test check_variable_bounded(model_m, FlowActivePowerToFromVariable, Transformer2W)\n    @test check_variable_unbounded(model_m, FlowReactivePowerToFromVariable, Transformer2W)\n\n    psi_constraint_test(model_m, ratelimit_constraint_keys)\n\n    @test solve!(model_m) == PSI.RunStatus.SUCCESSFULLY_FINALIZED\n\n    @test check_flow_variable_values(\n        model_m,\n        FlowActivePowerVariable,\n        FlowReactivePowerToFromVariable,\n        TwoTerminalGenericHVDCLine,\n        \"DCLine3\",\n        limits_max,\n    )\n    @test check_flow_variable_values(\n        model_m,\n        FlowActivePowerFromToVariable,\n        FlowReactivePowerFromToVariable,\n        TapTransformer,\n        \"Trans3\",\n        rate_limit,\n    )\n    @test check_flow_variable_values(\n        model_m,\n        FlowActivePowerToFromVariable,\n        FlowReactivePowerToFromVariable,\n        Transformer2W,\n        \"Trans4\",\n        rate_limit2w,\n    )\nend\n\n@testset \"Test Line and Monitored Line models with slacks\" begin\n    system = PSB.build_system(PSITestSystems, \"c_sys5_ml\")\n    # This rating (0.247479) was previously inferred in PSY.check_component after setting the rating to 0.0 in the tests\n    set_rating!(PSY.get_component(Line, system, \"2\"), 0.247479)\n    for (model, optimizer) in NETWORKS_FOR_TESTING\n        if model ∈ [PM.SDPWRMPowerModel, SOCWRConicPowerModel]\n            # Skip because the data is too in the feasibility margins for these models\n            continue\n        end\n        template = get_thermal_dispatch_template_network(\n            NetworkModel(model; use_slacks = true),\n        )\n        set_device_model!(template, DeviceModel(Line, StaticBranch; use_slacks = true))\n        set_device_model!(\n            template,\n            DeviceModel(MonitoredLine, StaticBranch; use_slacks = true),\n        )\n        model_m = DecisionModel(template, system; optimizer = optimizer)\n        @test build!(model_m; output_dir = mktempdir(; cleanup = true)) ==\n              PSI.ModelBuildStatus.BUILT\n        @test solve!(model_m) == PSI.RunStatus.SUCCESSFULLY_FINALIZED\n        res = OptimizationProblemResults(model_m)\n        vars = read_variable(\n            res,\n            \"FlowActivePowerSlackUpperBound__Line\";\n            table_format = TableFormat.WIDE,\n        )\n        # some relaxations will find a solution with 0.0 slack\n        @test sum(vars[!, \"2\"]) >= -1e-6\n    end\n\n    template = get_thermal_dispatch_template_network(\n        NetworkModel(PTDFPowerModel; use_slacks = true),\n    )\n    set_device_model!(template, DeviceModel(Line, StaticBranchBounds; use_slacks = true))\n    set_device_model!(\n        template,\n        DeviceModel(MonitoredLine, StaticBranchBounds; use_slacks = true),\n    )\n    model_m = DecisionModel(template, system; optimizer = fast_ipopt_optimizer)\n    @test build!(\n        model_m;\n        console_level = Logging.AboveMaxLevel,\n        output_dir = mktempdir(; cleanup = true),\n    ) == PSI.ModelBuildStatus.BUILT\n\n    @test solve!(model_m) == PSI.RunStatus.SUCCESSFULLY_FINALIZED\n    res = OptimizationProblemResults(model_m)\n    vars = read_variable(\n        res,\n        \"FlowActivePowerSlackUpperBound__Line\";\n        table_format = TableFormat.WIDE,\n    )\n    # some relaxations will find a solution with 0.0 slack\n    @test sum(vars[!, \"2\"]) >= -1e-6\n\n    template = get_thermal_dispatch_template_network(\n        NetworkModel(PTDFPowerModel; use_slacks = true),\n    )\n    set_device_model!(template, DeviceModel(Line, StaticBranch; use_slacks = true))\n    set_device_model!(\n        template,\n        DeviceModel(MonitoredLine, StaticBranch; use_slacks = true),\n    )\n    model_m = DecisionModel(template, system; optimizer = fast_ipopt_optimizer)\n    @test build!(model_m; output_dir = mktempdir(; cleanup = true)) ==\n          PSI.ModelBuildStatus.BUILT\n    @test solve!(model_m) == PSI.RunStatus.SUCCESSFULLY_FINALIZED\n    res = OptimizationProblemResults(model_m)\n    vars = read_variable(\n        res,\n        \"FlowActivePowerSlackUpperBound__Line\";\n        table_format = TableFormat.WIDE,\n    )\n    # some relaxations will find a solution with 0.0 slack\n    @test sum(vars[!, \"2\"]) >= -1e-6\nend\n\n@testset \"Three Winding Transformer Test - Basic Setup and Model\" begin\n    # Start with the base system\n    system = PSB.build_system(PSITestSystems, \"c_sys5_ml\")\n    busD = PSY.get_component(ACBus, system, \"nodeD\")\n    # Create a new bus for the tertiary winding (connected via transformer to Bus 4)\n    new_bus1 = ACBus(;\n        number = 101,\n        name = \"Bus3WT_1\",\n        available = true,\n        bustype = ACBusTypes.PQ,\n        angle = 0.0,\n        magnitude = 1.0,\n        voltage_limits = (min = 0.95, max = 1.05),\n        base_voltage = 230.0,\n        area = PSY.get_area(busD),\n        load_zone = PSY.get_load_zone(busD),\n    )\n    PSY.add_component!(system, new_bus1)\n\n    new_bus2 = ACBus(;\n        number = 102,\n        name = \"Bus3WT_2\",\n        available = true,\n        bustype = ACBusTypes.PQ,\n        angle = 0.0,\n        magnitude = 1.0,\n        voltage_limits = (min = 0.95, max = 1.05),\n        base_voltage = 230.0,\n        area = PSY.get_area(busD),\n        load_zone = PSY.get_load_zone(busD),\n    )\n    PSY.add_component!(system, new_bus2)\n\n    # Add a new load at the new bus\n    new_load = PowerLoad(;\n        name = \"Load_Bus3WT\",\n        available = true,\n        bus = new_bus1,\n        active_power = 0.5,\n        reactive_power = 0.1,\n        base_power = 100.0,\n        max_active_power = 0.5,\n        max_reactive_power = 0.1,\n    )\n    PSY.add_component!(system, new_load)\n\n    # Add a new generator at the new bus to provide power\n    new_gen = ThermalStandard(;\n        name = \"Gen_Bus100\",\n        available = true,\n        status = true,\n        bus = new_bus2,\n        active_power = 0.4,\n        reactive_power = 0.0,\n        rating = 0.5,\n        prime_mover_type = PrimeMovers.ST,\n        fuel = ThermalFuels.COAL,\n        active_power_limits = (min = 0.0, max = 0.5),\n        reactive_power_limits = (min = -0.3, max = 0.3),\n        ramp_limits = (up = 0.5, down = 0.5),\n        operation_cost = ThermalGenerationCost(;\n            variable = CostCurve(LinearCurve(0.0)),\n            start_up = 0.0,\n            shut_down = 0.0,\n            fixed = 0.0,\n        ),\n        base_power = 100.0,\n        time_limits = nothing,\n    )\n    PSY.add_component!(system, new_gen)\n\n    # Create a star bus for the Transformer3W\n    star_bus = ACBus(;\n        number = 103,\n        name = \"Star_Bus_T3W\",\n        available = true,\n        bustype = ACBusTypes.PQ,\n        angle = 0.0,\n        magnitude = 1.0,\n        voltage_limits = (min = 0.95, max = 1.05),\n        base_voltage = 230.0,\n        area = PSY.get_area(busD),\n        load_zone = PSY.get_load_zone(busD),\n    )\n    PSY.add_component!(system, star_bus)\n\n    transformer3w = Transformer3W(;\n        name = \"Transformer3W_busD\",\n        available = true,\n        primary_star_arc = Arc(; from = busD, to = star_bus),\n        secondary_star_arc = Arc(; from = new_bus1, to = star_bus),\n        tertiary_star_arc = Arc(; from = new_bus2, to = star_bus),\n        star_bus = star_bus,\n        active_power_flow_primary = 0.0,\n        reactive_power_flow_primary = 0.0,\n        active_power_flow_secondary = 0.0,\n        reactive_power_flow_secondary = 0.0,\n        active_power_flow_tertiary = 0.0,\n        reactive_power_flow_tertiary = 0.0,\n        # Star-to-winding impedances\n        r_primary = 0.01,\n        x_primary = 0.1,\n        r_secondary = 0.01,\n        x_secondary = 0.1,\n        r_tertiary = 0.01,\n        x_tertiary = 0.1,\n        # Winding-to-winding impedances\n        r_12 = 0.01,\n        x_12 = 0.1,\n        r_23 = 0.01,\n        x_23 = 0.1,\n        r_13 = 0.01,\n        x_13 = 0.1,\n        # Base powers for each winding pair\n        base_power_12 = 100.0,\n        base_power_23 = 100.0,\n        base_power_13 = 100.0,\n        # Ratings for each winding\n        rating = nothing,\n        rating_primary = 1.0,\n        rating_secondary = 1.0,\n        rating_tertiary = 0.5,\n    )\n    PSY.add_component!(system, transformer3w)\n\n    # Add Transformer3W device model when available\n    # Test with DC Power Flow Model\n    for net_model in [DCPPowerModel, PTDFPowerModel]\n        template = get_template_dispatch_with_network(\n            NetworkModel(net_model; PTDF_matrix = PTDF(system)),\n        )\n        # Set device model for Transformer3W\n        set_device_model!(template, DeviceModel(Transformer3W, StaticBranch))\n        set_device_model!(template, MonitoredLine, StaticBranch)\n\n        model_m = DecisionModel(template, system; optimizer = HiGHS_optimizer)\n        @test build!(model_m; output_dir = mktempdir(; cleanup = true)) ==\n              PSI.ModelBuildStatus.BUILT\n\n        @test solve!(model_m) == PSI.RunStatus.SUCCESSFULLY_FINALIZED\n\n        # Test flow constraints\n        transformer = PSY.get_component(Transformer3W, system, \"Transformer3W_busD\")\n        @test check_flow_variable_values(\n            model_m,\n            FlowActivePowerVariable,\n            Transformer3W,\n            \"Transformer3W_busD_winding_3\",\n            PSY.get_rating_tertiary(transformer),\n        )\n    end\n\n    template_ac = get_thermal_dispatch_template_network(ACPPowerModel)\n    set_device_model!(template_ac, DeviceModel(Transformer3W, StaticBranch))\n    model_ac = DecisionModel(template_ac, system; optimizer = ipopt_optimizer)\n    @test build!(model_ac; output_dir = mktempdir(; cleanup = true)) ==\n          PSI.ModelBuildStatus.BUILT\n    @test solve!(model_ac) == PSI.RunStatus.SUCCESSFULLY_FINALIZED\nend\n"
  },
  {
    "path": "test/test_device_hvdc.jl",
    "content": "@testset \"HVDC System Tests\" begin\n    sys_5 = build_system(PSISystems, \"sys10_pjm_ac_dc\")\n    template_uc = ProblemTemplate(NetworkModel(\n        DCPPowerModel,\n        #use_slacks=true,\n        #PTDF_matrix=PTDF(sys_5),\n        #duals=[CopperPlateBalanceConstraint],\n    ))\n\n    set_device_model!(template_uc, ThermalStandard, ThermalStandardUnitCommitment)\n    set_device_model!(template_uc, RenewableDispatch, RenewableFullDispatch)\n    set_device_model!(template_uc, PowerLoad, StaticPowerLoad)\n    set_device_model!(template_uc, DeviceModel(Line, StaticBranch))\n    set_device_model!(template_uc, DeviceModel(InterconnectingConverter, LosslessConverter))\n    set_device_model!(template_uc, DeviceModel(TModelHVDCLine, LosslessLine))\n    set_hvdc_network_model!(template_uc, TransportHVDCNetworkModel)\n    model = DecisionModel(template_uc, sys_5; name = \"UC\", optimizer = HiGHS_optimizer)\n    @test build!(model; output_dir = mktempdir()) == PSI.ModelBuildStatus.BUILT\n    moi_tests(model, 1656, 288, 1248, 528, 888, true)\n    @test solve!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED\n\n    template_uc = ProblemTemplate(NetworkModel(\n        PTDFPowerModel;\n        #use_slacks=true,\n        PTDF_matrix = PTDF(sys_5),\n        #duals=[CopperPlateBalanceConstraint],\n    ))\n\n    set_device_model!(template_uc, ThermalStandard, ThermalStandardUnitCommitment)\n    set_device_model!(template_uc, RenewableDispatch, RenewableFullDispatch)\n    set_device_model!(template_uc, PowerLoad, StaticPowerLoad)\n    set_device_model!(template_uc, DeviceModel(Line, StaticBranch))\n    set_device_model!(template_uc, DeviceModel(InterconnectingConverter, LosslessConverter))\n    set_device_model!(template_uc, DeviceModel(TModelHVDCLine, LosslessLine))\n    set_hvdc_network_model!(template_uc, TransportHVDCNetworkModel)\n    model = DecisionModel(template_uc, sys_5; name = \"UC\", optimizer = HiGHS_optimizer)\n    @test build!(model; output_dir = mktempdir()) == PSI.ModelBuildStatus.BUILT\n    moi_tests(model, 1128, 0, 1248, 528, 384, true)\n    @test solve!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED\nend\n\nfunction _generate_test_hvdc_sys()\n    sys = build_system(PSISystems, \"sys10_pjm_ac_dc\"; force_build = true)\n    th_names_2 = [\"Alta-2\", \"Sundance-2\", \"Park City-2\", \"Solitude-2\", \"Brighton-2\"]\n    for th_name in th_names_2\n        g = PSY.get_component(PSY.ThermalStandard, sys, th_name)\n        op_cost = g.operation_cost\n        val_curve = op_cost.variable.value_curve\n        new_prop_term = get_proportional_term(val_curve) * 2.0\n        if g.name == \"Park City-2\"\n            new_prop_term = new_prop_term + 5.0\n        end\n        new_quad_cost = QuadraticCurve(\n            get_quadratic_term(val_curve),\n            new_prop_term,\n            get_constant_term(val_curve),\n        )\n        new_op_cost = ThermalGenerationCost(\n            CostCurve(\n                new_quad_cost,\n                op_cost.variable.power_units,\n                op_cost.variable.vom_cost,\n            ),\n            op_cost.fixed,\n            op_cost.start_up,\n            op_cost.shut_down,\n        )\n        set_operation_cost!(g, new_op_cost)\n    end\n\n    for ipc in get_components(InterconnectingConverter, sys)\n        new_dc_loss = QuadraticCurve(0.01, 0.01, 0.0)\n        set_loss_function!(ipc, new_dc_loss)\n        set_max_dc_current!(ipc, 2.0)\n    end\n    return sys\nend\n\n@testset \"HVDC System with Transport Network\" begin\n    sys = _generate_test_hvdc_sys()\n    template = ProblemTemplate()\n    set_device_model!(template, ThermalStandard, ThermalDispatchNoMin)\n    set_device_model!(template, PowerLoad, StaticPowerLoad)\n    set_device_model!(template, TModelHVDCLine, LosslessLine)\n    set_device_model!(template, InterconnectingConverter, LosslessConverter)\n    set_hvdc_network_model!(template, TransportHVDCNetworkModel)\n    model =\n        DecisionModel(\n            template,\n            sys;\n            store_variable_names = true,\n            optimizer = HiGHS_optimizer,\n        )\n    @test build!(model; output_dir = mktempdir(; cleanup = true)) ==\n          PSI.ModelBuildStatus.BUILT\n    @test solve!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED\nend\n\n@testset \"HVDC System with Losses Network\" begin\n    sys = _generate_test_hvdc_sys()\n    template = ProblemTemplate()\n    set_device_model!(template, ThermalStandard, ThermalDispatchNoMin)\n    set_device_model!(template, PowerLoad, StaticPowerLoad)\n    set_device_model!(template, TModelHVDCLine, DCLossyLine)\n    ipc_model = DeviceModel(\n        InterconnectingConverter,\n        QuadraticLossConverter;\n        attributes = Dict(\n            \"voltage_segments\" => 3,\n            \"current_segments\" => 3,\n            \"bilinear_segments\" => 3,\n            \"use_linear_loss\" => true,\n        ),\n    )\n    set_device_model!(template, ipc_model)\n    set_hvdc_network_model!(template, VoltageDispatchHVDCNetworkModel)\n    model =\n        DecisionModel(\n            template,\n            sys;\n            store_variable_names = true,\n            optimizer = HiGHS_optimizer,\n        )\n    @test build!(model; output_dir = mktempdir(; cleanup = true)) ==\n          PSI.ModelBuildStatus.BUILT\n    @test solve!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED\nend\n"
  },
  {
    "path": "test/test_device_lcc.jl",
    "content": "@testset \"LCC HVDC System Tests\" begin\n    sys5 = build_system(PSISystems, \"2Area 5 Bus System\")\n    hvdc = first(get_components(TwoTerminalGenericHVDCLine, sys5))\n    lcc = TwoTerminalLCCLine(;\n        name = \"lcc\",\n        available = true,\n        arc = hvdc.arc,\n        active_power_flow = 0.1,\n        r = 0.000189,\n        transfer_setpoint = -100.0,\n        scheduled_dc_voltage = 7.5,\n        rectifier_bridges = 2,\n        rectifier_delay_angle_limits = (min = 0.31590, max = 1.570),\n        rectifier_rc = 2.6465e-5,\n        rectifier_xc = 0.001092,\n        rectifier_base_voltage = 230.0,\n        inverter_bridges = 2,\n        inverter_extinction_angle_limits = (min = 0.3037, max = 1.57076),\n        inverter_rc = 2.6465e-5,\n        inverter_xc = 0.001072,\n        inverter_base_voltage = 230.0,\n        power_mode = true,\n        switch_mode_voltage = 0.0,\n        compounding_resistance = 0.0,\n        min_compounding_voltage = 0.0,\n        rectifier_transformer_ratio = 0.09772,\n        rectifier_tap_setting = 1.0,\n        rectifier_tap_limits = (min = 1, max = 1),\n        rectifier_tap_step = 0.00624,\n        rectifier_delay_angle = 0.31590,\n        rectifier_capacitor_reactance = 0.1,\n        inverter_transformer_ratio = 0.07134,\n        inverter_tap_setting = 1.0,\n        inverter_tap_limits = (min = 1, max = 1),\n        inverter_tap_step = 0.00625,\n        inverter_extinction_angle = 0.31416,\n        inverter_capacitor_reactance = 0.0,\n        active_power_limits_from = (min = -3.0, max = 3.0),\n        active_power_limits_to = (min = -3.0, max = 3.0),\n        reactive_power_limits_from = (min = -3.0, max = 3.0),\n        reactive_power_limits_to = (min = -3.0, max = 3.0),\n    )\n\n    add_component!(sys5, lcc)\n    remove_component!(sys5, hvdc)\n\n    template = get_thermal_dispatch_template_network(\n        NetworkModel(\n            ACPPowerModel;\n            use_slacks = false,\n        ),\n    )\n\n    set_device_model!(template, TwoTerminalLCCLine, PSI.HVDCTwoTerminalLCC)\n    set_device_model!(template, ThermalStandard, ThermalDispatchNoMin)\n\n    model = DecisionModel(\n        template,\n        sys5;\n        optimizer = optimizer_with_attributes(Ipopt.Optimizer),\n        horizon = Hour(2),\n    )\n    @test build!(model; output_dir = mktempdir(; cleanup = true)) ==\n          PSI.ModelBuildStatus.BUILT\n    @test solve!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED\nend\n"
  },
  {
    "path": "test/test_device_load_constructors.jl",
    "content": "test_path = mktempdir()\n\n@testset \"StaticPowerLoad\" begin\n    models = [StaticPowerLoad, PowerLoadDispatch, PowerLoadInterruption]\n    c_sys5_il = PSB.build_system(PSITestSystems, \"c_sys5_il\")\n    networks = [DCPPowerModel, ACPPowerModel]\n    for m in models, n in networks\n        device_model = DeviceModel(PowerLoad, m)\n        model = DecisionModel(MockOperationProblem, n, c_sys5_il)\n        mock_construct_device!(model, device_model)\n        moi_tests(model, 0, 0, 0, 0, 0, false)\n        psi_checkobjfun_test(model, GAEVF)\n        model = DecisionModel(MockOperationProblem, n, c_sys5_il)\n        mock_construct_device!(model, device_model; add_event_model = true)\n        moi_tests(model, 0, 0, 0, 0, 0, false)\n    end\nend\n\n@testset \"PowerLoadDispatch DC- PF\" begin\n    models = [PowerLoadDispatch]\n    c_sys5_il = PSB.build_system(PSITestSystems, \"c_sys5_il\")\n    networks = [DCPPowerModel]\n    for m in models, n in networks\n        device_model = DeviceModel(InterruptiblePowerLoad, m)\n        model = DecisionModel(MockOperationProblem, n, c_sys5_il)\n        mock_construct_device!(model, device_model)\n        moi_tests(model, 24, 0, 24, 0, 0, false)\n        psi_checkobjfun_test(model, GAEVF)\n        model = DecisionModel(MockOperationProblem, n, c_sys5_il)\n        mock_construct_device!(model, device_model; add_event_model = true)\n        moi_tests(model, 24, 0, 48, 0, 0, false)\n    end\nend\n\n@testset \"PowerLoadDispatch AC- PF\" begin\n    models = [PowerLoadDispatch]\n    c_sys5_il = PSB.build_system(PSITestSystems, \"c_sys5_il\")\n    networks = [ACPPowerModel]\n    for m in models, n in networks\n        device_model = DeviceModel(InterruptiblePowerLoad, m)\n        model = DecisionModel(MockOperationProblem, n, c_sys5_il)\n        mock_construct_device!(model, device_model)\n        moi_tests(model, 48, 0, 24, 0, 24, false)\n        psi_checkobjfun_test(model, GAEVF)\n        model = DecisionModel(MockOperationProblem, n, c_sys5_il)\n        mock_construct_device!(model, device_model; add_event_model = true)\n        moi_tests(model, 48, 0, 48, 0, 24, false, 24)\n    end\nend\n\n@testset \"PowerLoadDispatch AC- PF with MarketBidCost Invalid\" begin\n    models = [PowerLoadDispatch]\n    c_sys5_il = PSB.build_system(PSITestSystems, \"c_sys5_il\")\n    iloadbus4 = get_component(InterruptiblePowerLoad, c_sys5_il, \"IloadBus4\")\n    set_operation_cost!(\n        iloadbus4,\n        MarketBidCost(;\n            no_load_cost = 0.0,\n            start_up = (hot = 0.0, warm = 0.0, cold = 0.0),\n            shut_down = 0.0,\n            incremental_offer_curves = make_market_bid_curve(\n                [0.0, 100.0, 200.0, 300.0, 400.0, 500.0, 600.0],\n                [25.0, 25.5, 26.0, 27.0, 28.0, 30.0],\n                0.0,\n            ),\n        ),\n    )\n    networks = [ACPPowerModel]\n    for m in models, n in networks\n        device_model = DeviceModel(InterruptiblePowerLoad, m)\n        model = DecisionModel(MockOperationProblem, n, c_sys5_il)\n        @test_throws ArgumentError mock_construct_device!(model, device_model)\n    end\nend\n\n@testset \"PowerLoadDispatch AC- PF with MarketBidCost\" begin\n    c_sys5_il = PSB.build_system(PSITestSystems, \"c_sys5_il\")\n    iloadbus4 = get_component(InterruptiblePowerLoad, c_sys5_il, \"IloadBus4\")\n    set_operation_cost!(\n        iloadbus4,\n        MarketBidCost(;\n            no_load_cost = 0.0,\n            start_up = (hot = 0.0, warm = 0.0, cold = 0.0),\n            shut_down = 0.0,\n            decremental_offer_curves = make_market_bid_curve(\n                [0.0, 10.0, 20.0, 30.0, 40.0, 50.0, 60.0, 70.0, 80.0, 90.0, 100.0],\n                [90.0, 85.0, 75.0, 70.0, 60.0, 50.0, 45.0, 40.0, 30.0, 25.0],\n                0.0,\n            ),\n        ),\n    )\n    template = ProblemTemplate(NetworkModel(CopperPlatePowerModel))\n    set_device_model!(template, ThermalStandard, ThermalBasicUnitCommitment)\n    set_device_model!(template, InterruptiblePowerLoad, PowerLoadDispatch)\n    model = DecisionModel(template,\n        c_sys5_il;\n        name = \"UC_fixed_market_bid_cost\",\n        optimizer = HiGHS_optimizer,\n        optimizer_solve_log_print = true)\n    @test build!(model; output_dir = test_path) == PSI.ModelBuildStatus.BUILT\n    @test solve!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED\n    results = OptimizationProblemResults(model)\n    expr = read_expression(\n        results,\n        \"ProductionCostExpression__InterruptiblePowerLoad\";\n        table_format = TableFormat.WIDE,\n    )\n    p_l = read_variable(\n        results,\n        \"ActivePowerVariable__InterruptiblePowerLoad\";\n        table_format = TableFormat.WIDE,\n    )\n    index = findfirst(row -> isapprox(100, row; atol = 1e-6), p_l.IloadBus4)\n    calculated_cost = expr[index, \"IloadBus4\"][1]\n    @test isapprox(-5700, calculated_cost; atol = 1)\nend\n\n@testset \"PowerLoadInterruption DC- PF\" begin\n    models = [PowerLoadInterruption]\n    c_sys5_il = PSB.build_system(PSITestSystems, \"c_sys5_il\")\n    networks = [DCPPowerModel]\n    for m in models, n in networks\n        device_model = DeviceModel(InterruptiblePowerLoad, m)\n        model = DecisionModel(MockOperationProblem, n, c_sys5_il)\n        mock_construct_device!(model, device_model)\n        moi_tests(model, 48, 0, 48, 0, 0, true)\n        psi_checkobjfun_test(model, GAEVF)\n        model = DecisionModel(MockOperationProblem, n, c_sys5_il)\n        mock_construct_device!(model, device_model; add_event_model = true)\n        moi_tests(model, 48, 0, 72, 0, 0, true)\n    end\nend\n\n@testset \"PowerLoadInterruption AC- PF\" begin\n    models = [PowerLoadInterruption]\n    c_sys5_il = PSB.build_system(PSITestSystems, \"c_sys5_il\")\n    networks = [ACPPowerModel]\n    for m in models, n in networks\n        device_model = DeviceModel(InterruptiblePowerLoad, m)\n        model = DecisionModel(MockOperationProblem, n, c_sys5_il)\n        mock_construct_device!(model, device_model)\n        moi_tests(model, 72, 0, 48, 0, 24, true)\n        psi_checkobjfun_test(model, GAEVF)\n        model = DecisionModel(MockOperationProblem, n, c_sys5_il)\n        mock_construct_device!(model, device_model; add_event_model = true)\n        moi_tests(model, 72, 0, 72, 0, 24, true), 24\n    end\nend\n\n@testset \"Loads without TimeSeries\" begin\n    sys = build_system(PSITestSystems, \"c_sys5_uc\"; force_build = true)\n    load = get_component(PowerLoad, sys, \"Bus2\")\n    remove_time_series!(sys, Deterministic, load, \"max_active_power\")\n\n    networks = [CopperPlatePowerModel, PTDFPowerModel, DCPPowerModel, ACPPowerModel]\n    solvers = [HiGHS_optimizer, HiGHS_optimizer, HiGHS_optimizer, ipopt_optimizer]\n    for (ix, net) in enumerate(networks)\n        template = ProblemTemplate(\n            NetworkModel(\n                net;\n            ),\n        )\n        set_device_model!(template, ThermalStandard, ThermalDispatchNoMin)\n        set_device_model!(template, PowerLoad, StaticPowerLoad)\n        set_device_model!(template, Line, StaticBranch)\n\n        model = DecisionModel(\n            template,\n            sys;\n            name = \"UC\",\n            store_variable_names = true,\n            optimizer = solvers[ix],\n        )\n\n        @test build!(model; output_dir = mktempdir(; cleanup = true)) ==\n              PSI.ModelBuildStatus.BUILT\n        @test solve!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED\n    end\nend\n\n@testset \"Loads with MotorLoad\" begin\n    sys = build_system(PSITestSystems, \"c_sys5_uc\"; force_build = true)\n    load = get_component(PowerLoad, sys, \"Bus2\")\n\n    mload = MotorLoad(;\n        name = \"MotorLoadBus2\",\n        available = true,\n        bus = load.bus,\n        active_power = load.active_power / 10.0,\n        reactive_power = load.reactive_power / 10.0,\n        base_power = load.base_power,\n        rating = load.max_active_power / 10.0,\n        max_active_power = load.max_active_power / 10.0,\n        reactive_power_limits = nothing,\n    )\n    add_component!(sys, mload)\n\n    networks = [CopperPlatePowerModel, PTDFPowerModel, DCPPowerModel, ACPPowerModel]\n    solvers = [HiGHS_optimizer, HiGHS_optimizer, HiGHS_optimizer, ipopt_optimizer]\n    for (ix, net) in enumerate(networks)\n        template = ProblemTemplate(\n            NetworkModel(\n                net;\n            ),\n        )\n        set_device_model!(template, ThermalStandard, ThermalDispatchNoMin)\n        set_device_model!(template, PowerLoad, StaticPowerLoad)\n        set_device_model!(template, MotorLoad, StaticPowerLoad)\n        set_device_model!(template, Line, StaticBranch)\n\n        model = DecisionModel(\n            template,\n            sys;\n            name = \"UC\",\n            store_variable_names = true,\n            optimizer = solvers[ix],\n        )\n\n        @test build!(model; output_dir = mktempdir(; cleanup = true)) ==\n              PSI.ModelBuildStatus.BUILT\n        @test solve!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED\n    end\nend\n\n@testset \"PowerLoadShift with NonAnticipativityConstraint\" begin\n    c_sys5_il =\n        PSB.build_system(PSITestSystems, \"c_sys5_il\"; add_single_time_series = true)\n    il_load = first(PSY.get_components(InterruptiblePowerLoad, c_sys5_il))\n\n    shiftable_load = ShiftablePowerLoad(;\n        name = \"shiftable_load\",\n        available = true,\n        bus = PSY.get_bus(il_load),\n        active_power = PSY.get_active_power(il_load),\n        active_power_limits = (min = 0.0, max = PSY.get_active_power(il_load)),\n        reactive_power = PSY.get_reactive_power(il_load),\n        max_active_power = PSY.get_max_active_power(il_load),\n        max_reactive_power = PSY.get_max_reactive_power(il_load),\n        base_power = PSY.get_base_power(il_load),\n        load_balance_time_horizon = 1,\n        operation_cost = LoadCost(;\n            variable = CostCurve(\n                LinearCurve(0.0),\n                UnitSystem.NATURAL_UNITS,\n                LinearCurve(1.0),\n            ),\n            fixed = 0.0,\n        ),\n    )\n    PSY.add_component!(c_sys5_il, shiftable_load)\n    PSY.set_available!(il_load, false)\n    PSY.copy_time_series!(shiftable_load, il_load)\n\n    tstamps = TimeSeries.timestamp(\n        PSY.get_time_series_array(SingleTimeSeries, shiftable_load, \"max_active_power\"),\n    )\n    n = length(tstamps)\n    up_vals = ones(n)\n    down_vals = ones(n)\n\n    PSY.add_time_series!(\n        c_sys5_il,\n        shiftable_load,\n        SingleTimeSeries(\n            \"shift_up_max_active_power\",\n            TimeArray(tstamps, up_vals);\n            scaling_factor_multiplier = PSY.get_max_active_power,\n        ),\n    )\n    PSY.add_time_series!(\n        c_sys5_il,\n        shiftable_load,\n        SingleTimeSeries(\n            \"shift_down_max_active_power\",\n            TimeArray(tstamps, down_vals);\n            scaling_factor_multiplier = PSY.get_max_active_power,\n        ),\n    )\n\n    PSY.transform_single_time_series!(c_sys5_il, Hour(24), Hour(24))\n\n    template = ProblemTemplate(\n        NetworkModel(\n            CopperPlatePowerModel;\n            duals = [CopperPlateBalanceConstraint],\n        ),\n    )\n    set_device_model!(template, ThermalStandard, ThermalBasicUnitCommitment)\n    set_device_model!(template, PowerLoad, StaticPowerLoad)\n    set_device_model!(\n        template,\n        DeviceModel(\n            ShiftablePowerLoad,\n            PowerLoadShift;\n            attributes = Dict{String, Any}(\"additional_balance_interval\" => Hour(12)),\n        ),\n    )\n\n    model = DecisionModel(\n        template,\n        c_sys5_il;\n        name = \"UC_shiftable\",\n        store_variable_names = true,\n        optimizer = HiGHS_optimizer,\n    )\n\n    @test build!(model; output_dir = mktempdir(; cleanup = true)) ==\n          PSI.ModelBuildStatus.BUILT\n\n    @test solve!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED\n\n    results = OptimizationProblemResults(model)\n    up = read_variable(\n        results,\n        \"ShiftUpActivePowerVariable__ShiftablePowerLoad\";\n        table_format = TableFormat.WIDE,\n    )\n    dn = read_variable(\n        results,\n        \"ShiftDownActivePowerVariable__ShiftablePowerLoad\";\n        table_format = TableFormat.WIDE,\n    )\n\n    # Verify the non-anticipativity constraint holds in the solution:\n    # the running sum of (shift_down - shift_up) must be >= 0 at every time step.\n    @test all(\n        cumsum(dn[!, \"shiftable_load\"] .- up[!, \"shiftable_load\"]) .>= -1e-6,\n    )\nend\n"
  },
  {
    "path": "test/test_device_renewable_generation_constructors.jl",
    "content": "@testset \"Renewable DCPLossless FullDispatch\" begin\n    device_model = DeviceModel(RenewableDispatch, RenewableFullDispatch)\n    c_sys5_re = PSB.build_system(PSITestSystems, \"c_sys5_re\")\n    model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys5_re)\n    mock_construct_device!(model, device_model)\n    moi_tests(model, 72, 0, 72, 0, 0, false)\n    psi_checkobjfun_test(model, GAEVF)\n    model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys5_re)\n    mock_construct_device!(model, device_model; add_event_model = true)\n    moi_tests(model, 72, 0, 96, 0, 0, false)\nend\n\n@testset \"Renewable ACPPower Full Dispatch\" begin\n    device_model = DeviceModel(RenewableDispatch, RenewableFullDispatch)\n    c_sys5_re = PSB.build_system(PSITestSystems, \"c_sys5_re\")\n    model = DecisionModel(MockOperationProblem, ACPPowerModel, c_sys5_re;)\n    mock_construct_device!(model, device_model)\n    moi_tests(model, 144, 0, 144, 72, 0, false)\n    psi_checkobjfun_test(model, GAEVF)\n    model = DecisionModel(MockOperationProblem, ACPPowerModel, c_sys5_re;)\n    mock_construct_device!(model, device_model; add_event_model = true)\n    moi_tests(model, 144, 0, 168, 72, 0, false, 24)\nend\n\n@testset \"Renewable DCPLossless Constantpower_factor\" begin\n    device_model = DeviceModel(RenewableDispatch, RenewableConstantPowerFactor)\n    c_sys5_re = PSB.build_system(PSITestSystems, \"c_sys5_re\")\n    model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys5_re)\n    mock_construct_device!(model, device_model)\n    moi_tests(model, 72, 0, 72, 0, 0, false)\n    psi_checkobjfun_test(model, GAEVF)\n    model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys5_re)\n    mock_construct_device!(model, device_model; add_event_model = true)\n    moi_tests(model, 72, 0, 96, 0, 0, false)\nend\n\n@testset \"Renewable ACPPower Constantpower_factor\" begin\n    device_model = DeviceModel(RenewableDispatch, RenewableConstantPowerFactor)\n    c_sys5_re = PSB.build_system(PSITestSystems, \"c_sys5_re\")\n    model = DecisionModel(MockOperationProblem, ACPPowerModel, c_sys5_re;)\n    mock_construct_device!(model, device_model)\n    moi_tests(model, 144, 0, 72, 0, 72, false)\n    psi_checkobjfun_test(model, GAEVF)\n    model = DecisionModel(MockOperationProblem, ACPPowerModel, c_sys5_re;)\n    mock_construct_device!(model, device_model; add_event_model = true)\n    moi_tests(model, 144, 0, 96, 0, 72, false, 24)\nend\n\n@testset \"Renewable DCPLossless FixedOutput\" begin\n    device_model = DeviceModel(RenewableDispatch, FixedOutput)\n    c_sys5_re = PSB.build_system(PSITestSystems, \"c_sys5_re\")\n    model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys5_re;)\n    mock_construct_device!(model, device_model)\n    moi_tests(model, 0, 0, 0, 0, 0, false)\n    psi_checkobjfun_test(model, GAEVF)\n    model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys5_re;)\n    mock_construct_device!(model, device_model; add_event_model = true)\n    moi_tests(model, 0, 0, 0, 0, 0, false)\nend\n\n@testset \"Renewable ACPPowerModel FixedOutput\" begin\n    device_model = DeviceModel(RenewableDispatch, FixedOutput)\n    c_sys5_re = PSB.build_system(PSITestSystems, \"c_sys5_re\")\n    model = DecisionModel(MockOperationProblem, ACPPowerModel, c_sys5_re;)\n    mock_construct_device!(model, device_model)\n    moi_tests(model, 0, 0, 0, 0, 0, false)\n    psi_checkobjfun_test(model, GAEVF)\n    model = DecisionModel(MockOperationProblem, ACPPowerModel, c_sys5_re;)\n    mock_construct_device!(model, device_model; add_event_model = true)\n    moi_tests(model, 0, 0, 0, 0, 0, false)\nend\n\n@testset \"Test Renewable CurtailmentCostExpression nonnegativity\" begin\n    c_sys5_re = PSB.build_system(PSITestSystems, \"c_sys5_re\")\n\n    template = ProblemTemplate(NetworkModel(CopperPlatePowerModel))\n    set_device_model!(template, RenewableDispatch, RenewableFullDispatch)\n    set_device_model!(template, ThermalStandard, ThermalStandardDispatch)\n    set_device_model!(template, PowerLoad, StaticPowerLoad)\n\n    model = DecisionModel(\n        template,\n        c_sys5_re;\n        name = \"RE_curtailment_cost\",\n        optimizer = HiGHS_optimizer,\n        optimizer_solve_log_print = true,\n    )\n\n    @test build!(model; output_dir = test_path) == PSI.ModelBuildStatus.BUILT\n    @test solve!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED\n\n    results = OptimizationProblemResults(model)\n\n    expr_curt = read_expression(\n        results,\n        \"CurtailmentCostExpression__RenewableDispatch\";\n        table_format = TableFormat.WIDE,\n    )\n\n    tol = 1e-8\n    for unit in names(expr_curt)[2:end]\n        @test all(expr_curt[!, unit] .>= -tol)\n    end\nend\n"
  },
  {
    "path": "test/test_device_source_constructors.jl",
    "content": "# See also test_import_export_cost.jl\n\nconst BASIC_SOURCE_CONSTRAINT_KEYS = [\n    PSI.ConstraintKey(ImportExportBudgetConstraint, PSY.Source, \"import\"),\n    PSI.ConstraintKey(ImportExportBudgetConstraint, PSY.Source, \"export\"),\n    PSI.ConstraintKey(ActivePowerVariableLimitsConstraint, PSY.Source, \"ub\"),\n    PSI.ConstraintKey(ActivePowerVariableLimitsConstraint, PSY.Source, \"lb\"),\n    PSI.ConstraintKey(InputActivePowerVariableLimitsConstraint, PSY.Source, \"ub\"),\n    PSI.ConstraintKey(InputActivePowerVariableLimitsConstraint, PSY.Source, \"lb\"),\n    PSI.ConstraintKey(PiecewiseLinearBlockIncrementalOfferConstraint, PSY.Source),\n    PSI.ConstraintKey(PiecewiseLinearBlockDecrementalOfferConstraint, PSY.Source),\n]\n\nconst TS_SOURCE_CONSTRAINT_KEYS = [\n    BASIC_SOURCE_CONSTRAINT_KEYS...,\n    PSI.ConstraintKey(ActivePowerOutVariableTimeSeriesLimitsConstraint, Source, \"ub\"),\n    PSI.ConstraintKey(ActivePowerInVariableTimeSeriesLimitsConstraint, Source, \"ub\"),\n]\n\n@testset \"ImportExportSource Source With CopperPlate\" begin\n    sys = make_5_bus_with_import_export(; add_single_time_series = false)\n\n    model = DecisionModel(MockOperationProblem, CopperPlatePowerModel, sys)\n    device_model = DeviceModel(\n        Source,\n        ImportExportSourceModel;\n        attributes = Dict(\"reservation\" => false),\n    )\n    mock_construct_device!(model, device_model)\n    moi_tests(model, 240, 0, 242, 48, 48, false)\n    psi_constraint_test(model, BASIC_SOURCE_CONSTRAINT_KEYS)\n    psi_checkobjfun_test(model, GAEVF)\n\n    model = DecisionModel(MockOperationProblem, CopperPlatePowerModel, sys)\n    device_model = DeviceModel(\n        Source,\n        ImportExportSourceModel;\n        attributes = Dict(\"reservation\" => true),\n    )\n    mock_construct_device!(model, device_model)\n    moi_tests(model, 264, 0, 242, 48, 48, true)\n    psi_constraint_test(model, BASIC_SOURCE_CONSTRAINT_KEYS)\n    psi_checkobjfun_test(model, GAEVF)\nend\n\n@testset \"ImportExportSource Source With ACPPowerModel\" begin\n    sys = make_5_bus_with_import_export(; add_single_time_series = false)\n\n    model = DecisionModel(MockOperationProblem, ACPPowerModel, sys)\n    device_model = DeviceModel(\n        Source,\n        ImportExportSourceModel;\n        attributes = Dict(\"reservation\" => false),\n    )\n    mock_construct_device!(model, device_model)\n    moi_tests(model, 264, 0, 266, 72, 48, false)\n    psi_constraint_test(model, BASIC_SOURCE_CONSTRAINT_KEYS)\n    psi_checkobjfun_test(model, GAEVF)\n\n    model = DecisionModel(MockOperationProblem, ACPPowerModel, sys)\n    device_model = DeviceModel(\n        Source,\n        ImportExportSourceModel;\n        attributes = Dict(\"reservation\" => true),\n    )\n    mock_construct_device!(model, device_model)\n    moi_tests(model, 288, 0, 266, 72, 48, true)\n    psi_constraint_test(model, BASIC_SOURCE_CONSTRAINT_KEYS)\n    psi_checkobjfun_test(model, GAEVF)\nend\n\n@testset \"Source With CopperPlate and TimeSeries\" begin\n    for source_formulation in [ImportExportSourceModel, FixedOutput]\n        sys = make_5_bus_with_import_export(; add_single_time_series = true)\n        source = get_component(Source, sys, \"source\")\n\n        load = first(get_components(PowerLoad, sys))\n        tstamp =\n            TimeSeries.timestamp(\n                get_time_series_array(SingleTimeSeries, load, \"max_active_power\"),\n            )\n\n        day_data = [\n            0.9, 0.85, 0.95, 0.2, 0.0, 0.0,\n            0.9, 0.85, 0.95, 0.2, 0.0, 0.0,\n            0.9, 0.85, 0.95, 0.2, 0.0, 0.0,\n            0.9, 0.85, 0.95, 0.2, 0.0, 0.0,\n        ]\n\n        ts_data = repeat(day_data, 2)\n        ts_out = SingleTimeSeries(\n            \"max_active_power_out\",\n            TimeArray(tstamp, ts_data);\n            scaling_factor_multiplier = get_max_active_power,\n        )\n        ts_in = SingleTimeSeries(\n            \"max_active_power_in\",\n            TimeArray(tstamp, ts_data);\n            scaling_factor_multiplier = get_max_active_power,\n        )\n        add_time_series!(sys, source, ts_out)\n        add_time_series!(sys, source, ts_in)\n        transform_single_time_series!(sys, Hour(24), Hour(24))\n\n        template = ProblemTemplate(NetworkModel(CopperPlatePowerModel))\n        set_device_model!(template, ThermalStandard, ThermalStandardUnitCommitment)\n        set_device_model!(template, PowerLoad, StaticPowerLoad)\n        source_model = DeviceModel(\n            Source,\n            source_formulation;\n            attributes = Dict(\"reservation\" => false),\n        )\n        set_device_model!(template, source_model)\n\n        model = DecisionModel(\n            template,\n            sys;\n            name = \"UC\",\n            optimizer = HiGHS_optimizer,\n            store_variable_names = true,\n            optimizer_solve_log_print = false,\n        )\n\n        @test build!(model; output_dir = mktempdir()) == PSI.ModelBuildStatus.BUILT\n        @test solve!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED\n\n        res = OptimizationProblemResults(model)\n        if source_formulation == ImportExportSourceModel\n            p_out = read_variable(\n                res,\n                \"ActivePowerOutVariable__Source\";\n                table_format = TableFormat.WIDE,\n            )[\n                !,\n                2,\n            ]\n            p_in =\n                read_variable(\n                    res,\n                    \"ActivePowerInVariable__Source\";\n                    table_format = TableFormat.WIDE,\n                )[\n                    !,\n                    2,\n                ]\n        elseif source_formulation == FixedOutput\n            p_out = read_parameter(\n                res,\n                \"ActivePowerOutTimeSeriesParameter__Source\";\n                table_format = TableFormat.WIDE,\n            )[\n                !,\n                2,\n            ]\n            p_in =\n                read_parameter(\n                    res,\n                    \"ActivePowerInTimeSeriesParameter__Source\";\n                    table_format = TableFormat.WIDE,\n                )[\n                    !,\n                    2,\n                ]\n        end\n        # Test that is zero when the time series is zero\n        @test p_out[5] == 0.0\n        @test p_in[5] == 0.0\n        @test p_out[6] == 0.0\n        @test p_in[6] == 0.0\n    end\nend\n\n@testset \"ImportExportSource Source With ACPPowerModel and TimeSeries\" begin\n    sys = make_5_bus_with_import_export(; add_single_time_series = true)\n    source = get_component(Source, sys, \"source\")\n\n    load = first(get_components(PowerLoad, sys))\n    tstamp =\n        TimeSeries.timestamp(\n            get_time_series_array(SingleTimeSeries, load, \"max_active_power\"),\n        )\n\n    day_data = [\n        0.9, 0.85, 0.95, 0.2, 0.0, 0.0,\n        0.9, 0.85, 0.95, 0.2, 0.0, 0.0,\n        0.9, 0.85, 0.95, 0.2, 0.0, 0.0,\n        0.9, 0.85, 0.95, 0.2, 0.0, 0.0,\n    ]\n\n    ts_data = repeat(day_data, 2)\n    ts_out = SingleTimeSeries(\n        \"max_active_power_out\",\n        TimeArray(tstamp, ts_data);\n        scaling_factor_multiplier = get_max_active_power,\n    )\n    ts_in = SingleTimeSeries(\n        \"max_active_power_in\",\n        TimeArray(tstamp, ts_data);\n        scaling_factor_multiplier = get_max_active_power,\n    )\n    add_time_series!(sys, source, ts_out)\n    add_time_series!(sys, source, ts_in)\n    transform_single_time_series!(sys, Hour(24), Hour(24))\n\n    model = DecisionModel(MockOperationProblem, ACPPowerModel, sys)\n    device_model = DeviceModel(\n        Source,\n        ImportExportSourceModel;\n        attributes = Dict(\"reservation\" => false),\n    )\n    mock_construct_device!(model, device_model)\n    moi_tests(model, 264, 0, 314, 72, 48, false)\n    psi_constraint_test(model, TS_SOURCE_CONSTRAINT_KEYS)\n    psi_checkobjfun_test(model, GAEVF)\n\n    model = DecisionModel(MockOperationProblem, ACPPowerModel, sys)\n    device_model = DeviceModel(\n        Source,\n        ImportExportSourceModel;\n        attributes = Dict(\"reservation\" => true),\n    )\n    mock_construct_device!(model, device_model)\n    moi_tests(model, 288, 0, 314, 72, 48, true)\n    psi_constraint_test(model, TS_SOURCE_CONSTRAINT_KEYS)\n    psi_checkobjfun_test(model, GAEVF)\nend\n\n@testset \"FixedOutput Source With PTDFPowerModel and TimeSeries\" begin\n    sys = make_5_bus_with_import_export(; add_single_time_series = true)\n    source = get_component(Source, sys, \"source\")\n\n    load = first(get_components(PowerLoad, sys))\n    tstamp =\n        TimeSeries.timestamp(\n            get_time_series_array(SingleTimeSeries, load, \"max_active_power\"),\n        )\n\n    day_data = [\n        0.9, 0.85, 0.95, 0.2, 0.0, 0.0,\n        0.9, 0.85, 0.95, 0.2, 0.0, 0.0,\n        0.9, 0.85, 0.95, 0.2, 0.0, 0.0,\n        0.9, 0.85, 0.95, 0.2, 0.0, 0.0,\n    ]\n\n    ts_data = repeat(day_data, 2)\n    ts_out = SingleTimeSeries(\n        \"max_active_power_out\",\n        TimeArray(tstamp, ts_data);\n        scaling_factor_multiplier = get_max_active_power,\n    )\n    ts_in = SingleTimeSeries(\n        \"max_active_power_in\",\n        TimeArray(tstamp, ts_data);\n        scaling_factor_multiplier = get_max_active_power,\n    )\n    add_time_series!(sys, source, ts_out)\n    add_time_series!(sys, source, ts_in)\n    transform_single_time_series!(sys, Hour(24), Hour(24))\n\n    model = DecisionModel(MockOperationProblem, PTDFPowerModel, sys)\n    device_model = DeviceModel(\n        Source,\n        FixedOutput;\n        attributes = Dict(\"reservation\" => false),\n    )\n    mock_construct_device!(model, device_model)\n    # Fixed output does not add variables nor constraints. \n    moi_tests(model, 0, 0, 0, 0, 0, false)\n    psi_checkobjfun_test(model, GAEVF)\n\n    model = DecisionModel(MockOperationProblem, PTDFPowerModel, sys)\n    device_model = DeviceModel(\n        Source,\n        FixedOutput;\n        attributes = Dict(\"reservation\" => true),\n    )\n    mock_construct_device!(model, device_model)\n    # Fixed output does not add variables nor constraints. \n    moi_tests(model, 0, 0, 0, 0, 0, false)\n    psi_checkobjfun_test(model, GAEVF)\nend\n"
  },
  {
    "path": "test/test_device_synchronous_condenser_constructors.jl",
    "content": "@testset \"SynchronousCondenserBasicDispatch SynchronousCondenser With Power Models\" begin\n    sys = build_system(PSITestSystems, \"c_sys5_uc\"; add_single_time_series = true)\n\n    syncon = SynchronousCondenser(;\n        name = \"syncon_test\",\n        available = true,\n        bus = get_component(ACBus, sys, \"nodeB\"),\n        reactive_power = 0.0,\n        rating = 2.0,\n        reactive_power_limits = (min = -2.0, max = 2.0),\n        base_power = 100.0,\n    )\n\n    add_component!(sys, syncon)\n\n    template = ProblemTemplate(ACPPowerModel)\n    set_device_model!(template, ThermalStandard, ThermalDispatchNoMin)\n    set_device_model!(template, PowerLoad, StaticPowerLoad)\n    set_device_model!(template, Line, StaticBranch)\n    set_device_model!(template, SynchronousCondenser, SynchronousCondenserBasicDispatch)\n\n    transform_single_time_series!(sys, Hour(24), Hour(24))\n    model = DecisionModel(\n        template,\n        sys;\n        name = \"UC\",\n        optimizer = Ipopt.Optimizer,\n        store_variable_names = true,\n    )\n    @test build!(model; output_dir = mktempdir(; cleanup = true)) ==\n          PSI.ModelBuildStatus.BUILT\n    @test solve!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED\n\n    res = OptimizationProblemResults(model)\n    q_syncon = read_variable(\n        res,\n        \"ReactivePowerVariable__SynchronousCondenser\";\n        table_format = TableFormat.WIDE,\n    )\n    @test any(q_syncon[!, 2] != 0.0)\n\n    template = ProblemTemplate(PTDFPowerModel)\n    set_device_model!(template, ThermalStandard, ThermalDispatchNoMin)\n    set_device_model!(template, PowerLoad, StaticPowerLoad)\n    set_device_model!(template, Line, StaticBranch)\n    set_device_model!(template, SynchronousCondenser, SynchronousCondenserBasicDispatch)\n\n    model = DecisionModel(\n        template,\n        sys;\n        name = \"UC\",\n        optimizer = HiGHS_optimizer,\n        store_variable_names = true,\n    )\n    @test build!(model; output_dir = mktempdir(; cleanup = true)) ==\n          PSI.ModelBuildStatus.BUILT\n    @test solve!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED\nend\n"
  },
  {
    "path": "test/test_device_thermal_generation_constructors.jl",
    "content": "test_path = mktempdir()\nconst TIME1 = DateTime(\"2024-01-01T00:00:00\")\n\n@testset \"Test Thermal Generation Cost Functions \" begin\n    test_cases = [\n        (\"linear_cost_test\", 4664.88, ThermalBasicUnitCommitment),\n        (\"linear_fuel_test\", 4664.88, ThermalBasicUnitCommitment),\n        (\"quadratic_cost_test\", 3301.81, ThermalDispatchNoMin),\n        (\"quadratic_fuel_test\", 3331.12, ThermalDispatchNoMin),\n        (\"pwl_io_cost_test\", 3421.64, ThermalBasicUnitCommitment),\n        (\"pwl_io_fuel_test\", 3421.64, ThermalBasicUnitCommitment),\n        (\"pwl_incremental_cost_test\", 3424.43, ThermalBasicUnitCommitment),\n        (\"pwl_incremental_fuel_test\", 3424.43, ThermalBasicUnitCommitment),\n        (\"pwl_average_cost_test\", 3424.43, ThermalBasicUnitCommitment),\n        (\"pwl_average_fuel_test\", 3424.43, ThermalBasicUnitCommitment),\n        (\"non_convex_io_pwl_cost_test\", 3047.14, ThermalBasicUnitCommitment),\n    ]\n    for (i, cost_reference, thermal_formulation) in test_cases\n        @testset \"$i\" begin\n            sys = build_system(PSITestSystems, \"c_$(i)\")\n            template = ProblemTemplate(NetworkModel(CopperPlatePowerModel))\n            set_device_model!(template, ThermalStandard, thermal_formulation)\n            set_device_model!(template, PowerLoad, StaticPowerLoad)\n            model = DecisionModel(\n                template,\n                sys;\n                name = \"UC_$(i)\",\n                optimizer = HiGHS_optimizer,\n                optimizer_solve_log_print = true,\n            )\n            @test build!(model; output_dir = test_path) == PSI.ModelBuildStatus.BUILT\n            @test solve!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED\n            results = OptimizationProblemResults(model)\n            expr = read_expression(\n                results,\n                \"ProductionCostExpression__ThermalStandard\";\n                table_format = TableFormat.WIDE,\n            )\n            unit = \"Test Unit\"\n            var_unit_cost = sum(expr[!, unit])\n            @test isapprox(var_unit_cost, cost_reference; atol = 1)\n            if thermal_formulation == ThermalBasicUnitCommitment\n                # Tests shut down cost\n                @test expr[!, unit][end] == 0.75\n\n                # Decomposition: production == fuel + startup + shutdown + fixed + VOM\n                expr_fuel = read_expression(\n                    results, \"FuelCostExpression__ThermalStandard\";\n                    table_format = TableFormat.WIDE,\n                )\n                expr_su = read_expression(\n                    results, \"StartUpCostExpression__ThermalStandard\";\n                    table_format = TableFormat.WIDE,\n                )\n                expr_sd = read_expression(\n                    results, \"ShutDownCostExpression__ThermalStandard\";\n                    table_format = TableFormat.WIDE,\n                )\n                expr_fixed = read_expression(\n                    results, \"FixedCostExpression__ThermalStandard\";\n                    table_format = TableFormat.WIDE,\n                )\n                expr_VOM = read_expression(\n                    results, \"VOMCostExpression__ThermalStandard\";\n                    table_format = TableFormat.WIDE,\n                )\n                decomp_vec =\n                    expr_fuel[!, unit] .+ expr_su[!, unit] .+ expr_sd[!, unit] .+\n                    expr_fixed[!, unit] .+ expr_VOM[!, unit]\n                @test all(isapprox.(decomp_vec, expr[!, unit]; atol = 1e-6))\n                @test isapprox(sum(expr[!, unit]), sum(decomp_vec); atol = 1e-6)\n\n                # Nonnegativity (tolerate tiny numerical negatives)\n                tol = 1e-8\n                @test all(expr_fuel[!, unit] .>= -tol)\n                @test all(expr_su[!, unit] .>= -tol)\n                @test all(expr_sd[!, unit] .>= -tol)\n                @test all(expr_fixed[!, unit] .>= -tol)\n                @test all(expr_VOM[!, unit] .>= -tol)\n            else\n                @test expr[!, unit][end] == 0.0\n            end\n        end\n    end\n\n    @testset \"Test startup cost tracking\" begin\n        template = ProblemTemplate(NetworkModel(CopperPlatePowerModel))\n        set_device_model!(template, ThermalStandard, ThermalBasicUnitCommitment)\n        set_device_model!(template, PowerLoad, StaticPowerLoad)\n        unit = \"Test Unit\"\n\n        # Run 1: units initially ON — no startup expected\n        sys_no_startup = build_system(PSITestSystems, \"c_linear_cost_test\")\n        model_no = DecisionModel(\n            template, sys_no_startup;\n            name = \"UC_no_startup\",\n            optimizer = HiGHS_optimizer,\n            optimizer_solve_log_print = true,\n        )\n        @test build!(model_no; output_dir = test_path) == PSI.ModelBuildStatus.BUILT\n        @test solve!(model_no) == PSI.RunStatus.SUCCESSFULLY_FINALIZED\n        res_no = OptimizationProblemResults(model_no)\n        prod_no = read_expression(\n            res_no, \"ProductionCostExpression__ThermalStandard\";\n            table_format = TableFormat.WIDE,\n        )\n        cost_no_t1 = prod_no[1, unit]\n\n        # Run 2: units initially OFF — startup forced in first timestep\n        sys_yes = build_system(PSITestSystems, \"c_linear_cost_test\")\n        for u in collect(get_components(ThermalStandard, sys_yes))\n            set_status!(u, false)\n            set_time_at_status!(u, 10.0)  # > min down time\n        end\n        model_yes = DecisionModel(\n            template, sys_yes;\n            name = \"UC_with_startup\",\n            optimizer = HiGHS_optimizer,\n            optimizer_solve_log_print = true,\n        )\n        @test build!(model_yes; output_dir = test_path) == PSI.ModelBuildStatus.BUILT\n        @test solve!(model_yes) == PSI.RunStatus.SUCCESSFULLY_FINALIZED\n        res_yes = OptimizationProblemResults(model_yes)\n        prod_yes = read_expression(\n            res_yes, \"ProductionCostExpression__ThermalStandard\";\n            table_format = TableFormat.WIDE,\n        )\n        cost_yes_t1 = prod_yes[1, unit]\n\n        start_vars = read_variable(\n            res_yes, \"StartVariable__ThermalStandard\";\n            table_format = TableFormat.WIDE,\n        )\n        @test start_vars[1, unit] > 0.5\n\n        expr_su = read_expression(\n            res_yes, \"StartUpCostExpression__ThermalStandard\";\n            table_format = TableFormat.WIDE,\n        )\n        startup_cost_t1 = expr_su[1, unit]\n        @test startup_cost_t1 > 0.0\n        @test cost_yes_t1 > cost_no_t1\n        @test isapprox(cost_yes_t1 - cost_no_t1, startup_cost_t1; atol = 1e-6)\n    end\nend\n\n#TODO: ThermalGen\n#=\n@testset \"Test Thermal Generation Cost Functions Fuel Cost time series\" begin\n    test_cases = [\n        \"linear_fuel_test_ts\",\n        \"quadratic_fuel_test_ts\",\n        \"pwl_io_fuel_test_ts\",\n        \"pwl_incremental_fuel_test_ts\",\n        \"market_bid_cost\",\n    ]\n    for i in test_cases\n        @testset \"$i\" begin\n            sys = build_system(PSITestSystems, \"c_$(i)\")\n            template = ProblemTemplate(NetworkModel(CopperPlatePowerModel))\n            set_device_model!(template, ThermalStandard, ThermalBasicUnitCommitment)\n            #=\n            model = DecisionModel(\n                template,\n                sys;\n                name = \"UC_$(i)\",\n                optimizer = HiGHS_optimizer,\n            )\n            @test build!(model; output_dir = test_path) == PSI.ModelBuildStatus.BUILT\n            @test solve!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED\n            =#\n        end\n    end\nend\n=#\n\n################################### Unit Commitment tests ##################################\n@testset \"Thermal UC With DC - PF\" begin\n    bin_variable_keys = [\n        PSI.VariableKey(OnVariable, PSY.ThermalStandard),\n        PSI.VariableKey(StartVariable, PSY.ThermalStandard),\n        PSI.VariableKey(StopVariable, PSY.ThermalStandard),\n    ]\n\n    uc_constraint_keys = [\n        PSI.ConstraintKey(RampConstraint, PSY.ThermalStandard, \"up\"),\n        PSI.ConstraintKey(RampConstraint, PSY.ThermalStandard, \"dn\"),\n        PSI.ConstraintKey(DurationConstraint, PSY.ThermalStandard, \"up\"),\n        PSI.ConstraintKey(DurationConstraint, PSY.ThermalStandard, \"dn\"),\n    ]\n\n    aux_variables_keys = [\n        PSI.AuxVarKey(PSI.TimeDurationOff, ThermalStandard),\n        PSI.AuxVarKey(PSI.TimeDurationOn, ThermalStandard),\n    ]\n    device_model = DeviceModel(ThermalStandard, ThermalStandardUnitCommitment)\n\n    c_sys5_uc = PSB.build_system(PSITestSystems, \"c_sys5_uc\")\n    model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys5_uc)\n    mock_construct_device!(model, device_model)\n    moi_tests(model, 480, 0, 480, 120, 120, true)\n    psi_constraint_test(model, uc_constraint_keys)\n    psi_checkbinvar_test(model, bin_variable_keys)\n    psi_checkobjfun_test(model, GAEVF)\n    psi_aux_variable_test(model, aux_variables_keys)\n    model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys5_uc)\n    mock_construct_device!(model, device_model; add_event_model = true)\n    moi_tests(model, 480, 0, 504, 120, 120, true)\n\n    c_sys14 = PSB.build_system(PSITestSystems, \"c_sys14\")\n    device_model = DeviceModel(ThermalStandard, ThermalStandardUnitCommitment)\n    model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys14)\n    mock_construct_device!(model, device_model)\n    moi_tests(model, 480, 0, 240, 120, 120, true)\n    psi_checkbinvar_test(model, bin_variable_keys)\n    psi_checkobjfun_test(model, GQEVF)\n    model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys14)\n    mock_construct_device!(model, device_model; add_event_model = true)\n    moi_tests(model, 480, 0, 264, 120, 120, true)\nend\n\n@testset \"Thermal UC With AC - PF\" begin\n    bin_variable_keys = [\n        PSI.VariableKey(OnVariable, PSY.ThermalStandard),\n        PSI.VariableKey(StartVariable, PSY.ThermalStandard),\n        PSI.VariableKey(StopVariable, PSY.ThermalStandard),\n    ]\n    uc_constraint_keys = [\n        PSI.ConstraintKey(RampConstraint, PSY.ThermalStandard, \"up\"),\n        PSI.ConstraintKey(RampConstraint, PSY.ThermalStandard, \"dn\"),\n        PSI.ConstraintKey(DurationConstraint, PSY.ThermalStandard, \"up\"),\n        PSI.ConstraintKey(DurationConstraint, PSY.ThermalStandard, \"dn\"),\n    ]\n\n    aux_variables_keys = [\n        PSI.AuxVarKey(PSI.TimeDurationOff, ThermalStandard),\n        PSI.AuxVarKey(PSI.TimeDurationOn, ThermalStandard),\n    ]\n\n    device_model = DeviceModel(ThermalStandard, ThermalStandardUnitCommitment)\n\n    c_sys5_uc = PSB.build_system(PSITestSystems, \"c_sys5_uc\")\n    model = DecisionModel(MockOperationProblem, ACPPowerModel, c_sys5_uc)\n    mock_construct_device!(model, device_model)\n    moi_tests(model, 600, 0, 600, 240, 120, true)\n    psi_constraint_test(model, uc_constraint_keys)\n    psi_checkbinvar_test(model, bin_variable_keys)\n    psi_checkobjfun_test(model, GAEVF)\n    psi_aux_variable_test(model, aux_variables_keys)\n    model = DecisionModel(MockOperationProblem, ACPPowerModel, c_sys5_uc)\n    mock_construct_device!(model, device_model; add_event_model = true)\n    moi_tests(model, 600, 0, 624, 240, 120, true, 24)\n\n    device_model = DeviceModel(ThermalStandard, ThermalStandardUnitCommitment)\n\n    c_sys14 = PSB.build_system(PSITestSystems, \"c_sys14\")\n    model = DecisionModel(MockOperationProblem, ACPPowerModel, c_sys14;)\n    mock_construct_device!(model, device_model)\n    moi_tests(model, 600, 0, 360, 240, 120, true)\n    psi_checkbinvar_test(model, bin_variable_keys)\n    psi_checkobjfun_test(model, GQEVF)\n    model = DecisionModel(MockOperationProblem, ACPPowerModel, c_sys14;)\n    mock_construct_device!(model, device_model; add_event_model = true)\n    moi_tests(model, 600, 0, 384, 240, 120, true, 24)\nend\n\n@testset \"Thermal MultiStart UC With DC - PF\" begin\n    bin_variable_keys = [\n        PSI.VariableKey(OnVariable, PSY.ThermalMultiStart),\n        PSI.VariableKey(StartVariable, PSY.ThermalMultiStart),\n        PSI.VariableKey(StopVariable, PSY.ThermalMultiStart),\n    ]\n    uc_constraint_keys = [\n        PSI.ConstraintKey(RampConstraint, PSY.ThermalMultiStart, \"up\"),\n        PSI.ConstraintKey(RampConstraint, PSY.ThermalMultiStart, \"dn\"),\n        PSI.ConstraintKey(DurationConstraint, PSY.ThermalMultiStart, \"up\"),\n        PSI.ConstraintKey(DurationConstraint, PSY.ThermalMultiStart, \"dn\"),\n    ]\n    device_model = DeviceModel(ThermalMultiStart, ThermalStandardUnitCommitment)\n\n    c_sys5_uc = PSB.build_system(PSITestSystems, \"c_sys5_pglib\")\n    model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys5_uc;)\n    mock_construct_device!(model, device_model)\n    moi_tests(model, 384, 0, 240, 48, 144, true)\n    psi_constraint_test(model, uc_constraint_keys)\n    psi_checkbinvar_test(model, bin_variable_keys)\n    psi_checkobjfun_test(model, GAEVF)\n    model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys5_uc;)\n    mock_construct_device!(model, device_model; add_event_model = true)\n    moi_tests(model, 384, 0, 264, 48, 144, true)\nend\n\n@testset \"Thermal MultiStart UC With AC - PF\" begin\n    bin_variable_keys = [\n        PSI.VariableKey(OnVariable, PSY.ThermalMultiStart),\n        PSI.VariableKey(StartVariable, PSY.ThermalMultiStart),\n        PSI.VariableKey(StopVariable, PSY.ThermalMultiStart),\n    ]\n    uc_constraint_keys = [\n        PSI.ConstraintKey(RampConstraint, PSY.ThermalMultiStart, \"up\"),\n        PSI.ConstraintKey(RampConstraint, PSY.ThermalMultiStart, \"dn\"),\n        PSI.ConstraintKey(DurationConstraint, PSY.ThermalMultiStart, \"up\"),\n        PSI.ConstraintKey(DurationConstraint, PSY.ThermalMultiStart, \"dn\"),\n    ]\n    device_model = DeviceModel(ThermalMultiStart, ThermalStandardUnitCommitment)\n\n    c_sys5_uc = PSB.build_system(PSITestSystems, \"c_sys5_pglib\")\n    model = DecisionModel(MockOperationProblem, ACPPowerModel, c_sys5_uc;)\n    mock_construct_device!(model, device_model)\n    moi_tests(model, 432, 0, 288, 96, 144, true)\n    psi_constraint_test(model, uc_constraint_keys)\n    psi_checkbinvar_test(model, bin_variable_keys)\n    psi_checkobjfun_test(model, GAEVF)\n    model = DecisionModel(MockOperationProblem, ACPPowerModel, c_sys5_uc;)\n    mock_construct_device!(model, device_model; add_event_model = true)\n    moi_tests(model, 432, 0, 312, 96, 144, true, 24)\nend\n\n################################### Basic Unit Commitment tests ############################\n@testset \"Thermal Basic UC With DC - PF\" begin\n    bin_variable_keys = [\n        PSI.VariableKey(OnVariable, PSY.ThermalStandard),\n        PSI.VariableKey(StartVariable, PSY.ThermalStandard),\n        PSI.VariableKey(StopVariable, PSY.ThermalStandard),\n    ]\n    device_model = DeviceModel(ThermalStandard, ThermalBasicUnitCommitment)\n\n    c_sys5_uc = PSB.build_system(PSITestSystems, \"c_sys5_uc\")\n    model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys5_uc)\n    mock_construct_device!(model, device_model)\n    moi_tests(model, 480, 0, 240, 120, 120, true)\n    psi_checkbinvar_test(model, bin_variable_keys)\n    psi_checkobjfun_test(model, GAEVF)\n    model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys5_uc)\n    mock_construct_device!(model, device_model; add_event_model = true)\n    moi_tests(model, 480, 0, 264, 120, 120, true)\n\n    device_model = DeviceModel(ThermalStandard, ThermalBasicUnitCommitment)\n\n    c_sys14 = PSB.build_system(PSITestSystems, \"c_sys14\")\n    model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys14;)\n    mock_construct_device!(model, device_model)\n    moi_tests(model, 480, 0, 240, 120, 120, true)\n    psi_checkbinvar_test(model, bin_variable_keys)\n    psi_checkobjfun_test(model, GQEVF)\n    model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys14;)\n    mock_construct_device!(model, device_model; add_event_model = true)\n    moi_tests(model, 480, 0, 264, 120, 120, true)\nend\n\n@testset \"Thermal Basic UC With AC - PF\" begin\n    bin_variable_keys = [\n        PSI.VariableKey(OnVariable, PSY.ThermalStandard),\n        PSI.VariableKey(StartVariable, PSY.ThermalStandard),\n        PSI.VariableKey(StopVariable, PSY.ThermalStandard),\n    ]\n    device_model = DeviceModel(ThermalStandard, ThermalBasicUnitCommitment)\n\n    c_sys5_uc = PSB.build_system(PSITestSystems, \"c_sys5_uc\")\n    model = DecisionModel(MockOperationProblem, ACPPowerModel, c_sys5_uc)\n    mock_construct_device!(model, device_model)\n    moi_tests(model, 600, 0, 360, 240, 120, true)\n    psi_checkbinvar_test(model, bin_variable_keys)\n    psi_checkobjfun_test(model, GAEVF)\n    model = DecisionModel(MockOperationProblem, ACPPowerModel, c_sys5_uc)\n    mock_construct_device!(model, device_model; add_event_model = true)\n    moi_tests(model, 600, 0, 384, 240, 120, true, 24)\n\n    device_model = DeviceModel(ThermalStandard, ThermalBasicUnitCommitment)\n\n    c_sys14 = PSB.build_system(PSITestSystems, \"c_sys14\")\n    model = DecisionModel(MockOperationProblem, ACPPowerModel, c_sys14;)\n    mock_construct_device!(model, device_model)\n    moi_tests(model, 600, 0, 360, 240, 120, true)\n    psi_checkbinvar_test(model, bin_variable_keys)\n    psi_checkobjfun_test(model, GQEVF)\n    model = DecisionModel(MockOperationProblem, ACPPowerModel, c_sys14;)\n    mock_construct_device!(model, device_model; add_event_model = true)\n    moi_tests(model, 600, 0, 384, 240, 120, true, 24)\nend\n\n@testset \"Thermal MultiStart Basic UC With DC - PF\" begin\n    bin_variable_keys = [\n        PSI.VariableKey(OnVariable, PSY.ThermalMultiStart),\n        PSI.VariableKey(StartVariable, PSY.ThermalMultiStart),\n        PSI.VariableKey(StopVariable, PSY.ThermalMultiStart),\n    ]\n    device_model = DeviceModel(ThermalMultiStart, ThermalBasicUnitCommitment)\n\n    c_sys5_uc = PSB.build_system(PSITestSystems, \"c_sys5_pglib\")\n    model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys5_uc;)\n    mock_construct_device!(model, device_model)\n    moi_tests(model, 384, 0, 96, 48, 144, true)\n    psi_checkbinvar_test(model, bin_variable_keys)\n    psi_checkobjfun_test(model, GAEVF)\n    model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys5_uc;)\n    mock_construct_device!(model, device_model; add_event_model = true)\n    moi_tests(model, 384, 0, 120, 48, 144, true)\nend\n\n@testset \"Thermal MultiStart Basic UC With AC - PF\" begin\n    bin_variable_keys = [\n        PSI.VariableKey(OnVariable, PSY.ThermalMultiStart),\n        PSI.VariableKey(StartVariable, PSY.ThermalMultiStart),\n        PSI.VariableKey(StopVariable, PSY.ThermalMultiStart),\n    ]\n    device_model = DeviceModel(ThermalMultiStart, ThermalBasicUnitCommitment)\n\n    c_sys5_uc = PSB.build_system(PSITestSystems, \"c_sys5_pglib\")\n    model = DecisionModel(MockOperationProblem, ACPPowerModel, c_sys5_uc;)\n    mock_construct_device!(model, device_model)\n    moi_tests(model, 432, 0, 144, 96, 144, true)\n    psi_checkbinvar_test(model, bin_variable_keys)\n    psi_checkobjfun_test(model, GAEVF)\n    model = DecisionModel(MockOperationProblem, ACPPowerModel, c_sys5_uc;)\n    mock_construct_device!(model, device_model; add_event_model = true)\n    moi_tests(model, 432, 0, 168, 96, 144, true, 24)\nend\n\n################################### Basic Dispatch tests ###################################\n@testset \"ThermalStandard with ThermalBasicDispatch With DC - PF\" begin\n    device_model = DeviceModel(ThermalStandard, ThermalBasicDispatch)\n    c_sys5 = PSB.build_system(PSITestSystems, \"c_sys5\")\n    model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys5)\n    mock_construct_device!(model, device_model)\n    moi_tests(model, 120, 0, 120, 120, 0, false)\n    psi_checkobjfun_test(model, GAEVF)\n\n    c_sys14 = PSB.build_system(PSITestSystems, \"c_sys14\")\n    model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys14)\n    mock_construct_device!(model, device_model)\n    moi_tests(model, 120, 0, 120, 120, 0, false)\n    psi_checkobjfun_test(model, GQEVF)\nend\n\n@testset \"ThermalStandard  with ThermalBasicDispatch With AC - PF\" begin\n    device_model = DeviceModel(ThermalStandard, ThermalBasicDispatch)\n    c_sys5 = PSB.build_system(PSITestSystems, \"c_sys5\")\n    model = DecisionModel(MockOperationProblem, ACPPowerModel, c_sys5)\n    mock_construct_device!(model, device_model)\n    moi_tests(model, 240, 0, 240, 240, 0, false)\n    psi_checkobjfun_test(model, GAEVF)\n    model = DecisionModel(MockOperationProblem, ACPPowerModel, c_sys5)\n    mock_construct_device!(model, device_model; add_event_model = true)\n    moi_tests(model, 240, 0, 264, 240, 0, false, 24)\n\n    device_model = DeviceModel(ThermalStandard, ThermalBasicDispatch)\n    c_sys14 = PSB.build_system(PSITestSystems, \"c_sys14\")\n    model = DecisionModel(MockOperationProblem, ACPPowerModel, c_sys14;)\n    mock_construct_device!(model, device_model)\n    moi_tests(model, 240, 0, 240, 240, 0, false)\n    psi_checkobjfun_test(model, GQEVF)\n    model = DecisionModel(MockOperationProblem, ACPPowerModel, c_sys14;)\n    mock_construct_device!(model, device_model; add_event_model = true)\n    moi_tests(model, 240, 0, 264, 240, 0, false, 24)\nend\n\n# This Formulation is currently broken\n@testset \"ThermalMultiStart with ThermalBasicDispatch With DC - PF\" begin\n    device_model = DeviceModel(ThermalMultiStart, ThermalBasicDispatch)\n    c_sys5 = PSB.build_system(PSITestSystems, \"c_sys5_pglib\")\n    model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys5)\n    mock_construct_device!(model, device_model)\n    moi_tests(model, 240, 0, 48, 48, 96, false)\n    psi_checkobjfun_test(model, GAEVF)\n    model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys5)\n    mock_construct_device!(model, device_model; add_event_model = true)\n    moi_tests(model, 240, 0, 72, 48, 96, false)\nend\n\n@testset \"ThermalMultiStart with ThermalBasicDispatch With AC - PF\" begin\n    device_model = DeviceModel(ThermalMultiStart, ThermalBasicDispatch)\n    c_sys5 = PSB.build_system(PSITestSystems, \"c_sys5_pglib\")\n    model = DecisionModel(MockOperationProblem, ACPPowerModel, c_sys5)\n    mock_construct_device!(model, device_model)\n    moi_tests(model, 288, 0, 96, 96, 96, false)\n    psi_checkobjfun_test(model, GAEVF)\n    model = DecisionModel(MockOperationProblem, ACPPowerModel, c_sys5)\n    mock_construct_device!(model, device_model; add_event_model = true)\n    moi_tests(model, 288, 0, 120, 96, 96, false, 24)\nend\n\n################################### No Minimum Dispatch tests ##############################\n@testset \"Thermal Dispatch NoMin With DC - PF\" begin\n    device_model = DeviceModel(ThermalStandard, ThermalDispatchNoMin)\n    c_sys5 = PSB.build_system(PSITestSystems, \"c_sys5\")\n    model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys5)\n    mock_construct_device!(model, device_model)\n    moi_tests(model, 120, 0, 120, 120, 0, false)\n    key = PSI.ConstraintKey(ActivePowerVariableLimitsConstraint, ThermalStandard, \"lb\")\n    moi_lbvalue_test(model, key, 0.0)\n    psi_checkobjfun_test(model, GAEVF)\n    model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys5)\n    mock_construct_device!(model, device_model; add_event_model = true)\n    moi_tests(model, 120, 0, 144, 120, 0, false)\n\n    device_model = DeviceModel(ThermalStandard, ThermalDispatchNoMin)\n    c_sys14 = PSB.build_system(PSITestSystems, \"c_sys14\")\n    model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys14)\n    mock_construct_device!(model, device_model)\n    moi_tests(model, 120, 0, 120, 120, 0, false)\n    key = PSI.ConstraintKey(ActivePowerVariableLimitsConstraint, ThermalStandard, \"lb\")\n    moi_lbvalue_test(model, key, 0.0)\n    psi_checkobjfun_test(model, GQEVF)\n    model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys14)\n    mock_construct_device!(model, device_model; add_event_model = true)\n    moi_tests(model, 120, 0, 144, 120, 0, false)\nend\n\n@testset \"Thermal Dispatch NoMin With AC - PF\" begin\n    device_model = DeviceModel(ThermalStandard, ThermalDispatchNoMin)\n    c_sys5 = PSB.build_system(PSITestSystems, \"c_sys5\")\n    model = DecisionModel(MockOperationProblem, ACPPowerModel, c_sys5)\n    mock_construct_device!(model, device_model)\n    moi_tests(model, 240, 0, 240, 240, 0, false)\n    key = PSI.ConstraintKey(ActivePowerVariableLimitsConstraint, ThermalStandard, \"lb\")\n    moi_lbvalue_test(model, key, 0.0)\n    psi_checkobjfun_test(model, GAEVF)\n    model = DecisionModel(MockOperationProblem, ACPPowerModel, c_sys5)\n    mock_construct_device!(model, device_model; add_event_model = true)\n    moi_tests(model, 240, 0, 264, 240, 0, false, 24)\n\n    device_model = DeviceModel(ThermalStandard, ThermalDispatchNoMin)\n    c_sys14 = PSB.build_system(PSITestSystems, \"c_sys14\")\n    model = DecisionModel(MockOperationProblem, ACPPowerModel, c_sys14;)\n    mock_construct_device!(model, device_model)\n    moi_tests(model, 240, 0, 240, 240, 0, false)\n    key = PSI.ConstraintKey(ActivePowerVariableLimitsConstraint, ThermalStandard, \"lb\")\n    moi_lbvalue_test(model, key, 0.0)\n    psi_checkobjfun_test(model, GQEVF)\n    model = DecisionModel(MockOperationProblem, ACPPowerModel, c_sys14;)\n    mock_construct_device!(model, device_model; add_event_model = true)\n    moi_tests(model, 240, 0, 264, 240, 0, false, 24)\nend\n\n@testset \"Thermal Dispatch NoMin With DC - PF\" begin\n    device_model = DeviceModel(ThermalMultiStart, ThermalDispatchNoMin)\n    c_sys5 = PSB.build_system(PSITestSystems, \"c_sys5_pglib\")\n    model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys5)\n    @test_throws IS.ConflictingInputsError mock_construct_device!(model, device_model)\nend\n\n@testset \"ThermalMultiStart Dispatch NoMin With AC - PF\" begin\n    device_model = DeviceModel(ThermalMultiStart, ThermalDispatchNoMin)\n    c_sys5 = PSB.build_system(PSITestSystems, \"c_sys5_pglib\")\n    model = DecisionModel(MockOperationProblem, ACPPowerModel, c_sys5;)\n    @test_throws IS.ConflictingInputsError mock_construct_device!(model, device_model)\nend\n\n@testset \"Operation Model ThermalDispatchNoMin - and PWL Non Convex\" begin\n    c_sys5_pwl_ed_nonconvex = PSB.build_system(PSITestSystems, \"c_sys5_pwl_ed_nonconvex\")\n    template = get_thermal_dispatch_template_network()\n    set_device_model!(template, DeviceModel(ThermalStandard, ThermalDispatchNoMin))\n    model = DecisionModel(\n        MockOperationProblem,\n        CopperPlatePowerModel,\n        c_sys5_pwl_ed_nonconvex;\n        export_pwl_vars = true,\n        initialize_model = false,\n    )\n    @test_throws IS.InvalidValue mock_construct_device!(\n        model,\n        DeviceModel(ThermalStandard, ThermalDispatchNoMin),\n    )\nend\n\n################################## Ramp Limited Testing ##################################\n@testset \"ThermalStandard with ThermalStandardDispatch With DC - PF\" begin\n    constraint_keys = [\n        PSI.ConstraintKey(RampConstraint, PSY.ThermalStandard, \"up\"),\n        PSI.ConstraintKey(RampConstraint, PSY.ThermalStandard, \"dn\"),\n    ]\n    device_model = DeviceModel(ThermalStandard, ThermalStandardDispatch)\n    c_sys5_uc = PSB.build_system(PSITestSystems, \"c_sys5_uc\")\n    model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys5_uc;)\n    mock_construct_device!(model, device_model)\n    moi_tests(model, 120, 0, 216, 120, 0, false)\n    psi_constraint_test(model, constraint_keys)\n    psi_checkobjfun_test(model, GAEVF)\n    model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys5_uc;)\n    mock_construct_device!(model, device_model; add_event_model = true)\n    moi_tests(model, 120, 0, 240, 120, 0, false)\n\n    device_model = DeviceModel(ThermalStandard, ThermalStandardDispatch)\n    c_sys14 = PSB.build_system(PSITestSystems, \"c_sys14\")\n    model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys14;)\n    mock_construct_device!(model, device_model)\n    moi_tests(model, 120, 0, 120, 120, 0, false)\n    psi_checkobjfun_test(model, GQEVF)\n    model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys14;)\n    mock_construct_device!(model, device_model; add_event_model = true)\n    moi_tests(model, 120, 0, 144, 120, 0, false)\nend\n\n@testset \"ThermalStandard with ThermalStandardDispatch With AC - PF\" begin\n    constraint_keys = [\n        PSI.ConstraintKey(RampConstraint, PSY.ThermalStandard, \"up\"),\n        PSI.ConstraintKey(RampConstraint, PSY.ThermalStandard, \"dn\"),\n    ]\n    device_model = DeviceModel(ThermalStandard, ThermalStandardDispatch)\n    c_sys5_uc = PSB.build_system(PSITestSystems, \"c_sys5_uc\")\n    model = DecisionModel(MockOperationProblem, ACPPowerModel, c_sys5_uc;)\n    mock_construct_device!(model, device_model)\n    moi_tests(model, 240, 0, 336, 240, 0, false)\n    psi_constraint_test(model, constraint_keys)\n    psi_checkobjfun_test(model, GAEVF)\n    model = DecisionModel(MockOperationProblem, ACPPowerModel, c_sys5_uc;)\n    mock_construct_device!(model, device_model; add_event_model = true)\n    moi_tests(model, 240, 0, 360, 240, 0, false, 24)\n\n    device_model = DeviceModel(ThermalStandard, ThermalStandardDispatch)\n    c_sys14 = PSB.build_system(PSITestSystems, \"c_sys14\")\n    model = DecisionModel(MockOperationProblem, ACPPowerModel, c_sys14;)\n    mock_construct_device!(model, device_model)\n    moi_tests(model, 240, 0, 240, 240, 0, false)\n    psi_checkobjfun_test(model, GQEVF)\n    model = DecisionModel(MockOperationProblem, ACPPowerModel, c_sys14;)\n    mock_construct_device!(model, device_model; add_event_model = true)\n    moi_tests(model, 240, 0, 264, 240, 0, false, 24)\nend\n\n@testset \"ThermalMultiStart with ThermalStandardDispatch With DC - PF\" begin\n    constraint_keys = [\n        PSI.ConstraintKey(RampConstraint, PSY.ThermalMultiStart, \"up\"),\n        PSI.ConstraintKey(RampConstraint, PSY.ThermalMultiStart, \"dn\"),\n    ]\n    device_model = DeviceModel(ThermalMultiStart, ThermalStandardDispatch)\n    c_sys5_uc = PSB.build_system(PSITestSystems, \"c_sys5_pglib\")\n    model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys5_uc;)\n    mock_construct_device!(model, device_model)\n    moi_tests(model, 240, 0, 144, 48, 96, false)\n    psi_constraint_test(model, constraint_keys)\n    psi_checkobjfun_test(model, GAEVF)\n    model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys5_uc;)\n    mock_construct_device!(model, device_model; add_event_model = true)\n    moi_tests(model, 240, 0, 168, 48, 96, false)\nend\n\n@testset \"ThermalMultiStart with ThermalStandardDispatch With AC - PF\" begin\n    constraint_keys = [\n        PSI.ConstraintKey(RampConstraint, PSY.ThermalMultiStart, \"up\"),\n        PSI.ConstraintKey(RampConstraint, PSY.ThermalMultiStart, \"dn\"),\n    ]\n    device_model = DeviceModel(ThermalMultiStart, ThermalStandardDispatch)\n    c_sys5_uc = PSB.build_system(PSITestSystems, \"c_sys5_pglib\")\n    model = DecisionModel(MockOperationProblem, ACPPowerModel, c_sys5_uc;)\n    mock_construct_device!(model, device_model)\n    moi_tests(model, 288, 0, 192, 96, 96, false)\n    psi_constraint_test(model, constraint_keys)\n    psi_checkobjfun_test(model, GAEVF)\n    model = DecisionModel(MockOperationProblem, ACPPowerModel, c_sys5_uc;)\n    mock_construct_device!(model, device_model; add_event_model = true)\n    moi_tests(model, 288, 0, 216, 96, 96, false, 24)\nend\n\n################################### ThermalMultiStart Testing ##############################\n\n@testset \"Thermal MultiStart with MultiStart UC and DC - PF\" begin\n    constraint_keys = [\n        PSI.ConstraintKey(ActiveRangeICConstraint, PSY.ThermalMultiStart),\n        PSI.ConstraintKey(StartTypeConstraint, PSY.ThermalMultiStart),\n        PSI.ConstraintKey(\n            StartupTimeLimitTemperatureConstraint,\n            PSY.ThermalMultiStart,\n            \"warm\",\n        ),\n        PSI.ConstraintKey(\n            StartupTimeLimitTemperatureConstraint,\n            PSY.ThermalMultiStart,\n            \"hot\",\n        ),\n        PSI.ConstraintKey(\n            StartupInitialConditionConstraint,\n            PSY.ThermalMultiStart,\n            \"lb\",\n        ),\n        PSI.ConstraintKey(\n            StartupInitialConditionConstraint,\n            PSY.ThermalMultiStart,\n            \"ub\",\n        ),\n    ]\n    device_model = DeviceModel(PSY.ThermalMultiStart, PSI.ThermalMultiStartUnitCommitment)\n    c_sys5_pglib = PSB.build_system(PSITestSystems, \"c_sys5_pglib\")\n    model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys5_pglib;)\n    mock_construct_device!(model, device_model)\n    moi_tests(model, 528, 0, 282, 108, 192, true)\n    psi_constraint_test(model, constraint_keys)\n    psi_checkobjfun_test(model, GAEVF)\n    model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys5_pglib;)\n    mock_construct_device!(model, device_model; add_event_model = true)\n    moi_tests(model, 528, 0, 306, 108, 192, true)\nend\n\n@testset \"Thermal MultiStart with MultiStart UC and AC - PF\" begin\n    constraint_keys = [\n        PSI.ConstraintKey(ActiveRangeICConstraint, PSY.ThermalMultiStart),\n        PSI.ConstraintKey(StartTypeConstraint, PSY.ThermalMultiStart),\n        PSI.ConstraintKey(\n            StartupTimeLimitTemperatureConstraint,\n            PSY.ThermalMultiStart,\n            \"warm\",\n        ),\n        PSI.ConstraintKey(\n            StartupTimeLimitTemperatureConstraint,\n            PSY.ThermalMultiStart,\n            \"hot\",\n        ),\n        PSI.ConstraintKey(\n            StartupInitialConditionConstraint,\n            PSY.ThermalMultiStart,\n            \"lb\",\n        ),\n        PSI.ConstraintKey(\n            StartupInitialConditionConstraint,\n            PSY.ThermalMultiStart,\n            \"ub\",\n        ),\n    ]\n    device_model = DeviceModel(PSY.ThermalMultiStart, PSI.ThermalMultiStartUnitCommitment)\n    c_sys5_pglib = PSB.build_system(PSITestSystems, \"c_sys5_pglib\")\n    model = DecisionModel(MockOperationProblem, ACPPowerModel, c_sys5_pglib;)\n    mock_construct_device!(model, device_model)\n    moi_tests(model, 576, 0, 330, 156, 192, true)\n    psi_constraint_test(model, constraint_keys)\n    psi_checkobjfun_test(model, GAEVF)\n    model = DecisionModel(MockOperationProblem, ACPPowerModel, c_sys5_pglib;)\n    mock_construct_device!(model, device_model; add_event_model = true)\n    moi_tests(model, 576, 0, 354, 156, 192, true, 24)\nend\n\n################################ Thermal Compact UC Testing ################################\n@testset \"Thermal Standard with Compact UC and DC - PF\" begin\n    device_model = DeviceModel(PSY.ThermalStandard, PSI.ThermalCompactUnitCommitment)\n    c_sys5 = PSB.build_system(PSITestSystems, \"c_sys5\")\n    model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys5)\n    mock_construct_device!(model, device_model)\n    moi_tests(model, 480, 0, 480, 120, 120, true)\n    psi_checkobjfun_test(model, GAEVF)\n    model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys5)\n    mock_construct_device!(model, device_model; add_event_model = true)\n    moi_tests(model, 480, 0, 504, 120, 120, true)\nend\n\n@testset \"Thermal MultiStart with Compact UC and DC - PF\" begin\n    device_model = DeviceModel(PSY.ThermalMultiStart, PSI.ThermalCompactUnitCommitment)\n    c_sys5_pglib = PSB.build_system(PSITestSystems, \"c_sys5_pglib\")\n    model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys5_pglib;)\n    mock_construct_device!(model, device_model)\n    moi_tests(model, 384, 0, 240, 48, 144, true)\n    psi_checkobjfun_test(model, GAEVF)\n    model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys5_pglib;)\n    mock_construct_device!(model, device_model; add_event_model = true)\n    moi_tests(model, 384, 0, 264, 48, 144, true)\nend\n\n@testset \"Thermal Standard with Compact UC and AC - PF\" begin\n    device_model = DeviceModel(PSY.ThermalStandard, PSI.ThermalCompactUnitCommitment)\n    c_sys5 = PSB.build_system(PSITestSystems, \"c_sys5\")\n    model = DecisionModel(MockOperationProblem, ACPPowerModel, c_sys5)\n    mock_construct_device!(model, device_model)\n    moi_tests(model, 600, 0, 600, 240, 120, true)\n    psi_checkobjfun_test(model, GAEVF)\n    model = DecisionModel(MockOperationProblem, ACPPowerModel, c_sys5)\n    mock_construct_device!(model, device_model; add_event_model = true)\n    moi_tests(model, 600, 0, 624, 240, 120, true, 24)\nend\n\n@testset \"Thermal MultiStart with Compact UC and AC - PF\" begin\n    device_model = DeviceModel(PSY.ThermalMultiStart, PSI.ThermalCompactUnitCommitment)\n    c_sys5_pglib = PSB.build_system(PSITestSystems, \"c_sys5_pglib\")\n    model = DecisionModel(MockOperationProblem, ACPPowerModel, c_sys5_pglib;)\n    mock_construct_device!(model, device_model)\n    moi_tests(model, 432, 0, 288, 96, 144, true)\n    psi_checkobjfun_test(model, GAEVF)\n    model = DecisionModel(MockOperationProblem, ACPPowerModel, c_sys5_pglib;)\n    mock_construct_device!(model, device_model; add_event_model = true)\n    moi_tests(model, 432, 0, 312, 96, 144, true, 24)\nend\n\n################################ Thermal Basic Compact UC Testing ################################\n@testset \"Thermal Standard with Compact UC and DC - PF\" begin\n    device_model = DeviceModel(PSY.ThermalStandard, PSI.ThermalBasicCompactUnitCommitment)\n    c_sys5 = PSB.build_system(PSITestSystems, \"c_sys5\")\n    model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys5)\n    mock_construct_device!(model, device_model)\n    moi_tests(model, 480, 0, 240, 120, 120, true)\n    psi_checkobjfun_test(model, GAEVF)\n    model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys5)\n    mock_construct_device!(model, device_model; add_event_model = true)\n    moi_tests(model, 480, 0, 264, 120, 120, true)\nend\n\n@testset \"Thermal MultiStart with Compact UC and DC - PF\" begin\n    device_model = DeviceModel(PSY.ThermalMultiStart, PSI.ThermalBasicCompactUnitCommitment)\n    c_sys5_pglib = PSB.build_system(PSITestSystems, \"c_sys5_pglib\")\n    model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys5_pglib;)\n    mock_construct_device!(model, device_model)\n    moi_tests(model, 384, 0, 96, 48, 144, true)\n    psi_checkobjfun_test(model, GAEVF)\n    model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys5_pglib;)\n    mock_construct_device!(model, device_model; add_event_model = true)\n    moi_tests(model, 384, 0, 120, 48, 144, true)\nend\n\n@testset \"Thermal Standard with Compact UC and AC - PF\" begin\n    device_model = DeviceModel(PSY.ThermalStandard, PSI.ThermalBasicCompactUnitCommitment)\n    c_sys5 = PSB.build_system(PSITestSystems, \"c_sys5\")\n    model = DecisionModel(MockOperationProblem, ACPPowerModel, c_sys5)\n    mock_construct_device!(model, device_model)\n    moi_tests(model, 600, 0, 360, 240, 120, true)\n    psi_checkobjfun_test(model, GAEVF)\n    model = DecisionModel(MockOperationProblem, ACPPowerModel, c_sys5)\n    mock_construct_device!(model, device_model; add_event_model = true)\n    moi_tests(model, 600, 0, 384, 240, 120, true, 24)\nend\n\n@testset \"Thermal MultiStart with Compact UC and AC - PF\" begin\n    device_model = DeviceModel(PSY.ThermalMultiStart, PSI.ThermalBasicCompactUnitCommitment)\n    c_sys5_pglib = PSB.build_system(PSITestSystems, \"c_sys5_pglib\")\n    model = DecisionModel(MockOperationProblem, ACPPowerModel, c_sys5_pglib;)\n    mock_construct_device!(model, device_model)\n    moi_tests(model, 432, 0, 144, 96, 144, true)\n    psi_checkobjfun_test(model, GAEVF)\n    model = DecisionModel(MockOperationProblem, ACPPowerModel, c_sys5_pglib;)\n    mock_construct_device!(model, device_model; add_event_model = true)\n    moi_tests(model, 432, 0, 168, 96, 144, true, 24)\nend\n\n############################ Thermal Compact Dispatch Testing ##############################\n@testset \"Thermal Standard with Compact Dispatch and DC - PF\" begin\n    device_model = DeviceModel(PSY.ThermalStandard, PSI.ThermalCompactDispatch)\n    c_sys5 = PSB.build_system(PSITestSystems, \"c_sys5\")\n    model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys5)\n    mock_construct_device!(model, device_model; built_for_recurrent_solves = true)\n    moi_tests(model, 245, 0, 144, 144, 0, false)\n    psi_checkobjfun_test(model, GAEVF)\n    model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys5)\n    mock_construct_device!(\n        model,\n        device_model;\n        built_for_recurrent_solves = true,\n        add_event_model = true,\n    )\n    moi_tests(model, 293, 0, 168, 144, 0, false)\nend\n\n@testset \"Thermal MultiStart with Compact Dispatch and DC - PF\" begin\n    device_model = DeviceModel(PSY.ThermalMultiStart, PSI.ThermalCompactDispatch)\n    c_sys5_pglib = PSB.build_system(PSITestSystems, \"c_sys5_pglib\")\n    model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys5_pglib)\n    mock_construct_device!(model, device_model; built_for_recurrent_solves = true)\n    moi_tests(model, 290, 0, 96, 96, 96, false)\n    psi_checkobjfun_test(model, GAEVF)\n    model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys5_pglib)\n    mock_construct_device!(\n        model,\n        device_model;\n        built_for_recurrent_solves = true,\n        add_event_model = true,\n    )\n    moi_tests(model, 338, 0, 120, 96, 96, false)\nend\n\n@testset \"Thermal Standard with Compact Dispatch and AC - PF\" begin\n    device_model = DeviceModel(PSY.ThermalStandard, PSI.ThermalCompactDispatch)\n    c_sys5 = PSB.build_system(PSITestSystems, \"c_sys5\")\n    model = DecisionModel(MockOperationProblem, ACPPowerModel, c_sys5)\n    mock_construct_device!(model, device_model; built_for_recurrent_solves = true)\n    moi_tests(model, 365, 0, 264, 264, 0, false)\n    psi_checkobjfun_test(model, GAEVF)\n    model = DecisionModel(MockOperationProblem, ACPPowerModel, c_sys5)\n    mock_construct_device!(\n        model,\n        device_model;\n        built_for_recurrent_solves = true,\n        add_event_model = true,\n    )\n    moi_tests(model, 413, 0, 288, 264, 0, false, 24)\nend\n\n@testset \"Thermal MultiStart with Compact Dispatch and AC - PF\" begin\n    device_model = DeviceModel(PSY.ThermalMultiStart, PSI.ThermalCompactDispatch)\n    c_sys5_pglib = PSB.build_system(PSITestSystems, \"c_sys5_pglib\")\n    model = DecisionModel(MockOperationProblem, ACPPowerModel, c_sys5_pglib)\n    mock_construct_device!(model, device_model; built_for_recurrent_solves = true)\n    moi_tests(model, 338, 0, 144, 144, 96, false)\n    psi_checkobjfun_test(model, GAEVF)\n    model = DecisionModel(MockOperationProblem, ACPPowerModel, c_sys5_pglib)\n    mock_construct_device!(\n        model,\n        device_model;\n        built_for_recurrent_solves = true,\n        add_event_model = true,\n    )\n    moi_tests(model, 386, 0, 168, 144, 96, false), 24\nend\n\n############################# Model validation tests #######################################\n@testset \"Solving ED with CopperPlate for testing Ramping Constraints\" begin\n    ramp_test_sys = PSB.build_system(PSITestSystems, \"c_ramp_test\")\n    template = ProblemTemplate(CopperPlatePowerModel)\n    set_device_model!(template, ThermalStandard, ThermalStandardDispatch)\n    set_device_model!(template, PowerLoad, StaticPowerLoad)\n    ED = DecisionModel(\n        EconomicDispatchProblem,\n        template,\n        ramp_test_sys;\n        optimizer = HiGHS_optimizer,\n        initialize_model = false,\n    )\n    @test build!(ED; output_dir = mktempdir(; cleanup = true)) == PSI.ModelBuildStatus.BUILT\n    moi_tests(ED, 10, 0, 20, 10, 5, false)\n    psi_checksolve_test(ED, [MOI.OPTIMAL], 11191.00)\nend\n\n# Testing Duration Constraints\n@testset \"Solving UC with CopperPlate for testing Duration Constraints\" begin\n    template = get_thermal_standard_uc_template()\n    UC = DecisionModel(\n        UnitCommitmentProblem,\n        template,\n        PSB.build_system(PSITestSystems, \"c_duration_test\");\n        optimizer = HiGHS_optimizer,\n        initialize_model = false,\n        store_variable_names = true,\n    )\n    build!(UC; output_dir = mktempdir(; cleanup = true))\n    @test build!(UC; output_dir = mktempdir(; cleanup = true)) == PSI.ModelBuildStatus.BUILT\n    moi_tests(UC, 56, 0, 56, 14, 21, true)\n    psi_checksolve_test(UC, [MOI.OPTIMAL], 8223.50)\nend\n\n@testset \"Solving UC Models with Linear Networks\" begin\n    c_sys5 = PSB.build_system(PSITestSystems, \"c_sys5\")\n    c_sys5_dc = PSB.build_system(PSITestSystems, \"c_sys5_dc\")\n    systems = [c_sys5, c_sys5_dc]\n    networks = [\n        DCPPowerModel,\n        NFAPowerModel,\n        PTDFPowerModel,\n        CopperPlatePowerModel,\n    ]\n    commitment_models = [ThermalStandardUnitCommitment, ThermalCompactUnitCommitment]\n\n    for net in networks, sys in systems, model in commitment_models\n        template = get_thermal_dispatch_template_network(\n            NetworkModel(net),\n        )\n        set_device_model!(template, ThermalStandard, model)\n        UC = DecisionModel(template, sys; optimizer = HiGHS_optimizer)\n        @test build!(UC; output_dir = mktempdir(; cleanup = true)) ==\n              PSI.ModelBuildStatus.BUILT\n        psi_checksolve_test(UC, [MOI.OPTIMAL, MOI.LOCALLY_SOLVED], 340000, 100000)\n    end\nend\n\n@testset \"Test Feedforwards to ThermalStandard with ThermalStandardDispatch\" begin\n    device_model = DeviceModel(ThermalStandard, ThermalStandardDispatch)\n    ff_sc = SemiContinuousFeedforward(;\n        component_type = ThermalStandard,\n        source = OnVariable,\n        affected_values = [ActivePowerVariable],\n    )\n\n    ff_ub = UpperBoundFeedforward(;\n        component_type = ThermalStandard,\n        source = ActivePowerVariable,\n        affected_values = [ActivePowerVariable],\n    )\n\n    PSI.attach_feedforward!(device_model, ff_sc)\n    PSI.attach_feedforward!(device_model, ff_ub)\n    c_sys5 = PSB.build_system(PSITestSystems, \"c_sys5\")\n    model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys5)\n    mock_construct_device!(model, device_model; built_for_recurrent_solves = true)\n    moi_tests(model, 365, 0, 288, 120, 0, false)\n    model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys5)\n    mock_construct_device!(\n        model,\n        device_model;\n        built_for_recurrent_solves = true,\n        add_event_model = true,\n    )\n    moi_tests(model, 413, 0, 312, 120, 0, false)\nend\n\n@testset \"Test Feedforwards to ThermalStandard with ThermalBasicDispatch\" begin\n    device_model = DeviceModel(ThermalStandard, ThermalBasicDispatch)\n    ff_sc = SemiContinuousFeedforward(;\n        component_type = ThermalStandard,\n        source = OnVariable,\n        affected_values = [ActivePowerVariable],\n    )\n\n    ff_ub = UpperBoundFeedforward(;\n        component_type = ThermalStandard,\n        source = ActivePowerVariable,\n        affected_values = [ActivePowerVariable],\n    )\n\n    PSI.attach_feedforward!(device_model, ff_sc)\n    PSI.attach_feedforward!(device_model, ff_ub)\n    c_sys5 = PSB.build_system(PSITestSystems, \"c_sys5\")\n    model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys5)\n    mock_construct_device!(model, device_model; built_for_recurrent_solves = true)\n    moi_tests(model, 360, 0, 240, 120, 0, false)\n    model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys5)\n    mock_construct_device!(\n        model,\n        device_model;\n        built_for_recurrent_solves = true,\n        add_event_model = true,\n    )\n    moi_tests(model, 408, 0, 264, 120, 0, false)\nend\n\n@testset \"Test Feedforwards to ThermalStandard with ThermalCompactDispatch\" begin\n    device_model = DeviceModel(PSY.ThermalStandard, PSI.ThermalCompactDispatch)\n    ff_sc = SemiContinuousFeedforward(;\n        component_type = ThermalStandard,\n        source = OnVariable,\n        affected_values = [PowerAboveMinimumVariable],\n    )\n\n    ff_ub = UpperBoundFeedforward(;\n        component_type = ThermalStandard,\n        source = PSI.PowerAboveMinimumVariable,\n        affected_values = [PSI.PowerAboveMinimumVariable],\n    )\n\n    PSI.attach_feedforward!(device_model, ff_sc)\n    PSI.attach_feedforward!(device_model, ff_ub)\n    c_sys5 = PSB.build_system(PSITestSystems, \"c_sys5\")\n    model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys5)\n    mock_construct_device!(model, device_model; built_for_recurrent_solves = true)\n    moi_tests(model, 365, 0, 264, 144, 0, false)\n    model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys5)\n    mock_construct_device!(\n        model,\n        device_model;\n        built_for_recurrent_solves = true,\n        add_event_model = true,\n    )\n    moi_tests(model, 413, 0, 288, 144, 0, false)\nend\n\n@testset \"Test Feedforwards to ThermalMultiStart with ThermalStandardDispatch\" begin\n    device_model = DeviceModel(ThermalMultiStart, ThermalStandardDispatch)\n    ff_sc = SemiContinuousFeedforward(;\n        component_type = ThermalMultiStart,\n        source = OnVariable,\n        affected_values = [ActivePowerVariable],\n    )\n\n    ff_ub = UpperBoundFeedforward(;\n        component_type = ThermalMultiStart,\n        source = ActivePowerVariable,\n        affected_values = [ActivePowerVariable],\n    )\n\n    PSI.attach_feedforward!(device_model, ff_sc)\n    PSI.attach_feedforward!(device_model, ff_ub)\n    c_sys5 = PSB.build_system(PSITestSystems, \"c_sys5_pglib\")\n    model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys5)\n    mock_construct_device!(model, device_model; built_for_recurrent_solves = true)\n    moi_tests(model, 338, 0, 192, 48, 96, false)\n    model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys5)\n    mock_construct_device!(\n        model,\n        device_model;\n        built_for_recurrent_solves = true,\n        add_event_model = true,\n    )\n    moi_tests(model, 386, 0, 216, 48, 96, false)\nend\n\n@testset \"Test Feedforwards to ThermalMultiStart with ThermalBasicDispatch\" begin\n    device_model = DeviceModel(ThermalMultiStart, ThermalBasicDispatch)\n    ff_sc = SemiContinuousFeedforward(;\n        component_type = ThermalMultiStart,\n        source = OnVariable,\n        affected_values = [ActivePowerVariable],\n    )\n\n    ff_ub = UpperBoundFeedforward(;\n        component_type = ThermalMultiStart,\n        source = ActivePowerVariable,\n        affected_values = [ActivePowerVariable],\n    )\n\n    PSI.attach_feedforward!(device_model, ff_sc)\n    PSI.attach_feedforward!(device_model, ff_ub)\n    c_sys5 = PSB.build_system(PSITestSystems, \"c_sys5_pglib\")\n    model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys5)\n    mock_construct_device!(model, device_model; built_for_recurrent_solves = true)\n    moi_tests(model, 336, 0, 96, 48, 96, false)\n    model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys5)\n    mock_construct_device!(\n        model,\n        device_model;\n        built_for_recurrent_solves = true,\n        add_event_model = true,\n    )\n    moi_tests(model, 384, 0, 120, 48, 96, false)\nend\n\n@testset \"Test Feedforwards to ThermalMultiStart with ThermalCompactDispatch\" begin\n    device_model = DeviceModel(PSY.ThermalMultiStart, PSI.ThermalCompactDispatch)\n    ff_sc = SemiContinuousFeedforward(;\n        component_type = ThermalMultiStart,\n        source = OnVariable,\n        affected_values = [PSI.PowerAboveMinimumVariable],\n    )\n\n    ff_ub = UpperBoundFeedforward(;\n        component_type = ThermalMultiStart,\n        source = PSI.PowerAboveMinimumVariable,\n        affected_values = [PSI.PowerAboveMinimumVariable],\n    )\n\n    PSI.attach_feedforward!(device_model, ff_sc)\n    PSI.attach_feedforward!(device_model, ff_ub)\n    c_sys5 = PSB.build_system(PSITestSystems, \"c_sys5_pglib\")\n    model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys5)\n    mock_construct_device!(model, device_model; built_for_recurrent_solves = true)\n    moi_tests(model, 338, 0, 144, 96, 96, false)\n    model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys5)\n    mock_construct_device!(\n        model,\n        device_model;\n        built_for_recurrent_solves = true,\n        add_event_model = true,\n    )\n    moi_tests(model, 386, 0, 168, 96, 96, false)\nend\n\n@testset \"Test Must Run ThermalGen\" begin\n    sys_5 = build_system(PSITestSystems, \"c_sys5_uc\")\n    template_uc =\n        ProblemTemplate(NetworkModel(CopperPlatePowerModel))\n    set_device_model!(template_uc, ThermalStandard, ThermalStandardUnitCommitment)\n    #set_device_model!(template_uc, RenewableDispatch, FixedOutput)\n    set_device_model!(template_uc, PowerLoad, StaticPowerLoad)\n    set_device_model!(template_uc, DeviceModel(Line, StaticBranchUnbounded))\n\n    # Set Must Run the most expensive one: Sundance\n    sundance = get_component(ThermalStandard, sys_5, \"Sundance\")\n    set_must_run!(sundance, true)\n    for rebuild in [true, false]\n        model = DecisionModel(\n            template_uc,\n            sys_5;\n            name = \"UC\",\n            optimizer = HiGHS_optimizer,\n            store_variable_names = true,\n            rebuild_model = rebuild,\n        )\n\n        solve!(model; output_dir = mktempdir())\n        ptdf_vars = read_variables(\n            OptimizationProblemResults(model);\n            table_format = TableFormat.WIDE,\n        )\n        power = ptdf_vars[\"ActivePowerVariable__ThermalStandard\"]\n        on = ptdf_vars[\"OnVariable__ThermalStandard\"]\n        start = ptdf_vars[\"StartVariable__ThermalStandard\"]\n        stop = ptdf_vars[\"StopVariable__ThermalStandard\"]\n        power_sundance = power[!, \"Sundance\"]\n        @test all(power_sundance .>= 1.0)\n        for v in [on, start, stop]\n            @test \"Sundance\" ∉ names(v)\n        end\n    end\nend\n\n@testset \"Thermal with max_active_power time series\" begin\n    device_model = DeviceModel(\n        ThermalStandard,\n        ThermalStandardUnitCommitment;\n        time_series_names = Dict(ActivePowerTimeSeriesParameter => \"max_active_power\"))\n    c_sys5 = PSB.build_system(PSITestSystems, \"c_sys5\")\n\n    derate_data = SortedDict{Dates.DateTime, TimeSeries.TimeArray}()\n    data_ts = collect(\n        DateTime(\"1/1/2024  0:00:00\", \"d/m/y  H:M:S\"):Hour(1):DateTime(\n            \"1/1/2024  23:00:00\",\n            \"d/m/y  H:M:S\",\n        ),\n    )\n    for t in 1:2\n        ini_time = data_ts[1] + Day(t - 1)\n        derate_data[ini_time] =\n            TimeArray(data_ts + Day(t - 1), fill!(Vector{Float64}(undef, 24), 0.8))\n    end\n    solitude = get_component(ThermalStandard, c_sys5, \"Solitude\")\n    PSY.add_time_series!(\n        c_sys5,\n        solitude,\n        PSY.Deterministic(\"max_active_power\", derate_data),\n    )\n\n    model = DecisionModel(\n        MockOperationProblem,\n        DCPPowerModel,\n        c_sys5)\n\n    mock_construct_device!(model, device_model)\n    moi_tests(model, 480, 0, 504, 120, 120, true)\n    key = PSI.ConstraintKey(\n        ActivePowerVariableTimeSeriesLimitsConstraint,\n        ThermalStandard,\n        \"ub\",\n    )\n    constraint = PSI.get_constraint(PSI.get_optimization_container(model), key)\n    ub_value = get_max_active_power(solitude) * 0.8\n    for ix in eachindex(constraint)\n        @test JuMP.normalized_rhs(constraint[ix]) == ub_value\n    end\n    psi_checkobjfun_test(model, GAEVF)\n    model = DecisionModel(\n        MockOperationProblem,\n        DCPPowerModel,\n        c_sys5)\n\n    mock_construct_device!(model, device_model; add_event_model = true)\n    moi_tests(model, 480, 0, 528, 120, 120, true)\nend\n\n@testset \"Thermal with fuel cost time series\" begin\n    sys = PSB.build_system(PSITestSystems, \"c_sys5_re_fuel_cost\")\n\n    template = ProblemTemplate(\n        NetworkModel(\n            CopperPlatePowerModel;\n            duals = [CopperPlateBalanceConstraint],\n        ),\n    )\n\n    set_device_model!(template, ThermalStandard, ThermalDispatchNoMin)\n    set_device_model!(template, PowerLoad, StaticPowerLoad)\n    set_device_model!(template, RenewableDispatch, RenewableFullDispatch)\n\n    model = DecisionModel(\n        template,\n        sys;\n        name = \"UC\",\n        optimizer = HiGHS_optimizer,\n        store_variable_names = true,\n        optimizer_solve_log_print = false,\n    )\n    models = SimulationModels(;\n        decision_models = [\n            model,\n        ],\n    )\n    sequence = SimulationSequence(;\n        models = models,\n        feedforwards = Dict(\n        ),\n        ini_cond_chronology = InterProblemChronology(),\n    )\n\n    sim = Simulation(;\n        name = \"compact_sim\",\n        steps = 2,\n        models = models,\n        sequence = sequence,\n        initial_time = TIME1,\n        simulation_folder = mktempdir(),\n    )\n\n    build!(sim; console_level = Logging.Error)\n    moi_tests(model, 432, 0, 192, 120, 72, false)\n    execute!(sim)\n\n    sim_res = SimulationResults(sim)\n    res_uc = get_decision_problem_results(sim_res, \"UC\")\n\n    # Test time series <-> parameter correspondence\n    fc_uc = read_parameter(\n        res_uc,\n        PSI.FuelCostParameter,\n        PSY.ThermalStandard;\n        table_format = TableFormat.WIDE,\n    )\n    for (step_dt, step_df) in pairs(fc_uc)\n        for gen_name in names(DataFrames.select(step_df, Not(:DateTime)))\n            fc_comp = get_fuel_cost(\n                get_component(ThermalStandard, sys, gen_name);\n                start_time = step_dt,\n            )\n            @test all(step_df[!, :DateTime] .== TimeSeries.timestamp(fc_comp))\n            @test all(isapprox.(step_df[!, gen_name], TimeSeries.values(fc_comp)))\n        end\n    end\n\n    # Test effect on decision\n    th_uc = read_realized_variable(\n        res_uc,\n        \"ActivePowerVariable__ThermalStandard\";\n        table_format = TableFormat.WIDE,\n    )\n    p_brighton = th_uc[!, \"Brighton\"]\n    p_solitude = th_uc[!, \"Solitude\"]\n\n    @test sum(p_brighton[1:24]) < 50.0 # Barely used when expensive\n    @test sum(p_brighton[25:48]) > 5000.0 # Used a lot when cheap\n    @test sum(p_solitude[1:24]) > 5000.0 # Used a lot when cheap\n    @test sum(p_solitude[25:48]) < 50.0 # Barely used when expensive\nend\n\n@testset \"Thermal with fuel cost time series with Quadratic and PWL\" begin\n    sys = PSB.build_system(PSITestSystems, \"c_sys5_re_fuel_cost\")\n\n    template = ProblemTemplate(\n        NetworkModel(\n            CopperPlatePowerModel;\n            duals = [CopperPlateBalanceConstraint],\n        ),\n    )\n\n    solitude = get_component(ThermalStandard, sys, \"Solitude\")\n    op_cost = get_operation_cost(solitude)\n    ts = deepcopy(get_time_series(Deterministic, solitude, \"fuel_cost\"))\n    remove_time_series!(sys, Deterministic, solitude, \"fuel_cost\")\n    quad_curve = QuadraticCurve(0.05, 1.0, 0.0)\n    new_th_cost = ThermalGenerationCost(;\n        variable = FuelCurve(;\n            value_curve = quad_curve,\n            fuel_cost = 1.0,\n        ),\n        fixed = op_cost.fixed,\n        start_up = op_cost.start_up,\n        shut_down = op_cost.shut_down,\n    )\n\n    set_operation_cost!(solitude, new_th_cost)\n    add_time_series!(\n        sys,\n        solitude,\n        ts,\n    )\n\n    # There is no free MIQP solver, we need to use ThermalDisptchNoMin for testing\n    set_device_model!(template, ThermalStandard, ThermalDispatchNoMin)\n    set_device_model!(template, PowerLoad, StaticPowerLoad)\n    set_device_model!(template, RenewableDispatch, RenewableFullDispatch)\n\n    model = DecisionModel(\n        template,\n        sys;\n        name = \"UC\",\n        optimizer = ipopt_optimizer,\n        store_variable_names = true,\n        optimizer_solve_log_print = false,\n    )\n    @test build!(model; output_dir = mktempdir(; cleanup = true)) ==\n          PSI.ModelBuildStatus.BUILT\n    solve!(model)\n    moi_tests(model, 288, 0, 192, 120, 72, false)\n    container = PSI.get_optimization_container(model)\n    @test isa(\n        PSI.get_invariant_terms(PSI.get_objective_expression(container)),\n        JuMP.QuadExpr,\n    )\nend\n\n@testset \"Thermal UC With Slack on Ramps\" begin\n    bin_variable_keys = [\n        PSI.VariableKey(OnVariable, PSY.ThermalStandard),\n        PSI.VariableKey(StartVariable, PSY.ThermalStandard),\n        PSI.VariableKey(StopVariable, PSY.ThermalStandard),\n    ]\n\n    uc_constraint_keys = [\n        PSI.ConstraintKey(RampConstraint, PSY.ThermalStandard, \"up\"),\n        PSI.ConstraintKey(RampConstraint, PSY.ThermalStandard, \"dn\"),\n        PSI.ConstraintKey(DurationConstraint, PSY.ThermalStandard, \"up\"),\n        PSI.ConstraintKey(DurationConstraint, PSY.ThermalStandard, \"dn\"),\n    ]\n\n    aux_variables_keys = [\n        PSI.AuxVarKey(PSI.TimeDurationOff, ThermalStandard),\n        PSI.AuxVarKey(PSI.TimeDurationOn, ThermalStandard),\n    ]\n    # Unit Commitment #\n    device_model =\n        DeviceModel(ThermalStandard, ThermalStandardUnitCommitment; use_slacks = true)\n\n    c_sys5_uc = PSB.build_system(PSITestSystems, \"c_sys5_uc\")\n    model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys5_uc)\n    mock_construct_device!(model, device_model)\n    moi_tests(model, 720, 0, 480, 120, 120, true)\n    psi_constraint_test(model, uc_constraint_keys)\n    psi_checkbinvar_test(model, bin_variable_keys)\n    psi_checkobjfun_test(model, GAEVF)\n    psi_aux_variable_test(model, aux_variables_keys)\n    model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys5_uc)\n    mock_construct_device!(model, device_model; add_event_model = true)\n    moi_tests(model, 720, 0, 504, 120, 120, true)\n\n    device_model =\n        DeviceModel(ThermalStandard, ThermalStandardUnitCommitment; use_slacks = true)\n\n    c_sys14 = PSB.build_system(PSITestSystems, \"c_sys14\")\n    model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys14)\n    mock_construct_device!(model, device_model)\n    moi_tests(model, 720, 0, 240, 120, 120, true)\n    psi_checkbinvar_test(model, bin_variable_keys)\n    psi_checkobjfun_test(model, GQEVF)\n    model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys14)\n    mock_construct_device!(model, device_model; add_event_model = true)\n    moi_tests(model, 720, 0, 264, 120, 120, true)\n\n    # Dispatch #\n    device_model =\n        DeviceModel(ThermalStandard, ThermalStandardDispatch; use_slacks = true)\n    uc_constraint_keys = [\n        PSI.ConstraintKey(RampConstraint, PSY.ThermalStandard, \"up\"),\n        PSI.ConstraintKey(RampConstraint, PSY.ThermalStandard, \"dn\"),\n    ]\n\n    c_sys5_uc = PSB.build_system(PSITestSystems, \"c_sys5_uc\")\n    model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys5_uc)\n    mock_construct_device!(model, device_model)\n    moi_tests(model, 360, 0, 216, 120, 0, false)\n    psi_constraint_test(model, uc_constraint_keys)\n    psi_checkobjfun_test(model, GAEVF)\n    model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys5_uc)\n    mock_construct_device!(model, device_model; add_event_model = true)\n    moi_tests(model, 360, 0, 240, 120, 0, false)\n\n    device_model =\n        DeviceModel(ThermalStandard, ThermalStandardDispatch; use_slacks = true)\n    c_sys14 = PSB.build_system(PSITestSystems, \"c_sys14\")\n    model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys14)\n    mock_construct_device!(model, device_model)\n    moi_tests(model, 360, 0, 120, 120, 0, false)\n    psi_checkobjfun_test(model, GQEVF)\n    model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys14)\n    mock_construct_device!(model, device_model; add_event_model = true)\n    moi_tests(model, 360, 0, 144, 120, 0, false)\nend\n\n@testset \"ThermalDispatchNoMin with PWL Costs\" begin\n    sys = build_system(PSISystems, \"modified_RTS_GMLC_DA_sys\")\n\n    template = ProblemTemplate(NetworkModel(PTDFPowerModel))\n    set_device_model!(template, ThermalStandard, ThermalDispatchNoMin)\n    set_device_model!(template, Line, StaticBranchBounds)\n    set_device_model!(template, TapTransformer, StaticBranchBounds)\n    set_device_model!(template, Transformer2W, StaticBranchBounds)\n    set_device_model!(template, PowerLoad, StaticPowerLoad)\n\n    solver = HiGHS_optimizer\n    problem = DecisionModel(template, sys;\n        optimizer = solver,\n        horizon = Hour(1),\n        optimizer_solve_log_print = true,\n        calculate_conflict = true,\n        store_variable_names = true,\n        detailed_optimizer_stats = false,\n    )\n\n    build!(problem; output_dir = mktempdir())\n\n    solve!(problem)\n\n    res = OptimizationProblemResults(problem)\n\n    # Test that plant 101_STEAM_3 (using max power) have proper cost expression\n    cost = read_expression(\n        res,\n        \"ProductionCostExpression__ThermalStandard\";\n        table_format = TableFormat.WIDE,\n    )\n    p_th = read_variable(\n        res,\n        \"ActivePowerVariable__ThermalStandard\";\n        table_format = TableFormat.WIDE,\n    )\n    steam3 = get_component(ThermalStandard, sys, \"101_STEAM_3\")\n    val_curve = PSY.get_value_curve(PSY.get_variable(PSY.get_operation_cost(steam3)))\n    io_curve = InputOutputCurve(val_curve)\n    fuel_cost = PSY.get_fuel_cost(steam3)\n    x_last = last(io_curve.function_data.points).x\n    y_last = last(io_curve.function_data.points).y * fuel_cost\n    p_steam3 = p_th[!, \"101_STEAM_3\"]\n    cost_steam3 = cost[!, \"101_STEAM_3\"]\n    @test isapprox(p_steam3[1], x_last) # max\n    @test isapprox(cost_steam3[1], y_last) # last cost\nend\n"
  },
  {
    "path": "test/test_events.jl",
    "content": "### HOURLY DATA ###\n#Note: if using basic for ed, emulator fails at timestep  after outage due to OutageConstraint_ub\n@testset \"Hourly; uc basic; ed nomin; no ff\" begin\n    res = run_events_simulation(;\n        sys_emulator = build_system(PSITestSystems, \"c_sys5_events\"),\n        networks = repeat([PSI.CopperPlatePowerModel], 3),\n        optimizers = repeat([HiGHS_optimizer_small_gap], 3),\n        outage_time = DateTime(\"2024-01-01T18:00:00\"),\n        outage_length = 3.0,\n        uc_formulation = \"basic\",\n        ed_formulation = \"nomin\",\n        feedforward = false,\n        in_memory = true,\n    )\n    test_event_results(;\n        res = res,\n        outage_time = DateTime(\"2024-01-01T18:00:00\"),\n        outage_length = 3.0,\n        expected_power_recovery = DateTime(\"2024-01-01T22:00:00\"),\n        expected_on_variable_recovery = DateTime(\"2024-01-01T22:00:00\"),\n    )\n    #Test no ramping constraint in D2 model results\n    d2 = get_decision_problem_results(res, \"D2\")\n    p_d2 =\n        read_realized_variables(d2; table_format = TableFormat.WIDE)[\"ActivePowerVariable__ThermalStandard\"]\n    p_recover_ix = indexin([DateTime(\"2024-01-01T22:00:00\")], p_d2[!, :DateTime])[1]\n    @test p_d2[p_recover_ix, \"Alta\"] == 40.0\nend\n\n#This passes with nomin or basic dispatch\n@testset \"Hourly; uc basic; ed basic; ff\" begin\n    res = run_events_simulation(;\n        sys_emulator = build_system(PSITestSystems, \"c_sys5_events\"),\n        networks = repeat([PSI.CopperPlatePowerModel], 3),\n        optimizers = repeat([HiGHS_optimizer_small_gap], 3),\n        outage_time = DateTime(\"2024-01-01T18:00:00\"),\n        outage_length = 3.0,\n        uc_formulation = \"basic\",\n        ed_formulation = \"basic\",  #should also pass with nomin\n        feedforward = true,\n        in_memory = true,\n    )\n    test_event_results(;\n        res = res,\n        outage_time = DateTime(\"2024-01-01T18:00:00\"),\n        outage_length = 3.0,\n        expected_power_recovery = DateTime(\"2024-01-01T22:00:00\"),\n        expected_on_variable_recovery = DateTime(\"2024-01-01T22:00:00\"),\n    )\n    #Test no ramping constraint in D2 model results\n    d2 = get_decision_problem_results(res, \"D2\")\n    p_d2 =\n        read_realized_variables(d2; table_format = TableFormat.WIDE)[\"ActivePowerVariable__ThermalStandard\"]\n    p_recover_ix = indexin([DateTime(\"2024-01-01T22:00:00\")], p_d2[!, :DateTime])[1]\n    @test p_d2[p_recover_ix, \"Alta\"] == 40.0\nend\n\n# Note: Running a standard UC formulation without a feedforward to the ED is not a feasible modeling setup\n#Active power can change in Em without regard for OnVariable which messes up initializing the standard UC models.\n\n# This tests for both min up and down times being handled properly with events.\n# Generator not turned back on until 4 hours after the event (event only lasts 3 hours)\n# Generator is only on for one hour when the event happens; the constraint is bypassed by resetting the TimeDurationOn variable to a large value.\n@testset \"Hourly; uc standard; ed basic; ff\" begin\n    res = run_events_simulation(;\n        sys_emulator = build_system(PSITestSystems, \"c_sys5_events\"),\n        networks = repeat([PSI.CopperPlatePowerModel], 3),\n        optimizers = repeat([HiGHS_optimizer_small_gap], 3),\n        outage_time = DateTime(\"2024-01-01T17:00:00\"),\n        outage_length = 3.0,\n        uc_formulation = \"standard\",\n        ed_formulation = \"basic\",  #should also pass with nomin\n        feedforward = true,\n        in_memory = true,\n    )\n    test_event_results(;\n        res = res,\n        outage_time = DateTime(\"2024-01-01T17:00:00\"),\n        outage_length = 3.0,\n        expected_power_recovery = DateTime(\"2024-01-01T22:00:00\"),\n        expected_on_variable_recovery = DateTime(\"2024-01-01T22:00:00\"),\n    )\n    #Test ramping constraint in D2 model results\n    d2 = get_decision_problem_results(res, \"D2\")\n    p_d2 =\n        read_realized_variables(d2; table_format = TableFormat.WIDE)[\"ActivePowerVariable__ThermalStandard\"]\n    p_recover_ix = indexin([DateTime(\"2024-01-01T22:00:00\")], p_d2[!, :DateTime])[1]\n    @test p_d2[p_recover_ix, \"Alta\"] < 40.0\nend\n\n### 5 MINUTE DATA (RESOLUTION MISMATCH) ###\n\n#Note: if using basic for ed, emulator fails at timestep  after outage due to OutageConstraint_ub\n@testset \"5 min; uc basic; ed nomin; no ff\" begin\n    res = run_events_simulation(;\n        sys_emulator = build_system(PSITestSystems, \"c_sys5_events_rt\"),\n        networks = repeat([PSI.CopperPlatePowerModel], 3),\n        optimizers = repeat([HiGHS_optimizer_small_gap], 3),\n        outage_time = DateTime(\"2024-01-01T18:00:00\"),\n        outage_length = 3.0,\n        uc_formulation = \"basic\",\n        ed_formulation = \"nomin\",\n        feedforward = false,\n        in_memory = true,\n    )\n    test_event_results(;\n        res = res,\n        outage_time = DateTime(\"2024-01-01T18:00:00\"),\n        outage_length = 3.0,\n        expected_power_recovery = DateTime(\"2024-01-01T21:05:00\"),\n        expected_on_variable_recovery = DateTime(\"2024-01-01T22:00:00\"),\n    )\nend\n\n@testset \"5 min; uc basic; ed basic; ff\" begin\n    res = run_events_simulation(;\n        sys_emulator = build_system(PSITestSystems, \"c_sys5_events_rt\"),\n        networks = repeat([PSI.CopperPlatePowerModel], 3),\n        optimizers = repeat([HiGHS_optimizer_small_gap], 3),\n        outage_time = DateTime(\"2024-01-01T18:00:00\"),\n        outage_length = 3.0,\n        uc_formulation = \"basic\",\n        ed_formulation = \"basic\",\n        feedforward = true,\n        in_memory = false,\n    )\n    test_event_results(;\n        res = res,\n        outage_time = DateTime(\"2024-01-01T18:00:00\"),\n        outage_length = 3.0,\n        expected_power_recovery = DateTime(\"2024-01-01T22:00:00\"),\n        expected_on_variable_recovery = DateTime(\"2024-01-01T22:00:00\"),\n    )\nend\n\n# Note: Running a standard UC formulation without a feedforward to the ED is not a feasible modeling setup\n#Active power can change in Em without regard for OnVariable which messes up initializing the standard UC models.\n\n@testset \"5 min; uc standard; ed basic; ff\" begin\n    res = run_events_simulation(;\n        sys_emulator = build_system(PSITestSystems, \"c_sys5_events_rt\"),\n        networks = repeat([PSI.CopperPlatePowerModel], 3),\n        optimizers = repeat([HiGHS_optimizer_small_gap], 3),\n        outage_time = DateTime(\"2024-01-01T17:00:00\"),\n        outage_length = 3.0,\n        uc_formulation = \"standard\",\n        ed_formulation = \"basic\",  #should also pass with nomin\n        feedforward = true,\n        in_memory = true,\n    )\n    test_event_results(;\n        res = res,\n        outage_time = DateTime(\"2024-01-01T17:00:00\"),\n        outage_length = 3.0,\n        expected_power_recovery = DateTime(\"2024-01-01T22:00:00\"),\n        expected_on_variable_recovery = DateTime(\"2024-01-01T22:00:00\"),\n    )\n    #Test ramping constraint in D2 model results\n    d2 = get_decision_problem_results(res, \"D2\")\n    p_d2 =\n        read_realized_variables(d2; table_format = TableFormat.WIDE)[\"ActivePowerVariable__ThermalStandard\"]\n    p_recover_ix = indexin([DateTime(\"2024-01-01T22:00:00\")], p_d2[!, :DateTime])[1]\n    @test p_d2[p_recover_ix, \"Alta\"] < 40.0\nend\n\n@testset \"FixedForcedOutage with timeseries\" begin\n    dates_ts = collect(\n        DateTime(\"2024-01-01T00:00:00\"):Hour(1):DateTime(\"2024-01-02T23:00:00\"),\n    )\n    outage_data = fill!(Vector{Int64}(undef, 48), 0)\n    outage_data[3] = 1\n    outage_data[10:11] .= 1\n    outage_data[23:22] .= 1\n    outage_timeseries = TimeArray(dates_ts, outage_data)\n    res = run_fixed_forced_outage_sim_with_timeseries(;\n        sys = build_system(PSITestSystems, \"c_sys5_events\"),\n        networks = repeat([PSI.CopperPlatePowerModel], 3),\n        optimizers = repeat([HiGHS_optimizer_small_gap], 3),\n        outage_status_timeseries = outage_timeseries,\n        device_type = ThermalStandard,\n        device_names = [\"Alta\"],\n        renewable_formulation = RenewableFullDispatch,\n    )\n    em = get_emulation_problem_results(res)\n    status = read_realized_variable(\n        em,\n        \"AvailableStatusParameter__ThermalStandard\";\n        table_format = TableFormat.WIDE,\n    )\n    apv = read_realized_variable(\n        em,\n        \"ActivePowerVariable__ThermalStandard\";\n        table_format = TableFormat.WIDE,\n    )\n    for (ix, x) in enumerate(outage_data[1:24])\n        @test x != Int64(status[!, \"Alta\"][ix])\n        if Int64(status[!, \"Alta\"][ix]) == 0.0\n            @test apv[!, \"Alta\"][ix] == 0.0\n        end\n    end\nend\n\n@testset \"Renewable outage\" begin\n    dates_ts = collect(\n        DateTime(\"2024-01-01T00:00:00\"):Hour(1):DateTime(\"2024-01-02T23:00:00\"),\n    )\n    outage_data = fill!(Vector{Int64}(undef, 48), 0)\n    outage_data[3] = 1\n    outage_data[10:11] .= 1\n    outage_data[23:22] .= 1\n    outage_timeseries = TimeArray(dates_ts, outage_data)\n    res = run_fixed_forced_outage_sim_with_timeseries(;\n        sys = build_system(PSITestSystems, \"c_sys5_events\"),\n        networks = repeat([PSI.CopperPlatePowerModel], 3),\n        optimizers = repeat([HiGHS_optimizer_small_gap], 3),\n        outage_status_timeseries = outage_timeseries,\n        device_type = RenewableDispatch,\n        device_names = [\"WindBus1\"],\n        renewable_formulation = RenewableFullDispatch,\n    )\n    em = get_emulation_problem_results(res)\n    status = read_realized_variable(\n        em,\n        \"AvailableStatusParameter__RenewableDispatch\";\n        table_format = TableFormat.WIDE,\n    )\n    apv = read_realized_variable(\n        em,\n        \"ActivePowerVariable__RenewableDispatch\";\n        table_format = TableFormat.WIDE,\n    )\n    for (ix, x) in enumerate(outage_data[1:24])\n        @test x != Int64(status[!, \"WindBus1\"][ix])\n        if Int64(status[!, \"WindBus1\"][ix]) == 0.0\n            @test apv[!, \"WindBus1\"][ix] == 0.0\n        end\n    end\nend\n\n@testset \"Load outage\" begin\n    dates_ts = collect(\n        DateTime(\"2024-01-01T00:00:00\"):Hour(1):DateTime(\"2024-01-02T23:00:00\"),\n    )\n    outage_data = fill!(Vector{Int64}(undef, 48), 0)\n    outage_data[3] = 1\n    outage_data[10:11] .= 1\n    outage_data[23:22] .= 1\n    outage_timeseries = TimeArray(dates_ts, outage_data)\n    res = run_fixed_forced_outage_sim_with_timeseries(;\n        sys = build_system(PSITestSystems, \"c_sys5_events\"),\n        networks = repeat([PSI.CopperPlatePowerModel], 3),\n        optimizers = repeat([HiGHS_optimizer_small_gap], 3),\n        outage_status_timeseries = outage_timeseries,\n        device_type = InterruptiblePowerLoad,\n        device_names = [\"IloadBus4\"],\n        renewable_formulation = RenewableFullDispatch,\n    )\n    em = get_emulation_problem_results(res)\n    status = read_realized_variable(\n        em,\n        \"AvailableStatusParameter__InterruptiblePowerLoad\";\n        table_format = TableFormat.WIDE,\n    )\n    apv = read_realized_variable(\n        em,\n        \"ActivePowerVariable__InterruptiblePowerLoad\";\n        table_format = TableFormat.WIDE,\n    )\n\n    for (ix, x) in enumerate(outage_data[1:24])\n        @test x != Int64(status[!, \"IloadBus4\"][ix])\n        if Int64(status[!, \"IloadBus4\"][ix]) == 0.0\n            @test apv[!, \"IloadBus4\"][ix] == 0.0\n        end\n    end\nend\n\n@testset \"StaticPowerLoad outage\" begin\n    dates_ts = collect(\n        DateTime(\"2024-01-01T00:00:00\"):Hour(1):DateTime(\"2024-01-02T23:00:00\"),\n    )\n    outage_data = fill!(Vector{Int64}(undef, 48), 0)\n    outage_timeseries = TimeArray(dates_ts, outage_data)\n    res = run_fixed_forced_outage_sim_with_timeseries(;\n        sys = build_system(PSITestSystems, \"c_sys5_events\"),\n        networks = repeat([PSI.CopperPlatePowerModel], 3),\n        optimizers = repeat([HiGHS_optimizer_small_gap], 3),\n        outage_status_timeseries = outage_timeseries,\n        device_type = PowerLoad,\n        device_names = [\"Bus2\"],\n        renewable_formulation = RenewableFullDispatch,\n    )\n    em = get_emulation_problem_results(res)\n    active_power_thermal_no_outage =\n        read_realized_variable(\n            em,\n            \"ActivePowerVariable__ThermalStandard\";\n            table_format = TableFormat.WIDE,\n        )\n    outage_data[3] = 1\n    outage_data[10:11] .= 1\n    outage_data[23:22] .= 1\n    outage_timeseries = TimeArray(dates_ts, outage_data)\n    res = run_fixed_forced_outage_sim_with_timeseries(;\n        sys = build_system(PSITestSystems, \"c_sys5_events\"),\n        networks = repeat([PSI.CopperPlatePowerModel], 3),\n        optimizers = repeat([HiGHS_optimizer_small_gap], 3),\n        outage_status_timeseries = outage_timeseries,\n        device_type = PowerLoad,\n        device_names = [\"Bus2\"],\n        renewable_formulation = RenewableFullDispatch,\n    )\n    em = get_emulation_problem_results(res)\n    status = read_realized_variable(\n        em,\n        \"AvailableStatusParameter__PowerLoad\";\n        table_format = TableFormat.WIDE,\n    )\n    active_power_load =\n        read_realized_variable(\n            em,\n            \"ActivePowerTimeSeriesParameter__PowerLoad\";\n            table_format = TableFormat.WIDE,\n        )\n    active_power_thermal_outage =\n        read_realized_variable(\n            em,\n            \"ActivePowerVariable__ThermalStandard\";\n            table_format = TableFormat.WIDE,\n        )\n    for (ix, x) in enumerate(outage_data[1:24])\n        @test x != Int64(status[!, \"Bus2\"][ix])\n        if outage_data[ix] == 1.0\n            change_in_thermal_generation = sum(\n                Vector(active_power_thermal_outage[ix, 2:end]) .-\n                Vector(active_power_thermal_no_outage[ix, 2:end]),\n            )\n            active_power_outaged_load = active_power_load[ix, \"Bus2\"]\n            @test isapprox(change_in_thermal_generation, active_power_outaged_load)\n        end\n    end\nend\n\n@testset \"FixedOutput outage\" begin\n    dates_ts = collect(\n        DateTime(\"2024-01-01T00:00:00\"):Hour(1):DateTime(\"2024-01-02T23:00:00\"),\n    )\n    outage_data = fill!(Vector{Int64}(undef, 48), 0)\n    outage_timeseries = TimeArray(dates_ts, outage_data)\n    res = run_fixed_forced_outage_sim_with_timeseries(;\n        sys = build_system(PSITestSystems, \"c_sys5_events\"),\n        networks = repeat([PSI.CopperPlatePowerModel], 3),\n        optimizers = repeat([HiGHS_optimizer_small_gap], 3),\n        outage_status_timeseries = outage_timeseries,\n        device_type = PowerLoad,\n        device_names = [\"Bus2\"],\n        renewable_formulation = RenewableFullDispatch,\n    )\n    em = get_emulation_problem_results(res)\n    active_power_thermal_no_outage =\n        read_realized_variable(\n            em,\n            \"ActivePowerVariable__ThermalStandard\";\n            table_format = TableFormat.WIDE,\n        )\n    outage_data[3] = 1\n    outage_data[10:11] .= 1\n    outage_data[23:22] .= 1\n    outage_timeseries = TimeArray(dates_ts, outage_data)\n    res = run_fixed_forced_outage_sim_with_timeseries(;\n        sys = build_system(PSITestSystems, \"c_sys5_events\"),\n        networks = repeat([PSI.CopperPlatePowerModel], 3),\n        optimizers = repeat([HiGHS_optimizer_small_gap], 3),\n        outage_status_timeseries = outage_timeseries,\n        device_type = RenewableDispatch,\n        device_names = [\"WindBus1\"],\n        renewable_formulation = FixedOutput,\n    )\n    em = get_emulation_problem_results(res)\n    renewable_status =\n        read_realized_variable(\n            em,\n            \"AvailableStatusParameter__RenewableDispatch\";\n            table_format = TableFormat.WIDE,\n        )\n    active_power_thermal_outage =\n        read_realized_variable(\n            em,\n            \"ActivePowerVariable__ThermalStandard\";\n            table_format = TableFormat.WIDE,\n        )\n    active_power_renewable =\n        read_realized_variable(\n            em,\n            \"ActivePowerTimeSeriesParameter__RenewableDispatch\";\n            table_format = TableFormat.WIDE,\n        )\n    for (ix, x) in enumerate(outage_data[1:24])\n        @test x != Int64(renewable_status[!, \"WindBus1\"][ix])\n        if outage_data[ix] == 1.0\n            change_in_thermal_generation = sum(\n                Vector(active_power_thermal_outage[ix, 2:end]) .-\n                Vector(active_power_thermal_no_outage[ix, 2:end]),\n            )\n            active_power_outaged_renewable = active_power_renewable[ix, \"WindBus1\"]\n            @test isapprox(change_in_thermal_generation, active_power_outaged_renewable)\n        end\n    end\nend\n\n@testset \"Reactive power formulation w/ outage\" begin\n    res = run_events_simulation(;\n        sys_emulator = build_system(PSITestSystems, \"c_sys5_events\"),\n        networks = [PSI.PTDFPowerModel, PSI.PTDFPowerModel, PSI.SOCWRPowerModel],\n        optimizers = [\n            HiGHS_optimizer_small_gap,\n            HiGHS_optimizer_small_gap,\n            ipopt_optimizer,\n        ],\n        outage_time = DateTime(\"2024-01-01T18:00:00\"),\n        outage_length = 3.0,\n        uc_formulation = \"basic\",\n        ed_formulation = \"basic\",\n        feedforward = true,\n        in_memory = true,\n    )\n    test_event_results(;\n        res = res,\n        outage_time = DateTime(\"2024-01-01T18:00:00\"),\n        outage_length = 3.0,\n        expected_power_recovery = DateTime(\"2024-01-01T22:00:00\"),\n        expected_on_variable_recovery = DateTime(\"2024-01-01T22:00:00\"),\n        test_reactive_power = true,\n    )\nend\n"
  },
  {
    "path": "test/test_formulation_combinations.jl",
    "content": "@testset \"Test generate_formulation_combinations\" begin\n    res = PSI.generate_formulation_combinations()\n    found_valid_device = false\n    found_invalid_device = false\n    found_valid_service = false\n    found_invalid_service = false\n\n    for item in res[\"device_formulations\"]\n        if item[\"device_type\"] == PSY.ThermalStandard &&\n           item[\"formulation\"] == PSI.ThermalBasicCompactUnitCommitment\n            found_valid_device = true\n        end\n    end\n\n    for item in res[\"service_formulations\"]\n        if item[\"service_type\"] == PSY.ConstantReserveNonSpinning &&\n           item[\"formulation\"] == PSI.NonSpinningReserve\n            found_valid_service = true\n        end\n        #if item[\"service_type\"] == PSY.AGC && item[\"formulation\"] == PSI.NonSpinningReserve\n        #    found_invalid_service = true\n        #end\n    end\n\n    @test found_valid_device\n    @test !found_invalid_device\n    @test found_valid_service\n    @test !found_invalid_service\nend\n\n@testset \"Test generate_formulation_combinations with system\" begin\n    sys = PSB.build_system(PSITestSystems, \"c_sys5_hy_uc\")\n    res1 = PSI.generate_formulation_combinations()\n    res2 = PSI.generate_formulation_combinations(sys)\n    @test length(res1[\"device_formulations\"]) > length(res2[\"device_formulations\"])\n    @test length(res1[\"service_formulations\"]) > length(res2[\"service_formulations\"])\n\n    device_types = Set((typeof(x) for x in PSY.get_components(PSY.Device, sys)))\n    diff = setdiff((x[\"device_type\"] for x in res2[\"device_formulations\"]), device_types)\n    @test isempty(diff)\n\n    service_types = Set((typeof(x) for x in PSY.get_components(PSY.Service, sys)))\n    diff = setdiff((x[\"service_type\"] for x in res2[\"service_formulations\"]), service_types)\n    @test isempty(diff)\nend\n\n@testset \"Test write_formulation_combinations\" begin\n    res = PSI.generate_formulation_combinations()\n\n    filename = joinpath(tempdir(), \"data.json\")\n    @test !isfile(filename)\n    try\n        PSI.write_formulation_combinations(filename)\n        @test isfile(filename)\n        data = open(filename) do io\n            JSON3.read(io, Dict)\n        end\n        @test \"device_formulations\" in keys(data)\n        @test length(data[\"device_formulations\"]) == length(res[\"device_formulations\"])\n        @test \"service_formulations\" in keys(data)\n        @test length(data[\"service_formulations\"]) == length(res[\"service_formulations\"])\n    finally\n        isfile(filename) && rm(filename)\n    end\nend\n"
  },
  {
    "path": "test/test_import_export_cost.jl",
    "content": "# See also test_device_source_constructors.jl\n@testset \"ImportExportCost incremental+decremental Source, no time series versus constant time series, reservation off\" begin\n    sys_no_ts = make_5_bus_with_import_export(; name = \"sys_no_ts\")\n    sys_constant_ts =\n        make_5_bus_with_ie_ts(false, false, false, false; name = \"sys_constant_ts\")\n    test_generic_mbc_equivalence(sys_no_ts, sys_constant_ts;\n        device_to_formulation = FormulationDict(\n            Source => DeviceModel(\n                Source,\n                ImportExportSourceModel;\n                attributes = Dict(\"reservation\" => false),\n            ),\n        ),\n    )\nend\n\n@testset \"ImportExportCost incremental+decremental Source, no time series versus constant time series, reservation on\" begin\n    sys_no_ts = make_5_bus_with_import_export(; name = \"sys_no_ts\")\n    sys_constant_ts =\n        make_5_bus_with_ie_ts(false, false, false, false; name = \"sys_constant_ts\")\n    test_generic_mbc_equivalence(sys_no_ts, sys_constant_ts;\n        device_to_formulation = FormulationDict(\n            Source => DeviceModel(\n                Source,\n                ImportExportSourceModel;\n                attributes = Dict(\"reservation\" => true),\n            ),\n        ),\n    )\nend\n\n@testset \"ImportExportCost constant time series, reservation sanity checks\" begin\n    sys_constant_ts =\n        make_5_bus_with_ie_ts(false, false, false, false; name = \"sys_constant_ts\")\n\n    for use_simulation in (false, true),\n        in_memory_store in (use_simulation ? (false, true) : (false,)),\n        reservation in (false, true)\n\n        run_iec_sim(sys_constant_ts,\n            IEC_COMPONENT_NAME,\n            IECComponentType;\n            simulation = use_simulation,\n            in_memory_store = in_memory_store,\n            reservation = true,\n        )\n    end\nend\n\n@testset \"ImportExportCost with time varying import slopes, reservation off\" begin\n    import_scalar = 0.5  # ultimately multiplies ActivePowerOutVariable objective function coefficient\n    export_scalar = 2.0  # ultimately multiplies ActivePowerInVariable objective function coefficient\n    sys_constant = make_5_bus_with_ie_ts(false, false, false, false;\n        import_scalar = import_scalar, export_scalar = export_scalar,\n        name = \"sys_constant\")\n    sys_varying_import_slopes = make_5_bus_with_ie_ts(false, true, false, false;\n        import_scalar = import_scalar, export_scalar = export_scalar,\n        name = \"sys_varying_import_slopes\")\n    iec_obj_fun_test_wrapper(sys_constant, sys_varying_import_slopes)\nend\n\n@testset \"ImportExportCost with time varying import breakpoints, reservation off\" begin\n    import_scalar = 0.2  # NOTE this maxes out ActivePowerOutVariable\n    export_scalar = 2.0\n    sys_constant = make_5_bus_with_ie_ts(false, false, false, false;\n        import_scalar = import_scalar, export_scalar = export_scalar,\n        name = \"sys_constant\")\n    sys_varying_import_breakpoints = make_5_bus_with_ie_ts(true, false, false, false;\n        import_scalar = import_scalar, export_scalar = export_scalar,\n        name = \"sys_varying_import_breakpoints\")\n    iec_obj_fun_test_wrapper(sys_constant, sys_varying_import_breakpoints)\nend\n\n@testset \"ImportExportCost with time varying export slopes, reservation off\" begin\n    import_scalar = 0.5\n    export_scalar = 2.0\n    sys_constant = make_5_bus_with_ie_ts(false, false, false, false;\n        import_scalar = import_scalar, export_scalar = export_scalar,\n        name = \"sys_constant\")\n    sys_varying_export_slopes = make_5_bus_with_ie_ts(false, false, false, true;\n        import_scalar = import_scalar, export_scalar = export_scalar,\n        name = \"sys_varying_export_slopes\")\n    iec_obj_fun_test_wrapper(sys_constant, sys_varying_export_slopes)\nend\n\n@testset \"ImportExportCost with time varying export breakpoints, reservation off\" begin\n    import_scalar = 1.0\n    export_scalar = 50.0  # NOTE this maxes out ActivePowerInVariable\n    sys_constant = make_5_bus_with_ie_ts(false, false, false, false;\n        import_scalar = import_scalar, export_scalar = export_scalar,\n        name = \"sys_constant\")\n    sys_varying_export_breakpoints = make_5_bus_with_ie_ts(false, false, true, false;\n        import_scalar = import_scalar, export_scalar = export_scalar,\n        name = \"sys_varying_export_breakpoints\")\n    iec_obj_fun_test_wrapper(sys_constant, sys_varying_export_breakpoints)\nend\n\n@testset \"ImportExportCost with time varying everything, reservation off\" begin\n    import_scalar = 0.2\n    export_scalar = 40.0\n    sys_constant = make_5_bus_with_ie_ts(false, false, false, false;\n        import_scalar = import_scalar, export_scalar = export_scalar,\n        name = \"sys_constant\")\n    sys_varying_everything = make_5_bus_with_ie_ts(true, true, true, true;\n        import_scalar = import_scalar, export_scalar = export_scalar,\n        name = \"sys_varying_everything\")\n    iec_obj_fun_test_wrapper(sys_constant, sys_varying_everything)\nend\n"
  },
  {
    "path": "test/test_initialization_problem.jl",
    "content": "# Cbc isn't performant enough and SCIP is too problematic\ntest_months = (get(ENV, \"CI\", nothing) == \"true\") ? 4 : 6\nsys_rts = PSB.build_system(PSISystems, \"modified_RTS_GMLC_DA_sys\")\n@testset \"Decision Model test for Initialization with RTS GMLC system, Case 1\" begin\n    ######## Test with ThermalStandardUnitCommitment ########\n    template = ProblemTemplate(CopperPlatePowerModel)\n    set_device_model!(template, ThermalStandard, ThermalStandardUnitCommitment)\n    set_device_model!(template, PowerLoad, StaticPowerLoad)\n    set_device_model!(template, RenewableDispatch, RenewableFullDispatch)\n    set_device_model!(template, HydroDispatch, HydroDispatchRunOfRiver)\n    for init_time in\n        DateTime(\"2020-01-01T00:00:00\"):Month(test_months):DateTime(\"2020-12-31T00:00:00\")\n        @info(\"Decision Model initial_conditions test with RTS-GMLC for $init_time\")\n        model = DecisionModel(\n            template,\n            sys_rts;\n            optimizer = HiGHS_optimizer,\n            initial_time = init_time,\n            horizon = Hour(48),\n        )\n        @test build!(model; output_dir = mktempdir(; cleanup = true)) ==\n              PSI.ModelBuildStatus.BUILT\n\n        ####### Check initialization problem\n        check_initialization_variable_count(model, ActivePowerVariable(), ThermalStandard)\n        check_initialization_variable_count(model, OnVariable(), ThermalStandard)\n        check_initialization_variable_count(model, StopVariable(), ThermalStandard)\n        check_initialization_variable_count(model, StartVariable(), ThermalStandard)\n        check_initialization_variable_count(model, ActivePowerVariable(), RenewableDispatch)\n        check_initialization_variable_count(model, ActivePowerVariable(), HydroDispatch)\n        ####### Check initial condition from initialization step\n        check_duration_on_initial_conditions_values(model, ThermalStandard)\n        check_duration_off_initial_conditions_values(model, ThermalStandard)\n        check_active_power_initial_condition_values(model, ThermalStandard)\n        check_status_initial_conditions_values(model, ThermalStandard)\n        ####### Check variables\n        check_variable_count(model, ActivePowerVariable(), ThermalStandard)\n        check_variable_count(model, StopVariable(), ThermalStandard)\n        check_variable_count(model, OnVariable(), ThermalStandard)\n        check_variable_count(model, StartVariable(), ThermalStandard)\n        check_variable_count(model, ActivePowerVariable(), RenewableDispatch)\n        check_variable_count(model, ActivePowerVariable(), HydroDispatch)\n        ####### Check constraints\n        check_constraint_count(\n            model,\n            ActivePowerVariableLimitsConstraint(),\n            ThermalStandard;\n            meta = \"lb\",\n        )\n        check_constraint_count(\n            model,\n            ActivePowerVariableLimitsConstraint(),\n            ThermalStandard;\n            meta = \"ub\",\n        )\n        check_constraint_count(model, DurationConstraint(), ThermalStandard)\n        check_constraint_count(model, RampConstraint(), ThermalStandard)\n        check_constraint_count(model, CommitmentConstraint(), ThermalStandard)\n        check_constraint_count(model, CommitmentConstraint(), ThermalStandard; meta = \"aux\")\n        check_constraint_count(\n            model,\n            PSI.ActivePowerVariableTimeSeriesLimitsConstraint(),\n            HydroDispatch;\n            meta = \"ub\",\n        )\n        check_constraint_count(\n            model,\n            PSI.ActivePowerVariableTimeSeriesLimitsConstraint(),\n            RenewableDispatch;\n            meta = \"ub\",\n        )\n\n        # @test solve!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED\n    end\nend\n\n@testset \"Decision Model test for Initialization with RTS GMLC system, Case 2\" begin\n    ######## Test with HydroCommitmentRunOfRiver ########\n    template = ProblemTemplate(CopperPlatePowerModel)\n    set_device_model!(template, ThermalStandard, ThermalBasicUnitCommitment)\n    set_device_model!(template, PowerLoad, StaticPowerLoad)\n    set_device_model!(template, RenewableDispatch, RenewableFullDispatch)\n    set_device_model!(template, HydroDispatch, HydroCommitmentRunOfRiver)\n\n    for init_time in\n        DateTime(\"2020-01-01T00:00:00\"):Month(test_months):DateTime(\"2020-12-31T00:00:00\")\n        @info(\"Decision Model initial_conditions test with RTS-GMLC for $init_time\")\n        model = DecisionModel(\n            template,\n            sys_rts;\n            optimizer = HiGHS_optimizer,\n            initial_time = init_time,\n            horizon = Hour(48),\n        )\n        @test build!(model; output_dir = mktempdir(; cleanup = true)) ==\n              PSI.ModelBuildStatus.BUILT\n\n        ####### Check initialization problem\n        check_initialization_variable_count(model, ActivePowerVariable(), ThermalStandard)\n        check_initialization_variable_count(model, OnVariable(), ThermalStandard)\n        check_initialization_variable_count(model, ActivePowerVariable(), RenewableDispatch)\n        check_initialization_variable_count(model, ActivePowerVariable(), HydroDispatch)\n        ####### Check initial condition from initialization step\n        check_status_initial_conditions_values(model, ThermalStandard)\n\n        ####### Check variables\n        check_variable_count(model, ActivePowerVariable(), ThermalStandard)\n        check_variable_count(model, StopVariable(), ThermalStandard)\n        check_variable_count(model, OnVariable(), ThermalStandard)\n        check_variable_count(model, StartVariable(), ThermalStandard)\n        check_variable_count(model, ActivePowerVariable(), RenewableDispatch)\n        check_variable_count(model, ActivePowerVariable(), HydroDispatch)\n        check_variable_count(model, OnVariable(), HydroDispatch)\n        ####### Check constraints\n        check_constraint_count(\n            model,\n            ActivePowerVariableLimitsConstraint(),\n            ThermalStandard;\n            meta = \"lb\",\n        )\n        check_constraint_count(\n            model,\n            ActivePowerVariableLimitsConstraint(),\n            ThermalStandard;\n            meta = \"ub\",\n        )\n        check_constraint_count(model, CommitmentConstraint(), ThermalStandard)\n        check_constraint_count(model, CommitmentConstraint(), ThermalStandard; meta = \"aux\")\n\n        check_constraint_count(\n            model,\n            PSI.ActivePowerVariableTimeSeriesLimitsConstraint(),\n            RenewableDispatch;\n            meta = \"ub\",\n        )\n        check_constraint_count(\n            model,\n            PSI.ActivePowerVariableTimeSeriesLimitsConstraint(),\n            HydroDispatch;\n            meta = \"ub\",\n        )\n        check_constraint_count(\n            model,\n            PSI.ActivePowerVariableLimitsConstraint(),\n            HydroDispatch;\n            meta = \"lb\",\n        )\n        check_constraint_count(\n            model,\n            PSI.ActivePowerVariableLimitsConstraint(),\n            HydroDispatch;\n            meta = \"ub\",\n        )\n\n        # @test solve!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED\n    end\nend\n\n@testset \"Decision Model test for Initialization with RTS GMLC system, Case 4\" begin\n    ######## Test with ThermalCompactUnitCommitment ########\n    template = ProblemTemplate(CopperPlatePowerModel)\n    set_device_model!(template, ThermalStandard, ThermalCompactUnitCommitment)\n    set_device_model!(template, PowerLoad, StaticPowerLoad)\n    set_device_model!(template, RenewableDispatch, RenewableFullDispatch)\n    set_device_model!(template, HydroDispatch, HydroDispatchRunOfRiver)\n\n    for init_time in\n        DateTime(\"2020-01-01T00:00:00\"):Month(test_months):DateTime(\"2020-12-31T00:00:00\")\n        @info(\"Decision Model initial_conditions test with RTS-GMLC for $init_time\")\n        model = DecisionModel(\n            template,\n            sys_rts;\n            optimizer = HiGHS_optimizer,\n            initial_time = init_time,\n            horizon = Hour(48),\n        )\n        PSI.instantiate_network_model!(model)\n        PSI.build_pre_step!(model)\n        setup_ic_model_container!(model)\n        ####### Check initialization problem constraints #####\n        check_initialization_constraint_count(\n            model,\n            ActivePowerVariableLimitsConstraint(),\n            ThermalStandard;\n            meta = \"lb\",\n        )\n        check_initialization_constraint_count(\n            model,\n            ActivePowerVariableLimitsConstraint(),\n            ThermalStandard;\n            meta = \"ub\",\n        )\n        check_initialization_constraint_count(\n            model,\n            CommitmentConstraint(),\n            ThermalStandard,\n        )\n        check_initialization_constraint_count(\n            model,\n            CommitmentConstraint(),\n            ThermalStandard;\n            meta = \"aux\",\n        )\n        check_initialization_constraint_count(\n            model,\n            PSI.ActivePowerVariableTimeSeriesLimitsConstraint(),\n            RenewableDispatch;\n            meta = \"ub\",\n        )\n        check_initialization_constraint_count(\n            model,\n            ActivePowerVariableLimitsConstraint(),\n            HydroDispatch;\n            meta = \"lb\",\n        )\n        check_initialization_constraint_count(\n            model,\n            ActivePowerVariableLimitsConstraint(),\n            HydroDispatch;\n            meta = \"ub\",\n        )\n        check_initialization_constraint_count(\n            model,\n            PSI.ActivePowerVariableTimeSeriesLimitsConstraint(),\n            HydroDispatch;\n            meta = \"ub\",\n        )\n        PSI.reset!(model)\n        @test build!(model; output_dir = mktempdir(; cleanup = true)) ==\n              PSI.ModelBuildStatus.BUILT\n\n        ####### Check initialization problem\n        check_initialization_variable_count(\n            model,\n            PSI.PowerAboveMinimumVariable(),\n            ThermalStandard,\n        )\n        check_initialization_variable_count(model, OnVariable(), ThermalStandard)\n        check_initialization_variable_count(model, StopVariable(), ThermalStandard)\n        check_initialization_variable_count(model, StartVariable(), ThermalStandard)\n        check_initialization_variable_count(model, ActivePowerVariable(), RenewableDispatch)\n        check_initialization_variable_count(model, ActivePowerVariable(), HydroDispatch)\n\n        ####### Check initial condition from initialization step\n        check_duration_on_initial_conditions_values(model, ThermalStandard)\n        check_duration_off_initial_conditions_values(model, ThermalStandard)\n        check_active_power_abovemin_initial_condition_values(model, ThermalStandard)\n        check_status_initial_conditions_values(model, ThermalStandard)\n\n        ####### Check variables\n        check_variable_count(model, PSI.PowerAboveMinimumVariable(), ThermalStandard)\n        check_variable_count(model, OnVariable(), ThermalStandard)\n        check_variable_count(model, StopVariable(), ThermalStandard)\n        check_variable_count(model, StartVariable(), ThermalStandard)\n        check_variable_count(model, ActivePowerVariable(), RenewableDispatch)\n        check_variable_count(model, ActivePowerVariable(), HydroDispatch)\n\n        ####### Check constraints\n        check_constraint_count(\n            model,\n            ActivePowerVariableLimitsConstraint(),\n            ThermalStandard;\n            meta = \"lb\",\n        )\n        check_constraint_count(\n            model,\n            ActivePowerVariableLimitsConstraint(),\n            ThermalStandard;\n            meta = \"ub\",\n        )\n        check_constraint_count(model, RampConstraint(), ThermalStandard)\n        check_constraint_count(model, DurationConstraint(), ThermalStandard)\n        check_constraint_count(model, CommitmentConstraint(), ThermalStandard)\n        check_constraint_count(model, CommitmentConstraint(), ThermalStandard; meta = \"aux\")\n        check_constraint_count(\n            model,\n            PSI.ActivePowerVariableTimeSeriesLimitsConstraint(),\n            RenewableDispatch;\n            meta = \"ub\",\n        )\n        check_constraint_count(\n            model,\n            PSI.ActivePowerVariableTimeSeriesLimitsConstraint(),\n            HydroDispatch;\n            meta = \"ub\",\n        )\n        check_constraint_count(\n            model,\n            PSI.ActivePowerVariableLimitsConstraint(),\n            HydroDispatch;\n            meta = \"lb\",\n        )\n        check_constraint_count(\n            model,\n            PSI.ActivePowerVariableLimitsConstraint(),\n            HydroDispatch;\n            meta = \"ub\",\n        )\n\n        # @test solve!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED\n    end\nend\n"
  },
  {
    "path": "test/test_jump_utils.jl",
    "content": "@testset \"Test get_column_names_from_key\" begin\n    key = PSI.VariableKey(PSI.ActivePowerVariable, PSY.ThermalStandard)\n    @test PSI.get_column_names_from_key(key) == ([\"ActivePowerVariable__ThermalStandard\"],)\nend\n\n@testset \"Test get_column_names_from_axis_array with DenseAxisArray 1D\" begin\n    @test PSI.get_column_names_from_axis_array(DenseAxisArray(rand(3), 1:3)) ==\n          ([\"1\", \"2\", \"3\"],)\n    @test PSI.get_column_names_from_axis_array(DenseAxisArray(rand(3), collect(1:3))) ==\n          ([\"1\", \"2\", \"3\"],)\n\n    key = PSI.VariableKey(PSI.ActivePowerVariable, PSY.ThermalStandard)\n    @test PSI.get_column_names_from_axis_array(key, DenseAxisArray(rand(3), 1:3)) ==\n          ([\"ActivePowerVariable__ThermalStandard\"],)\nend\n\n@testset \"Test get_column_names_from_axis_array with DenseAxisArray 2D\" begin\n    key = PSI.VariableKey(PSI.ActivePowerVariable, PSY.ThermalStandard)\n    components = [\"component1\", \"component2\"]\n    array = DenseAxisArray(rand(2, 3), components, 1:3)\n    @test PSI.get_column_names_from_axis_array(key, array) == (components,)\n    @test PSI.get_column_names_from_axis_array(array) == (components,)\n\n    @test PSI.get_column_names_from_axis_array(DenseAxisArray(rand(2, 3), [1, 2], 1:3)) ==\n          ([\"1\", \"2\"],)\n    @test PSI.get_column_names_from_axis_array(DenseAxisArray(rand(2, 3), 1:2, 1:3)) ==\n          ([\"1\", \"2\"],)\nend\n\n@testset \"Test get_column_names_from_axis_array with DenseAxisArray 3D\" begin\n    key = PSI.VariableKey(PSI.ActivePowerVariable, PSY.ThermalStandard)\n    components = [\"component1\", \"component2\"]\n    extra = [\"1\", \"2\", \"3\", \"4\"]\n    array = DenseAxisArray(rand(2, 4, 3), components, extra, 1:3)\n    @test PSI.get_column_names_from_axis_array(key, array) == (components, extra)\n    @test PSI.get_column_names_from_axis_array(array) == (components, extra)\n    @test PSI.get_column_names_from_axis_array(\n        DenseAxisArray(rand(2, 4, 3), components, 1:4, 1:3),\n    ) == (components, extra)\nend\n\n@testset \"Test to_dataframe with DenseAxisArray 1D\" begin\n    data = rand(3)\n    array = DenseAxisArray(data, 1:3)\n    key = PSI.VariableKey(PSI.ActivePowerVariable, PSY.ThermalStandard)\n    df = PSI.to_dataframe(array, key)\n\n    @test size(df) == (3, 1)\n    @test names(df) == [\"ActivePowerVariable__ThermalStandard\"]\n    @test df[!, \"ActivePowerVariable__ThermalStandard\"] == data\nend\n\n@testset \"Test to_dataframe with DenseAxisArray 2D\" begin\n    data = rand(2, 3)\n    components = [\"component1\", \"component2\"]\n    array = DenseAxisArray(data, components, 1:3)\n    key = PSI.VariableKey(PSI.ActivePowerVariable, PSY.ThermalStandard)\n    df = PSI.to_dataframe(array, key)\n\n    @test size(df) == (3, 2)\n    @test names(df) == components\n    @test df[!, \"component1\"] == permutedims(data)[:, 1]\n    @test df[!, \"component2\"] == permutedims(data)[:, 2]\nend\n\n@testset \"Test to_results_dataframe with 2D DenseAxisArray - LONG format with timestamps\" begin\n    data = rand(2, 3)\n    components = [\"component1\", \"component2\"]\n    array = DenseAxisArray(data, components, 1:3)\n    timestamps = [\n        DateTime(2024, 1, 1, 0),\n        DateTime(2024, 1, 1, 1),\n        DateTime(2024, 1, 1, 2),\n    ]\n    df = PSI.to_results_dataframe(array, timestamps, Val(IS.TableFormat.LONG))\n\n    @test size(df) == (6, 3)  # 2 components × 3 timestamps = 6 rows, 3 columns\n    @test names(df) == [\"DateTime\", \"name\", \"value\"]\n    @test df.DateTime == repeat(timestamps, 2)\n    @test df.name == repeat(components; inner = 3)\n    @test df.value == reshape(permutedims(data), 6)\n\n    # Test error with mismatched timestamps.\n    wrong_timestamps = [DateTime(2024, 1, 1, 1)]\n    @test_throws ErrorException PSI.to_results_dataframe(\n        array, wrong_timestamps, Val(IS.TableFormat.LONG),\n    )\nend\n\n@testset \"Test to_results_dataframe with 2D DenseAxisArray - LONG format without timestamps\" begin\n    data = rand(2, 3)\n    components = [\"component1\", \"component2\"]\n    array = DenseAxisArray(data, components, 1:3)\n    df = PSI.to_results_dataframe(array, nothing, Val(IS.TableFormat.LONG))\n\n    @test size(df) == (6, 3)  # 2 components × 3 timestamps = 6 rows, 3 columns\n    @test names(df) == [\"time_index\", \"name\", \"value\"]\n    @test df.time_index == repeat([1, 2, 3], 2)\n    @test df.name == repeat(components; inner = 3)\n    @test df.value == reshape(permutedims(data), 6)\nend\n\n@testset \"Test to_results_dataframe with 2D DenseAxisArray - WIDE format with timestamps\" begin\n    data = rand(2, 3)\n    components = [\"component1\", \"component2\"]\n    array = DenseAxisArray(data, components, 1:3)\n    timestamps = [\n        DateTime(2024, 1, 1, 0),\n        DateTime(2024, 1, 1, 1),\n        DateTime(2024, 1, 1, 2),\n    ]\n    df = PSI.to_results_dataframe(array, timestamps, Val(IS.TableFormat.WIDE))\n\n    @test size(df) == (3, 3)  # 3 timestamps, 3 columns (DateTime + 2 components)\n    @test names(df) == [\"DateTime\", \"component1\", \"component2\"]\n    @test df.DateTime == timestamps\n    exp_data = permutedims(data)\n    @test df.component1 == exp_data[:, 1]\n    @test df.component2 == exp_data[:, 2]\nend\n\n@testset \"Test to_results_dataframe with 2D DenseAxisArray - WIDE format without timestamps\" begin\n    data = rand(2, 3)\n    components = [\"component1\", \"component2\"]\n    array = DenseAxisArray(data, components, 1:3)\n    df = PSI.to_results_dataframe(array, nothing, Val(IS.TableFormat.WIDE))\n\n    @test size(df) == (3, 3)  # 3 timestamps, 3 columns (DateTime + 2 components)\n    @test names(df) == [\"time_index\", \"component1\", \"component2\"]\n    @test df.time_index == [1, 2, 3]\n    exp_data = permutedims(data)\n    @test df.component1 == exp_data[:, 1]\n    @test df.component2 == exp_data[:, 2]\nend\n\nfunction _fill_3d_data()\n    components = [\"component1\", \"component2\"]\n    extra = [\"1\", \"2\", \"3\", \"4\"]\n    array = DenseAxisArray(zeros(2, 4, 3), components, extra, 1:3)\n    array[\"component1\", \"1\", :] = [1.0, 2.0, 3.0]\n    array[\"component1\", \"2\", :] = [2.0, 3.0, 4.0]\n    array[\"component1\", \"3\", :] = [3.0, 4.0, 5.0]\n    array[\"component1\", \"4\", :] = [6.0, 7.0, 8.0]\n    array[\"component2\", \"1\", :] = [11.0, 12.0, 13.0]\n    array[\"component2\", \"2\", :] = [12.0, 13.0, 14.0]\n    array[\"component2\", \"3\", :] = [13.0, 14.0, 15.0]\n    array[\"component2\", \"4\", :] = [16.0, 17.0, 18.0]\n    return array\nend\n\nfunction _check_3d_data(df)\n    @test size(df) == (24, 4)  # 2 components x 4 extra × 3 timestamps = 24 rows, 4 columns\n    @test @rsubset(df, :name == \"component1\" && :name2 == \"1\")[!, :value] ==\n          [1.0, 2.0, 3.0]\n    @test @rsubset(df, :name == \"component1\" && :name2 == \"2\")[!, :value] ==\n          [2.0, 3.0, 4.0]\n    @test @rsubset(df, :name == \"component1\" && :name2 == \"3\")[!, :value] ==\n          [3.0, 4.0, 5.0]\n    @test @rsubset(df, :name == \"component1\" && :name2 == \"4\")[!, :value] ==\n          [6.0, 7.0, 8.0]\n    @test @rsubset(df, :name == \"component2\" && :name2 == \"1\")[!, :value] ==\n          [11.0, 12.0, 13.0]\n    @test @rsubset(df, :name == \"component2\" && :name2 == \"2\")[!, :value] ==\n          [12.0, 13.0, 14.0]\n    @test @rsubset(df, :name == \"component2\" && :name2 == \"3\")[!, :value] ==\n          [13.0, 14.0, 15.0]\n    @test @rsubset(df, :name == \"component2\" && :name2 == \"4\")[!, :value] ==\n          [16.0, 17.0, 18.0]\nend\n\n@testset \"Test to_results_dataframe with 3D DenseAxisArray - LONG format with timestamps\" begin\n    array = _fill_3d_data()\n    timestamps = [\n        DateTime(2024, 1, 1, 0),\n        DateTime(2024, 1, 1, 1),\n        DateTime(2024, 1, 1, 2),\n    ]\n    df = PSI.to_results_dataframe(array, timestamps, Val(IS.TableFormat.LONG))\n    _check_3d_data(df)\n\n    # Test error with mismatched timestamps.\n    wrong_timestamps = [DateTime(2024, 1, 1, 1)]\n    @test_throws ErrorException PSI.to_results_dataframe(\n        array, wrong_timestamps, Val(IS.TableFormat.LONG),\n    )\nend\n\n@testset \"Test to_results_dataframe with 3D DenseAxisArray - LONG format without timestamps\" begin\n    array = _fill_3d_data()\n    df = PSI.to_results_dataframe(array, nothing, Val(IS.TableFormat.LONG))\n    @test names(df) == [\"time_index\", \"name\", \"name2\", \"value\"]\n    _check_3d_data(df)\n    @test df.time_index == repeat([1, 2, 3], 8)\nend\n\n@testset \"Test to_matrix\" begin\n    @test PSI.to_matrix([1, 2, 3]) == [1; 2; 3;;]\n    @test PSI.to_matrix([1 2 3]) == [1 2 3]\n\n    data = rand(2, 3)\n    @test PSI.to_matrix(DenseAxisArray(data, [\"a\", \"b\"], 1:3)) == permutedims(data)\nend\n\n@testset \"Dual processing helpers\" begin\n    @testset \"_first_element with DenseAxisArray\" begin\n        arr = DenseAxisArray([1.1, 2.9], 1:2)\n        @test PSI._first_element(arr) == 1.1\n    end\n\n    @testset \"_first_element with SparseAxisArray\" begin\n        arr = SparseAxisArray(Dict((1,) => 1.1, (2,) => 2.9))\n        @test PSI._first_element(arr) ∈ [1.1, 2.9]\n    end\n\n    @testset \"_round_cache_values! with DenseAxisArray\" begin\n        arr = DenseAxisArray([1.1, 2.9], 1:2)\n        PSI._round_cache_values!(arr)\n        @test arr[1] == 1.0\n        @test arr[2] == 3.0\n    end\n\n    @testset \"_round_cache_values! with SparseAxisArray\" begin\n        arr = SparseAxisArray(Dict((1,) => 1.1, (2,) => 2.9))\n        PSI._round_cache_values!(arr)\n        @test arr[(1,)] == 1.0\n        @test arr[(2,)] == 3.0\n    end\nend\n"
  },
  {
    "path": "test/test_market_bid_cost.jl",
    "content": "function test_market_bid_cost_models(sys::PSY.System,\n    test_unit::PSY.Component,\n    my_no_load::Float64,\n    my_initial_input::Float64;\n    skip_setting = false,\n    device_to_formulation = FormulationDict(),\n    filename::Union{String, Nothing} = nothing,\n)\n    fcn_data = get_function_data(\n        get_value_curve(\n            get_incremental_offer_curves(get_operation_cost(test_unit)),\n        ),\n    )\n    if !skip_setting\n        new_vc = PiecewiseIncrementalCurve(fcn_data, my_initial_input, my_no_load)\n        set_incremental_offer_curves!(\n            get_operation_cost(test_unit),\n            CostCurve(new_vc),\n        )\n    end\n    set_no_load_cost!(get_operation_cost(test_unit), my_no_load)\n    template = ProblemTemplate(NetworkModel(CopperPlatePowerModel))\n\n    set_formulations!(\n        template,\n        sys,\n        device_to_formulation,\n    )\n\n    model = DecisionModel(\n        template,\n        sys;\n        name = \"UC_test_mbc\",\n        optimizer = HiGHS_optimizer_small_gap,\n        optimizer_solve_log_print = true,\n        store_variable_names = true,\n    )\n    @test build!(model; output_dir = test_path) == PSI.ModelBuildStatus.BUILT\n    @test solve!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED\n    if !isnothing(filename)\n        save_loc = joinpath(DOWNLOADS, \"thermal_vs_renewable\")\n        @assert isdir(save_loc)\n\n        save_objective_function(\n            model,\n            joinpath(\n                save_loc,\n                \"objective_function_$(filename)_$(get_name(test_unit)).txt\",\n            ),\n        )\n        save_constraints(\n            model,\n            joinpath(save_loc, \"constraints_$(filename)_$(get_name(test_unit)).txt\"),\n        )\n    end\n\n    return OptimizationProblemResults(model)\nend\n\nfunction verify_market_bid_cost_models(\n    sys::PSY.System,\n    test_unit::PSY.Component,\n    cost_reference::Float64,\n    no_load_cost::Float64,\n    my_initial_input::Float64,\n)\n    results = test_market_bid_cost_models(\n        sys,\n        test_unit,\n        no_load_cost,\n        my_initial_input,\n    )\n    expr = read_expression(results, \"ProductionCostExpression__ThermalStandard\")\n    component_df = @rsubset(expr, :name == get_name(test_unit))\n    shutdown_cost = PSY.get_shut_down(PSY.get_operation_cost(test_unit))\n    var_unit_cost =\n        sum(@rsubset(component_df, :value != shutdown_cost)[:, :value])\n    unit_cost_due_to_initial =\n        nrow(@rsubset(component_df, :value != shutdown_cost)) * my_initial_input\n    @test isapprox(\n        var_unit_cost - PSY.get_start_up(PSY.get_operation_cost(test_unit))[:hot],\n        cost_reference + unit_cost_due_to_initial;\n        atol = 1,\n    )\nend\n\n@testset \"Test Thermal Generation MarketBidCost models\" begin\n    test_cases = [\n        (\"Base case\", \"fixed_market_bid_cost\", 18487.236, 30.0, 30.0),\n        (\"Greater initial input, no load\", \"fixed_market_bid_cost\", 18487.236, 31.0, 31.0),\n        (\"Greater initial input only\", \"fixed_market_bid_cost\", 18487.236, 30.0, 31.0),\n    ]\n    for (name, sys_name, cost_reference, my_no_load, my_initial_input) in test_cases\n        @testset \"$name\" begin\n            sys = PSB.build_system(PSITestSystems, \"c_$(sys_name)\")\n            unit1 = get_component(ThermalStandard, sys, \"Test Unit1\")\n            verify_market_bid_cost_models(\n                sys,\n                unit1,\n                cost_reference,\n                my_no_load,\n                my_initial_input,\n            )\n        end\n    end\nend\n\n@testset \"Test Renewable Dispatch MarketBidCost models\" begin\n    test_cases = [\n        (\"Base case\", \"fixed_market_bid_cost\", 18487.236, 30.0, 30.0),\n        (\"Greater initial input, no load\", \"fixed_market_bid_cost\", 18487.236, 31.0, 31.0),\n        (\"Greater initial input only\", \"fixed_market_bid_cost\", 18487.236, 30.0, 31.0),\n    ]\n    for (name, sys_name, _, my_no_load, my_initial_input) in test_cases\n        @testset \"$name\" begin\n            sys = build_system(PSITestSystems, \"c_$(sys_name)\")\n            unit1 = get_component(ThermalStandard, sys, \"Test Unit1\")\n            replace_with_renewable!(sys, unit1)\n            rg1 = get_component(PSY.RenewableDispatch, sys, \"RG1\")\n            test_market_bid_cost_models(\n                sys,\n                rg1,\n                my_no_load,\n                my_initial_input,\n            )\n        end\n    end\nend\n\n@testset \"Compare Renewable and Standard Thermal MarketBidCost\" begin\n    (name, sys_name) = (\"Base case\", \"fixed_market_bid_cost\")\n    sys = build_system(PSITestSystems, \"c_$(sys_name)\")\n    unit1 = get_component(ThermalStandard, sys, \"Test Unit1\")\n    replace_with_renewable!(sys, unit1; use_thermal_max_power = true)\n    rg1 = get_component(PSY.RenewableDispatch, sys, \"RG1\")\n    zero_out_non_incremental_curve!(sys, rg1)\n    set_name!(sys, \"sys_renewable\")\n    results_renewable = test_market_bid_cost_models(\n        sys,\n        rg1,\n        0.0,\n        0.0;\n        skip_setting = true,\n    )\n\n    sys_thermal = build_system(PSITestSystems, \"c_$(sys_name)\")\n    unit1 = get_component(ThermalStandard, sys_thermal, \"Test Unit1\")\n    set_active_power_limits!(\n        unit1,\n        (min = 0.0, max = get_active_power_limits(unit1).max),\n    )\n    set_operation_cost!(unit1, deepcopy(get_operation_cost(rg1)))\n    set_name!(sys_thermal, \"sys_thermal\")\n\n    results_thermal = test_market_bid_cost_models(\n        sys_thermal,\n        unit1,\n        0.0,\n        0.0;\n        skip_setting = true,\n    )\n\n    # check that the operation costs are the same.\n    IS.compare_values(get_operation_cost(rg1), get_operation_cost(unit1))\n    for thermal_unit in get_components(ThermalStandard, sys)\n        sys_thermal_unit =\n            get_component(ThermalStandard, sys_thermal, get_name(thermal_unit))\n        IS.compare_values(\n            thermal_unit,\n            sys_thermal_unit,\n        )\n    end\n    for load in get_components(PSY.PowerLoad, sys)\n        IS.compare_values(load, get_component(PSY.PowerLoad, sys_thermal, get_name(load)))\n    end\n\n    @test isapprox(PSI.read_optimizer_stats(results_thermal)[!, \"objective_value\"],\n        PSI.read_optimizer_stats(results_renewable)[!, \"objective_value\"])\nend\n\n\"\"\"\nRun a simple simulation with the system and return information useful for testing\ntime-varying startup and shutdown functionality. Pass `simulation = false` to use a single\ndecision model, `true` for a full simulation.\n\"\"\"\nfunction run_startup_shutdown_test(\n    sys::System;\n    multistart::Bool = false,\n    simulation = true,\n    in_memory_store::Bool = false,\n)\n    model, res = if simulation\n        run_generic_mbc_sim(sys; multistart = multistart, in_memory_store = in_memory_store)\n    else\n        run_generic_mbc_prob(sys; multistart = multistart)\n    end\n\n    # Test correctness of written shutdown cost parameters\n    # TODO test startup too once we are able to write those\n    gentype = multistart ? ThermalMultiStart : ThermalStandard\n    genname = multistart ? \"115_STEAM_1\" : \"Test Unit1\"\n    sh_param = read_parameter_dict(res, PSI.ShutdownCostParameter, gentype)\n    for (step_dt, step_df) in pairs(sh_param)\n        for gen_name in unique(step_df.name)\n            comp = get_component(gentype, sys, gen_name)\n            fc_comp =\n                get_shut_down(comp, PSY.get_operation_cost(comp); start_time = step_dt)\n            @test all(step_df[!, :DateTime] .== TimeSeries.timestamp(fc_comp))\n            @test all(\n                isapprox.(\n                    @rsubset(step_df, :name == gen_name).value,\n                    TimeSeries.values(fc_comp),\n                ),\n            )\n        end\n    end\n\n    # These decisions need to be equal between certain pairs of problems/simulations and also need to be approx_geq_1\n    decisions = if multistart\n        (\n            _read_one_value(res, PSI.HotStartVariable, gentype, genname),\n            _read_one_value(res, PSI.WarmStartVariable, gentype, genname),\n            _read_one_value(res, PSI.ColdStartVariable, gentype, genname),\n            _read_one_value(res, PSI.StopVariable, gentype, genname),\n            _read_one_value(res, PSI.OnVariable, gentype, genname),\n        )\n    else\n        (\n            _read_one_value(res, PSI.StartVariable, gentype, genname),\n            _read_one_value(res, PSI.StopVariable, gentype, genname),\n            _read_one_value(res, PSI.OnVariable, gentype, genname),\n        )\n    end\n\n    # 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\n    nullable_decisions = if multistart\n        (\n            _read_one_value(res, PSI.PowerAboveMinimumVariable, gentype, genname),\n            # sometimes useful for debugging clarity to check *another* generator's decisions\n            _read_one_value(res, PSI.OnVariable, gentype, \"101_CT_1\"),\n        )\n    else\n        ()\n    end\n    return model, res, decisions, nullable_decisions\nend\n\n\"Read the relevant startup variables: no multistart case\"\n_read_start_vars(::Val{false}, res::IS.Results) =\n    read_variable_dict(res, PSI.StartVariable, ThermalStandard)\n\n\"Read the relevant startup variables: yes multistart case\"\nfunction _read_start_vars(::Val{true}, res::IS.Results)\n    hot_vars =\n        read_variable_dict(res, PSI.HotStartVariable, ThermalMultiStart)\n    warm_vars =\n        read_variable_dict(res, PSI.WarmStartVariable, ThermalMultiStart)\n    cold_vars =\n        read_variable_dict(res, PSI.ColdStartVariable, ThermalMultiStart)\n\n    @assert all(keys(hot_vars) .== keys(warm_vars))\n    @assert all(keys(hot_vars) .== keys(cold_vars))\n    @assert all(\n        all(hot_vars[k][!, :DateTime] .== warm_vars[k][!, :DateTime]) for\n        k in keys(hot_vars)\n    )\n    @assert all(\n        all(hot_vars[k][!, :DateTime] .== cold_vars[k][!, :DateTime]) for\n        k in keys(hot_vars)\n    )\n    combined_vars = Dict{DateTime, DataFrame}()\n    for timestamp in keys(hot_vars)\n        hot = hot_vars[timestamp]\n        warm = warm_vars[timestamp]\n        cold = cold_vars[timestamp]\n        combined_vars[timestamp] = @chain DataFrames.rename(hot, :value => :hot) begin\n            innerjoin(DataFrames.rename(warm, :value => :warm); on = [:DateTime, :name])\n            innerjoin(\n                DataFrames.rename(cold, :value => :cold);\n                on = [:DateTime, :name],\n            )\n            @transform(@byrow(:value = (:hot, :warm, :cold)))\n            @select(:DateTime, :name, :value)\n        end\n    end\n    return combined_vars\nend\n\n\"\"\"\nRead startup and shutdown cost time series from a `System` and multiply by relevant start\nand stop variables in the `IS.Results` to determine the cost that should have been incurred\nby time-varying `MarketBidCost` startup and shutdown costs. Must run separately for\nmultistart vs. not.\n\"\"\"\nfunction cost_due_to_time_varying_startup_shutdown(\n    sys::System,\n    res::IS.Results;\n    multistart = false,\n)\n    gentype = multistart ? ThermalMultiStart : ThermalStandard\n    start_vars = _read_start_vars(Val(multistart), res)\n    stop_vars = read_variable_dict(res, PSI.StopVariable, gentype)\n    result = SortedDict{DateTime, DataFrame}()\n    IS.@assert_op Set(collect(keys(start_vars))) == Set(collect(keys(stop_vars)))\n    for step_dt in keys(start_vars)\n        start_df = start_vars[step_dt]\n        stop_df = stop_vars[step_dt]\n        @assert unique(start_df.name) == unique(stop_df.name)\n        @assert start_df[!, :DateTime] == stop_df[!, :DateTime]\n        timestamps = unique(start_df.DateTime)\n        component_names = unique(start_df.name)\n        dfs = Vector{DataFrame}()\n        for gen_name in component_names\n            comp = get_component(gentype, sys, gen_name)\n            cost = PSY.get_operation_cost(comp)\n            (cost isa PSY.MarketBidCost) || continue\n            PSI.is_time_variant(get_start_up(cost)) || continue\n            @assert PSI.is_time_variant(get_shut_down(cost))\n            startup_ts = get_start_up(comp, cost; start_time = step_dt)\n            shutdown_ts = get_shut_down(comp, cost; start_time = step_dt)\n\n            @assert all(unique(start_df.DateTime) .== TimeSeries.timestamp(startup_ts))\n            @assert all(unique(start_df.DateTime) .== TimeSeries.timestamp(shutdown_ts))\n            startup_values = if multistart\n                TimeSeries.values(startup_ts)\n            else\n                getproperty.(TimeSeries.values(startup_ts), :hot)\n            end\n            push!(\n                dfs,\n                DataFrame(\n                    :DateTime => timestamps,\n                    :name => repeat([gen_name], length(timestamps)),\n                    :value =>\n                        LinearAlgebra.dot.(\n                            @rsubset(start_df, :name == gen_name).value,\n                            startup_values,\n                        ) .+\n                        @rsubset(stop_df, :name == gen_name).value .*\n                        TimeSeries.values(shutdown_ts),\n                ),\n            )\n        end\n        if !isempty(dfs)\n            result[step_dt] = vcat(dfs...)\n        end\n    end\n    return result\nend\n\n\"\"\"\nThe methodology here is: run a model or simulation where the startup and shutdown time\nseries have constant values through time, then run a nearly identical model/simulation where\nthe values vary very slightly through time, not enough to affect the decisions but enough to\naffect the objective value, then compare the size of the objective value change to an\nexpectation computed manually.\n\nPass `simulation = false` to use a single decision model, `true` for a full simulation.\nPass `in_memory_store = true` to use an in-memory store for the simulation. Default is HDF5.\n\"\"\"\nfunction run_startup_shutdown_obj_fun_test(\n    sys1,\n    sys2;\n    multistart::Bool = false,\n    simulation = true,\n    in_memory_store::Bool = false,\n)\n    _, res1, decisions1, nullable_decisions1 =\n        run_startup_shutdown_test(\n            sys1;\n            multistart = multistart,\n            simulation = simulation,\n            in_memory_store = in_memory_store,\n        )\n    _, res2, decisions2, nullable_decisions2 =\n        run_startup_shutdown_test(\n            sys2;\n            multistart = multistart,\n            simulation = simulation,\n            in_memory_store = in_memory_store,\n        )\n\n    all_decisions1 = (decisions1..., nullable_decisions1...)\n    all_decisions2 = (decisions2..., nullable_decisions2...)\n\n    if !all(isapprox.(all_decisions1, all_decisions2; atol = 1))\n        @error all_decisions1\n        @error all_decisions2\n        # Given the solver tolerance, this method can result in up to 1 change in the commitment result\n        @assert false \"Decisions between constant and time-varying startup/shutdown do not match approximately\"\n    end\n\n    # The last decision is the objetive function we can test that with a smaller tolerance\n    @test (isapprox(all_decisions1[end], all_decisions2[end]; atol = 1e-3))\n\n    ground_truth_1 =\n        cost_due_to_time_varying_startup_shutdown(sys1, res1; multistart = multistart)\n    ground_truth_2 =\n        cost_due_to_time_varying_startup_shutdown(sys2, res2; multistart = multistart)\n\n    obj_fun_test_helper(ground_truth_1, ground_truth_2, res1, res2)\n    return decisions1, decisions2\nend\n\n@testset \"MarketBidCost with time series startup and shutdown, ThermalStandard\" begin\n    # Test that constant time series has the same objective value as no time series\n    sys0 = load_and_fix_system(PSITestSystems, \"c_fixed_market_bid_cost\")\n    tweak_for_startup_shutdown!(sys0)\n    cost = get_operation_cost(get_component(ThermalStandard, sys0, \"Test Unit1\"))\n    set_start_up!(cost, (hot = 1.0, warm = 1.5, cold = 2.0))\n    set_shut_down!(cost, 0.5)\n    sys1 = load_and_fix_system(PSITestSystems, \"c_fixed_market_bid_cost\")\n    tweak_for_startup_shutdown!(sys1)\n    add_startup_shutdown_ts_a!(sys1, false)\n    test_generic_mbc_equivalence(sys0, sys1; multistart = false)\n\n    # Test that perturbing the time series perturbs the objective value as expected\n    sys2 = load_and_fix_system(PSITestSystems, \"c_fixed_market_bid_cost\")\n    tweak_for_startup_shutdown!(sys2)\n    add_startup_shutdown_ts_a!(sys2, true)\n\n    for use_simulation in (false, true)\n        in_memory_store_opts = use_simulation ? [false, true] : [false]\n        for in_memory_store in in_memory_store_opts\n            (decisions1, decisions2) =\n                run_startup_shutdown_obj_fun_test(\n                    sys1,\n                    sys2;\n                    simulation = use_simulation,\n                    in_memory_store = in_memory_store,\n                )\n            # Make sure our tests included sufficent startups and shutdowns\n            @assert all(approx_geq_1.(decisions1))\n        end\n    end\nend\n\n@testset \"MarketBidCost with time series startup and shutdown, ThermalMultiStart\" begin\n    # The arguments to create_multistart_sys were tuned empirically to ensure (a) the\n    # behavior under test is exercised and (b) the small perturbations to the costs aren't\n    # enough to change the decisions that form the correct solution\n\n    # Scenario 1: hot and warm starts\n    # TODO the process to empirically tune these values so the tests work everywhere is\n    # absolutely horrible, we need a more robust system ASAP\n    # https://github.com/Sienna-Platform/PowerSimulations.jl/issues/1460\n    load_pow_mult_a = 1.01\n    therm_pow_mult_a = 1.07\n    therm_price_mult_a = 7.40\n    c_sys5_pglib0a = create_multistart_sys(\n        false,\n        load_pow_mult_a,\n        therm_pow_mult_a,\n        therm_price_mult_a;\n        add_ts = false,\n    )\n    c_sys5_pglib1a =\n        create_multistart_sys(false, load_pow_mult_a, therm_pow_mult_a, therm_price_mult_a)\n    c_sys5_pglib2a =\n        create_multistart_sys(true, load_pow_mult_a, therm_pow_mult_a, therm_price_mult_a)\n\n    # Scenario 2: hot and cold starts\n    load_pow_mult_b = 1.05\n    therm_pow_mult_b = 1.0\n    therm_price_mult_b = 7.4\n    c_sys5_pglib0b = create_multistart_sys(\n        false,\n        load_pow_mult_b,\n        therm_pow_mult_b,\n        therm_price_mult_b;\n        add_ts = false,\n    )\n    c_sys5_pglib1b =\n        create_multistart_sys(false, load_pow_mult_b, therm_pow_mult_b, therm_price_mult_b)\n    c_sys5_pglib2b =\n        create_multistart_sys(true, load_pow_mult_b, therm_pow_mult_b, therm_price_mult_b)\n\n    test_generic_mbc_equivalence(c_sys5_pglib0a, c_sys5_pglib1a; multistart = true)\n    test_generic_mbc_equivalence(c_sys5_pglib0b, c_sys5_pglib1b; multistart = true)\n\n    for use_simulation in (false, true)\n        (decisions1, decisions2) = run_startup_shutdown_obj_fun_test(\n            c_sys5_pglib1a,\n            c_sys5_pglib2a;\n            multistart = true,\n            simulation = use_simulation,\n        )\n        # 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:\n\n        (decisions1_2, decisions2_2) = run_startup_shutdown_obj_fun_test(\n            c_sys5_pglib1b,\n            c_sys5_pglib2b;\n            multistart = true,\n            simulation = use_simulation,\n        )\n        @test all(isapprox.(decisions1, decisions2))\n        @test all(isapprox.(decisions1_2, decisions2_2))\n        # Make sure our tests included all types of startups and shutdowns\n        @test all(approx_geq_1.(decisions1 .+ decisions1_2))\n    end\nend\n\n@testset \"MarketBidCost incremental ThermalStandard, no time series versus constant time series\" begin\n    sys_no_ts = load_sys_incr()\n    set_name!(sys_no_ts, \"thermal_no_ts\")\n    sys_constant_ts = build_sys_incr(false, false, false)\n    set_name!(sys_constant_ts, \"thermal_constant_ts\")\n    test_generic_mbc_equivalence(\n        sys_no_ts,\n        sys_constant_ts,\n    )\nend\n\n@testset \"MarketBidCost incremental RenewableDispatch, no time series versus constant time series\" begin\n    sys_no_ts = load_sys_incr()\n    sys_constant_ts = build_sys_incr(false, false, false)\n    for sys in (sys_no_ts, sys_constant_ts)\n        unit1 = get_component(SEL_INCR, sys)\n        replace_with_renewable!(sys, unit1; magnitude = 1.0, random_variation = 0.1)\n    end\n    test_generic_mbc_equivalence(sys_no_ts, sys_constant_ts)\nend\n\n# debugging option: change to true to save text files of objective functions for\n# certain tests that aren't passing.\nconst SAVE_FILES = false\n\nfor decremental in (false, true)\n    adj = decremental ? \"decremental\" : \"incremental\"\n    build_func = decremental ? build_sys_decr2 : build_sys_incr\n    comp_type = decremental ? InterruptiblePowerLoad : ThermalStandard\n    comp_name = decremental ? \"Bus1_interruptible\" : \"Test Unit1\"\n    device_models = if decremental\n        [PowerLoadInterruption, PowerLoadDispatch]\n    else\n        [ThermalBasicUnitCommitment]\n    end\n    @testset for device_model in device_models\n        device_to_formulation = FormulationDict(comp_type => device_model)\n        init_input_bool = !decremental || device_model != PowerLoadDispatch\n        if init_input_bool\n            @testset \"MarketBidCost $(adj) with time varying min gen cost\" begin\n                baseline = build_func(false, false, false)\n                varying = build_func(true, false, false)\n                if decremental\n                    tweak_for_decremental_initial!(varying)\n                    tweak_for_decremental_initial!(baseline)\n                end\n                for use_simulation in (false, true)\n                    in_memory_store_opts = use_simulation ? [false, true] : [false]\n                    for in_memory_store in in_memory_store_opts\n                        decisions1, decisions2 =\n                            run_mbc_obj_fun_test(\n                                baseline,\n                                varying,\n                                comp_name,\n                                comp_type;\n                                is_decremental = decremental,\n                                has_initial_input = init_input_bool,\n                                simulation = use_simulation,\n                                in_memory_store = in_memory_store,\n                                device_to_formulation = device_to_formulation,\n                            )\n                        if !all(isapprox.(decisions1, decisions2))\n                            @error decisions1\n                            @error decisions2\n                        end\n                        @assert all(approx_geq_1.(decisions1))\n                    end\n                end\n            end\n        end\n\n        @testset \"MarketBidCost $(adj) with time varying slopes\" begin\n            baseline = build_func(false, false, false)\n            varying = build_func(false, false, true)\n\n            set_name!(baseline, \"baseline\")\n            set_name!(varying, \"varying\")\n\n            for use_simulation in (false, true)\n                in_memory_store_opts = use_simulation ? [false, true] : [false]\n                for in_memory_store in in_memory_store_opts\n                    decisions1, decisions2 =\n                        run_mbc_obj_fun_test(\n                            baseline,\n                            varying,\n                            comp_name,\n                            comp_type;\n                            is_decremental = decremental,\n                            has_initial_input = init_input_bool,\n                            simulation = use_simulation,\n                            in_memory_store = in_memory_store,\n                            filename = SAVE_FILES ? \"slopes_\" : nothing,\n                            device_to_formulation = device_to_formulation,\n                        )\n                    if !all(isapprox.(decisions1, decisions2))\n                        @error decisions1\n                        @error decisions2\n                    end\n                    @assert all(approx_geq_1.(decisions1))\n                end\n            end\n        end\n\n        @testset \"MarketBidCost $(adj) with time varying breakpoints\" begin\n            baseline = build_func(false, false, false)\n            varying = build_func(false, true, false)\n\n            set_name!(baseline, \"baseline\")\n            set_name!(varying, \"varying\")\n            for use_simulation in (false, true)\n                in_memory_store_opts = use_simulation ? [false, true] : [false]\n                for in_memory_store in in_memory_store_opts\n                    decisions1, decisions2 =\n                        run_mbc_obj_fun_test(\n                            baseline,\n                            varying,\n                            comp_name,\n                            comp_type;\n                            is_decremental = decremental,\n                            has_initial_input = init_input_bool,\n                            simulation = use_simulation,\n                            in_memory_store = in_memory_store,\n                            filename = SAVE_FILES ? \"breakpoints_\" : nothing,\n                            device_to_formulation = device_to_formulation,\n                        )\n                    if !all(isapprox.(decisions1, decisions2))\n                        @error decisions1\n                        @error decisions2\n                    end\n                    @assert all(approx_geq_1.(decisions1))\n                end\n            end\n        end\n\n        @testset \"MarketBidCost $(adj) with time varying everything\" begin\n            baseline = build_func(false, false, false)\n            varying = build_func(init_input_bool, true, true)\n            set_name!(baseline, \"baseline\")\n            set_name!(varying, \"varying\")\n            for use_simulation in (false, true)\n                decisions1, decisions2 =\n                    run_mbc_obj_fun_test(\n                        baseline,\n                        varying,\n                        comp_name,\n                        comp_type;\n                        simulation = use_simulation,\n                        has_initial_input = init_input_bool,\n                        is_decremental = decremental,\n                        filename = SAVE_FILES ? \"everything_\" : nothing,\n                        device_to_formulation = device_to_formulation,\n                    )\n                if !all(isapprox.(decisions1, decisions2))\n                    @error decisions1\n                    @error decisions2\n                end\n                @assert all(approx_geq_1.(decisions1))\n            end\n        end\n\n        @testset \"MarketBidCost $(adj) with variable number of tranches\" begin\n            baseline = build_func(init_input_bool, true, true)\n            set_name!(baseline, \"baseline\")\n            variable_tranches =\n                build_func(init_input_bool, true, true; create_extra_tranches = true)\n            set_name!(variable_tranches, \"variable\")\n            test_generic_mbc_equivalence(\n                baseline,\n                variable_tranches;\n                filename = SAVE_FILES ? \"tranches_\" : nothing,\n                is_decremental = decremental,\n                device_to_formulation = device_to_formulation,\n            )\n        end\n    end\nend\n\n@testset \"MarketBidCost incremental with heterogeneous time series names\" begin\n    sel = make_selector(x -> get_operation_cost(x) isa MarketBidCost, ThermalStandard)\n    baseline = build_sys_incr(true, true, true; active_components = sel)\n    @assert length(get_components(sel, baseline)) == 2\n\n    # Should succeed for varying initial input time series names:\n    variable_ii_names = build_sys_incr(\n        true,\n        true,\n        true;\n        active_components = sel,\n        initial_input_names_vary = true,\n    )\n    test_generic_mbc_equivalence(baseline, variable_ii_names)\n\n    # Should give an informative error for varying variable cost time series names:\n    variable_vc_names = build_sys_incr(\n        true,\n        true,\n        true;\n        active_components = sel,\n        variable_cost_names_vary = true,\n    )\n    model = build_generic_mbc_model(variable_vc_names; multistart = false)\n    test_path = mktempdir()\n    PSI.set_output_dir!(model, test_path)\n    # Commented out temporarily as the error changed\n    # @test_throws \"All time series names must be equal\" PSI.build_impl!(model)  # see below re: build_impl!\nend\n\n@testset \"Test some MarketBidCost data validations\" begin\n    # Test multistart and convexity validation\n    nonconvex = build_sys_incr(\n        false,\n        false,\n        false;\n        modify_baseline_pwl = pwl -> begin\n            y_coords = get_y_coords(pwl)\n            y_coords[3] = y_coords[1]\n            pwl\n        end,\n    )\n    set_start_up!(\n        get_operation_cost(get_component(ThermalStandard, nonconvex, \"Test Unit2\")),\n        (hot = 1.0, warm = 1.5, cold = 2.0),\n    )\n    model = build_generic_mbc_model(nonconvex; multistart = false)\n    # We'll use build_impl! rather than build! to keep PSI's logging configuration from interfering with @test_logs and polluting the test output\n    mkpath(test_path)\n    PSI.set_output_dir!(model, test_path)\n    @test_logs (:warn, r\"Multi-start costs detected for non-multi-start unit Test Unit2.*\") (\n        match_mode = :any\n    ) (@test_throws \"is non-convex\" PSI.build_impl!(model))\n\n    # Test constant P1 validation\n    variable_p1 = build_sys_incr(false, true, false; do_override_min_x = false)\n    model = build_generic_mbc_model(variable_p1; multistart = false)\n    mkpath(test_path)\n    PSI.set_output_dir!(model, test_path)\n    @test_throws \"Inconsistent minimum breakpoint values\" PSI.build_impl!(model)\nend\n\n@testset \"Test 3d results\" begin\n    # TODO: Test actual values\n    varying = build_sys_incr(true, true, true)\n    for in_memory_store in (false, true)\n        # model1, res1 = run_generic_mbc_sim(baseline)\n        model2, res2 = run_generic_mbc_sim(varying; in_memory_store = in_memory_store)\n        parameters = read_parameters(res2)\n        @test haskey(\n            parameters,\n            \"IncrementalPiecewiseLinearBreakpointParameter__ThermalStandard\",\n        )\n        for df in\n            values(\n            parameters[\"IncrementalPiecewiseLinearBreakpointParameter__ThermalStandard\"],\n        )\n            @test names(df) == [\"DateTime\", \"name\", \"name2\", \"value\"]\n        end\n        for (key, df) in read_realized_parameters(res2)\n            if key in (\n                \"IncrementalPiecewiseLinearBreakpointParameter__ThermalStandard\",\n                \"IncrementalPiecewiseLinearSlopeParameter__ThermalStandard\",\n            )\n                @test names(df) == [\"DateTime\", \"name\", \"name2\", \"value\"]\n            else\n                @test names(df) == [\"DateTime\", \"name\", \"value\"]\n            end\n        end\n\n        # TODO: Test actual values\n    end\nend\n\n@testset \"concavity check error\" begin\n    sys = build_system(PSITestSystems, \"c_sys5_il\")\n    load = first(get_components(PSY.InterruptiblePowerLoad, sys))\n    selector = make_selector(PSY.InterruptiblePowerLoad, get_name(load))\n    non_decr_slopes = [0.13, 0.11, 0.12]  # Non-decreasing slopes (should trigger error)\n    x_coords = [0.1, 0.3, 0.6, 1.0]\n    pw_curve = PiecewiseIncrementalCurve(0.0, 0.0, x_coords, non_decr_slopes)\n    add_mbc_inner!(sys, selector; decr_curve = pw_curve)  # Fixed: pass selector, not slopes\n\n    comp = first(get_components(selector, sys))\n    @assert typeof(get_operation_cost(comp)) == PSY.MarketBidCost\n\n    model = build_generic_mbc_model(sys)\n    mkpath(test_path)\n    PSI.set_output_dir!(model, test_path)\n    msg = \"ArgumentError: Decremental MarketBidCost for component $(get_name(load)) is non-concave\"\n    @test_throws msg PSI.build_impl!(model)\nend\n\n@testset \"MarketBidCost decremental basic: single problem\" begin\n    sys = build_system(PSITestSystems, \"c_sys5_il\")\n    load = first(get_components(PSY.InterruptiblePowerLoad, sys))\n    selector = make_selector(PSY.InterruptiblePowerLoad, get_name(load))\n    add_mbc!(sys, selector; incremental = false, decremental = true)\n    @assert typeof(get_operation_cost(load)) == PSY.MarketBidCost\n    _, res = run_generic_mbc_prob(sys)\nend\n\n@testset \"MarketBidCost decremental basic: simulation\" begin\n    sys = build_system(PSITestSystems, \"c_sys5_il\")\n    load = first(get_components(PSY.InterruptiblePowerLoad, sys))\n    selector = make_selector(PSY.InterruptiblePowerLoad, get_name(load))\n    add_mbc!(sys, selector; incremental = false, decremental = true)\n    extend_mbc!(sys, selector)\n    op_cost = get_operation_cost(load)\n    @assert typeof(op_cost) == PSY.MarketBidCost\n    @assert typeof(get_decremental_offer_curves(op_cost)) <: PSY.TimeSeriesKey\n    _, res = run_generic_mbc_sim(sys)\nend\n\n@testset \"MarketBidCost decremental PowerLoadInterruption, no time series vs constant time series\" begin\n    sys_no_ts = load_sys_decr2()\n    sys_constant_ts = build_sys_decr2(false, false, false)\n    test_generic_mbc_equivalence(sys_no_ts, sys_constant_ts)\nend\n\n# TODO error if there's nonzero decremental initial input for PowerLoadDispatch.\n@testset \"MarketBidCost decremental PowerLoadDispatch, no time series vs constant time series\" begin\n    device_to_formulation = FormulationDict(PSY.InterruptiblePowerLoad => PowerLoadDispatch)\n    sys_no_ts = load_sys_decr2()\n    sys_constant_ts = build_sys_decr2(false, false, false)\n    test_generic_mbc_equivalence(\n        sys_no_ts,\n        sys_constant_ts;\n        device_to_formulation = device_to_formulation,\n    )\nend\n\n@testset \"Test VOM cost time normalization across different resolutions\" begin\n    # Test that VOM costs scale correctly with time resolution\n    # This validates the bugfix in common.jl lines 188-196\n\n    # Build system at hourly resolution\n    sys_hourly = build_system(PSITestSystems, \"c_sys5\")\n\n    # Add VOM cost to a thermal unit\n    thermal_unit = first(get_components(ThermalStandard, sys_hourly))\n    op_cost = get_operation_cost(thermal_unit)\n\n    # Modify the VOM cost on the existing variable cost structure\n    # VOM cost is stored in the CostCurve's vom_cost field\n    if op_cost isa PSY.ThermalGenerationCost\n        var_cost = PSY.get_variable(op_cost)\n        value_curve = PSY.get_value_curve(var_cost)\n        power_units = PSY.get_power_units(var_cost)\n\n        # Create new CostCurve with non-zero VOM (LinearCurve with proportional term = 5.0)\n        vom_value = LinearCurve(5.0)  # $/MWh\n        new_var_cost = CostCurve(value_curve, power_units, vom_value)\n\n        new_op_cost = PSY.ThermalGenerationCost(;\n            variable = new_var_cost,\n            fixed = get_fixed(op_cost),\n            start_up = get_start_up(op_cost),\n            shut_down = get_shut_down(op_cost),\n        )\n        set_operation_cost!(thermal_unit, new_op_cost)\n    end\n\n    # Build and solve at hourly resolution\n    template_hourly = ProblemTemplate(NetworkModel(CopperPlatePowerModel))\n    set_device_model!(template_hourly, ThermalStandard, ThermalDispatchNoMin)\n    set_device_model!(template_hourly, PowerLoad, StaticPowerLoad)\n\n    model_hourly = DecisionModel(\n        template_hourly,\n        sys_hourly;\n        name = \"VOM_hourly\",\n        optimizer = HiGHS_optimizer,\n        optimizer_solve_log_print = false,\n    )\n    @test build!(model_hourly; output_dir = test_path) == PSI.ModelBuildStatus.BUILT\n    @test solve!(model_hourly) == PSI.RunStatus.SUCCESSFULLY_FINALIZED\n\n    results_hourly = OptimizationProblemResults(model_hourly)\n    expr_hourly = read_expression(\n        results_hourly,\n        \"ProductionCostExpression__ThermalStandard\";\n        table_format = TableFormat.WIDE,\n    )\n\n    # Build system at 30-minute resolution (same system, different model resolution)\n    sys_30min = build_system(PSITestSystems, \"c_sys5\")\n\n    # Add same VOM cost to thermal unit\n    thermal_unit_30 = first(get_components(ThermalStandard, sys_30min))\n    op_cost_30 = get_operation_cost(thermal_unit_30)\n\n    if op_cost_30 isa PSY.ThermalGenerationCost\n        var_cost_30 = PSY.get_variable(op_cost_30)\n        value_curve_30 = PSY.get_value_curve(var_cost_30)\n        power_units_30 = PSY.get_power_units(var_cost_30)\n\n        # Create new CostCurve with same VOM cost\n        vom_value_30 = LinearCurve(5.0)  # $/MWh\n        new_var_cost_30 = CostCurve(value_curve_30, power_units_30, vom_value_30)\n\n        new_op_cost_30 = PSY.ThermalGenerationCost(;\n            variable = new_var_cost_30,\n            fixed = get_fixed(op_cost_30),\n            start_up = get_start_up(op_cost_30),\n            shut_down = get_shut_down(op_cost_30),\n        )\n        set_operation_cost!(thermal_unit_30, new_op_cost_30)\n    end\n\n    # Build and solve at 30-minute resolution\n    template_30min = ProblemTemplate(NetworkModel(CopperPlatePowerModel))\n    set_device_model!(template_30min, ThermalStandard, ThermalDispatchNoMin)\n    set_device_model!(template_30min, PowerLoad, StaticPowerLoad)\n\n    model_30min = DecisionModel(\n        template_30min,\n        sys_30min;\n        name = \"VOM_30min\",\n        optimizer = HiGHS_optimizer,\n        optimizer_solve_log_print = false,\n        resolution = Dates.Minute(30),  # Set 30-minute resolution here\n    )\n    @test build!(model_30min; output_dir = test_path) == PSI.ModelBuildStatus.BUILT\n    @test solve!(model_30min) == PSI.RunStatus.SUCCESSFULLY_FINALIZED\n\n    results_30min = OptimizationProblemResults(model_30min)\n    expr_30min = read_expression(\n        results_30min,\n        \"ProductionCostExpression__ThermalStandard\";\n        table_format = TableFormat.WIDE,\n    )\n\n    # Get active power values to compute expected VOM costs\n    p_hourly = read_variable(\n        results_hourly,\n        \"ActivePowerVariable__ThermalStandard\";\n        table_format = TableFormat.WIDE,\n    )\n    p_30min = read_variable(\n        results_30min,\n        \"ActivePowerVariable__ThermalStandard\";\n        table_format = TableFormat.WIDE,\n    )\n\n    # Verify VOM costs scale with resolution\n    # For 30-min resolution, each time step is 0.5 hours, so VOM cost = vom_value * power * 0.5\n    # For hourly resolution, each time step is 1.0 hours, so VOM cost = vom_value * power * 1.0\n\n    unit_name = get_name(thermal_unit)\n\n    # Sum total costs over all time steps\n    total_cost_hourly = sum(expr_hourly[!, unit_name])\n    total_cost_30min = sum(expr_30min[!, unit_name])\n\n    # The total costs should be approximately equal because:\n    # - Hourly: 24 steps × power × VOM × 1.0 hour\n    # - 30-min: 48 steps × power × VOM × 0.5 hour\n    # Both should sum to roughly the same total cost over 24 hours\n\n    @test isapprox(total_cost_hourly, total_cost_30min; rtol = 0.05)\nend\n\n@testset \"Test MarketBidCost VOM cost time normalization across different resolutions\" begin\n    # Test that VOM costs in MarketBidCost (OfferCurveCost path) scale correctly\n    # with time resolution. This validates the bugfix in market_bid.jl\n    # _add_vom_cost_to_objective_helper! (GitHub issue #1531)\n\n    function _build_mbc_vom_system()\n        sys = load_sys_incr()\n        unit = get_component(ThermalStandard, sys, \"Test Unit1\")\n        mbc = get_operation_cost(unit)\n        offer_curves = get_incremental_offer_curves(mbc)\n\n        # Add VOM cost to the existing offer curves\n        new_offer_curves = CostCurve(\n            get_value_curve(offer_curves),\n            get_power_units(offer_curves),\n            LinearCurve(5.0),  # $/MWh VOM cost\n        )\n        set_incremental_offer_curves!(mbc, new_offer_curves)\n        return sys, get_name(unit)\n    end\n\n    # Build and solve at hourly resolution\n    sys_hourly, unit_name = _build_mbc_vom_system()\n\n    template_hourly = ProblemTemplate(NetworkModel(CopperPlatePowerModel))\n    set_device_model!(template_hourly, ThermalStandard, ThermalBasicUnitCommitment)\n    set_device_model!(template_hourly, PowerLoad, StaticPowerLoad)\n\n    model_hourly = DecisionModel(\n        template_hourly,\n        sys_hourly;\n        name = \"MBC_VOM_hourly\",\n        optimizer = HiGHS_optimizer,\n        optimizer_solve_log_print = false,\n    )\n    @test build!(model_hourly; output_dir = test_path) == PSI.ModelBuildStatus.BUILT\n    @test solve!(model_hourly) == PSI.RunStatus.SUCCESSFULLY_FINALIZED\n\n    results_hourly = OptimizationProblemResults(model_hourly)\n    expr_hourly = read_expression(\n        results_hourly,\n        \"ProductionCostExpression__ThermalStandard\";\n        table_format = TableFormat.WIDE,\n    )\n\n    # Build and solve at 30-minute resolution\n    sys_30min, _ = _build_mbc_vom_system()\n\n    template_30min = ProblemTemplate(NetworkModel(CopperPlatePowerModel))\n    set_device_model!(template_30min, ThermalStandard, ThermalBasicUnitCommitment)\n    set_device_model!(template_30min, PowerLoad, StaticPowerLoad)\n\n    model_30min = DecisionModel(\n        template_30min,\n        sys_30min;\n        name = \"MBC_VOM_30min\",\n        optimizer = HiGHS_optimizer,\n        optimizer_solve_log_print = false,\n        resolution = Dates.Minute(30),\n    )\n    @test build!(model_30min; output_dir = test_path) == PSI.ModelBuildStatus.BUILT\n    @test solve!(model_30min) == PSI.RunStatus.SUCCESSFULLY_FINALIZED\n\n    results_30min = OptimizationProblemResults(model_30min)\n    expr_30min = read_expression(\n        results_30min,\n        \"ProductionCostExpression__ThermalStandard\";\n        table_format = TableFormat.WIDE,\n    )\n\n    total_cost_hourly = sum(expr_hourly[!, unit_name])\n    total_cost_30min = sum(expr_30min[!, unit_name])\n\n    # Total costs should be approximately equal:\n    # - Hourly: 24 steps × power × cost × 1.0 hour\n    # - 30-min: 48 steps × power × cost × 0.5 hour\n    @test isapprox(total_cost_hourly, total_cost_30min; rtol = 0.05)\nend\n\n@testset \"Test Market Bid Cost With Single Time Serie\" begin\n    sys = build_system(PSITestSystems, \"c_sys5_uc\"; add_single_time_series = true)\n    existing_ts = get_time_series_array(\n        SingleTimeSeries,\n        first(get_components(PowerLoad, sys)),\n        \"max_active_power\",\n    )\n    tstamps = timestamp(existing_ts)\n    psd1 = PiecewiseStepData([0.0, 40.0], [5.0])\n    psd2 = PiecewiseStepData([0.0, 30.0, 400.0], [10.0, 20.0])\n    psd3 = PiecewiseStepData([0.0, 40.0], [500.0])\n\n    # Cheap the first 10 hours, moderate next 4 hours, expensive last 34 hours\n    total_step_data = vcat([psd1 for x in 1:10], [psd2 for x in 1:4], [psd3 for x in 1:34])\n    mbid_tarray = TimeArray(tstamps, total_step_data)\n    ts_mbid = SingleTimeSeries(; name = \"variable_cost\", data = mbid_tarray)\n\n    th = get_component(ThermalStandard, sys, \"Alta\")\n    # Create an empty market bid and set it\n    th_cost = MarketBidCost(;\n        no_load_cost = 0.0,\n        start_up = (hot = 0.0, warm = 0.0, cold = 0.0),\n        shut_down = 0.0,\n    )\n    set_operation_cost!(th, th_cost)\n    # Wrapper for adding the timeseries in incremental market bid cost\n    set_variable_cost!(sys, th, ts_mbid, UnitSystem.NATURAL_UNITS)\n\n    # 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.\n    zero_input = zeros(length(tstamps))\n    zero_tarray = TimeArray(tstamps, zero_input)\n    ts_zero = SingleTimeSeries(; name = \"initial_input\", data = zero_tarray)\n    set_incremental_initial_input!(sys, th, ts_zero)\n\n    transform_single_time_series!(sys, Hour(24), Hour(24))\n\n    template = ProblemTemplate(NetworkModel(CopperPlatePowerModel))\n    set_device_model!(template, ThermalStandard, ThermalBasicUnitCommitment)\n    set_device_model!(template, HydroDispatch, HydroDispatchRunOfRiver)\n    set_device_model!(template, PowerLoad, StaticPowerLoad)\n\n    model = DecisionModel(\n        template,\n        sys;\n        name = \"UC_MBCost\",\n        optimizer = HiGHS_optimizer)\n    @test build!(model; output_dir = mktempdir()) == PSI.ModelBuildStatus.BUILT\n    @test solve!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED\n\n    p_var = read_variable(\n        OptimizationProblemResults(model),\n        \"ActivePowerVariable__ThermalStandard\";\n        table_format = TableFormat.WIDE,\n    )\n\n    @test isapprox(sum(p_var[!, \"Alta\"][15:24]), 0.0, atol = 1e-4)\nend\n"
  },
  {
    "path": "test/test_mbc_sanity_check.jl",
    "content": "# Test setup\ntest_path = mktempdir()\n\n\"\"\"\nCreate cost curves with varying slope breakpoints, slope magnitudes, and cost at minimum generation\nfor InterruptibleLoad market bid cost testing. Each test scenario should produce measurably \ndifferent objective values when the load curtailment levels change.\n\"\"\"\nfunction build_test_systems_with_different_curves()\n    systems = Dict{String, PSY.System}()\n\n    # Base system\n    base_sys = PSB.build_system(PSITestSystems, \"c_sys5_il\")\n    load_selector = make_selector(PSY.InterruptiblePowerLoad, \"IloadBus4\")\n\n    # Test Case 1: Baseline - moderate slopes, moderate cost at min\n    sys1 = deepcopy(base_sys)\n    x_coords = [0.0, 25.0, 50.0, 75.0, 100.0]  # MW breakpoints\n    slopes = [40.0, 25.0, 15.0, 10.0]  # $/MWh slopes (decreasing for decremental)\n\n    initial_input = 8.0  # $/h cost at minimum generation\n    curve1 = PiecewiseIncrementalCurve(0.0, initial_input, x_coords, slopes)\n    add_mbc_inner!(sys1, load_selector; decr_curve = curve1)\n    systems[\"baseline\"] = sys1\n\n    # Test Case 2: Steeper slopes - should make curtailment more expensive\n    sys2 = deepcopy(base_sys)\n    steep_slopes = [80.0, 50.0, 30.0, 20.0]  # Steeper decreasing slopes\n    curve2 = PiecewiseIncrementalCurve(0.0, initial_input, x_coords, steep_slopes)\n    add_mbc_inner!(sys2, load_selector; decr_curve = curve2)\n    systems[\"steep_slopes\"] = sys2\n\n    # Test Case 3: Different breakpoints - earlier steep decrease\n    sys3 = deepcopy(base_sys)\n    early_steep_x = [0.0, 10.0, 30.0, 60.0, 100.0]  # Earlier transition to low cost\n    early_steep_slopes = [80.0, 60.0, 40.0, 5.0]  # High initial, then very low\n    curve3 =\n        PiecewiseIncrementalCurve(0.0, initial_input, early_steep_x, early_steep_slopes)\n    add_mbc_inner!(sys3, load_selector; decr_curve = curve3)\n    systems[\"early_steep\"] = sys3\n\n    # Test Case 4: Higher cost at minimum generation\n    sys4 = deepcopy(base_sys)\n    high_min_cost = 25.0  # Much higher fixed cost\n    curve4 = PiecewiseIncrementalCurve(0.0, high_min_cost, x_coords, slopes)  # Use the correct decreasing slopes\n    add_mbc_inner!(sys4, load_selector; decr_curve = curve4)\n    systems[\"high_min_cost\"] = sys4\n\n    # Test Case 5: Very flat curve - cheap curtailment\n    sys5 = deepcopy(base_sys)\n    flat_slopes = [5.0, 4.0, 3.0, 2.0]  # Very shallow decreasing slopes\n    low_min_cost = 2.0  # Low fixed cost\n    curve5 = PiecewiseIncrementalCurve(0.0, low_min_cost, x_coords, flat_slopes)\n    add_mbc_inner!(sys5, load_selector; decr_curve = curve5)\n    systems[\"cheap_curtailment\"] = sys5\n\n    return systems\nend\n\nfunction run_test_simulations(systems)\n    results = Dict{String, Any}()\n\n    for (name, sys) in systems\n\n        # Build model\n        # TODO move run_generic_mbc_sim from test_market_bid_cost.jl into test_utils\n        # to prevent against include order \"undefined function\" errors.\n        _, res = run_generic_mbc_sim(sys)\n\n        # Extract key metrics\n        obj_value = read_optimizer_stats(res)[1, \"objective_value\"]\n\n        il = first(get_components(InterruptiblePowerLoad, sys))\n        il_ts = get_time_series(il, first(get_time_series_keys(il)))\n        @assert il !== nothing \"InterruptibleLoad component not found in system\"\n        @assert length(PSY.get_components(InterruptiblePowerLoad, sys)) == 1 \"Expected exactly one InterruptibleLoad component\"\n\n        load_power = read_variable(res, ActivePowerVariable, InterruptiblePowerLoad)\n        load_curtailments = Dict{DateTime, TimeArray}()\n        total_curtailment = 0.0\n\n        for window in iterate_windows(il_ts)\n            initial_time = first(TimeSeries.timestamp(window))\n            load_curtailments[initial_time] = TimeArray(\n                window .- load_power[initial_time][!, :value];\n                colnames = [:value],\n            )\n            total_curtailment += sum(TimeSeries.values(load_curtailments[initial_time]))\n        end\n\n        # Get load cost expression if available\n        load_cost = read_expression(res, \"ProductionCostExpression__InterruptiblePowerLoad\")\n        load_cost_out = Dict(\n            k => TimeArray(v[!, [:DateTime, :value]]; timestamp = :DateTime) for\n            (k, v) in load_cost\n        )\n\n        total_load_cost = sum(sum(v[!, :value]) for v in values(load_cost))\n\n        results[name] = Dict(\n            \"objective\" => obj_value,\n            \"curtailment\" => load_curtailments,\n            \"total_curtailment\" => total_curtailment,\n            \"load_cost\" => load_cost_out,\n            \"total_load_cost\" => total_load_cost,\n        )\n    end\n\n    return results\nend\n\nfunction analyze_results(results)\n    baseline_obj = results[\"baseline\"][\"objective\"]\n    baseline_curtail = results[\"baseline\"][\"total_curtailment\"]\n\n    for (name, data) in results\n        name == \"baseline\" && continue\n\n        obj_diff = data[\"objective\"] - baseline_obj\n        curtail_diff = data[\"total_curtailment\"] - baseline_curtail\n        obj_pct = (obj_diff / baseline_obj) * 100\n\n        # we're minimizing the objective function\n        if name == \"steep_slopes\"\n            @test obj_diff < 0 # steeper demand curve => more $ of benefit per MWh => lower objective\n            @test curtail_diff < 0 # Steeper slopes should reduce curtailment\n        elseif name == \"high_min_cost\"\n            @test obj_diff < 0 # more benefit per MWh => lower objective\n        elseif name == \"cheap_curtailment\"\n            @test obj_diff > 0 # less $ benefit per MWh => higher objective\n            @test curtail_diff > 0 # Cheaper curtailment should increase curtailment amount\n        elseif name == \"early_steep\"\n            # The early steep curve should affect behavior differently depending on curtailment levels\n        end\n    end\nend\n\n@testset \"MBC Sanity Check\" begin\n    systems = build_test_systems_with_different_curves()\n    results = run_test_simulations(systems)\n    analyze_results(results)\nend\n"
  },
  {
    "path": "test/test_model_decision.jl",
    "content": "@testset \"Decision Model kwargs\" begin\n    template = get_thermal_dispatch_template_network()\n    c_sys5 = PSB.build_system(PSITestSystems, \"c_sys5\")\n    c_sys5_re = PSB.build_system(PSITestSystems, \"c_sys5_re\")\n\n    @test_throws MethodError DecisionModel(template, c_sys5; bad_kwarg = 10)\n\n    model = DecisionModel(template, c_sys5; optimizer = HiGHS_optimizer)\n    @test build!(model; output_dir = mktempdir(; cleanup = true)) ==\n          PSI.ModelBuildStatus.BUILT\n\n    model = DecisionModel(\n        MockOperationProblem,\n        get_thermal_dispatch_template_network(\n            NetworkModel(CopperPlatePowerModel; use_slacks = true),\n        ),\n        c_sys5_re;\n        optimizer = HiGHS_optimizer,\n    )\n    @test build!(model; output_dir = mktempdir(; cleanup = true)) ==\n          PSI.ModelBuildStatus.BUILT\n    model = DecisionModel(\n        get_thermal_dispatch_template_network(),\n        c_sys5;\n        optimizer = HiGHS_optimizer,\n    )\n    @test build!(model; output_dir = mktempdir(; cleanup = true)) ==\n          PSI.ModelBuildStatus.BUILT\n\n    #\"Test passing custom JuMP model\"\n    my_model = JuMP.Model()\n    my_model.ext[:PSI_Testing] = 1\n    c_sys5 = PSB.build_system(PSITestSystems, \"c_sys5\")\n    model = DecisionModel(\n        get_thermal_dispatch_template_network(),\n        c_sys5,\n        my_model;\n        optimizer = HiGHS_optimizer,\n    )\n    @test build!(model; output_dir = mktempdir(; cleanup = true)) ==\n          PSI.ModelBuildStatus.BUILT\n    @test haskey(PSI.get_optimization_container(model).JuMPmodel.ext, :PSI_Testing)\nend\n\n@testset \"Set optimizer at solve call\" begin\n    c_sys5 = PSB.build_system(PSITestSystems, \"c_sys5\")\n    template = get_thermal_standard_uc_template()\n    UC = DecisionModel(template, c_sys5; optimizer = HiGHS_optimizer)\n    output_dir = mktempdir(; cleanup = true)\n    @test build!(UC; output_dir = output_dir) == PSI.ModelBuildStatus.BUILT\n    @test solve!(UC; optimizer = HiGHS_optimizer) == PSI.RunStatus.SUCCESSFULLY_FINALIZED\n    res = OptimizationProblemResults(UC)\n    @test isapprox(get_objective_value(res), 340000.0; atol = 100000.0)\n    vars = res.variable_values\n    @test PSI.VariableKey(ActivePowerVariable, PSY.ThermalStandard) in keys(vars)\n    @test size(read_variable(res, \"StartVariable__ThermalStandard\")) == (120, 3)\n    @test size(\n        read_variable(\n            res,\n            \"StartVariable__ThermalStandard\";\n            table_format = TableFormat.WIDE,\n        ),\n    ) == (24, 6)\n    @test size(read_parameter(res, \"ActivePowerTimeSeriesParameter__PowerLoad\")) == (72, 3)\n    @test size(read_expression(res, \"ProductionCostExpression__ThermalStandard\")) ==\n          (120, 3)\n    @test size(read_aux_variable(res, \"TimeDurationOn__ThermalStandard\")) == (120, 3)\n    @test length(read_variables(res; table_format = TableFormat.WIDE)) == 4\n    @test length(read_parameters(res; table_format = TableFormat.WIDE)) == 1\n    @test length(read_duals(res; table_format = TableFormat.WIDE)) == 0\n    @test length(read_expressions(res; table_format = TableFormat.WIDE)) == 7\n    @test read_variables(\n        res,\n        [\"StartVariable__ThermalStandard\"];\n        table_format = TableFormat.WIDE,\n    )[\"StartVariable__ThermalStandard\"] ==\n          read_variable(\n        res,\n        \"StartVariable__ThermalStandard\";\n        table_format = TableFormat.WIDE,\n    )\n    @test read_variables(\n        res,\n        [(StartVariable, ThermalStandard)];\n        table_format = TableFormat.WIDE,\n    )[\"StartVariable__ThermalStandard\"] ==\n          read_variable(\n        res,\n        StartVariable,\n        ThermalStandard;\n        table_format = TableFormat.WIDE,\n    )\n    @test read_parameters(\n        res,\n        [\"ActivePowerTimeSeriesParameter__PowerLoad\"];\n        table_format = TableFormat.WIDE,\n    )[\"ActivePowerTimeSeriesParameter__PowerLoad\"] ==\n          read_parameter(\n        res,\n        \"ActivePowerTimeSeriesParameter__PowerLoad\";\n        table_format = TableFormat.WIDE,\n    )\n    @test read_parameters(\n        res,\n        [(ActivePowerTimeSeriesParameter, PowerLoad)];\n        table_format = TableFormat.WIDE,\n    )[\"ActivePowerTimeSeriesParameter__PowerLoad\"] ==\n          read_parameter(\n        res,\n        ActivePowerTimeSeriesParameter,\n        PowerLoad;\n        table_format = TableFormat.WIDE,\n    )\n    @test read_aux_variables(\n        res,\n        [\"TimeDurationOff__ThermalStandard\"];\n        table_format = TableFormat.WIDE,\n    )[\"TimeDurationOff__ThermalStandard\"] ==\n          read_aux_variable(\n        res,\n        \"TimeDurationOff__ThermalStandard\";\n        table_format = TableFormat.WIDE,\n    )\n    @test read_aux_variables(\n        res,\n        [(TimeDurationOff, ThermalStandard)];\n        table_format = TableFormat.WIDE,\n    )[\"TimeDurationOff__ThermalStandard\"] ==\n          read_aux_variable(\n        res,\n        TimeDurationOff,\n        ThermalStandard;\n        table_format = TableFormat.WIDE,\n    )\n    @test read_expressions(\n        res,\n        [\"ProductionCostExpression__ThermalStandard\"];\n        table_format = TableFormat.WIDE,\n    )[\"ProductionCostExpression__ThermalStandard\"] == read_expression(\n        res,\n        \"ProductionCostExpression__ThermalStandard\";\n        table_format = TableFormat.WIDE,\n    )\n    @test read_expressions(\n        res,\n        [(PSI.ProductionCostExpression, ThermalStandard)];\n        table_format = TableFormat.WIDE,\n    )[\"ProductionCostExpression__ThermalStandard\"] ==\n          read_expression(\n        res,\n        PSI.ProductionCostExpression,\n        ThermalStandard;\n        table_format = TableFormat.WIDE,\n    )\n    @test length(read_aux_variables(res; table_format = TableFormat.WIDE)) == 2\n    @test first(\n        keys(\n            read_aux_variables(\n                res,\n                [(PSI.TimeDurationOff, ThermalStandard)];\n                table_format = TableFormat.WIDE,\n            ),\n        ),\n    ) ==\n          \"TimeDurationOff__ThermalStandard\"\n    export_results(res)\n    results_dir = joinpath(output_dir, \"results\")\n    @test isfile(joinpath(results_dir, \"optimizer_stats.csv\"))\n    variables_dir = joinpath(results_dir, \"variables\")\n    @test isfile(joinpath(variables_dir, \"ActivePowerVariable__ThermalStandard.csv\"))\nend\n\n@testset \"Test optimization debugging functions\" begin\n    c_sys5 = PSB.build_system(PSITestSystems, \"c_sys5\")\n    template = get_thermal_standard_uc_template()\n    model = DecisionModel(template, c_sys5; optimizer = HiGHS_optimizer)\n    @test build!(model; output_dir = mktempdir(; cleanup = true)) ==\n          PSI.ModelBuildStatus.BUILT\n    container = PSI.get_optimization_container(model)\n    MOIU.attach_optimizer(container.JuMPmodel)\n    constraint_indices = get_all_constraint_index(model)\n    for (key, index, moi_index) in constraint_indices\n        val1 = get_constraint_index(model, moi_index)\n        val2 = container.constraints[key].data[index]\n        @test val1 == val2\n    end\n    @test get_constraint_index(model, length(constraint_indices) + 1) === nothing\n\n    var_keys = PSI.get_all_variable_keys(model)\n    var_index = get_all_variable_index(model)\n    for (ix, (key, index, moi_index)) in enumerate(var_keys)\n        index_tuple = var_index[ix]\n        @test index_tuple[1] == ISOPT.encode_key(key)\n        @test index_tuple[2] == index\n        @test index_tuple[3] == moi_index\n        val1 = get_variable_index(model, moi_index)\n        val2 = container.variables[key].data[index]\n        @test val1 == val2\n    end\n    @test get_variable_index(model, length(var_index) + 1) === nothing\nend\n\n@testset \"Decision Model Solve with Slacks\" begin\n    c_sys5_re = PSB.build_system(PSITestSystems, \"c_sys5_re\")\n    networks = [PTDFPowerModel, DCPPowerModel, ACPPowerModel]\n    for network in networks\n        template = get_thermal_dispatch_template_network(\n            NetworkModel(network; use_slacks = true, PTDF_matrix = PTDF(c_sys5_re)),\n        )\n        model = DecisionModel(template, c_sys5_re; optimizer = ipopt_optimizer)\n        @test build!(model; output_dir = mktempdir(; cleanup = true)) ==\n              PSI.ModelBuildStatus.BUILT\n        @test solve!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED\n    end\nend\n\n@testset \"Test Locational Marginal Prices between DC lossless with PowerModels vs PTDFPowerModel\" begin\n    networks = [DCPPowerModel, PTDFPowerModel]\n    sys = PSB.build_system(PSITestSystems, \"c_sys5\")\n    ptdf = VirtualPTDF(sys)\n    # These are the duals of interest for the test\n    dual_constraint = [[NodalBalanceActiveConstraint], [CopperPlateBalanceConstraint]]\n    LMPs = []\n    for (ix, network) in enumerate(networks)\n        template = get_template_dispatch_with_network(\n            NetworkModel(network; PTDF_matrix = ptdf, duals = dual_constraint[ix]),\n        )\n        if network == PTDFPowerModel\n            set_device_model!(\n                template,\n                DeviceModel(PSY.Line, PSI.StaticBranch; duals = [FlowRateConstraint]),\n            )\n        end\n        model = DecisionModel(template, sys; optimizer = HiGHS_optimizer)\n        @test build!(model; output_dir = mktempdir(; cleanup = true)) ==\n              PSI.ModelBuildStatus.BUILT\n        @test solve!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED\n        res = OptimizationProblemResults(model)\n\n        # These tests require results to be working\n        if network == PTDFPowerModel\n            push!(LMPs, abs.(psi_ptdf_lmps(res, ptdf)))\n        else\n            duals = read_dual(\n                res,\n                NodalBalanceActiveConstraint,\n                ACBus;\n                table_format = TableFormat.WIDE,\n            )\n            duals = abs.(duals[:, propertynames(duals) .!== :DateTime])\n            push!(LMPs, duals[!, sort(propertynames(duals))])\n        end\n    end\n    @test isapprox(LMPs[1], LMPs[2], atol = 100.0)\nend\n\n@testset \"Test OptimizationProblemResults interfaces\" begin\n    sys = PSB.build_system(PSITestSystems, \"c_sys5_re\")\n    template = get_template_dispatch_with_network(\n        NetworkModel(CopperPlatePowerModel; duals = [CopperPlateBalanceConstraint]),\n    )\n    model = DecisionModel(template, sys; optimizer = HiGHS_optimizer)\n    @test build!(model; output_dir = mktempdir(; cleanup = true)) ==\n          PSI.ModelBuildStatus.BUILT\n    @test solve!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED\n\n    res = OptimizationProblemResults(model)\n    container = PSI.get_optimization_container(model)\n    constraint_key = PSI.ConstraintKey(CopperPlateBalanceConstraint, PSY.System)\n    constraints = PSI.get_constraints(container)[constraint_key]\n    dual_results = PSI.read_duals(container)[constraint_key]\n    dual_results_read = read_dual(res, constraint_key; table_format = TableFormat.WIDE)\n    realized_dual_results =\n        read_duals(res, [constraint_key]; table_format = TableFormat.WIDE)[PSI.encode_key_as_string(\n            constraint_key,\n        )]\n    realized_dual_results_string =\n        read_duals(\n            res,\n            [PSI.encode_key_as_string(constraint_key)];\n            table_format = TableFormat.WIDE,\n        )[PSI.encode_key_as_string(\n            constraint_key,\n        )]\n    @test dual_results ==\n          dual_results_read[:, propertynames(dual_results_read) .!= :DateTime] ==\n          realized_dual_results[:, propertynames(realized_dual_results) .!= :DateTime] ==\n          realized_dual_results_string[\n              :,\n              propertynames(realized_dual_results_string) .!= :DateTime,\n          ]\n    for i in axes(constraints)[1], j in axes(constraints)[2]\n        dual = JuMP.dual(constraints[i, j])\n        @test isapprox(dual, dual_results[j, 1])\n    end\n\n    system = PSI.get_system(model)\n    parameter_key = PSI.ParameterKey(ActivePowerTimeSeriesParameter, PSY.PowerLoad)\n    param_vals = PSI.read_parameters(container)[parameter_key]\n    for load in get_components(PowerLoad, system)\n        name = get_name(load)\n        vals = get_time_series_values(Deterministic, load, \"max_active_power\")\n        vals = vals .* get_max_active_power(load) * -1.0\n        @test all(vals .== param_vals[name, :])\n    end\n\n    res = OptimizationProblemResults(model)\n    @test length(list_variable_names(res)) == 1\n    @test length(list_dual_names(res)) == 1\n    @test get_model_base_power(res) == 100.0\n    @test isa(get_objective_value(res), Float64)\n    @test isa(res.variable_values, Dict{PSI.VariableKey, DataFrames.DataFrame})\n    @test isa(\n        read_variables(res; table_format = TableFormat.WIDE),\n        Dict{String, DataFrames.DataFrame},\n    )\n    @test isa(ISOPT.get_total_cost(res), Float64)\n    @test isa(get_optimizer_stats(res), DataFrames.DataFrame)\n    @test isa(res.dual_values, Dict{PSI.ConstraintKey, DataFrames.DataFrame})\n    @test isa(\n        read_duals(res; table_format = TableFormat.WIDE),\n        Dict{String, DataFrames.DataFrame},\n    )\n    @test isa(res.parameter_values, Dict{PSI.ParameterKey, DataFrames.DataFrame})\n    @test isa(\n        read_parameters(res; table_format = TableFormat.WIDE),\n        Dict{String, DataFrames.DataFrame},\n    )\n    @test isa(PSI.get_resolution(res), Dates.TimePeriod)\n    @test isa(PSI.get_forecast_horizon(res), Int64)\n    @test isa(get_realized_timestamps(res), StepRange{DateTime})\n    @test isa(ISOPT.get_source_data(res), PSY.System)\n    @test length(get_timestamps(res)) == 24\n\n    PSY.set_available!(first(get_components(ThermalStandard, get_system(res))), false)\n    @test collect(get_components(ThermalStandard, res)) ==\n          collect(get_available_components(ThermalStandard, get_system(res)))\n    sel = PSY.make_selector(ThermalStandard; groupby = :each)\n    @test collect(get_groups(sel, res)) ==\n          collect(get_available_groups(sel, get_system(res)))\nend\n\n@testset \"Solve DecisionModelModel with auto-build\" begin\n    c_sys5 = PSB.build_system(PSITestSystems, \"c_sys5\")\n    template = get_thermal_standard_uc_template()\n    UC = DecisionModel(template, c_sys5; optimizer = HiGHS_optimizer)\n    output_dir = mktempdir(; cleanup = true)\n    @test_throws ErrorException solve!(UC)\n    @test solve!(UC; optimizer = HiGHS_optimizer, output_dir = output_dir) ==\n          PSI.RunStatus.SUCCESSFULLY_FINALIZED\nend\n\n@testset \"Test NonSpinning reseve model\" begin\n    c_sys5 = PSB.build_system(PSITestSystems, \"c_sys5_uc_non_spin\"; add_reserves = true)\n    template = get_thermal_standard_uc_template()\n    set_device_model!(\n        template,\n        DeviceModel(ThermalMultiStart, ThermalStandardUnitCommitment),\n    )\n    set_service_model!(\n        template,\n        ServiceModel(VariableReserveNonSpinning, NonSpinningReserve, \"NonSpinningReserve\"),\n    )\n\n    UC = DecisionModel(template, c_sys5; optimizer = HiGHS_optimizer)\n    output_dir = mktempdir(; cleanup = true)\n    @test build!(UC; output_dir = output_dir) == PSI.ModelBuildStatus.BUILT\n    @test solve!(UC) == PSI.RunStatus.SUCCESSFULLY_FINALIZED\n    res = OptimizationProblemResults(UC)\n    # This test needs to be reviewed\n    # @test isapprox(get_objective_value(res), 256937.0; atol = 10000.0)\n    vars = res.variable_values\n    service_key = PSI.VariableKey(\n        ActivePowerReserveVariable,\n        PSY.VariableReserveNonSpinning,\n        \"NonSpinningReserve\",\n    )\n    @test service_key in keys(vars)\nend\n\n@testset \"Test serialization/deserialization of DecisionModel results\" begin\n    path = mktempdir(; cleanup = true)\n    sys = PSB.build_system(PSITestSystems, \"c_sys5_re\")\n    template = get_template_dispatch_with_network(\n        NetworkModel(CopperPlatePowerModel; duals = [CopperPlateBalanceConstraint]),\n    )\n    model = DecisionModel(template, sys; optimizer = HiGHS_optimizer)\n    @test build!(model; output_dir = path) == PSI.ModelBuildStatus.BUILT\n    @test solve!(model; export_problem_results = true) ==\n          PSI.RunStatus.SUCCESSFULLY_FINALIZED\n    results1 = OptimizationProblemResults(model)\n    var1_a = read_variable(results1, ActivePowerVariable, ThermalStandard)\n    # Ensure that we can deserialize strings into keys.\n    var1_b = read_variable(results1, \"ActivePowerVariable__ThermalStandard\")\n\n    # Results were automatically serialized here.\n    results2 = OptimizationProblemResults(PSI.get_output_dir(model))\n    var2 = read_variable(results2, ActivePowerVariable, ThermalStandard)\n    @test var1_a == var2\n\n    # Serialize to a new directory with the exported function.\n    results_path = joinpath(path, \"results\")\n    serialize_results(results1, results_path)\n    @test isfile(joinpath(results_path, ISOPT._PROBLEM_RESULTS_FILENAME))\n    results3 = OptimizationProblemResults(results_path)\n    var3 = read_variable(results3, ActivePowerVariable, ThermalStandard)\n    @test var1_a == var3\n    @test get_system(results3) === nothing\n    set_system!(results3, get_system(results1))\n    @test get_system(results3) isa PSY.System\n\n    exp_file =\n        joinpath(path, \"results\", \"variables\", \"ActivePowerVariable__ThermalStandard.csv\")\n    var4 = PSI.read_dataframe(exp_file)\n    # Manually Multiply by the base power var1_a has natural units and export writes directly from the solver\n    @test var1_a.value == var4.value .* 100.0\n\n    exported = readdir(ISOPT.export_realized_results(results1))\n    @test length(exported) >= 12\n    @test any(contains.(exported, \"ProductionCostExpression\"))\nend\n\n@testset \"Test Numerical Stability of Constraints\" begin\n    template = get_thermal_dispatch_template_network()\n    c_sys5 = PSB.build_system(PSITestSystems, \"c_sys5\")\n    valid_bounds =\n        (coefficient = (min = 1.0, max = 1.0), rhs = (min = 0.4, max = 9.930296584))\n    model = DecisionModel(template, c_sys5; optimizer = HiGHS_optimizer)\n    @test build!(model; output_dir = mktempdir(; cleanup = true)) ==\n          PSI.ModelBuildStatus.BUILT\n\n    bounds = PSI.get_constraint_numerical_bounds(model)\n    _check_constraint_bounds(bounds, valid_bounds)\n\n    model_bounds = PSI.get_detailed_constraint_numerical_bounds(model)\n    valid_model_bounds = Dict(\n        :CopperPlateBalanceConstraint__System => (\n            coefficient = (min = 1.0, max = 1.0),\n            rhs = (min = 6.434489705000001, max = 9.930296584),\n        ),\n        :ActivePowerVariableLimitsConstraint__ThermalStandard__lb =>\n            (coefficient = (min = 1.0, max = 1.0), rhs = (min = Inf, max = -Inf)),\n        :ActivePowerVariableLimitsConstraint__ThermalStandard__ub =>\n            (coefficient = (min = 1.0, max = 1.0), rhs = (min = 0.4, max = 6.0)),\n    )\n    for (constraint_key, constraint_bounds) in model_bounds\n        _check_constraint_bounds(\n            constraint_bounds,\n            valid_model_bounds[ISOPT.encode_key(constraint_key)],\n        )\n    end\nend\n\n@testset \"Test Numerical Stability of Variables\" begin\n    template = get_template_basic_uc_simulation()\n    c_sys5 = PSB.build_system(PSITestSystems, \"c_sys5_uc\")\n    valid_bounds = (min = 0.0, max = 6.0)\n    model = DecisionModel(template, c_sys5; optimizer = HiGHS_optimizer)\n    @test build!(model; output_dir = mktempdir(; cleanup = true)) ==\n          PSI.ModelBuildStatus.BUILT\n\n    bounds = PSI.get_variable_numerical_bounds(model)\n    _check_variable_bounds(bounds, valid_bounds)\n\n    model_bounds = PSI.get_detailed_variable_numerical_bounds(model)\n    valid_model_bounds = Dict(\n        :StopVariable__ThermalStandard => (min = 0.0, max = 1.0),\n        :StartVariable__ThermalStandard => (min = 0.0, max = 1.0),\n        :ActivePowerVariable__ThermalStandard => (min = 0.4, max = 6.0),\n        :OnVariable__ThermalStandard => (min = 0.0, max = 1.0),\n    )\n    for (variable_key, variable_bounds) in model_bounds\n        _check_variable_bounds(\n            variable_bounds,\n            valid_model_bounds[ISOPT.encode_key(variable_key)],\n        )\n    end\nend\n\n@testset \"Decision Model initial_conditions test for ThermalGen\" begin\n    ######## Test with ThermalStandardUnitCommitment ########\n    template = get_thermal_standard_uc_template()\n    c_sys5_uc = PSB.build_system(PSITestSystems, \"c_sys5_pglib\"; force_build = true)\n    set_device_model!(template, ThermalMultiStart, ThermalStandardUnitCommitment)\n    model = DecisionModel(template, c_sys5_uc; optimizer = HiGHS_optimizer)\n    @test build!(model; output_dir = mktempdir(; cleanup = true)) ==\n          PSI.ModelBuildStatus.BUILT\n    check_duration_on_initial_conditions_values(model, ThermalStandard)\n    check_duration_off_initial_conditions_values(model, ThermalStandard)\n    @test solve!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED\n\n    ######## Test with ThermalMultiStartUnitCommitment ########\n    template = get_thermal_standard_uc_template()\n    c_sys5_uc = PSB.build_system(PSITestSystems, \"c_sys5_pglib\"; force_build = true)\n    set_device_model!(template, ThermalMultiStart, ThermalMultiStartUnitCommitment)\n    model = DecisionModel(template, c_sys5_uc; optimizer = HiGHS_optimizer)\n    @test build!(model; output_dir = mktempdir(; cleanup = true)) ==\n          PSI.ModelBuildStatus.BUILT\n\n    check_duration_on_initial_conditions_values(model, ThermalStandard)\n    check_duration_off_initial_conditions_values(model, ThermalStandard)\n    check_duration_on_initial_conditions_values(model, ThermalMultiStart)\n    check_duration_off_initial_conditions_values(model, ThermalMultiStart)\n    @test solve!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED\n\n    ######## Test with ThermalStandardUnitCommitment ########\n    template = get_thermal_standard_uc_template()\n    c_sys5_uc = PSB.build_system(PSITestSystems, \"c_sys5_pglib\"; force_build = true)\n    set_device_model!(template, ThermalMultiStart, ThermalStandardUnitCommitment)\n    set_device_model!(template, ThermalStandard, ThermalStandardUnitCommitment)\n    model = DecisionModel(template, c_sys5_uc; optimizer = HiGHS_optimizer)\n    @test build!(model; output_dir = mktempdir(; cleanup = true)) ==\n          PSI.ModelBuildStatus.BUILT\n    check_duration_on_initial_conditions_values(model, ThermalStandard)\n    check_duration_off_initial_conditions_values(model, ThermalStandard)\n    check_duration_on_initial_conditions_values(model, ThermalMultiStart)\n    check_duration_off_initial_conditions_values(model, ThermalMultiStart)\n    @test solve!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED\nend\n\n@testset \"Decision Model initial_conditions test for Hydro\" begin\n    ######## Test with HydroDispatchRunOfRiver ########\n    template = get_thermal_dispatch_template_network()\n    c_sys5_hyd = PSB.build_system(PSITestSystems, \"c_sys5_hyd\"; force_build = true)\n    set_device_model!(template, HydroDispatch, HydroDispatchRunOfRiver)\n    set_device_model!(template, HydroTurbine, HydroTurbineEnergyDispatch)\n    set_device_model!(template, HydroReservoir, HydroEnergyModelReservoir)\n    model = DecisionModel(template, c_sys5_hyd; optimizer = HiGHS_optimizer)\n    @test build!(model; output_dir = mktempdir(; cleanup = true)) ==\n          PSI.ModelBuildStatus.BUILT\n    initial_conditions_data =\n        PSI.get_initial_conditions_data(PSI.get_optimization_container(model))\n    @test !PSI.has_initial_condition_value(\n        initial_conditions_data,\n        ActivePowerVariable(),\n        HydroTurbine,\n    )\n    @test solve!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED\n\n    ######## Test with HydroCommitmentRunOfRiver ########\n    template = get_thermal_dispatch_template_network()\n    c_sys5_hyd = PSB.build_system(PSITestSystems, \"c_sys5_hyd\"; force_build = true)\n    set_device_model!(template, HydroDispatch, HydroCommitmentRunOfRiver)\n    set_device_model!(template, HydroTurbine, HydroTurbineEnergyCommitment)\n    set_device_model!(template, HydroReservoir, HydroEnergyModelReservoir)\n    model = DecisionModel(template, c_sys5_hyd; optimizer = HiGHS_optimizer)\n\n    @test build!(model; output_dir = mktempdir(; cleanup = true)) ==\n          PSI.ModelBuildStatus.BUILT\n    initial_conditions_data =\n        PSI.get_initial_conditions_data(PSI.get_optimization_container(model))\n    @test PSI.has_initial_condition_value(\n        initial_conditions_data,\n        OnVariable(),\n        HydroTurbine,\n    )\n    @test solve!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED\nend\n\n@testset \"Test serialization of InitialConditionsData\" begin\n    sys = PSB.build_system(PSITestSystems, \"c_sys5\")\n    template = get_thermal_standard_uc_template()\n    optimizer = HiGHS_optimizer\n\n    # Construct and build with default behavior that builds initial conditions.\n    model = DecisionModel(template, sys; optimizer = optimizer)\n    output_dir = mktempdir(; cleanup = true)\n\n    @test build!(model; output_dir = output_dir) == PSI.ModelBuildStatus.BUILT\n    ic_file = PSI.get_initial_conditions_file(model)\n    test_ic_serialization_outputs(model; ic_file_exists = true, message = \"make\")\n    @test solve!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED\n\n    # Build again. Initial conditions should be rebuilt.\n    PSI.reset!(model)\n    @test build!(model; output_dir = output_dir) == PSI.ModelBuildStatus.BUILT\n    test_ic_serialization_outputs(model; ic_file_exists = true, message = \"make\")\n    @test solve!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED\n\n    # Build again, use existing initial conditions.\n    model = DecisionModel(\n        template,\n        sys;\n        optimizer = optimizer,\n        deserialize_initial_conditions = true,\n    )\n    @test build!(model; output_dir = output_dir) == PSI.ModelBuildStatus.BUILT\n    test_ic_serialization_outputs(model; ic_file_exists = true, message = \"deserialize\")\n    @test solve!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED\n\n    # Construct and build again with custom initial conditions file.\n    initialization_file = joinpath(output_dir, ic_file * \".old\")\n    mv(ic_file, initialization_file)\n    touch(ic_file)\n    model = DecisionModel(\n        template,\n        sys;\n        optimizer = optimizer,\n        initialization_file = initialization_file,\n        deserialize_initial_conditions = true,\n    )\n    @test build!(model; output_dir = output_dir) == PSI.ModelBuildStatus.BUILT\n    test_ic_serialization_outputs(model; ic_file_exists = true, message = \"deserialize\")\n    @test solve!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED\n\n    # Construct and build again while skipping build of initial conditions.\n    rm(ic_file)\n    model = DecisionModel(template, sys; optimizer = optimizer, initialize_model = false)\n    @test build!(model; output_dir = output_dir) == PSI.ModelBuildStatus.BUILT\n    test_ic_serialization_outputs(model; ic_file_exists = false, message = \"skip\")\n    @test solve!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED\n\n    # Conflicting inputs\n    model = DecisionModel(\n        template,\n        sys;\n        optimizer = optimizer,\n        initialize_model = false,\n        deserialize_initial_conditions = true,\n    )\n    @test build!(model; output_dir = output_dir, console_level = Logging.AboveMaxLevel) ==\n          PSI.ModelBuildStatus.FAILED\n    model = DecisionModel(\n        template,\n        sys;\n        optimizer = optimizer,\n        initialize_model = false,\n        initialization_file = \"init_file.bin\",\n    )\n    build!(model; output_dir = output_dir, console_level = Logging.AboveMaxLevel) ==\n    PSI.ModelBuildStatus.FAILED\nend\n\n@testset \"Solve with detailed optimizer stats\" begin\n    c_sys5 = PSB.build_system(PSITestSystems, \"c_sys5\")\n    template = get_thermal_standard_uc_template()\n    UC = DecisionModel(\n        template,\n        c_sys5;\n        optimizer = HiGHS_optimizer,\n        detailed_optimizer_stats = true,\n    )\n    output_dir = mktempdir(; cleanup = true)\n    @test build!(UC; output_dir = output_dir) == PSI.ModelBuildStatus.BUILT\n    @test solve!(UC) == PSI.RunStatus.SUCCESSFULLY_FINALIZED\n    # We only test this field because most free solvers don't support detailed stats\n    @test !ismissing(get_optimizer_stats(UC).objective_bound)\nend\n\n@testset \"Test filter function atttribute\" begin\n    c_sys5 = PSB.build_system(PSITestSystems, \"c_sys5\")\n    template = get_thermal_standard_uc_template()\n    new_model = DeviceModel(\n        ThermalStandard,\n        ThermalBasicUnitCommitment;\n        attributes = Dict(\"filter_function\" => x -> PSY.get_name(x) != \"Alta\"),\n    )\n    set_device_model!(template, new_model)\n    UC = DecisionModel(\n        template,\n        c_sys5;\n        optimizer = HiGHS_optimizer,\n        detailed_optimizer_stats = true,\n    )\n    output_dir = mktempdir(; cleanup = true)\n    @test build!(UC; output_dir = output_dir) == PSI.ModelBuildStatus.BUILT\n    @test solve!(UC) == PSI.RunStatus.SUCCESSFULLY_FINALIZED\n    # We only test this field because most free solvers don't support detailed stats\n    p_variable = PSI.get_variable(\n        PSI.get_optimization_container(UC),\n        ActivePowerVariable(),\n        ThermalStandard,\n    )\n    @test \"Alta\" ∉ axes(p_variable, 1)\nend\n\n@testset \"Test for isolated buses\" begin\n    c_sys5 = PSB.build_system(PSITestSystems, \"c_sys5\")\n    add_component!(c_sys5,\n        ACBus(\n            10,\n            \"node_none\",\n            true,\n            \"ISOLATED\",\n            0,\n            1.0,\n            (min = 0.9, max = 1.05),\n            230,\n            nothing,\n            nothing,\n        ),\n    )\n\n    template = get_thermal_standard_uc_template()\n    new_model = DeviceModel(\n        ThermalStandard,\n        ThermalBasicUnitCommitment;\n    )\n    set_device_model!(template, new_model)\n    UC = DecisionModel(\n        template,\n        c_sys5;\n        optimizer = HiGHS_optimizer,\n        detailed_optimizer_stats = true,\n    )\n    output_dir = mktempdir(; cleanup = true)\n    @test build!(UC; output_dir = output_dir) == PSI.ModelBuildStatus.BUILT\n    @test solve!(UC) == PSI.RunStatus.SUCCESSFULLY_FINALIZED\nend\n\n@testset \"Test for single row result variables\" begin\n    template = get_thermal_dispatch_template_network()\n    c_sys5_bat = PSB.build_system(PSITestSystems, \"c_sys5_bat_ems\"; force_build = true)\n    device_model = DeviceModel(\n        EnergyReservoirStorage,\n        StorageDispatchWithReserves;\n        attributes = Dict{String, Any}(\n            \"reservation\" => true,\n            \"cycling_limits\" => false,\n            \"energy_target\" => true,\n            \"complete_coverage\" => false,\n            \"regularization\" => false,\n        ),\n    )\n    set_device_model!(template, device_model)\n    output_dir = mktempdir(; cleanup = true)\n    model = DecisionModel(\n        template,\n        c_sys5_bat;\n        optimizer = HiGHS_optimizer,\n    )\n    @test build!(model; output_dir = output_dir) == PSI.ModelBuildStatus.BUILT\n    @test solve!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED\n    res = OptimizationProblemResults(model)\n    shortage = read_variable(res, \"StorageEnergyShortageVariable__EnergyReservoirStorage\")\n    @test nrow(shortage) == 1\nend\n"
  },
  {
    "path": "test/test_model_emulation.jl",
    "content": "@testset \"Emulation Model Build\" begin\n    template = get_thermal_dispatch_template_network()\n    c_sys5 = PSB.build_system(\n        PSITestSystems,\n        \"c_sys5_uc\";\n        add_single_time_series = true,\n        force_build = true,\n    )\n\n    model = EmulationModel(template, c_sys5; optimizer = HiGHS_optimizer)\n    @test build!(model; executions = 10, output_dir = mktempdir(; cleanup = true)) ==\n          PSI.ModelBuildStatus.BUILT\n    @test run!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED\n\n    template = get_thermal_standard_uc_template()\n    c_sys5_uc_re = PSB.build_system(\n        PSITestSystems,\n        \"c_sys5_uc_re\";\n        add_single_time_series = true,\n        force_build = true,\n    )\n    set_device_model!(template, RenewableDispatch, RenewableFullDispatch)\n    model = EmulationModel(template, c_sys5_uc_re; optimizer = HiGHS_optimizer)\n\n    @test build!(model; executions = 10, output_dir = mktempdir(; cleanup = true)) ==\n          PSI.ModelBuildStatus.BUILT\n    @test run!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED\n    @test !isempty(collect(readdir(PSI.get_recorder_dir(model))))\nend\n\n@testset \"Emulation Model initial_conditions test for ThermalGen\" begin\n    ######## Test with ThermalStandardUnitCommitment ########\n    template = get_thermal_standard_uc_template()\n    c_sys5_uc_re = PSB.build_system(\n        PSITestSystems,\n        \"c_sys5_uc_re\";\n        add_single_time_series = true,\n        force_build = true,\n    )\n    set_device_model!(template, RenewableDispatch, RenewableFullDispatch)\n    model = EmulationModel(template, c_sys5_uc_re; optimizer = HiGHS_optimizer)\n    @test build!(model; executions = 10, output_dir = mktempdir(; cleanup = true)) ==\n          PSI.ModelBuildStatus.BUILT\n    check_duration_on_initial_conditions_values(model, ThermalStandard)\n    check_duration_off_initial_conditions_values(model, ThermalStandard)\n    @test run!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED\n\n    ######## Test with ThermalMultiStartUnitCommitment ########\n    template = get_thermal_standard_uc_template()\n    c_sys5_uc = PSB.build_system(\n        PSITestSystems,\n        \"c_sys5_pglib\";\n        add_single_time_series = true,\n        force_build = true,\n    )\n    set_device_model!(template, ThermalMultiStart, ThermalMultiStartUnitCommitment)\n    model = EmulationModel(template, c_sys5_uc; optimizer = HiGHS_optimizer)\n    @test build!(model; executions = 1, output_dir = mktempdir(; cleanup = true)) ==\n          PSI.ModelBuildStatus.BUILT\n\n    check_duration_on_initial_conditions_values(model, ThermalStandard)\n    check_duration_off_initial_conditions_values(model, ThermalStandard)\n    check_duration_on_initial_conditions_values(model, ThermalMultiStart)\n    check_duration_off_initial_conditions_values(model, ThermalMultiStart)\n    @test run!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED\n\n    ######## Test with ThermalStandardUnitCommitment ########\n    template = get_thermal_standard_uc_template()\n    c_sys5_uc = PSB.build_system(\n        PSITestSystems,\n        \"c_sys5_pglib\";\n        add_single_time_series = true,\n        force_build = true,\n    )\n    set_device_model!(template, ThermalMultiStart, ThermalStandardUnitCommitment)\n    model = EmulationModel(template, c_sys5_uc; optimizer = HiGHS_optimizer)\n    @test build!(model; executions = 1, output_dir = mktempdir(; cleanup = true)) ==\n          PSI.ModelBuildStatus.BUILT\n    check_duration_on_initial_conditions_values(model, ThermalStandard)\n    check_duration_off_initial_conditions_values(model, ThermalStandard)\n    check_duration_on_initial_conditions_values(model, ThermalMultiStart)\n    check_duration_off_initial_conditions_values(model, ThermalMultiStart)\n    @test run!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED\n\n    ######## Test with ThermalStandardDispatch ########\n    template = get_thermal_standard_uc_template()\n    c_sys5_uc = PSB.build_system(\n        PSITestSystems,\n        \"c_sys5_pglib\";\n        add_single_time_series = true,\n        force_build = true,\n    )\n    device_model = DeviceModel(PSY.ThermalStandard, PSI.ThermalStandardDispatch)\n    set_device_model!(template, device_model)\n    model = EmulationModel(template, c_sys5_uc; optimizer = HiGHS_optimizer)\n    @test build!(model; executions = 10, output_dir = mktempdir(; cleanup = true)) ==\n          PSI.ModelBuildStatus.BUILT\nend\n\n@testset \"Emulation Model initial_conditions test for Hydro\" begin\n    ######## Test with HydroDispatchRunOfRiver ########\n    template = get_thermal_dispatch_template_network()\n    c_sys5_hyd = PSB.build_system(\n        PSITestSystems,\n        \"c_sys5_hyd\";\n        add_single_time_series = true,\n        force_build = true,\n    )\n    set_device_model!(template, HydroDispatch, HydroDispatchRunOfRiver)\n    set_device_model!(template, HydroTurbine, HydroTurbineEnergyDispatch)\n    set_device_model!(template, HydroReservoir, HydroEnergyModelReservoir)\n    model = EmulationModel(template, c_sys5_hyd; optimizer = HiGHS_optimizer)\n    @test build!(model; executions = 10, output_dir = mktempdir(; cleanup = true)) ==\n          PSI.ModelBuildStatus.BUILT\n    initial_conditions_data =\n        PSI.get_initial_conditions_data(PSI.get_optimization_container(model))\n    @test !PSI.has_initial_condition_value(\n        initial_conditions_data,\n        ActivePowerVariable(),\n        HydroTurbine,\n    )\n    @test run!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED\n\n    ######## Test with HydroCommitmentRunOfRiver ########\n    template = get_thermal_dispatch_template_network()\n    c_sys5_hyd = PSB.build_system(\n        PSITestSystems,\n        \"c_sys5_hyd\";\n        add_single_time_series = true,\n        force_build = true,\n    )\n    set_device_model!(template, HydroDispatch, HydroCommitmentRunOfRiver)\n    set_device_model!(template, HydroTurbine, HydroTurbineEnergyCommitment)\n    set_device_model!(template, HydroReservoir, HydroEnergyModelReservoir)\n    model = EmulationModel(template, c_sys5_hyd; optimizer = HiGHS_optimizer)\n\n    @test build!(model; executions = 10, output_dir = mktempdir(; cleanup = true)) ==\n          PSI.ModelBuildStatus.BUILT\n    initial_conditions_data =\n        PSI.get_initial_conditions_data(PSI.get_optimization_container(model))\n    @test PSI.has_initial_condition_value(\n        initial_conditions_data,\n        OnVariable(),\n        HydroTurbine,\n    )\n    @test run!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED\nend\n\n@testset \"Emulation Model Results\" begin\n    template = get_thermal_dispatch_template_network()\n    c_sys5 = PSB.build_system(\n        PSITestSystems,\n        \"c_sys5_uc\";\n        add_single_time_series = true,\n        force_build = true,\n    )\n\n    model = EmulationModel(template, c_sys5; optimizer = HiGHS_optimizer)\n    executions = 10\n    @test build!(\n        model;\n        executions = executions,\n        output_dir = mktempdir(; cleanup = true),\n    ) ==\n          PSI.ModelBuildStatus.BUILT\n    @test run!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED\n    results = OptimizationProblemResults(model)\n    @test list_aux_variable_names(results) == []\n    @test list_aux_variable_keys(results) == []\n    @test list_variable_names(results) == [\"ActivePowerVariable__ThermalStandard\"]\n    @test list_variable_keys(results) ==\n          [PSI.VariableKey(ActivePowerVariable, ThermalStandard)]\n    @test list_dual_names(results) == []\n    @test list_dual_keys(results) == []\n    @test list_parameter_names(results) == [\"ActivePowerTimeSeriesParameter__PowerLoad\"]\n    @test list_parameter_keys(results) ==\n          [PSI.ParameterKey(ActivePowerTimeSeriesParameter, PowerLoad)]\n\n    @test read_variable(results, \"ActivePowerVariable__ThermalStandard\") isa DataFrame\n    @test read_variable(results, ActivePowerVariable, ThermalStandard) isa DataFrame\n    @test read_variable(\n        results,\n        PSI.VariableKey(ActivePowerVariable, ThermalStandard),\n    ) isa\n          DataFrame\n\n    @test read_parameter(results, \"ActivePowerTimeSeriesParameter__PowerLoad\") isa DataFrame\n    @test read_parameter(results, ActivePowerTimeSeriesParameter, PowerLoad) isa DataFrame\n    @test read_parameter(\n        results,\n        PSI.ParameterKey(ActivePowerTimeSeriesParameter, PowerLoad),\n    ) isa DataFrame\n\n    @test read_optimizer_stats(model) isa DataFrame\n    for n in names(read_optimizer_stats(model))\n        stats_values = read_optimizer_stats(model)[!, n]\n        if any(ismissing.(stats_values))\n            @test ismissing.(stats_values) ==\n                  ismissing.(read_optimizer_stats(results)[!, n])\n        elseif any(isnan.(stats_values))\n            @test isnan.(stats_values) == isnan.(read_optimizer_stats(results)[!, n])\n        else\n            @test stats_values == read_optimizer_stats(results)[!, n]\n        end\n    end\n\n    for i in 1:executions\n        @test get_objective_value(results, i) isa Float64\n    end\nend\n\n@testset \"Run EmulationModel with auto-build\" begin\n    for serialize in (true, false)\n        template = get_thermal_dispatch_template_network()\n        c_sys5 = PSB.build_system(\n            PSITestSystems,\n            \"c_sys5_uc\";\n            add_single_time_series = true,\n            force_build = true,\n        )\n\n        model = EmulationModel(template, c_sys5; optimizer = HiGHS_optimizer)\n        @test_throws ErrorException run!(model, executions = 10)\n        @test run!(\n            model;\n            executions = 10,\n            output_dir = mktempdir(; cleanup = true),\n            export_optimization_model = serialize,\n        ) == PSI.RunStatus.SUCCESSFULLY_FINALIZED\n    end\nend\n\n@testset \"Test serialization/deserialization of EmulationModel results\" begin\n    path = mktempdir(; cleanup = true)\n    template = get_thermal_dispatch_template_network()\n    c_sys5 = PSB.build_system(\n        PSITestSystems,\n        \"c_sys5_uc\";\n        add_single_time_series = true,\n        force_build = true,\n    )\n\n    model = EmulationModel(template, c_sys5; optimizer = HiGHS_optimizer)\n    executions = 10\n    @test build!(model; executions = executions, output_dir = path) ==\n          PSI.ModelBuildStatus.BUILT\n    @test run!(model; export_problem_results = true) == PSI.RunStatus.SUCCESSFULLY_FINALIZED\n    results1 = OptimizationProblemResults(model)\n    var1_a = read_variable(results1, ActivePowerVariable, ThermalStandard)\n    # Ensure that we can deserialize strings into keys.\n    var1_b = read_variable(results1, \"ActivePowerVariable__ThermalStandard\")\n    @test var1_a == var1_b\n\n    # Results were automatically serialized here.\n    results2 = OptimizationProblemResults(PSI.get_output_dir(model))\n    var2 = read_variable(results2, ActivePowerVariable, ThermalStandard)\n    @test var1_a == var2\n    @test get_system(results2) === nothing\n    # Commented out for now, as we no longer automatically serialize the system with results, but this should be added back in the future.\n    #get_system!(results2)\n    #@test get_system(results2) isa PSY.System\n\n    # Serialize to a new directory with the exported function.\n    results_path = joinpath(path, \"results\")\n    serialize_results(results1, results_path)\n    @test isfile(joinpath(results_path, ISOPT._PROBLEM_RESULTS_FILENAME))\n    results3 = OptimizationProblemResults(results_path)\n    var3 = read_variable(results3, ActivePowerVariable, ThermalStandard)\n    @test var1_a == var3\n    @test get_system(results3) === nothing\n    set_system!(results3, get_system(results1))\n    @test get_system(results3) !== nothing\n\n    exp_file =\n        joinpath(path, \"results\", \"variables\", \"ActivePowerVariable__ThermalStandard.csv\")\n    var4 = PSI.read_dataframe(exp_file)\n    # Manually Multiply by the base power var1_a has natural units and export writes directly from the solver\n    @test var1_a.value == var4.value .* 100.0\nend\n\n@testset \"Test deserialization and re-run of EmulationModel\" begin\n    path = mktempdir(; cleanup = true)\n    template = get_thermal_dispatch_template_network()\n    c_sys5 = PSB.build_system(\n        PSITestSystems,\n        \"c_sys5_uc\";\n        add_single_time_series = true,\n        force_build = true,\n    )\n\n    model = EmulationModel(template, c_sys5; optimizer = HiGHS_optimizer)\n    executions = 10\n    @test build!(model; executions = executions, output_dir = path) ==\n          PSI.ModelBuildStatus.BUILT\n    @test run!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED\n    results = OptimizationProblemResults(model)\n    var1 = read_variable(results, ActivePowerVariable, ThermalStandard)\n\n    file_list = sort!(collect(readdir(path)))\n    @test PSI._JUMP_MODEL_FILENAME in file_list\nend\n\n@testset \"Test serialization of InitialConditionsData\" begin\n    template = get_thermal_standard_uc_template()\n    sys = PSB.build_system(\n        PSITestSystems,\n        \"c_sys5_pglib\";\n        add_single_time_series = true,\n        force_build = true,\n    )\n    optimizer = HiGHS_optimizer\n    set_device_model!(template, ThermalMultiStart, ThermalMultiStartUnitCommitment)\n    model = EmulationModel(template, sys; optimizer = HiGHS_optimizer)\n    output_dir = mktempdir(; cleanup = true)\n\n    @test build!(model; executions = 1, output_dir = output_dir) ==\n          PSI.ModelBuildStatus.BUILT\n    ic_file = PSI.get_initial_conditions_file(model)\n    test_ic_serialization_outputs(model; ic_file_exists = true, message = \"make\")\n    @test run!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED\n\n    # Build again, use existing initial conditions.\n    PSI.reset!(model)\n    @test build!(model; executions = 1, output_dir = output_dir) ==\n          PSI.ModelBuildStatus.BUILT\n    test_ic_serialization_outputs(model; ic_file_exists = true, message = \"make\")\n    @test run!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED\n\n    # Build again, use existing initial conditions.\n    model = EmulationModel(\n        template,\n        sys;\n        optimizer = optimizer,\n        deserialize_initial_conditions = true,\n    )\n    @test build!(model; executions = 1, output_dir = output_dir) ==\n          PSI.ModelBuildStatus.BUILT\n    test_ic_serialization_outputs(model; ic_file_exists = true, message = \"deserialize\")\n    @test run!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED\n\n    # Construct and build again with custom initial conditions file.\n    initialization_file = joinpath(output_dir, ic_file * \".old\")\n    mv(ic_file, initialization_file)\n    touch(ic_file)\n    model = EmulationModel(\n        template,\n        sys;\n        optimizer = optimizer,\n        initialization_file = initialization_file,\n        deserialize_initial_conditions = true,\n    )\n    @test build!(model; executions = 1, output_dir = output_dir) ==\n          PSI.ModelBuildStatus.BUILT\n    test_ic_serialization_outputs(model; ic_file_exists = true, message = \"deserialize\")\n\n    # Construct and build again while skipping build of initial conditions.\n    model = EmulationModel(template, sys; optimizer = optimizer, initialize_model = false)\n    rm(ic_file)\n    @test build!(model; executions = 1, output_dir = output_dir) ==\n          PSI.ModelBuildStatus.BUILT\n    test_ic_serialization_outputs(model; ic_file_exists = false, message = \"skip\")\n    @test run!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED\nend\n"
  },
  {
    "path": "test/test_multi_interval.jl",
    "content": "@testset \"Multi-interval validation errors\" begin\n    c_sys5 = PSB.build_system(PSITestSystems, \"c_sys5\"; add_single_time_series = true)\n    transform_single_time_series!(c_sys5, Hour(24), Hour(12); delete_existing = false)\n    transform_single_time_series!(c_sys5, Hour(24), Hour(24); delete_existing = false)\n\n    template = get_thermal_dispatch_template_network()\n\n    # Without interval kwarg on a multi-interval system, should error\n    @test_throws IS.ConflictingInputsError DecisionModel(\n        template,\n        c_sys5;\n        optimizer = HiGHS_optimizer,\n        horizon = Hour(24),\n    )\n\n    # With a non-existent interval on a system that has no SingleTimeSeries to auto-transform from, should error.\n    c_sys5_forecasts = PSB.build_system(PSITestSystems, \"c_sys5\"; add_forecasts = true)\n    @test_throws IS.ConflictingInputsError DecisionModel(\n        template,\n        c_sys5_forecasts;\n        optimizer = HiGHS_optimizer,\n        horizon = Hour(24),\n        interval = Hour(6),\n    )\nend\n\n@testset \"DecisionModel with explicit interval\" begin\n    c_sys5 = PSB.build_system(PSITestSystems, \"c_sys5\"; add_single_time_series = true)\n    transform_single_time_series!(c_sys5, Hour(24), Hour(12); delete_existing = false)\n    transform_single_time_series!(c_sys5, Hour(24), Hour(24); delete_existing = false)\n\n    template = get_thermal_dispatch_template_network()\n\n    # Build with interval = 24h\n    model_24h = DecisionModel(\n        template,\n        c_sys5;\n        optimizer = HiGHS_optimizer,\n        horizon = Hour(24),\n        interval = Hour(24),\n    )\n    @test build!(model_24h; output_dir = mktempdir(; cleanup = true)) ==\n          PSI.ModelBuildStatus.BUILT\n    @test solve!(model_24h) == PSI.RunStatus.SUCCESSFULLY_FINALIZED\n\n    # Build with interval = 12h on the same system\n    model_12h = DecisionModel(\n        template,\n        c_sys5;\n        optimizer = HiGHS_optimizer,\n        horizon = Hour(24),\n        interval = Hour(12),\n    )\n    @test build!(model_12h; output_dir = mktempdir(; cleanup = true)) ==\n          PSI.ModelBuildStatus.BUILT\n    @test solve!(model_12h) == PSI.RunStatus.SUCCESSFULLY_FINALIZED\n\n    # Verify models have the correct interval in store params\n    @test PSI.get_interval(model_24h) == Dates.Millisecond(Hour(24))\n    @test PSI.get_interval(model_12h) == Dates.Millisecond(Hour(12))\nend\n\n@testset \"Auto-transform SingleTimeSeries with interval\" begin\n    c_sys5 = PSB.build_system(PSITestSystems, \"c_sys5\"; add_single_time_series = true)\n    template = get_thermal_dispatch_template_network()\n\n    model = DecisionModel(\n        template,\n        c_sys5;\n        optimizer = HiGHS_optimizer,\n        horizon = Hour(24),\n        interval = Hour(24),\n    )\n    @test build!(model; output_dir = mktempdir(; cleanup = true)) ==\n          PSI.ModelBuildStatus.BUILT\n    @test solve!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED\n\n    # Second model with a different interval reuses the same system and auto-transforms.\n    model2 = DecisionModel(\n        template,\n        c_sys5;\n        optimizer = HiGHS_optimizer,\n        horizon = Hour(24),\n        interval = Hour(12),\n    )\n    @test build!(model2; output_dir = mktempdir(; cleanup = true)) ==\n          PSI.ModelBuildStatus.BUILT\n    @test solve!(model2) == PSI.RunStatus.SUCCESSFULLY_FINALIZED\nend\n\n@testset \"RTS system shared across two intervals - build only\" begin\n    sys_rts = PSB.build_system(PSISystems, \"modified_RTS_GMLC_DA_sys\")\n    # Clear any pre-existing transform so we can attach two fresh intervals.\n    PSY.transform_single_time_series!(sys_rts, Hour(24), Hour(24); delete_existing = true)\n    PSY.transform_single_time_series!(sys_rts, Hour(24), Hour(12); delete_existing = false)\n\n    template = get_template_standard_uc_simulation()\n    set_network_model!(template, NetworkModel(CopperPlatePowerModel))\n\n    model_24h = DecisionModel(\n        template,\n        sys_rts;\n        name = \"UC_24h\",\n        optimizer = HiGHS_optimizer,\n        horizon = Hour(24),\n        interval = Hour(24),\n    )\n    @test build!(model_24h; output_dir = mktempdir(; cleanup = true)) ==\n          PSI.ModelBuildStatus.BUILT\n    @test PSI.get_interval(model_24h) == Dates.Millisecond(Hour(24))\n\n    model_12h = DecisionModel(\n        template,\n        sys_rts;\n        name = \"UC_12h\",\n        optimizer = HiGHS_optimizer,\n        horizon = Hour(24),\n        interval = Hour(12),\n    )\n    @test build!(model_12h; output_dir = mktempdir(; cleanup = true)) ==\n          PSI.ModelBuildStatus.BUILT\n    @test PSI.get_interval(model_12h) == Dates.Millisecond(Hour(12))\n\n    # Same underlying system, different intervals selected per model.\n    @test get_system(model_24h) === get_system(model_12h)\nend\n\n@testset \"Single interval system works without interval kwarg\" begin\n    # Backward compatibility: existing single-interval systems work without changes\n    c_sys5 = PSB.build_system(PSITestSystems, \"c_sys5\")\n    template = get_thermal_dispatch_template_network()\n    model = DecisionModel(template, c_sys5; optimizer = HiGHS_optimizer)\n    @test build!(model; output_dir = mktempdir(; cleanup = true)) ==\n          PSI.ModelBuildStatus.BUILT\n    @test solve!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED\nend\n"
  },
  {
    "path": "test/test_network_constructors.jl",
    "content": "# Note to devs. Use HiGHS for models with linear constraints and linear cost functions\n# Use OSQP for models with quadratic cost function and linear constraints and ipopt otherwise\n\n@testset \"All PowerModels models construction\" begin\n    c_sys5 = PSB.build_system(PSITestSystems, \"c_sys5\")\n    for (network, solver) in NETWORKS_FOR_TESTING\n        template = get_thermal_dispatch_template_network(\n            NetworkModel(network; PTDF_matrix = PTDF(c_sys5)),\n        )\n        ps_model = DecisionModel(template, c_sys5; optimizer = solver)\n        @test build!(ps_model; output_dir = mktempdir(; cleanup = true)) ==\n              PSI.ModelBuildStatus.BUILT\n        opt_container = PSI.get_optimization_container(ps_model)\n        @test opt_container.pm !== nothing\n        @test PSI.has_container_key(opt_container, ActivePowerBalance, ACBus)\n    end\nend\n\n@testset \"Network Copper Plate\" begin\n    template = get_thermal_dispatch_template_network(CopperPlatePowerModel)\n    c_sys5 = PSB.build_system(PSITestSystems, \"c_sys5\")\n    c_sys14 = PSB.build_system(PSITestSystems, \"c_sys14\")\n    c_sys14_dc = PSB.build_system(PSITestSystems, \"c_sys14_dc\")\n    systems = [c_sys5, c_sys14, c_sys14_dc]\n    test_results = IdDict{System, Vector{Int}}(\n        c_sys5 => [120, 0, 120, 120, 24],\n        c_sys14 => [120, 0, 120, 120, 24],\n        c_sys14_dc => [120, 0, 120, 120, 24],\n    )\n    constraint_keys = [PSI.ConstraintKey(CopperPlateBalanceConstraint, PSY.System)]\n    objfuncs = [GAEVF, GQEVF, GQEVF]\n    test_obj_values = IdDict{System, Float64}(\n        c_sys5 => 240000.0,\n        c_sys14 => 142000.0,\n        c_sys14_dc => 142000.0,\n    )\n\n    for (ix, sys) in enumerate(systems)\n        ps_model = DecisionModel(template, sys; optimizer = HiGHS_optimizer)\n\n        @test build!(ps_model; output_dir = mktempdir(; cleanup = true)) ==\n              PSI.ModelBuildStatus.BUILT\n        psi_constraint_test(ps_model, constraint_keys)\n        moi_tests(\n            ps_model,\n            test_results[sys]...,\n            false,\n        )\n        psi_checkobjfun_test(ps_model, objfuncs[ix])\n        psi_checksolve_test(ps_model, [MOI.OPTIMAL], test_obj_values[sys], 10000)\n    end\n    template = get_thermal_dispatch_template_network(\n        NetworkModel(CopperPlatePowerModel; use_slacks = true),\n    )\n    ps_model_re = DecisionModel(\n        template,\n        PSB.build_system(PSITestSystems, \"c_sys5_re\");\n        optimizer = HiGHS_optimizer,\n    )\n    @test build!(ps_model_re; output_dir = mktempdir(; cleanup = true)) ==\n          PSI.ModelBuildStatus.BUILT\n    psi_checksolve_test(ps_model_re, [MOI.OPTIMAL], 240000.0, 10000)\nend\n\n@testset \"Network DC-PF with PTDF Model\" begin\n    template = get_thermal_dispatch_template_network(PTDFPowerModel)\n    c_sys5 = PSB.build_system(PSITestSystems, \"c_sys5\")\n    c_sys14 = PSB.build_system(PSITestSystems, \"c_sys14\")\n    c_sys14_dc = PSB.build_system(PSITestSystems, \"c_sys14_dc\")\n    systems = [c_sys5, c_sys14, c_sys14_dc]\n    constraint_keys = [\n        PSI.ConstraintKey(FlowRateConstraint, PSY.Line, \"lb\"),\n        PSI.ConstraintKey(FlowRateConstraint, PSY.Line, \"ub\"),\n        PSI.ConstraintKey(CopperPlateBalanceConstraint, PSY.System),\n    ]\n    PTDF_ref = IdDict{System, PTDF}(\n        c_sys5 => PTDF(c_sys5),\n        c_sys14 => PTDF(c_sys14),\n        c_sys14_dc => PTDF(c_sys14_dc),\n    )\n    test_results = IdDict{System, Vector{Int}}(\n        c_sys5 => [120, 0, 264, 264, 24],\n        c_sys14 => [120, 0, 600, 600, 24],\n        c_sys14_dc => [168, 0, 648, 552, 24],\n    )\n    test_obj_values = IdDict{System, Float64}(\n        c_sys5 => 240000.0,\n        c_sys14 => 142000.0,\n        c_sys14_dc => 142000.0,\n    )\n    test_objfuncs_types = IdDict{System, Type}(\n        c_sys5 => GAEVF,\n        c_sys14 => GQEVF,\n        c_sys14_dc => GQEVF,\n    )\n    for sys in systems\n        template = get_thermal_dispatch_template_network(\n            NetworkModel(PTDFPowerModel; PTDF_matrix = PTDF_ref[sys]),\n        )\n        ps_model = DecisionModel(template, sys; optimizer = HiGHS_optimizer)\n\n        @test build!(ps_model; output_dir = mktempdir(; cleanup = true)) ==\n              PSI.ModelBuildStatus.BUILT\n        psi_constraint_test(ps_model, constraint_keys)\n        moi_tests(\n            ps_model,\n            test_results[sys]...,\n            false,\n        )\n        psi_checkobjfun_test(ps_model, test_objfuncs_types[sys])\n        psi_checksolve_test(\n            ps_model,\n            [MOI.OPTIMAL, MOI.ALMOST_OPTIMAL],\n            test_obj_values[sys],\n            10000,\n        )\n    end\nend\n\n@testset \"Network DC-PF with VirtualPTDF Model\" begin\n    c_sys5 = PSB.build_system(PSITestSystems, \"c_sys5\")\n    c_sys14 = PSB.build_system(PSITestSystems, \"c_sys14\")\n    c_sys14_dc = PSB.build_system(PSITestSystems, \"c_sys14_dc\")\n    systems = [c_sys5, c_sys14, c_sys14_dc]\n    objfuncs = [GAEVF, GQEVF, GQEVF]\n    constraint_keys = [\n        PSI.ConstraintKey(FlowRateConstraint, PSY.Line, \"lb\"),\n        PSI.ConstraintKey(FlowRateConstraint, PSY.Line, \"ub\"),\n        PSI.ConstraintKey(CopperPlateBalanceConstraint, PSY.System),\n    ]\n    PTDF_ref = IdDict{System, VirtualPTDF}(\n        c_sys5 => VirtualPTDF(c_sys5),\n        c_sys14 => VirtualPTDF(c_sys14),\n        c_sys14_dc => VirtualPTDF(c_sys14_dc),\n    )\n    test_results = IdDict{System, Vector{Int}}(\n        c_sys5 => [120, 0, 264, 264, 24],\n        c_sys14 => [120, 0, 600, 600, 24],\n        c_sys14_dc => [168, 0, 648, 552, 24],\n    )\n    test_obj_values = IdDict{System, Float64}(\n        c_sys5 => 240000.0,\n        c_sys14 => 142000.0,\n        c_sys14_dc => 142000.0,\n    )\n    test_objfuncs_types = IdDict{System, Type}(\n        c_sys5 => GAEVF,\n        c_sys14 => GQEVF,\n        c_sys14_dc => GQEVF,\n    )\n    for (ix, sys) in enumerate(systems)\n        template = get_thermal_dispatch_template_network(PTDFPowerModel)\n        template = get_thermal_dispatch_template_network(\n            NetworkModel(PTDFPowerModel; PTDF_matrix = PTDF_ref[sys]),\n        )\n        ps_model = DecisionModel(template, sys; optimizer = HiGHS_optimizer)\n\n        @test build!(ps_model; output_dir = mktempdir(; cleanup = true)) ==\n              PSI.ModelBuildStatus.BUILT\n        psi_constraint_test(ps_model, constraint_keys)\n        moi_tests(\n            ps_model,\n            test_results[sys]...,\n            false,\n        )\n        psi_checkobjfun_test(ps_model, objfuncs[ix])\n        psi_checksolve_test(\n            ps_model,\n            [MOI.OPTIMAL, MOI.ALMOST_OPTIMAL],\n            test_obj_values[sys],\n            10000,\n        )\n    end\nend\n\n@testset \"Network DC lossless -PF network with PowerModels DCPlosslessForm\" begin\n    c_sys5 = PSB.build_system(PSITestSystems, \"c_sys5\")\n    c_sys14 = PSB.build_system(PSITestSystems, \"c_sys14\")\n    c_sys14_dc = PSB.build_system(PSITestSystems, \"c_sys14_dc\")\n    systems = [c_sys5, c_sys14, c_sys14_dc]\n    objfuncs = [GAEVF, GQEVF, GQEVF]\n    constraint_keys = [\n        PSI.ConstraintKey(PSI.FlowRateConstraint, PSY.Line, \"ub\"),\n        PSI.ConstraintKey(PSI.FlowRateConstraint, PSY.Line, \"lb\"),\n        PSI.ConstraintKey(PSI.NodalBalanceActiveConstraint, PSY.ACBus),\n    ]\n    test_results = IdDict{System, Vector{Int}}(\n        c_sys5 => [384, 144, 264, 264, 288],\n        c_sys14 => [936, 480, 600, 600, 840],\n        c_sys14_dc => [984, 432, 648, 552, 840],\n    )\n    test_obj_values = IdDict{System, Float64}(\n        c_sys5 => 242000.0,\n        c_sys14 => 143000.0,\n        c_sys14_dc => 143000.0,\n    )\n    for (ix, sys) in enumerate(systems)\n        template = get_thermal_dispatch_template_network(DCPPowerModel)\n        ps_model = DecisionModel(template, sys; optimizer = ipopt_optimizer)\n        @test build!(ps_model; output_dir = mktempdir(; cleanup = true)) ==\n              PSI.ModelBuildStatus.BUILT\n        psi_constraint_test(ps_model, constraint_keys)\n        moi_tests(\n            ps_model,\n            test_results[sys]...,\n            false,\n        )\n        psi_checkobjfun_test(ps_model, objfuncs[ix])\n        psi_checksolve_test(\n            ps_model,\n            [MOI.OPTIMAL, MOI.LOCALLY_SOLVED],\n            test_obj_values[sys],\n            1000,\n        )\n    end\nend\n\n@testset \"Network Solve AC-PF PowerModels StandardACPModel\" begin\n    c_sys5 = PSB.build_system(PSITestSystems, \"c_sys5\")\n    c_sys14 = PSB.build_system(PSITestSystems, \"c_sys14\")\n    c_sys14_dc = PSB.build_system(PSITestSystems, \"c_sys14_dc\")\n    systems = [c_sys5, c_sys14, c_sys14_dc]\n    objfuncs = [GAEVF, GQEVF, GQEVF]\n    # Check for voltage and angle constraints\n    constraint_keys = [\n        PSI.ConstraintKey(FlowRateConstraintFromTo, PSY.Line),\n        PSI.ConstraintKey(FlowRateConstraintToFrom, PSY.Line),\n        PSI.ConstraintKey(PSI.NodalBalanceActiveConstraint, PSY.ACBus),\n        PSI.ConstraintKey(PSI.NodalBalanceReactiveConstraint, PSY.ACBus),\n    ]\n    test_results = IdDict{System, Vector{Int}}(\n        c_sys5 => [1056, 144, 240, 240, 264],\n        c_sys14 => [2832, 480, 240, 240, 696],\n        c_sys14_dc => [2832, 432, 336, 240, 744],\n    )\n    test_obj_values = IdDict{System, Float64}(\n        c_sys5 => 240000.0,\n        c_sys14 => 142000.0,\n        c_sys14_dc => 142000.0,\n    )\n    for (ix, sys) in enumerate(systems)\n        template = get_thermal_dispatch_template_network(ACPPowerModel)\n        ps_model = DecisionModel(template, sys; optimizer = ipopt_optimizer)\n        @test build!(ps_model; output_dir = mktempdir(; cleanup = true)) ==\n              PSI.ModelBuildStatus.BUILT\n        psi_constraint_test(ps_model, constraint_keys)\n        moi_tests(\n            ps_model,\n            test_results[sys]...,\n            false,\n        )\n        psi_checkobjfun_test(ps_model, objfuncs[ix])\n        psi_checksolve_test(\n            ps_model,\n            [MOI.TIME_LIMIT, MOI.OPTIMAL, MOI.LOCALLY_SOLVED],\n            test_obj_values[sys],\n            10000,\n        )\n    end\nend\n\n@testset \"Network Solve AC-PF PowerModels NFAPowerModel\" begin\n    c_sys5 = PSB.build_system(PSITestSystems, \"c_sys5\")\n    c_sys14 = PSB.build_system(PSITestSystems, \"c_sys14\")\n    c_sys14_dc = PSB.build_system(PSITestSystems, \"c_sys14_dc\")\n    systems = [c_sys5, c_sys14, c_sys14_dc]\n    objfuncs = [GAEVF, GQEVF, GQEVF]\n    constraint_keys = [PSI.ConstraintKey(PSI.NodalBalanceActiveConstraint, PSY.ACBus)]\n    test_results = Dict{System, Vector{Int}}(\n        c_sys5 => [264, 0, 264, 264, 120],\n        c_sys14 => [600, 0, 600, 600, 336],\n        c_sys14_dc => [648, 0, 648, 552, 384],\n    )\n    test_obj_values = IdDict{System, Float64}(\n        c_sys5 => 240000.0,\n        c_sys14 => 142000.0,\n        c_sys14_dc => 142000.0,\n    )\n    for (ix, sys) in enumerate(systems)\n        template = get_thermal_dispatch_template_network(NFAPowerModel)\n        ps_model = DecisionModel(template, sys; optimizer = ipopt_optimizer)\n        @test build!(ps_model; output_dir = mktempdir(; cleanup = true)) ==\n              PSI.ModelBuildStatus.BUILT\n        psi_constraint_test(ps_model, constraint_keys)\n        moi_tests(\n            ps_model,\n            test_results[sys]...,\n            false,\n        )\n        psi_checkobjfun_test(ps_model, objfuncs[ix])\n        psi_checksolve_test(\n            ps_model,\n            [MOI.OPTIMAL, MOI.LOCALLY_SOLVED],\n            test_obj_values[sys],\n            10000,\n        )\n    end\nend\n\n@testset \"Other Network AC PowerModels models\" begin\n    networks = [#ACPPowerModel, Already tested\n        ACRPowerModel,\n        ACTPowerModel,\n    ]\n    c_sys5 = PSB.build_system(PSITestSystems, \"c_sys5\")\n    c_sys14 = PSB.build_system(PSITestSystems, \"c_sys14\")\n    c_sys14_dc = PSB.build_system(PSITestSystems, \"c_sys14_dc\")\n    systems = [c_sys5, c_sys14, c_sys14_dc]\n    # TODO: add model specific constraints to this list. Voltages, etc.\n    constraint_keys = [\n        PSI.ConstraintKey(PSI.NodalBalanceActiveConstraint, PSY.ACBus),\n        PSI.ConstraintKey(PSI.NodalBalanceReactiveConstraint, PSY.ACBus),\n    ]\n    ACR_test_results = Dict{System, Vector{Int}}(\n        c_sys5 => [1056, 0, 240, 240, 264],\n        c_sys14 => [2832, 0, 240, 240, 696],\n        c_sys14_dc => [2832, 0, 336, 240, 744],\n    )\n    ACT_test_results = Dict{System, Vector{Int}}(\n        c_sys5 => [1344, 144, 240, 240, 840],\n        c_sys14 => [3792, 480, 240, 240, 2616],\n        c_sys14_dc => [3696, 432, 336, 240, 2472],\n    )\n    test_results = Dict(zip(networks, [ACR_test_results, ACT_test_results]))\n    for network in networks, sys in systems\n        template = get_thermal_dispatch_template_network(network)\n        ps_model = DecisionModel(template, sys; optimizer = fast_ipopt_optimizer)\n        @test build!(ps_model; output_dir = mktempdir(; cleanup = true)) ==\n              PSI.ModelBuildStatus.BUILT\n        psi_constraint_test(ps_model, constraint_keys)\n        moi_tests(\n            ps_model,\n            test_results[network][sys][1],\n            test_results[network][sys][2],\n            test_results[network][sys][3],\n            test_results[network][sys][4],\n            test_results[network][sys][5],\n            false,\n        )\n        @test PSI.get_optimization_container(ps_model).pm !== nothing\n    end\nend\n\n# TODO: Add constraint tests for these models, other is redundant with first test\n@testset \"Network DC-PF PowerModels quadratic loss approximations models\" begin\n    networks = [DCPLLPowerModel, LPACCPowerModel]\n    c_sys5 = PSB.build_system(PSITestSystems, \"c_sys5\")\n    c_sys14 = PSB.build_system(PSITestSystems, \"c_sys14\")\n    c_sys14_dc = PSB.build_system(PSITestSystems, \"c_sys14_dc\")\n    systems = [c_sys5, c_sys14, c_sys14_dc]\n    # TODO: add model specific constraints to this list. Bi-directional flows etc\n    constraint_keys = [PSI.ConstraintKey(PSI.NodalBalanceActiveConstraint, PSY.ACBus)]\n    test_obj_values = IdDict{System, Float64}(\n        c_sys5 => 240000.0,\n        c_sys14 => 142000.0,\n        c_sys14_dc => 142000.0,\n    )\n    DCPLL_test_results = Dict{System, Vector{Int}}(\n        c_sys5 => [528, 144, 264, 264, 288],\n        c_sys14 => [1416, 480, 600, 600, 840],\n        c_sys14_dc => [1416, 432, 648, 552, 840],\n    )\n    LPACC_test_results = Dict{System, Vector{Int}}(\n        c_sys5 => [1200, 144, 240, 240, 840],\n        c_sys14 => [3312, 480, 240, 240, 2616],\n        c_sys14_dc => [3264, 432, 336, 240, 2472],\n    )\n    test_results = Dict(zip(networks, [DCPLL_test_results, LPACC_test_results]))\n    for network in networks, (ix, sys) in enumerate(systems)\n        template = get_thermal_dispatch_template_network(network)\n        ps_model = DecisionModel(template, sys; optimizer = ipopt_optimizer)\n        @test build!(ps_model; output_dir = mktempdir(; cleanup = true)) ==\n              PSI.ModelBuildStatus.BUILT\n        psi_constraint_test(ps_model, constraint_keys)\n        moi_tests(\n            ps_model,\n            test_results[network][sys][1],\n            test_results[network][sys][2],\n            test_results[network][sys][3],\n            test_results[network][sys][4],\n            test_results[network][sys][5],\n            false,\n        )\n        @test PSI.get_optimization_container(ps_model).pm !== nothing\n        psi_checksolve_test(\n            ps_model,\n            [MOI.OPTIMAL, MOI.LOCALLY_SOLVED],\n            test_obj_values[sys],\n            10000,\n        )\n    end\nend\n\n@testset \"Network Unsupported Power Model Formulations\" begin\n    for network in PSI.UNSUPPORTED_POWERMODELS\n        template = get_thermal_dispatch_template_network(network)\n        ps_model = DecisionModel(\n            template,\n            PSB.build_system(PSITestSystems, \"c_sys5\");\n            optimizer = ipopt_optimizer,\n        )\n        @test build!(\n            ps_model;\n            console_level = Logging.AboveMaxLevel,  # Ignore expected errors.\n            output_dir = mktempdir(; cleanup = true),\n        ) == PSI.ModelBuildStatus.FAILED\n    end\nend\n\n@testset \"2 Subnetworks HVDC DC-PF with CopperPlatePowerModel\" begin\n    c_sys5 = PSB.build_system(PSISystems, \"2Area 5 Bus System\")\n    template = get_thermal_dispatch_template_network(NetworkModel(CopperPlatePowerModel))\n    ps_model = DecisionModel(template, c_sys5; optimizer = HiGHS_optimizer)\n\n    @test build!(ps_model; output_dir = mktempdir(; cleanup = true)) ==\n          PSI.ModelBuildStatus.BUILT\n    solve!(ps_model)\n\n    moi_tests(ps_model, 264, 0, 288, 240, 48, false)\n\n    opt_container = PSI.get_optimization_container(ps_model)\n    copper_plate_constraints =\n        PSI.get_constraint(opt_container, CopperPlateBalanceConstraint(), PSY.System)\n    @test size(copper_plate_constraints) == (2, 24)\n\n    psi_checksolve_test(ps_model, [MOI.OPTIMAL], 480288, 100)\n\n    results = OptimizationProblemResults(ps_model)\n    hvdc_flow =\n        read_variable(\n            results,\n            \"FlowActivePowerVariable__TwoTerminalGenericHVDCLine\";\n            table_format = TableFormat.WIDE,\n        )\n    @test all(hvdc_flow[!, \"nodeC-nodeC2\"] .<= 200)\n    @test all(hvdc_flow[!, \"nodeC-nodeC2\"] .>= -200)\n\n    load = read_parameter(\n        results,\n        \"ActivePowerTimeSeriesParameter__PowerLoad\";\n        table_format = TableFormat.WIDE,\n    )\n    thermal_gen = read_variable(\n        results,\n        \"ActivePowerVariable__ThermalStandard\";\n        table_format = TableFormat.WIDE,\n    )\n\n    zone_1_load = sum(eachcol(load[!, [\"Load-nodeC\", \"Load-nodeD\", \"Load-nodeB\"]]))\n    zone_1_gen = sum(\n        eachcol(thermal_gen[!, [\"Solitude\", \"Park City\", \"Sundance\", \"Brighton\", \"Alta\"]]),\n    )\n    @test all(\n        isapprox.(\n            sum(zone_1_gen .+ zone_1_load .- hvdc_flow[!, \"nodeC-nodeC2\"]; dims = 2),\n            0.0;\n            atol = 1e-3,\n        ),\n    )\n\n    zone_2_load = sum(eachcol(load[!, [\"Load-nodeC2\", \"Load-nodeD2\", \"Load-nodeB2\"]]))\n    zone_2_gen = sum(\n        eachcol(\n            thermal_gen[\n                !,\n                [\"Solitude-2\", \"Park City-2\", \"Sundance-2\", \"Brighton-2\", \"Alta-2\"],\n            ],\n        ),\n    )\n    @test all(\n        isapprox.(\n            sum(zone_2_gen .+ zone_2_load .+ hvdc_flow[!, \"nodeC-nodeC2\"]; dims = 2),\n            0.0;\n            atol = 1e-3,\n        ),\n    )\n\n    # Test forcing flows to 0.0\n    hvdc_link = get_component(TwoTerminalGenericHVDCLine, c_sys5, \"nodeC-nodeC2\")\n    set_active_power_limits_from!(hvdc_link, (min = 0.0, max = 0.0))\n    set_active_power_limits_to!(hvdc_link, (min = 0.0, max = 0.0))\n\n    # Test not passing the PTDF to the Template\n    template = get_thermal_dispatch_template_network(NetworkModel(PTDFPowerModel))\n    ps_model = DecisionModel(template, c_sys5; optimizer = HiGHS_optimizer)\n    @test build!(ps_model; output_dir = mktempdir(; cleanup = true)) ==\n          PSI.ModelBuildStatus.BUILT\n    solve!(ps_model)\n\n    opt_container = PSI.get_optimization_container(ps_model)\n    copper_plate_constraints =\n        PSI.get_constraint(opt_container, CopperPlateBalanceConstraint(), PSY.System)\n\n    results = OptimizationProblemResults(ps_model)\n    hvdc_flow =\n        read_variable(\n            results,\n            \"FlowActivePowerVariable__TwoTerminalGenericHVDCLine\";\n            table_format = TableFormat.WIDE,\n        )\n    @test all(hvdc_flow[!, \"nodeC-nodeC2\"] .== 0.0)\n    @test all(hvdc_flow[!, \"nodeC-nodeC2\"] .== 0.0)\n\n    load = read_parameter(\n        results,\n        \"ActivePowerTimeSeriesParameter__PowerLoad\";\n        table_format = TableFormat.WIDE,\n    )\n    thermal_gen = read_variable(\n        results,\n        \"ActivePowerVariable__ThermalStandard\";\n        table_format = TableFormat.WIDE,\n    )\n\n    zone_1_load = sum(eachcol(load[!, [\"Load-nodeC\", \"Load-nodeD\", \"Load-nodeB\"]]))\n    zone_1_gen = sum(\n        eachcol(thermal_gen[!, [\"Solitude\", \"Park City\", \"Sundance\", \"Brighton\", \"Alta\"]]),\n    )\n    @test all(isapprox.(sum(zone_1_gen .+ zone_1_load; dims = 2), 0.0; atol = 1e-3))\n\n    zone_2_load = sum(eachcol(load[!, [\"Load-nodeC2\", \"Load-nodeD2\", \"Load-nodeB2\"]]))\n    zone_2_gen = sum(\n        eachcol(\n            thermal_gen[\n                !,\n                [\"Solitude-2\", \"Park City-2\", \"Sundance-2\", \"Brighton-2\", \"Alta-2\"],\n            ],\n        ),\n    )\n    @test all(isapprox.(sum(zone_2_gen .+ zone_2_load; dims = 2), 0.0; atol = 1e-3))\nend\n\n@testset \"2 Subnetworks DC-PF with PTDF Model\" begin\n    c_sys5 = PSB.build_system(PSISystems, \"2Area 5 Bus System\")\n    # Test passing a VirtualPTDF Model\n    template = get_thermal_dispatch_template_network(\n        NetworkModel(PTDFPowerModel; PTDF_matrix = VirtualPTDF(c_sys5)),\n    )\n    ps_model = DecisionModel(template, c_sys5; optimizer = HiGHS_optimizer)\n\n    @test build!(ps_model; output_dir = mktempdir(; cleanup = true)) ==\n          PSI.ModelBuildStatus.BUILT\n    solve!(ps_model)\n\n    moi_tests(ps_model, 264, 0, 576, 528, 48, false)\n\n    opt_container = PSI.get_optimization_container(ps_model)\n    copper_plate_constraints =\n        PSI.get_constraint(opt_container, CopperPlateBalanceConstraint(), PSY.System)\n    @test size(copper_plate_constraints) == (2, 24)\n\n    psi_checksolve_test(ps_model, [MOI.OPTIMAL], 482587, 100)\n\n    results = OptimizationProblemResults(ps_model)\n    hvdc_flow =\n        read_variable(\n            results,\n            \"FlowActivePowerVariable__TwoTerminalGenericHVDCLine\";\n            table_format = TableFormat.WIDE,\n        )\n    @test all(hvdc_flow[!, \"nodeC-nodeC2\"] .<= 200 + PSI.ABSOLUTE_TOLERANCE)\n    @test all(hvdc_flow[!, \"nodeC-nodeC2\"] .>= -200 - PSI.ABSOLUTE_TOLERANCE)\n\n    load = read_parameter(\n        results,\n        \"ActivePowerTimeSeriesParameter__PowerLoad\";\n        table_format = TableFormat.WIDE,\n    )\n    thermal_gen = read_variable(\n        results,\n        \"ActivePowerVariable__ThermalStandard\";\n        table_format = TableFormat.WIDE,\n    )\n\n    zone_1_load = sum(eachcol(load[!, [\"Load-nodeC\", \"Load-nodeD\", \"Load-nodeB\"]]))\n    zone_1_gen = sum(\n        eachcol(thermal_gen[!, [\"Solitude\", \"Park City\", \"Sundance\", \"Brighton\", \"Alta\"]]),\n    )\n    @test all(\n        isapprox.(\n            sum(zone_1_gen .+ zone_1_load .- hvdc_flow[!, \"nodeC-nodeC2\"]; dims = 2),\n            0.0;\n            atol = 1e-3,\n        ),\n    )\n\n    zone_2_load = sum(eachcol(load[!, [\"Load-nodeC2\", \"Load-nodeD2\", \"Load-nodeB2\"]]))\n    zone_2_gen = sum(\n        eachcol(\n            thermal_gen[\n                !,\n                [\"Solitude-2\", \"Park City-2\", \"Sundance-2\", \"Brighton-2\", \"Alta-2\"],\n            ],\n        ),\n    )\n    @test all(\n        isapprox.(\n            sum(zone_2_gen .+ zone_2_load .+ hvdc_flow[!, \"nodeC-nodeC2\"]; dims = 2),\n            0.0;\n            atol = 1e-3,\n        ),\n    )\n\n    # Test forcing flows to 0.0\n    hvdc_link = get_component(PSY.TwoTerminalGenericHVDCLine, c_sys5, \"nodeC-nodeC2\")\n    set_active_power_limits_from!(hvdc_link, (min = 0.0, max = 0.0))\n    set_active_power_limits_to!(hvdc_link, (min = 0.0, max = 0.0))\n\n    # Test not passing the PTDF to the Template\n    template = get_thermal_dispatch_template_network(NetworkModel(PTDFPowerModel))\n    ps_model = DecisionModel(template, c_sys5; optimizer = HiGHS_optimizer)\n    @test build!(ps_model; output_dir = mktempdir(; cleanup = true)) ==\n          PSI.ModelBuildStatus.BUILT\n    solve!(ps_model)\n\n    opt_container = PSI.get_optimization_container(ps_model)\n    copper_plate_constraints =\n        PSI.get_constraint(opt_container, CopperPlateBalanceConstraint(), PSY.System)\n\n    results = OptimizationProblemResults(ps_model)\n    hvdc_flow =\n        read_variable(\n            results,\n            \"FlowActivePowerVariable__TwoTerminalGenericHVDCLine\";\n            table_format = TableFormat.WIDE,\n        )\n    @test all(hvdc_flow[!, \"nodeC-nodeC2\"] .== 0.0)\n    @test all(hvdc_flow[!, \"nodeC-nodeC2\"] .== 0.0)\n\n    load = read_parameter(\n        results,\n        \"ActivePowerTimeSeriesParameter__PowerLoad\";\n        table_format = TableFormat.WIDE,\n    )\n    thermal_gen = read_variable(\n        results,\n        \"ActivePowerVariable__ThermalStandard\";\n        table_format = TableFormat.WIDE,\n    )\n\n    zone_1_load = sum(eachcol(load[!, [\"Load-nodeC\", \"Load-nodeD\", \"Load-nodeB\"]]))\n    zone_1_gen = sum(\n        eachcol(thermal_gen[!, [\"Solitude\", \"Park City\", \"Sundance\", \"Brighton\", \"Alta\"]]),\n    )\n    @test all(isapprox.(sum(zone_1_gen .+ zone_1_load; dims = 2), 0.0; atol = 1e-3))\n\n    zone_2_load = sum(eachcol(load[!, [\"Load-nodeC2\", \"Load-nodeD2\", \"Load-nodeB2\"]]))\n    zone_2_gen = sum(\n        eachcol(\n            thermal_gen[\n                !,\n                [\"Solitude-2\", \"Park City-2\", \"Sundance-2\", \"Brighton-2\", \"Alta-2\"],\n            ],\n        ),\n    )\n    @test all(isapprox.(sum(zone_2_gen .+ zone_2_load; dims = 2), 0.0; atol = 1e-3))\n\n    # Test passing a Virtual PTDF Model with higher tolerance\n    c_sys5 = PSB.build_system(PSISystems, \"2Area 5 Bus System\")\n    # Test passing a VirtualPTDF Model\n    template = get_thermal_dispatch_template_network(\n        NetworkModel(PTDFPowerModel; PTDF_matrix = VirtualPTDF(c_sys5; tol = 1e-2)),\n    )\n    ps_model = DecisionModel(template, c_sys5; optimizer = HiGHS_optimizer)\n\n    @test build!(ps_model; output_dir = mktempdir(; cleanup = true)) ==\n          PSI.ModelBuildStatus.BUILT\n    @test solve!(ps_model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED\nend\n\n# These models are easier to test due to their lossless nature\n@testset \"StandardPTDF/DCPPowerModel Radial Branches Test\" begin\n    new_sys = PSB.build_system(PSITestSystems, \"c_sys5_radial\")\n    for net_model in [DCPPowerModel, PTDFPowerModel]\n        template_uc = template_unit_commitment(;\n            network = NetworkModel(net_model;\n                reduce_radial_branches = true,\n                use_slacks = false,\n            ),\n        )\n        thermal_model = ThermalStandardUnitCommitment\n        set_device_model!(template_uc, ThermalStandard, thermal_model)\n\n        ##### Solve Reduced Model ####\n        uc_model_red = DecisionModel(\n            template_uc,\n            new_sys;\n            optimizer = HiGHS_optimizer,\n            name = \"UC_RED\",\n            store_variable_names = true,\n        )\n\n        @test build!(uc_model_red; output_dir = mktempdir(; cleanup = true)) ==\n              PSI.ModelBuildStatus.BUILT\n        solve!(uc_model_red)\n\n        res_red = OptimizationProblemResults(uc_model_red)\n\n        if net_model == DCPPowerModel\n            flow_lines = read_variable(\n                res_red,\n                \"FlowActivePowerVariable__Line\";\n                table_format = TableFormat.WIDE,\n            )\n        else\n            flow_lines = read_expression(\n                res_red,\n                \"PTDFBranchFlow__Line\";\n                table_format = TableFormat.WIDE,\n            )\n        end\n        line_names = DataFrames.names(flow_lines)[2:end]\n\n        ##### Solve Original Model ####\n        template_uc_orig = template_unit_commitment(;\n            network = NetworkModel(net_model;\n                reduce_radial_branches = false,\n                use_slacks = false,\n            ),\n        )\n        set_device_model!(template_uc_orig, ThermalStandard, thermal_model)\n\n        uc_model_orig = DecisionModel(\n            template_uc_orig,\n            new_sys;\n            optimizer = HiGHS_optimizer,\n            name = \"UC_ORIG\",\n            store_variable_names = true,\n        )\n\n        @test build!(uc_model_orig; output_dir = mktempdir(; cleanup = true)) ==\n              PSI.ModelBuildStatus.BUILT\n        solve!(uc_model_orig)\n\n        res_orig = OptimizationProblemResults(uc_model_orig)\n\n        if net_model == DCPPowerModel\n            flow_lines_orig = read_variable(\n                res_orig,\n                \"FlowActivePowerVariable__Line\";\n                table_format = TableFormat.WIDE,\n            )\n        else\n            flow_lines_orig = read_expression(\n                res_orig,\n                \"PTDFBranchFlow__Line\";\n                table_format = TableFormat.WIDE,\n            )\n        end\n\n        for line in line_names\n            @test isapprox(flow_lines[!, line], flow_lines_orig[!, line])\n        end\n    end\nend\n\n@testset \"StandardPTDF with Ward reduction Test\" begin\n    new_sys = PSB.build_system(PSITestSystems, \"c_sys5_radial\")\n    #This ward reduction is equivalent to the radial reduciton, therefore flows should be unchanged\n    nr = NetworkReduction[WardReduction([1, 2, 3, 4, 5])]\n    ptdf = PTDF(new_sys; network_reductions = nr)\n    template_uc = template_unit_commitment(;\n        network = NetworkModel(PTDFPowerModel;\n            PTDF_matrix = ptdf,\n            use_slacks = false,\n        ),\n    )\n    thermal_model = ThermalStandardUnitCommitment\n    set_device_model!(template_uc, ThermalStandard, thermal_model)\n\n    ##### Solve Reduced Model ####\n    solver = HiGHS_optimizer\n    uc_model_red = DecisionModel(\n        template_uc,\n        new_sys;\n        optimizer = solver,\n        name = \"UC_RED\",\n        store_variable_names = true,\n    )\n\n    @test build!(uc_model_red; output_dir = mktempdir(; cleanup = true)) ==\n          PSI.ModelBuildStatus.BUILT\n    solve!(uc_model_red)\n\n    res_red = OptimizationProblemResults(uc_model_red)\n\n    flow_lines = read_expression(\n        res_red,\n        \"PTDFBranchFlow__Line\";\n        table_format = TableFormat.WIDE,\n    )\n    line_names = DataFrames.names(flow_lines)[2:end]\n\n    ##### Solve Original Model ####\n    template_uc_orig = template_unit_commitment(;\n        network = NetworkModel(PTDFPowerModel;\n            reduce_radial_branches = false,\n            use_slacks = false,\n        ),\n    )\n    set_device_model!(template_uc_orig, ThermalStandard, thermal_model)\n\n    uc_model_orig = DecisionModel(\n        template_uc_orig,\n        new_sys;\n        optimizer = solver,\n        name = \"UC_ORIG\",\n        store_variable_names = true,\n    )\n\n    @test build!(uc_model_orig; output_dir = mktempdir(; cleanup = true)) ==\n          PSI.ModelBuildStatus.BUILT\n    solve!(uc_model_orig)\n\n    res_orig = OptimizationProblemResults(uc_model_orig)\n\n    flow_lines_orig = read_expression(\n        res_orig,\n        \"PTDFBranchFlow__Line\";\n        table_format = TableFormat.WIDE,\n    )\n\n    for line in line_names\n        @test isapprox(flow_lines[!, line], flow_lines_orig[!, line])\n    end\nend\n\n@testset \"All PowerModels models construction with reduced radial branches\" begin\n    new_sys = PSB.build_system(PSITestSystems, \"c_sys5_radial\")\n    for (network, solver) in NETWORKS_FOR_TESTING\n        template = get_thermal_dispatch_template_network(\n            NetworkModel(network;\n                PTDF_matrix = PTDF(new_sys),\n                reduce_radial_branches = true,\n                use_slacks = true),\n        )\n        ps_model = DecisionModel(template, new_sys; optimizer = solver)\n        @test build!(ps_model; output_dir = mktempdir(; cleanup = true)) ==\n              PSI.ModelBuildStatus.BUILT\n        @test PSI.get_optimization_container(ps_model).pm !== nothing\n    end\nend\n\n@testset \"2 Areas AreaBalance PowerModel - with slacks\" begin\n    c_sys = build_system(PSITestSystems, \"c_sys5_uc\")\n    # Extend the system with two areas\n    areas = [Area(\"Area_1\", 0, 0, 0), Area(\"Area_2\", 0, 0, 0)]\n    add_components!(c_sys, areas)\n    for (i, comp) in enumerate(get_components(ACBus, c_sys))\n        (i < 3) ? set_area!(comp, areas[1]) : set_area!(comp, areas[2])\n    end\n    # Deactivate generators on Area 1: as there is no area interchange defined,\n    # slacks will be required for feasibility\n    for gen in get_components(x -> (get_area(get_bus(x)) == areas[1]), Generator, c_sys)\n        set_available!(gen, false)\n    end\n\n    template = get_thermal_dispatch_template_network(\n        NetworkModel(AreaBalancePowerModel; use_slacks = true),\n    )\n    ps_model = DecisionModel(template, c_sys; optimizer = HiGHS_optimizer)\n\n    @test build!(ps_model; output_dir = mktempdir(; cleanup = true)) ==\n          PSI.ModelBuildStatus.BUILT\n    @test solve!(ps_model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED\n\n    opt_container = PSI.get_optimization_container(ps_model)\n    copper_plate_constraints =\n        PSI.get_constraint(opt_container, CopperPlateBalanceConstraint(), PSY.Area)\n    @test size(copper_plate_constraints) == (2, 24)\n\n    results = OptimizationProblemResults(ps_model)\n    slacks_up = read_variable(\n        results,\n        \"SystemBalanceSlackUp__Area\";\n        table_format = TableFormat.WIDE,\n    )\n    @test all(slacks_up[!, \"Area_1\"] .> 0.0)\n    @test all(slacks_up[!, \"Area_2\"] .≈ 0.0)\nend\n\n@testset \"2 Areas AreaBalance PowerModel\" begin\n    c_sys = PSB.build_system(PSISystems, \"two_area_pjm_DA\")\n    transform_single_time_series!(c_sys, Hour(24), Hour(1))\n    template = get_thermal_dispatch_template_network(NetworkModel(AreaBalancePowerModel))\n    set_device_model!(template, AreaInterchange, StaticBranch)\n    ps_model =\n        DecisionModel(template, c_sys; resolution = Hour(1), optimizer = HiGHS_optimizer)\n\n    @test build!(ps_model; output_dir = mktempdir(; cleanup = true)) ==\n          PSI.ModelBuildStatus.BUILT\n    @test solve!(ps_model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED\n\n    moi_tests(ps_model, 264, 0, 264, 264, 48, false)\n\n    opt_container = PSI.get_optimization_container(ps_model)\n    copper_plate_constraints =\n        PSI.get_constraint(opt_container, CopperPlateBalanceConstraint(), PSY.Area)\n    @test size(copper_plate_constraints) == (2, 24)\n\n    psi_checksolve_test(ps_model, [MOI.OPTIMAL], 482055, 1)\n\n    results = OptimizationProblemResults(ps_model)\n    interarea_flow = read_variable(\n        results,\n        \"FlowActivePowerVariable__AreaInterchange\";\n        table_format = TableFormat.WIDE,\n    )\n    # The values for these tests come from the data\n    @test all(interarea_flow[!, \"1_2\"] .<= 150)\n    @test all(interarea_flow[!, \"1_2\"] .>= -150)\n\n    load = read_parameter(\n        results,\n        \"ActivePowerTimeSeriesParameter__PowerLoad\";\n        table_format = TableFormat.WIDE,\n    )\n    thermal_gen = read_variable(\n        results,\n        \"ActivePowerVariable__ThermalStandard\";\n        table_format = TableFormat.WIDE,\n    )\n\n    zone_1_load = sum(eachcol(load[!, [\"Bus4_1\", \"Bus3_1\", \"Bus2_1\"]]))\n    zone_1_gen = sum(\n        eachcol(\n            thermal_gen[\n                !,\n                [\"Solitude_1\", \"Park City_1\", \"Sundance_1\", \"Brighton_1\", \"Alta_1\"],\n            ],\n        ),\n    )\n    @test all(\n        isapprox.(\n            sum(zone_1_gen .+ zone_1_load .- interarea_flow[!, \"1_2\"]; dims = 2),\n            0.0;\n            atol = 1e-3,\n        ),\n    )\n\n    zone_2_load = sum(eachcol(load[!, [\"Bus4_2\", \"Bus3_2\", \"Bus2_2\"]]))\n    zone_2_gen = sum(\n        eachcol(\n            thermal_gen[\n                !,\n                [\"Solitude_2\", \"Park City_2\", \"Sundance_2\", \"Brighton_2\", \"Alta_2\"],\n            ],\n        ),\n    )\n    @test all(\n        isapprox.(\n            sum(zone_2_gen .+ zone_2_load .+ interarea_flow[!, \"1_2\"]; dims = 2),\n            0.0;\n            atol = 1e-3,\n        ),\n    )\nend\n\n@testset \"2 Areas AreaBalance PowerModel with TimeSeries\" begin\n    c_sys = PSB.build_system(PSISystems, \"two_area_pjm_DA\")\n    load = first(get_components(PowerLoad, c_sys))\n    ts_array = get_time_series_array(SingleTimeSeries, load, \"max_active_power\")\n    tstamp = timestamp(ts_array)\n    area_int = first(get_components(AreaInterchange, c_sys))\n    day_data = [\n        0.9, 0.85, 0.95, 0.2, 0.0, 0.0,\n        0.9, 0.85, 0.95, 0.2, 0.0, 0.0,\n        0.9, 0.85, 0.95, 0.2, 0.0, 0.0,\n        0.9, 0.85, 0.95, 0.2, 0.0, 0.0,\n    ]\n    weekly_data = repeat(day_data, 7)\n    ts_from_to = SingleTimeSeries(\n        \"from_to_flow_limit\",\n        TimeArray(tstamp, weekly_data);\n        scaling_factor_multiplier = get_from_to_flow_limit,\n    )\n    ts_to_from = SingleTimeSeries(\n        \"to_from_flow_limit\",\n        TimeArray(tstamp, weekly_data);\n        scaling_factor_multiplier = get_from_to_flow_limit,\n    )\n    add_time_series!(c_sys, area_int, ts_from_to)\n    add_time_series!(c_sys, area_int, ts_to_from)\n    ## Transform Time Series ##\n    transform_single_time_series!(c_sys, Hour(24), Hour(24))\n    template = get_thermal_dispatch_template_network(NetworkModel(AreaBalancePowerModel))\n    set_device_model!(template, AreaInterchange, StaticBranch)\n    ps_model =\n        DecisionModel(template, c_sys; resolution = Hour(1), optimizer = HiGHS_optimizer)\n\n    @test build!(ps_model; output_dir = mktempdir(; cleanup = true)) ==\n          PSI.ModelBuildStatus.BUILT\n    @test solve!(ps_model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED\n\n    moi_tests(ps_model, 264, 0, 264, 264, 48, false)\n\n    opt_container = PSI.get_optimization_container(ps_model)\n    copper_plate_constraints =\n        PSI.get_constraint(opt_container, CopperPlateBalanceConstraint(), PSY.Area)\n    @test size(copper_plate_constraints) == (2, 24)\n\n    psi_checksolve_test(ps_model, [MOI.OPTIMAL], 482055, 1)\n\n    results = OptimizationProblemResults(ps_model)\n    interarea_flow = read_variable(\n        results,\n        \"FlowActivePowerVariable__AreaInterchange\";\n        table_format = TableFormat.WIDE,\n    )\n    # The values for these tests come from the data\n    @test interarea_flow[4, \"1_2\"] != 0.0\n    @test interarea_flow[5, \"1_2\"] == 0.0\n    @test interarea_flow[6, \"1_2\"] == 0.0\nend\n\n@testset \"2 Areas AreaPTDFPowerModel\" begin\n    c_sys = PSB.build_system(PSISystems, \"two_area_pjm_DA\")\n    transform_single_time_series!(c_sys, Hour(24), Hour(1))\n    set_flow_limits!(\n        get_component(AreaInterchange, c_sys, \"1_2\"),\n        (from_to = 1.0, to_from = 1.0),\n    )\n    template = get_thermal_dispatch_template_network(NetworkModel(AreaPTDFPowerModel))\n    set_device_model!(template, AreaInterchange, StaticBranch)\n    set_device_model!(template, MonitoredLine, StaticBranchUnbounded)\n    ps_model =\n        DecisionModel(template, c_sys; resolution = Hour(1), optimizer = HiGHS_optimizer)\n\n    @test build!(ps_model; output_dir = mktempdir(; cleanup = true)) ==\n          PSI.ModelBuildStatus.BUILT\n    @test solve!(ps_model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED\n\n    moi_tests(ps_model, 264, 0, 576, 576, 48, false)\n\n    opt_container = PSI.get_optimization_container(ps_model)\n    copper_plate_constraints =\n        PSI.get_constraint(opt_container, CopperPlateBalanceConstraint(), PSY.Area)\n    @test size(copper_plate_constraints) == (2, 24)\n\n    psi_checksolve_test(ps_model, [MOI.OPTIMAL], 497551, 1)\n\n    results = OptimizationProblemResults(ps_model)\n    interarea_flow = read_variable(\n        results,\n        \"FlowActivePowerVariable__AreaInterchange\";\n        table_format = TableFormat.WIDE,\n    )\n    # The values for these tests come from the data\n    @test all(interarea_flow[!, \"1_2\"] .<= 100.0 + PSI.ABSOLUTE_TOLERANCE)\n    @test all(interarea_flow[!, \"1_2\"] .>= -100.0 - PSI.ABSOLUTE_TOLERANCE)\n\n    load = read_parameter(\n        results,\n        \"ActivePowerTimeSeriesParameter__PowerLoad\";\n        table_format = TableFormat.WIDE,\n    )\n    thermal_gen = read_variable(\n        results,\n        \"ActivePowerVariable__ThermalStandard\";\n        table_format = TableFormat.WIDE,\n    )\n\n    zone_1_load = sum(eachcol(load[!, [\"Bus4_1\", \"Bus3_1\", \"Bus2_1\"]]))\n    zone_1_gen = sum(\n        eachcol(\n            thermal_gen[\n                !,\n                [\"Solitude_1\", \"Park City_1\", \"Sundance_1\", \"Brighton_1\", \"Alta_1\"],\n            ],\n        ),\n    )\n    @test all(\n        isapprox.(\n            sum(zone_1_gen .+ zone_1_load .- interarea_flow[!, \"1_2\"]; dims = 2),\n            0.0;\n            atol = 1e-3,\n        ),\n    )\n\n    zone_2_load = sum(eachcol(load[!, [\"Bus4_2\", \"Bus3_2\", \"Bus2_2\"]]))\n    zone_2_gen = sum(\n        eachcol(\n            thermal_gen[\n                !,\n                [\"Solitude_2\", \"Park City_2\", \"Sundance_2\", \"Brighton_2\", \"Alta_2\"],\n            ],\n        ),\n    )\n    @test all(\n        isapprox.(\n            sum(zone_2_gen .+ zone_2_load .+ interarea_flow[!, \"1_2\"]; dims = 2),\n            0.0;\n            atol = 1e-3,\n        ),\n    )\nend\n\n@testset \"2 Areas AreaPTDFPowerModel with Double Circuit\" begin\n    c_sys = PSB.build_system(PSISystems, \"two_area_pjm_DA\")\n    l = get_component(MonitoredLine, c_sys, \"inter_area_line\")\n    l2 = MonitoredLine(;\n        name = \"inter_area_line_2\",\n        available = get_available(l),\n        active_power_flow = get_active_power_flow(l),\n        reactive_power_flow = get_reactive_power_flow(l),\n        arc = get_arc(l),\n        r = get_r(l),\n        x = get_x(l),\n        b = get_b(l),\n        flow_limits = get_flow_limits(l),\n        rating = get_rating(l),\n        angle_limits = get_angle_limits(l),\n        rating_b = get_rating_b(l),\n        rating_c = get_rating_c(l),\n        g = get_g(l),\n        services = get_services(l),\n    )\n    add_component!(c_sys, l2)\n    transform_single_time_series!(c_sys, Hour(24), Hour(1))\n    set_flow_limits!(\n        get_component(AreaInterchange, c_sys, \"1_2\"),\n        (from_to = 1.0, to_from = 1.0),\n    )\n    template = get_thermal_dispatch_template_network(NetworkModel(AreaPTDFPowerModel))\n    set_device_model!(template, AreaInterchange, StaticBranch)\n    set_device_model!(template, MonitoredLine, StaticBranchUnbounded)\n    ps_model =\n        DecisionModel(template, c_sys; resolution = Hour(1), optimizer = HiGHS_optimizer)\n\n    @test build!(ps_model; output_dir = mktempdir(; cleanup = true)) ==\n          PSI.ModelBuildStatus.BUILT\n    @test solve!(ps_model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED\n\n    moi_tests(ps_model, 264, 0, 576, 576, 48, false)\n\n    opt_container = PSI.get_optimization_container(ps_model)\n    copper_plate_constraints =\n        PSI.get_constraint(opt_container, CopperPlateBalanceConstraint(), PSY.Area)\n    @test size(copper_plate_constraints) == (2, 24)\n\n    psi_checksolve_test(ps_model, [MOI.OPTIMAL], 497551, 1)\n\n    results = OptimizationProblemResults(ps_model)\n    interarea_flow = read_variable(\n        results,\n        \"FlowActivePowerVariable__AreaInterchange\";\n        table_format = TableFormat.WIDE,\n    )\n    # The values for these tests come from the data\n    @test all(interarea_flow[!, \"1_2\"] .<= 100.0 + PSI.ABSOLUTE_TOLERANCE)\n    @test all(interarea_flow[!, \"1_2\"] .>= -100.0 - PSI.ABSOLUTE_TOLERANCE)\n\n    load = read_parameter(\n        results,\n        \"ActivePowerTimeSeriesParameter__PowerLoad\";\n        table_format = TableFormat.WIDE,\n    )\n    thermal_gen = read_variable(\n        results,\n        \"ActivePowerVariable__ThermalStandard\";\n        table_format = TableFormat.WIDE,\n    )\n\n    zone_1_load = sum(eachcol(load[!, [\"Bus4_1\", \"Bus3_1\", \"Bus2_1\"]]))\n    zone_1_gen = sum(\n        eachcol(\n            thermal_gen[\n                !,\n                [\"Solitude_1\", \"Park City_1\", \"Sundance_1\", \"Brighton_1\", \"Alta_1\"],\n            ],\n        ),\n    )\n    @test all(\n        isapprox.(\n            sum(zone_1_gen .+ zone_1_load .- interarea_flow[!, \"1_2\"]; dims = 2),\n            0.0;\n            atol = 1e-3,\n        ),\n    )\n\n    zone_2_load = sum(eachcol(load[!, [\"Bus4_2\", \"Bus3_2\", \"Bus2_2\"]]))\n    zone_2_gen = sum(\n        eachcol(\n            thermal_gen[\n                !,\n                [\"Solitude_2\", \"Park City_2\", \"Sundance_2\", \"Brighton_2\", \"Alta_2\"],\n            ],\n        ),\n    )\n    @test all(\n        isapprox.(\n            sum(zone_2_gen .+ zone_2_load .+ interarea_flow[!, \"1_2\"]; dims = 2),\n            0.0;\n            atol = 1e-3,\n        ),\n    )\nend\n\n@testset \"2 Areas AreaPTDFPowerModel with Time Series\" begin\n    c_sys = PSB.build_system(PSISystems, \"two_area_pjm_DA\")\n    load = first(get_components(PowerLoad, c_sys))\n    new_line = Line(;\n        name = \"C2_D1\",\n        available = true,\n        active_power_flow = 0.0,\n        reactive_power_flow = 0.0,\n        arc = Arc(;\n            from = get_component(ACBus, c_sys, \"Bus_nodeC_2\"),\n            to = get_component(ACBus, c_sys, \"Bus_nodeD_1\"),\n        ),\n        r = 0.00297,\n        x = 0.0297,\n        b = (from = 0.00337, to = 0.00337),\n        rating = 40.53,\n        angle_limits = (min = -0.7, max = 0.7),\n    )\n    add_component!(c_sys, new_line)\n    ts_array = get_time_series_array(SingleTimeSeries, load, \"max_active_power\")\n    tstamp = timestamp(ts_array)\n    area_int = first(get_components(AreaInterchange, c_sys))\n    day_data = [\n        0.9, 0.85, 0.95, 0.2, 0.0, 0.0,\n        0.9, 0.85, 0.95, 0.2, 0.0, 0.0,\n        0.9, 0.85, 0.95, 0.2, 0.0, 0.0,\n        0.9, 0.85, 0.95, 0.2, 0.0, 0.0,\n    ]\n    weekly_data = repeat(day_data, 7)\n    ts_from_to = SingleTimeSeries(\n        \"from_to_flow_limit\",\n        TimeArray(tstamp, weekly_data);\n        scaling_factor_multiplier = get_from_to_flow_limit,\n    )\n    ts_to_from = SingleTimeSeries(\n        \"to_from_flow_limit\",\n        TimeArray(tstamp, weekly_data);\n        scaling_factor_multiplier = get_from_to_flow_limit,\n    )\n    add_time_series!(c_sys, area_int, ts_from_to)\n    add_time_series!(c_sys, area_int, ts_to_from)\n    ## Transform Time Series ##\n    transform_single_time_series!(c_sys, Hour(24), Hour(24))\n    set_flow_limits!(\n        get_component(AreaInterchange, c_sys, \"1_2\"),\n        (from_to = 1.0, to_from = 1.0),\n    )\n    template = get_thermal_dispatch_template_network(NetworkModel(AreaPTDFPowerModel))\n    set_device_model!(template, AreaInterchange, StaticBranch)\n    set_device_model!(template, MonitoredLine, StaticBranchUnbounded)\n    ps_model =\n        DecisionModel(template, c_sys; resolution = Hour(1), optimizer = HiGHS_optimizer)\n\n    @test build!(ps_model; output_dir = mktempdir(; cleanup = true)) ==\n          PSI.ModelBuildStatus.BUILT\n    @test solve!(ps_model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED\n\n    moi_tests(ps_model, 264, 0, 600, 600, 48, false)\n\n    opt_container = PSI.get_optimization_container(ps_model)\n    copper_plate_constraints =\n        PSI.get_constraint(opt_container, CopperPlateBalanceConstraint(), PSY.Area)\n    @test size(copper_plate_constraints) == (2, 24)\n\n    psi_checksolve_test(ps_model, [MOI.OPTIMAL], 489842, 1)\n\n    results = OptimizationProblemResults(ps_model)\n    interarea_flow = read_variable(\n        results,\n        \"FlowActivePowerVariable__AreaInterchange\";\n        table_format = TableFormat.WIDE,\n    )\n    # The values for these tests come from the data\n    @test interarea_flow[1, \"1_2\"] != 0.0\n    @test interarea_flow[5, \"1_2\"] == 0.0\n    @test interarea_flow[6, \"1_2\"] == 0.0\nend\n\nfunction add_dummy_time_series_data!(sys)\n    # Attach dummy data so the problem builds:\n    dummy_data = Dict(\n        DateTime(\"2020-01-01T08:00:00\") => [5.0, 6, 7, 7, 7],\n        DateTime(\"2020-01-01T08:30:00\") => [9.0, 9, 9, 9, 8],\n        DateTime(\"2020-01-01T09:00:00\") => [6.0, 6, 5, 5, 4],\n    )\n    resolution = Dates.Minute(5)\n    dummy_forecast = Deterministic(\"max_active_power\", dummy_data, resolution)\n    load = collect(get_components(StandardLoad, sys))[1]\n    add_time_series!(sys, load, dummy_forecast)\n    return sys\nend\n\n@testset \"Network reductions - PTDF StaticBranch\" begin\n    # Base Case : Only reductions for double circuits:\n    sys = build_system(PSITestSystems, \"case11_network_reductions\")\n    add_dummy_time_series_data!(sys)\n    nr = NetworkReduction[]\n    ptdf = PTDF(sys; network_reductions = nr)\n    template = ProblemTemplate(\n        NetworkModel(PTDFPowerModel;\n            PTDF_matrix = ptdf,\n            reduce_radial_branches = PNM.has_radial_reduction(ptdf.network_reduction_data),\n            reduce_degree_two_branches = PNM.has_degree_two_reduction(\n                ptdf.network_reduction_data,\n            ),\n            use_slacks = false),\n    )\n    set_device_model!(template, Line, StaticBranch)\n    set_device_model!(template, Transformer2W, StaticBranch)\n    ps_model = DecisionModel(template, sys; optimizer = HiGHS_optimizer)\n    @test build!(ps_model; output_dir = mktempdir(; cleanup = true)) ==\n          PSI.ModelBuildStatus.BUILT\n    moi_tests(ps_model, 0, 0, 60, 60, 5, false)\n    container = PSI.get_optimization_container(ps_model)\n    expression = PSI.get_expression(\n        container,\n        PTDFBranchFlow(),\n        Line,\n    )\n    @test size(expression) == (10, 5)\n\n    # Radial Reduction :\n    sys = build_system(PSITestSystems, \"case11_network_reductions\")\n    add_dummy_time_series_data!(sys)\n    nr = NetworkReduction[RadialReduction()]\n    ptdf = PTDF(sys; network_reductions = nr)\n    template = ProblemTemplate(\n        NetworkModel(PTDFPowerModel;\n            PTDF_matrix = ptdf,\n            reduce_radial_branches = PNM.has_radial_reduction(ptdf.network_reduction_data),\n            reduce_degree_two_branches = PNM.has_degree_two_reduction(\n                ptdf.network_reduction_data,\n            ),\n            use_slacks = false),\n    )\n    set_device_model!(template, Line, StaticBranch)\n    set_device_model!(template, Transformer2W, StaticBranch)\n    ps_model = DecisionModel(template, sys; optimizer = HiGHS_optimizer)\n    @test build!(ps_model; output_dir = mktempdir(; cleanup = true)) ==\n          PSI.ModelBuildStatus.BUILT\n    moi_tests(ps_model, 0, 0, 55, 55, 5, false)\n    container = PSI.get_optimization_container(ps_model)\n    expression = PSI.get_expression(\n        container,\n        PTDFBranchFlow(),\n        Line,\n    )\n    @test size(expression) == (9, 5)\n\n    # Degree Two Reduction :\n    sys = build_system(PSITestSystems, \"case11_network_reductions\")\n    add_dummy_time_series_data!(sys)\n    nr = NetworkReduction[DegreeTwoReduction()]\n    ptdf = PTDF(sys; network_reductions = nr)\n    template = ProblemTemplate(\n        NetworkModel(PTDFPowerModel;\n            PTDF_matrix = ptdf,\n            reduce_radial_branches = PNM.has_radial_reduction(ptdf.network_reduction_data),\n            reduce_degree_two_branches = PNM.has_degree_two_reduction(\n                ptdf.network_reduction_data,\n            ),\n            use_slacks = false),\n    )\n    set_device_model!(template, Line, StaticBranch)\n    set_device_model!(template, Transformer2W, StaticBranch)\n    ps_model = DecisionModel(template, sys; optimizer = HiGHS_optimizer)\n    @test build!(ps_model; output_dir = mktempdir(; cleanup = true)) ==\n          PSI.ModelBuildStatus.BUILT\n    moi_tests(ps_model, 0, 0, 35, 35, 5, false)\n    expression = PSI.get_expression(\n        container,\n        PTDFBranchFlow(),\n        Line,\n    )\n    @test size(expression) == (9, 5)\n\n    container = PSI.get_optimization_container(ps_model)\n    line_flow = PSI.get_expression(container, PTDFBranchFlow(), Line)\n    tfw_flow = PSI.get_expression(container, PTDFBranchFlow(), Transformer2W)\n    # Parallel branch within chain to d2:\n    @test line_flow[\"2-10-i_double_circuit\", :] == line_flow[\"10-3-i_1\", :]\n    # D2 chain with different component types:\n    @test line_flow[\"1-9-i_1\", :] == tfw_flow[\"9-5-i_1\", :]\n    # Parallel branch within chain to d2 with mixed types (parallel comes from tracker):\n    @test line_flow[\"3-11-i_1\", :] == tfw_flow[\"11-4-i_double_circuit\", :]\n\n    # Radial + Degree Two Reduction :\n    sys = build_system(PSITestSystems, \"case11_network_reductions\")\n    add_dummy_time_series_data!(sys)\n    nr = NetworkReduction[RadialReduction(), DegreeTwoReduction()]\n    ptdf = PTDF(sys; network_reductions = nr)\n    template = ProblemTemplate(\n        NetworkModel(PTDFPowerModel;\n            PTDF_matrix = ptdf,\n            reduce_radial_branches = PNM.has_radial_reduction(ptdf.network_reduction_data),\n            reduce_degree_two_branches = PNM.has_degree_two_reduction(\n                ptdf.network_reduction_data,\n            ),\n            use_slacks = false),\n    )\n    set_device_model!(template, Line, StaticBranch)\n    set_device_model!(template, Transformer2W, StaticBranch)\n    ps_model = DecisionModel(template, sys; optimizer = HiGHS_optimizer)\n    @test build!(ps_model; output_dir = mktempdir(; cleanup = true)) ==\n          PSI.ModelBuildStatus.BUILT\n    moi_tests(ps_model, 0, 0, 30, 30, 5, false)\n    expression = PSI.get_expression(\n        container,\n        PTDFBranchFlow(),\n        Line,\n    )\n    @test size(expression) == (10, 5)\nend\n\n@testset \"Network reductions - PTDF StaticBranchBounds\" begin\n    # Base Case : Only reductions for double circuits:\n    sys = build_system(PSITestSystems, \"case11_network_reductions\")\n    add_dummy_time_series_data!(sys)\n    nr = NetworkReduction[]\n    ptdf = PTDF(sys; network_reductions = nr)\n    template = ProblemTemplate(\n        NetworkModel(PTDFPowerModel;\n            PTDF_matrix = ptdf,\n            reduce_radial_branches = PNM.has_radial_reduction(ptdf.network_reduction_data),\n            reduce_degree_two_branches = PNM.has_degree_two_reduction(\n                ptdf.network_reduction_data,\n            ),\n            use_slacks = false),\n    )\n    set_device_model!(template, Line, StaticBranchBounds)\n    set_device_model!(template, Transformer2W, StaticBranchBounds)\n    ps_model = DecisionModel(template, sys; optimizer = HiGHS_optimizer)\n    @test build!(ps_model; output_dir = mktempdir(; cleanup = true)) ==\n          PSI.ModelBuildStatus.BUILT\n    moi_tests(ps_model, 60, 0, 0, 0, 65, false)\n    container = PSI.get_optimization_container(ps_model)\n    expression = PSI.get_expression(\n        container,\n        PTDFBranchFlow(),\n        Line,\n    )\n    @test size(expression) == (10, 5)\n    flow_var = PSI.get_variable(\n        container,\n        FlowActivePowerVariable(),\n        Line,\n    )\n    @test size(expression) == (10, 5)\n\n    # Radial Reduction :\n    sys = build_system(PSITestSystems, \"case11_network_reductions\")\n    add_dummy_time_series_data!(sys)\n    nr = NetworkReduction[RadialReduction()]\n    ptdf = PTDF(sys; network_reductions = nr)\n    template = ProblemTemplate(\n        NetworkModel(PTDFPowerModel;\n            PTDF_matrix = ptdf,\n            reduce_radial_branches = PNM.has_radial_reduction(ptdf.network_reduction_data),\n            reduce_degree_two_branches = PNM.has_degree_two_reduction(\n                ptdf.network_reduction_data,\n            ),\n            use_slacks = false),\n    )\n    set_device_model!(template, Line, StaticBranchBounds)\n    set_device_model!(template, Transformer2W, StaticBranchBounds)\n    ps_model = DecisionModel(template, sys; optimizer = HiGHS_optimizer)\n    @test build!(ps_model; output_dir = mktempdir(; cleanup = true)) ==\n          PSI.ModelBuildStatus.BUILT\n    moi_tests(ps_model, 55, 0, 0, 0, 60, false)\n    container = PSI.get_optimization_container(ps_model)\n    expression = PSI.get_expression(\n        container,\n        PTDFBranchFlow(),\n        Line,\n    )\n    @test size(expression) == (9, 5)\n\n    # Degree Two Reduction :\n    sys = build_system(PSITestSystems, \"case11_network_reductions\")\n    add_dummy_time_series_data!(sys)\n    nr = NetworkReduction[DegreeTwoReduction()]\n    ptdf = PTDF(sys; network_reductions = nr)\n    template = ProblemTemplate(\n        NetworkModel(PTDFPowerModel;\n            PTDF_matrix = ptdf,\n            reduce_radial_branches = PNM.has_radial_reduction(ptdf.network_reduction_data),\n            reduce_degree_two_branches = PNM.has_degree_two_reduction(\n                ptdf.network_reduction_data,\n            ),\n            use_slacks = false),\n    )\n    set_device_model!(template, Line, StaticBranchBounds)\n    set_device_model!(template, Transformer2W, StaticBranchBounds)\n    ps_model = DecisionModel(template, sys; optimizer = HiGHS_optimizer)\n    @test build!(ps_model; output_dir = mktempdir(; cleanup = true)) ==\n          PSI.ModelBuildStatus.BUILT\n    moi_tests(ps_model, 35, 0, 0, 0, 40, false)\n    expression = PSI.get_expression(\n        container,\n        PTDFBranchFlow(),\n        Line,\n    )\n    @test size(expression) == (9, 5)\n\n    flow_var = PSI.get_variable(\n        container,\n        FlowActivePowerVariable(),\n        Line,\n    )\n    @test size(expression) == (9, 5)\n\n    container = PSI.get_optimization_container(ps_model)\n    line_flow_exp = PSI.get_expression(container, PTDFBranchFlow(), Line)\n    tfw_flow_exp = PSI.get_expression(container, PTDFBranchFlow(), Transformer2W)\n    # Parallel branch within chain to d2:\n    @test line_flow_exp[\"2-10-i_double_circuit\", :] == line_flow_exp[\"10-3-i_1\", :]\n    # D2 chain with different component types:\n    @test line_flow_exp[\"1-9-i_1\", :] == tfw_flow_exp[\"9-5-i_1\", :]\n    # Parallel branch within chain to d2 with mixed types (parallel comes from tracker):\n    @test line_flow_exp[\"3-11-i_1\", :] == tfw_flow_exp[\"11-4-i_double_circuit\", :]\n\n    container = PSI.get_optimization_container(ps_model)\n    line_flow_var = PSI.get_variable(container, FlowActivePowerVariable(), Line)\n    tfw_flow_var = PSI.get_variable(container, FlowActivePowerVariable(), Transformer2W)\n    # Parallel branch within chain to d2:\n    @test line_flow_var[\"2-10-i_double_circuit\", :] == line_flow_var[\"10-3-i_1\", :]\n    # D2 chain with different component types:\n    @test line_flow_var[\"1-9-i_1\", :] == tfw_flow_var[\"9-5-i_1\", :]\n    # Parallel branch within chain to d2 with mixed types (parallel comes from tracker):\n    @test line_flow_var[\"3-11-i_1\", :] == tfw_flow_var[\"11-4-i_double_circuit\", :]\n\n    # Radial + Degree Two Reduction :\n    sys = build_system(PSITestSystems, \"case11_network_reductions\")\n    add_dummy_time_series_data!(sys)\n    nr = NetworkReduction[RadialReduction(), DegreeTwoReduction()]\n    ptdf = PTDF(sys; network_reductions = nr)\n    template = ProblemTemplate(\n        NetworkModel(PTDFPowerModel;\n            PTDF_matrix = ptdf,\n            reduce_radial_branches = PNM.has_radial_reduction(ptdf.network_reduction_data),\n            reduce_degree_two_branches = PNM.has_degree_two_reduction(\n                ptdf.network_reduction_data,\n            ),\n            use_slacks = false),\n    )\n    set_device_model!(template, Line, StaticBranchBounds)\n    set_device_model!(template, Transformer2W, StaticBranchBounds)\n    ps_model = DecisionModel(template, sys; optimizer = HiGHS_optimizer)\n    @test build!(ps_model; output_dir = mktempdir(; cleanup = true)) ==\n          PSI.ModelBuildStatus.BUILT\n    moi_tests(ps_model, 30, 0, 0, 0, 35, false)\n    expression = PSI.get_expression(\n        container,\n        PTDFBranchFlow(),\n        Line,\n    )\n    @test size(expression) == (10, 5)\n\n    flow_var = PSI.get_variable(\n        container,\n        FlowActivePowerVariable(),\n        Line,\n    )\n    @test size(expression) == (10, 5)\nend\n\n@testset \"Network reductions for system with subnetworks\" begin\n    sys = build_system(PSISystems, \"HVDC_TWO_RTO_RTS_1Hr_sys\")\n    nr = NetworkReduction[RadialReduction(), DegreeTwoReduction()]\n    ptdf = PTDF(sys; network_reductions = nr)\n    template = ProblemTemplate(\n        NetworkModel(PTDFPowerModel;\n            PTDF_matrix = ptdf,\n            reduce_radial_branches = PNM.has_radial_reduction(ptdf.network_reduction_data),\n            reduce_degree_two_branches = PNM.has_degree_two_reduction(\n                ptdf.network_reduction_data,\n            ),\n            use_slacks = false),\n    )\n    set_device_model!(template, Line, StaticBranch)\n    set_device_model!(template, Transformer2W, StaticBranch)\n    ps_model = DecisionModel(template, sys; optimizer = HiGHS_optimizer)\n    @test build!(ps_model; output_dir = mktempdir(; cleanup = true)) ==\n          PSI.ModelBuildStatus.BUILT\nend\n\n# Tests a case with series reductions containing different branch types, some with and some without filters applied\n@testset \"Network reductions + branch filter edge cases\" begin\n    sys = build_system(PSITestSystems, \"case11_network_reductions\")\n    add_dummy_time_series_data!(sys)\n    nr = NetworkReduction[RadialReduction(), DegreeTwoReduction()]\n    ptdf = PTDF(sys; network_reductions = nr)\n    template = ProblemTemplate(\n        NetworkModel(PTDFPowerModel;\n            PTDF_matrix = ptdf,\n            reduce_radial_branches = PNM.has_radial_reduction(ptdf.network_reduction_data),\n            reduce_degree_two_branches = PNM.has_degree_two_reduction(\n                ptdf.network_reduction_data,\n            ),\n            use_slacks = false),\n    )\n    set_device_model!(template, DeviceModel(Line, StaticBranch))\n    modeled_transformer_names = [\"9-5-i_1\"]\n    set_device_model!(\n        template,\n        DeviceModel(\n            Transformer2W,\n            StaticBranch;\n            attributes = Dict(\n                \"filter_function\" => x -> PSY.get_name(x) in modeled_transformer_names,\n            ),\n        ),\n    )\n    ps_model = DecisionModel(template, sys; optimizer = HiGHS_optimizer)\n    @test build!(ps_model; output_dir = mktempdir(; cleanup = true)) ==\n          PSI.ModelBuildStatus.BUILT\nend\n\n# Regression test for https://github.com/Sienna-Platform/PowerSimulations.jl/issues/1594\n# Combines a NetworkModel with radial + degree-two reductions, a Line DeviceModel\n# with a filter_function, and a request for FlowRateConstraint duals. Before the\n# fix in src/devices_models/devices/common/add_constraint_dual.jl, the dual\n# container was sized along PSY.get_name.(devices) — every device passing the\n# filter — while the FlowRateConstraint container was sized along the\n# post-reduction axis from get_branch_argument_constraint_axis. The resulting\n# axis mismatch raised DimensionMismatch in process_duals during dual\n# extraction. Building a model is enough to detect the regression: after the\n# fix, axes(dual)[1] must equal axes(constraint)[1] for every meta.\n@testset \"FlowRateConstraint duals with branch filter and network reductions\" begin\n    sys = build_system(PSITestSystems, \"case11_network_reductions\")\n    add_dummy_time_series_data!(sys)\n    nr = NetworkReduction[RadialReduction(), DegreeTwoReduction()]\n    ptdf = PTDF(sys; network_reductions = nr)\n\n    template = ProblemTemplate(\n        NetworkModel(PTDFPowerModel;\n            PTDF_matrix = ptdf,\n            duals = [CopperPlateBalanceConstraint],\n            reduce_radial_branches = PNM.has_radial_reduction(ptdf.network_reduction_data),\n            reduce_degree_two_branches = PNM.has_degree_two_reduction(\n                ptdf.network_reduction_data,\n            ),\n            use_slacks = false),\n    )\n    # Mirror the filter shape from issue #1594: a voltage threshold that selects\n    # all lines in this all-230 kV system. The filter is registered (so the\n    # filter_function code path runs) but does not exclude any branch from a\n    # series chain, so reductions still drop lines from the constraint axis.\n    set_device_model!(\n        template,\n        DeviceModel(\n            Line,\n            StaticBranch;\n            duals = [FlowRateConstraint],\n            attributes = Dict(\n                \"filter_function\" =>\n                    x -> PSY.get_base_voltage(PSY.get_from(PSY.get_arc(x))) >= 230.0,\n            ),\n        ),\n    )\n    set_device_model!(template, Transformer2W, StaticBranch)\n    ps_model = DecisionModel(template, sys; optimizer = HiGHS_optimizer)\n    @test build!(ps_model; output_dir = mktempdir(; cleanup = true)) ==\n          PSI.ModelBuildStatus.BUILT\n\n    container = PSI.get_optimization_container(ps_model)\n    # The unfiltered Line set has 12 entries; full reduction leaves 6 entries\n    # in the constraint axis. The dual container must use the same 6 entries.\n    for meta in (\"lb\", \"ub\")\n        cons_key = PSI.ConstraintKey(FlowRateConstraint, Line, meta)\n        cons = PSI.get_constraint(container, cons_key)\n        dual = PSI.get_duals(container)[cons_key]\n        @test axes(dual)[1] == axes(cons)[1]\n        @test length(axes(cons)[1]) <\n              length(collect(get_components(Line, sys)))\n        @test \"4-5-i_1\" in axes(cons)[1]\n    end\nend\n\n@testset \"Branch bounds of parallel and series reductions\" begin\n    sys = build_system(PSITestSystems, \"case11_network_reductions\")\n    add_dummy_time_series_data!(sys)\n    nr = NetworkReduction[DegreeTwoReduction()]\n    ptdf = PTDF(sys; network_reductions = nr)\n    template = ProblemTemplate(\n        NetworkModel(PTDFPowerModel;\n            PTDF_matrix = ptdf,\n            reduce_radial_branches = PNM.has_radial_reduction(ptdf.network_reduction_data),\n            reduce_degree_two_branches = PNM.has_degree_two_reduction(\n                ptdf.network_reduction_data,\n            ),\n            use_slacks = false),\n    )\n    set_device_model!(template, Line, StaticBranchBounds)\n    set_device_model!(template, Transformer2W, StaticBranchBounds)\n    ps_model = DecisionModel(template, sys; optimizer = HiGHS_optimizer)\n    @test build!(ps_model; output_dir = mktempdir(; cleanup = true)) ==\n          PSI.ModelBuildStatus.BUILT\n    l1_parallel = PSY.get_rating(PSY.get_component(ACTransmission, sys, \"1-4-i_1\"))\n    l2_parallel = PSY.get_rating(PSY.get_component(ACTransmission, sys, \"1-4-i_2\"))\n    container = PSI.get_optimization_container(ps_model)\n    line_flow_var = PSI.get_variable(\n        container,\n        FlowActivePowerVariable(),\n        Line,\n    )\n    @test JuMP.upper_bound(line_flow_var[\"1-4-i_double_circuit\", 1]) ==\n          minimum([l1_parallel, l2_parallel])\n    l1_series = PSY.get_rating(PSY.get_component(ACTransmission, sys, \"9-5-i_1\"))\n    l2_series = PSY.get_rating(PSY.get_component(ACTransmission, sys, \"1-9-i_1\"))\n    tx_flow_var = PSI.get_variable(\n        container,\n        FlowActivePowerVariable(),\n        Transformer2W,\n    )\n    @test JuMP.upper_bound(tx_flow_var[\"9-5-i_1\", 1]) == minimum([l1_series, l2_series])\n    @test JuMP.upper_bound(line_flow_var[\"1-9-i_1\", 1]) == minimum([l1_series, l2_series])\nend\n\n@testset \"Network reductions - PowerModels\" begin\n    sys = build_system(PSITestSystems, \"case11_network_reductions\")\n    add_dummy_time_series_data!(sys)\n    for (network_model, optimizer) in NETWORKS_FOR_TESTING\n        @testset \"Network Model: $(network_model)\" begin\n            # Only default reductions:\n            template = ProblemTemplate(\n                NetworkModel(network_model;\n                    reduce_radial_branches = false,\n                    reduce_degree_two_branches = false,\n                    use_slacks = false),\n            )\n            set_device_model!(template, Line, StaticBranch)\n            set_device_model!(template, Transformer2W, StaticBranch)\n            ps_model = DecisionModel(template, sys; optimizer = optimizer)\n            @test build!(ps_model; output_dir = mktempdir(; cleanup = true)) ==\n                  PSI.ModelBuildStatus.BUILT\n            JuMPmodel = PSI.get_jump_model(ps_model)\n            n_vars = JuMP.num_variables(JuMPmodel)\n\n            # Radial reductions:\n            template = ProblemTemplate(\n                NetworkModel(network_model;\n                    reduce_radial_branches = true,\n                    reduce_degree_two_branches = false,\n                    use_slacks = false),\n            )\n            set_device_model!(template, Line, StaticBranch)\n            set_device_model!(template, Transformer2W, StaticBranch)\n            ps_model = DecisionModel(template, sys; optimizer = optimizer)\n            @test build!(ps_model; output_dir = mktempdir(; cleanup = true)) ==\n                  PSI.ModelBuildStatus.BUILT\n            JuMPmodel = PSI.get_jump_model(ps_model)\n            n_vars_radial = JuMP.num_variables(JuMPmodel)\n\n            # Radial + degree two reductions:\n            template = ProblemTemplate(\n                NetworkModel(network_model;\n                    reduce_radial_branches = true,\n                    reduce_degree_two_branches = true,\n                    use_slacks = false),\n            )\n            set_device_model!(template, Line, StaticBranch)\n            set_device_model!(template, Transformer2W, StaticBranch)\n            ps_model = DecisionModel(template, sys; optimizer = optimizer)\n            @test build!(ps_model; output_dir = mktempdir(; cleanup = true)) ==\n                  PSI.ModelBuildStatus.BUILT\n            JuMPmodel = PSI.get_jump_model(ps_model)\n            n_vars_radial_d2 = JuMP.num_variables(JuMPmodel)\n\n            @test n_vars_radial_d2 < n_vars_radial < n_vars\n        end\n    end\nend\n\n@testset \"Network reductions - PowerModels with slacks\" begin\n    sys = build_system(PSITestSystems, \"case11_network_reductions\")\n    add_dummy_time_series_data!(sys)\n    for (network_model, optimizer) in NETWORKS_FOR_TESTING\n        @testset \"Network Model: $(network_model)\" begin\n            template = ProblemTemplate(\n                NetworkModel(network_model;\n                    reduce_radial_branches = true,\n                    reduce_degree_two_branches = true,\n                    use_slacks = true),\n            )\n            set_device_model!(\n                template,\n                DeviceModel(Line, StaticBranch; use_slacks = true),\n            )\n            set_device_model!(\n                template,\n                DeviceModel(Transformer2W, StaticBranch; use_slacks = true),\n            )\n            ps_model = DecisionModel(template, sys; optimizer = optimizer)\n            @test build!(ps_model; output_dir = mktempdir(; cleanup = true)) ==\n                  PSI.ModelBuildStatus.BUILT\n        end\n    end\nend\n"
  },
  {
    "path": "test/test_network_constructors_with_dlr.jl",
    "content": "function check_dlr_branch_flows!(\n    res::Union{OptimizationProblemResults, PSI.SimulationProblemResults},\n    sys::PSY.System,\n    branches_dlr::Vector{<:AbstractString},\n    dlr_factors::Vector{Float64},\n    add_parallel_line_name::Union{Nothing, AbstractString} = nothing,\n)\n    for branch_name in branches_dlr\n        branch = get_component(PSY.ACTransmission, sys, branch_name)\n        col_key =\n            if (\n                add_parallel_line_name !== nothing &&\n                contains(branch_name, add_parallel_line_name)\n            )\n                replace(branch_name, \"_copy\" => \"\") * \"double_circuit\"\n            else\n                branch_name\n            end\n\n        static_rating = get_rating(branch) * get_base_power(sys)\n        branch_type = string(typeof(branch))\n        if typeof(res) <: PSI.SimulationProblemResults\n            flow = read_realized_expression(\n                res,\n                \"PTDFBranchFlow__$branch_type\";\n                table_format = TableFormat.WIDE,\n            )[\n                :,\n                col_key,\n            ]\n        else\n            flow = read_expression(\n                res,\n                \"PTDFBranchFlow__$branch_type\";\n                table_format = TableFormat.WIDE,\n            )[\n                :,\n                col_key,\n            ]\n        end\n        n_dlr = length(dlr_factors)\n        for (i, f) in enumerate(flow)\n            dlr_idx = mod1(i, n_dlr)\n            @test f <= static_rating * dlr_factors[dlr_idx] + 1e-5\n            @test f >= -static_rating * dlr_factors[dlr_idx] - 1e-5\n        end\n    end\nend\n\n@testset \"Network DC-PF with VirtualPTDF Model and implementing Dynamic Branch Ratings\" begin\n    line_device_model = DeviceModel(\n        Line,\n        StaticBranch;\n        time_series_names = Dict(\n            DynamicBranchRatingTimeSeriesParameter => \"dynamic_line_ratings\",\n        ))\n    TapTransf_device_model = DeviceModel(\n        TapTransformer,\n        StaticBranch;\n        time_series_names = Dict(\n            DynamicBranchRatingTimeSeriesParameter => \"dynamic_line_ratings\",\n        ))\n    c_sys5 = PSB.build_system(PSITestSystems, \"c_sys5\")\n    c_sys14 = PSB.build_system(PSITestSystems, \"c_sys14\")\n    c_sys14_dc = PSB.build_system(PSITestSystems, \"c_sys14_dc\")\n    systems = [c_sys5, c_sys14, c_sys14_dc]\n    objfuncs = [GAEVF, GQEVF, GQEVF]\n    constraint_keys = [\n        PSI.ConstraintKey(FlowRateConstraint, PSY.Line, \"lb\"),\n        PSI.ConstraintKey(FlowRateConstraint, PSY.Line, \"ub\"),\n        PSI.ConstraintKey(CopperPlateBalanceConstraint, PSY.System),\n    ]\n    PTDF_ref = IdDict{System, PTDF}(\n        c_sys5 => PTDF(c_sys5),\n        c_sys14 => PTDF(c_sys14),\n        c_sys14_dc => PTDF(c_sys14_dc),\n    )\n    branches_dlr = IdDict{System, Vector{String}}(\n        c_sys5 => [\"1\", \"2\", \"6\"],\n        c_sys14 => [\"Line1\", \"Line2\", \"Line9\", \"Line10\", \"Line12\", \"Trans2\"],\n        c_sys14_dc => [\"Line1\", \"Line9\", \"Line10\", \"Line12\", \"Trans2\"],\n    )\n    dlr_factors = vcat([fill(x, 6) for x in [0.99, 0.98, 1.0, 0.95]]...)\n    test_results = IdDict{System, Vector{Int}}(\n        c_sys5 => [120, 0, 264, 264, 24],\n        c_sys14 => [120, 0, 600, 600, 24],\n        c_sys14_dc => [168, 0, 648, 552, 24],\n    )\n    test_obj_values = IdDict{System, Float64}(\n        c_sys5 => 241293.703,\n        c_sys14 => 143365.0,\n        c_sys14_dc => 142000.0,\n    )\n    n_steps = 2\n    for (ix, sys) in enumerate(systems)\n        add_dlr_to_system_branches!(\n            sys,\n            branches_dlr[sys],\n            n_steps,\n            dlr_factors;\n            initial_date = \"2024-01-01\",\n        )\n        template = get_thermal_dispatch_template_network(\n            NetworkModel(\n                PTDFPowerModel;\n                PTDF_matrix = PTDF_ref[sys],\n            ),\n        )\n\n        set_device_model!(template, line_device_model)\n        set_device_model!(template, TapTransf_device_model)\n        ps_model = DecisionModel(template, sys; optimizer = HiGHS_optimizer)\n\n        @test build!(ps_model; output_dir = mktempdir(; cleanup = true)) ==\n              PSI.ModelBuildStatus.BUILT\n        psi_constraint_test(ps_model, constraint_keys)\n\n        moi_tests(\n            ps_model,\n            test_results[sys]...,\n            false,\n        )\n        psi_checkobjfun_test(ps_model, objfuncs[ix])\n        psi_checksolve_test(\n            ps_model,\n            [MOI.OPTIMAL, MOI.ALMOST_OPTIMAL],\n            test_obj_values[sys],\n            10000,\n        )\n\n        res = OptimizationProblemResults(ps_model)\n        check_dlr_branch_flows!(res, sys, branches_dlr[sys], dlr_factors, nothing)\n    end\nend\n\n@testset \"Network DC-PF with PTDF Model and implementing Dynamic Branch Ratings with BranchesParallel of different types\" begin\n    objfuncs = [GAEVF, GQEVF, GQEVF]\n    constraint_keys = [\n        PSI.ConstraintKey(FlowRateConstraint, PSY.Line, \"lb\"),\n        PSI.ConstraintKey(FlowRateConstraint, PSY.Line, \"ub\"),\n        PSI.ConstraintKey(CopperPlateBalanceConstraint, PSY.System),\n    ]\n    branches_dlr = [\"1\", \"2\", \"6\"]\n    dlr_factors = vcat([fill(x, 6) for x in [0.99, 0.98, 1.0, 0.95]]...)\n\n    # DLR constraints are now correctly applied to parallel arcs shared between different branch types.\n    # The first two cases (parallel on lines \"1\" and \"2\") have DLR, resulting in a higher optimal cost.\n    test_obj_values = [375109.0, 320486.0, 241293.703]\n    parallel_lines_names_to_add = [\"1\", \"2\", \"3\"]#Add parallel lines in lines with and without DLRs\n    n_steps = 2\n\n    for slack_flag in [false, true]\n        if slack_flag\n            test_results = [408, 0, 264, 264, 24]\n        else\n            test_results = [120, 0, 264, 264, 24]\n        end\n        line_device_model = DeviceModel(\n            Line,\n            StaticBranch;\n            time_series_names = Dict(\n                DynamicBranchRatingTimeSeriesParameter => \"dynamic_line_ratings\",\n            ),\n            use_slacks = slack_flag,\n        )\n        for (ix, add_parallel_line_name) in enumerate(parallel_lines_names_to_add)\n            sys = PSB.build_system(PSITestSystems, \"c_sys5\")\n            line_to_add_parallel = get_component(Line, sys, add_parallel_line_name)\n            add_equivalent_ac_transmission_with_parallel_circuits!(\n                sys,\n                line_to_add_parallel,\n                PSY.Line,\n                PSY.MonitoredLine,\n            )\n\n            add_dlr_to_system_branches!(\n                sys,\n                branches_dlr,\n                n_steps,\n                dlr_factors;\n                initial_date = \"2024-01-01\",\n            )\n\n            template = get_thermal_dispatch_template_network(\n                NetworkModel(\n                    PTDFPowerModel;\n                    PTDF_matrix = PTDF(sys),\n                ),\n            )\n            set_device_model!(template, line_device_model)\n            set_device_model!(template, PSY.MonitoredLine, StaticBranch)\n            ps_model = DecisionModel(template, sys; optimizer = HiGHS_optimizer)\n\n            @test build!(ps_model; output_dir = mktempdir(; cleanup = true)) ==\n                  PSI.ModelBuildStatus.BUILT\n            psi_constraint_test(ps_model, constraint_keys)\n\n            moi_tests(\n                ps_model,\n                test_results...,\n                false,\n            )\n            psi_checkobjfun_test(ps_model, objfuncs[1])\n            psi_checksolve_test(\n                ps_model,\n                [MOI.OPTIMAL, MOI.ALMOST_OPTIMAL],\n                test_obj_values[ix],\n                10000,\n            )\n\n            res = OptimizationProblemResults(ps_model)\n            check_dlr_branch_flows!(\n                res,\n                sys,\n                branches_dlr,\n                dlr_factors,\n                add_parallel_line_name,\n            )\n        end\n    end\nend\n\n@testset \"Network DC-PF with PTDF Model and implementing Dynamic Branch Ratings with BranchesParallel of different types (MonitoredLine with DLR)\" begin\n    objfuncs = [GAEVF, GQEVF, GQEVF]\n    constraint_keys = [\n        PSI.ConstraintKey(FlowRateConstraint, PSY.Line, \"lb\"),\n        PSI.ConstraintKey(FlowRateConstraint, PSY.Line, \"ub\"),\n        PSI.ConstraintKey(CopperPlateBalanceConstraint, PSY.System),\n    ]\n\n    dlr_factors = vcat([fill(x, 6) for x in [0.99, 0.98, 1.0, 0.95]]...)\n\n    test_obj_values = [375109.0, 320486.0, 241293.703]\n    parallel_lines_names_to_add = [\"1\", \"2\", \"3\"]#Add parallel lines in lines with and without DLRs\n    n_steps = 2\n\n    for slack_flag in [false, true]\n        if slack_flag\n            test_results = [408, 0, 264, 264, 24]\n        else\n            test_results = [120, 0, 264, 264, 24]\n        end\n        line_device_model = DeviceModel(\n            Line,\n            StaticBranch;\n            time_series_names = Dict(\n                DynamicBranchRatingTimeSeriesParameter => \"dynamic_line_ratings\",\n            ),\n            use_slacks = slack_flag,\n        )\n        for (ix, add_parallel_line_name) in enumerate(parallel_lines_names_to_add)\n            sys = PSB.build_system(PSITestSystems, \"c_sys5\")\n            line_to_add_parallel = get_component(Line, sys, add_parallel_line_name)\n            add_equivalent_ac_transmission_with_parallel_circuits!(\n                sys,\n                line_to_add_parallel,\n                PSY.Line,\n                PSY.MonitoredLine,\n            )\n\n            add_dlr_to_system_branches!(\n                sys,\n                [add_parallel_line_name * \"_copy\"],\n                n_steps,\n                dlr_factors;\n                initial_date = \"2024-01-01\",\n            )\n\n            template = get_thermal_dispatch_template_network(\n                NetworkModel(\n                    PTDFPowerModel;\n                    PTDF_matrix = PTDF(sys),\n                ),\n            )\n            set_device_model!(template, line_device_model)\n            set_device_model!(template, PSY.MonitoredLine, StaticBranch)\n            ps_model = DecisionModel(template, sys; optimizer = HiGHS_optimizer)\n\n            @test build!(ps_model; output_dir = mktempdir(; cleanup = true)) ==\n                  PSI.ModelBuildStatus.BUILT\n            psi_constraint_test(ps_model, constraint_keys)\n\n            moi_tests(\n                ps_model,\n                test_results...,\n                false,\n            )\n            psi_checkobjfun_test(ps_model, objfuncs[1])\n            psi_checksolve_test(\n                ps_model,\n                [MOI.OPTIMAL, MOI.ALMOST_OPTIMAL],\n                test_obj_values[ix],\n                10000,\n            )\n\n            res = OptimizationProblemResults(ps_model)\n            check_dlr_branch_flows!(\n                res,\n                sys,\n                [add_parallel_line_name * \"_copy\"],\n                dlr_factors,\n                add_parallel_line_name,\n            )\n        end\n    end\nend\n\n@testset \"Network DC-PF with PTDF Model and implementing Dynamic Branch Ratings with BranchesParallel\" begin\n    objfuncs = [GAEVF, GQEVF, GQEVF]\n    constraint_keys = [\n        PSI.ConstraintKey(FlowRateConstraint, PSY.Line, \"lb\"),\n        PSI.ConstraintKey(FlowRateConstraint, PSY.Line, \"ub\"),\n        PSI.ConstraintKey(CopperPlateBalanceConstraint, PSY.System),\n    ]\n    branches_dlr = [\"1\", \"2\", \"6\"]\n    dlr_factors = vcat([fill(x, 6) for x in [0.99, 0.98, 1.0, 0.95]]...)\n\n    test_obj_values = [356577.0, 279735.0, 241293.703]\n    parallel_lines_names_to_add = [\"1\", \"2\", \"3\"]#Add parallel lines in lines with and without DLRs\n    n_steps = 2\n\n    for slack_flag in [false, true]\n        if slack_flag\n            test_results = [408, 0, 264, 264, 24]\n        else\n            test_results = [120, 0, 264, 264, 24]\n        end\n        line_device_model = DeviceModel(\n            Line,\n            StaticBranch;\n            time_series_names = Dict(\n                DynamicBranchRatingTimeSeriesParameter => \"dynamic_line_ratings\",\n            ),\n            use_slacks = slack_flag,\n        )\n        for (ix, add_parallel_line_name) in enumerate(parallel_lines_names_to_add)\n            sys = PSB.build_system(PSITestSystems, \"c_sys5\")\n            line_to_add_parallel = get_component(Line, sys, add_parallel_line_name)\n            add_equivalent_ac_transmission_with_parallel_circuits!(\n                sys,\n                line_to_add_parallel,\n                PSY.Line,\n            )\n\n            add_dlr_to_system_branches!(\n                sys,\n                branches_dlr,\n                n_steps,\n                dlr_factors;\n                initial_date = \"2024-01-01\",\n            )\n\n            template = get_thermal_dispatch_template_network(\n                NetworkModel(\n                    PTDFPowerModel;\n                    PTDF_matrix = PTDF(sys),\n                ),\n            )\n            set_device_model!(template, line_device_model)\n            ps_model = DecisionModel(template, sys; optimizer = HiGHS_optimizer)\n\n            @test build!(ps_model; output_dir = mktempdir(; cleanup = true)) ==\n                  PSI.ModelBuildStatus.BUILT\n            psi_constraint_test(ps_model, constraint_keys)\n\n            moi_tests(\n                ps_model,\n                test_results...,\n                false,\n            )\n            psi_checkobjfun_test(ps_model, objfuncs[1])\n            psi_checksolve_test(\n                ps_model,\n                [MOI.OPTIMAL, MOI.ALMOST_OPTIMAL],\n                test_obj_values[ix],\n                10000,\n            )\n            res = OptimizationProblemResults(ps_model)\n            check_dlr_branch_flows!(\n                res,\n                sys,\n                branches_dlr,\n                dlr_factors,\n                add_parallel_line_name,\n            )\n        end\n    end\nend\n\n@testset \"Network DC-PF with PTDF Model and implementing Dynamic Branch Ratings with Reductions\" begin\n    objfuncs = [GAEVF, GQEVF, GQEVF]\n    constraint_keys = [\n        PSI.ConstraintKey(FlowRateConstraint, PSY.Line, \"lb\"),\n        PSI.ConstraintKey(FlowRateConstraint, PSY.Line, \"ub\"),\n        PSI.ConstraintKey(CopperPlateBalanceConstraint, PSY.System),\n    ]\n    branches_dlr = [\"1\", \"2\", \"6\"]\n    dlr_factors = vcat([fill(x, 6) for x in [0.99, 0.98, 1.0, 0.95]]...)\n\n    test_obj_values = [356577.0, 279735.0, 241293.703]\n    parallel_lines_names_to_add = [\"1\", \"2\", \"3\"]#Add parallel lines in lines with and without DLRs\n    n_steps = 2\n    test_results_slacks = Dict(\n        1 => [456, 0, 288, 288, 24],\n        2 => [456, 0, 288, 288, 24],\n        3 => [408, 0, 264, 264, 24],\n    )\n    test_results_no_slacks = Dict(\n        1 => [120, 0, 288, 288, 24],\n        2 => [120, 0, 288, 288, 24],\n        3 => [120, 0, 264, 264, 24],\n    )\n\n    for slack_flag in [false, true]\n        line_device_model = DeviceModel(\n            Line,\n            StaticBranch;\n            time_series_names = Dict(\n                DynamicBranchRatingTimeSeriesParameter => \"dynamic_line_ratings\",\n            ),\n            use_slacks = slack_flag,\n        )\n        for (ix, add_parallel_line_name) in enumerate(parallel_lines_names_to_add)\n            if slack_flag\n                test_results = test_results_slacks[ix]\n            else\n                test_results = test_results_no_slacks[ix]\n            end\n            sys = PSB.build_system(PSITestSystems, \"c_sys5\")\n\n            line_to_add_parallel = get_component(Line, sys, add_parallel_line_name)\n            add_equivalent_ac_transmission_with_series_parallel_circuits!(\n                sys,\n                line_to_add_parallel,\n                PSY.Line,\n            )\n\n            add_dlr_to_system_branches!(\n                sys,\n                branches_dlr,\n                n_steps,\n                dlr_factors;\n                initial_date = \"2024-01-01\",\n            )\n            nr = NetworkReduction[DegreeTwoReduction()]\n            ptdf = PTDF(sys; network_reductions = nr)\n            template = get_thermal_dispatch_template_network(\n                NetworkModel(\n                    PTDFPowerModel;\n                    #PTDF_matrix = ptdf,\n                    reduce_degree_two_branches = PNM.has_degree_two_reduction(\n                        ptdf.network_reduction_data,\n                    ),\n                ),\n            )\n            set_device_model!(template, line_device_model)\n            ps_model = DecisionModel(template, sys; optimizer = HiGHS_optimizer)\n\n            @test build!(ps_model; output_dir = mktempdir(; cleanup = true)) ==\n                  PSI.ModelBuildStatus.BUILT\n            psi_constraint_test(ps_model, constraint_keys)\n\n            moi_tests(\n                ps_model,\n                test_results...,\n                false,\n            )\n            psi_checkobjfun_test(ps_model, objfuncs[1])\n            psi_checksolve_test(\n                ps_model,\n                [MOI.OPTIMAL, MOI.ALMOST_OPTIMAL],\n                test_obj_values[ix],\n                10000,\n            )\n            res = OptimizationProblemResults(ps_model)\n            check_dlr_branch_flows!(\n                res,\n                sys,\n                branches_dlr,\n                dlr_factors,\n                add_parallel_line_name,\n            )\n        end\n    end\nend\n\n@testset \"Network DC-PF Simulation with PTDF Model and implementing Dynamic Branch Ratings with Reductions\" begin\n    objfuncs = [GAEVF, GQEVF, GQEVF]\n    constraint_keys = [\n        PSI.ConstraintKey(FlowRateConstraint, PSY.Line, \"lb\"),\n        PSI.ConstraintKey(FlowRateConstraint, PSY.Line, \"ub\"),\n        PSI.ConstraintKey(CopperPlateBalanceConstraint, PSY.System),\n    ]\n    branches_dlr = [\"1\", \"2\", \"6\"]\n    dlr_factors = vcat([fill(x, 6) for x in [0.99, 0.98, 1.0, 0.95]]...)\n\n    parallel_lines_names_to_add = [\"1\", \"2\", \"3\"]#Add parallel lines in lines with and without DLRs\n    n_steps = 2\n    test_results_slacks = Dict(\n        1 => [600, 0, 288, 288, 24],\n        2 => [600, 0, 288, 288, 24],\n        3 => [552, 0, 264, 264, 24],\n    )\n    test_results_no_slacks = Dict(\n        1 => [264, 0, 288, 288, 24],\n        2 => [264, 0, 288, 288, 24],\n        3 => [264, 0, 264, 264, 24],\n    )\n\n    for slack_flag in [false, true]\n        line_device_model = DeviceModel(\n            Line,\n            StaticBranch;\n            time_series_names = Dict(\n                DynamicBranchRatingTimeSeriesParameter => \"dynamic_line_ratings\",\n            ),\n            use_slacks = slack_flag,\n        )\n        for (ix, add_parallel_line_name) in enumerate(parallel_lines_names_to_add)\n            if slack_flag\n                test_results = test_results_slacks[ix]\n            else\n                test_results = test_results_no_slacks[ix]\n            end\n\n            sys = PSB.build_system(PSITestSystems, \"c_sys5\")\n\n            line_to_add_parallel = get_component(Line, sys, add_parallel_line_name)\n            add_equivalent_ac_transmission_with_series_parallel_circuits!(\n                sys,\n                line_to_add_parallel,\n                PSY.Line,\n            )\n\n            add_dlr_to_system_branches!(\n                sys,\n                branches_dlr,\n                n_steps,\n                dlr_factors;\n                initial_date = \"2024-01-01\",\n            )\n            nr = NetworkReduction[DegreeTwoReduction()]\n            ptdf = PTDF(sys; network_reductions = nr)\n            template = get_thermal_dispatch_template_network(\n                NetworkModel(\n                    PTDFPowerModel;\n                    #PTDF_matrix = ptdf,\n                    reduce_degree_two_branches = PNM.has_degree_two_reduction(\n                        ptdf.network_reduction_data,\n                    ),\n                ),\n            )\n            set_device_model!(template, line_device_model)\n            ps_model =\n                DecisionModel(template, sys; optimizer = HiGHS_optimizer, name = \"UC\")\n\n            models = SimulationModels(;\n                decision_models = [ps_model],\n            )\n\n            DA_sequence = SimulationSequence(;\n                models = models,\n                ini_cond_chronology = InterProblemChronology(),\n            )\n\n            current_date = string(today())\n            steps_sim = 2\n            sim = Simulation(;\n                name = \"\",\n                steps = steps_sim,\n                models = models,\n                initial_time = DateTime(\"2024-01-01T00:00:00\"),\n                sequence = DA_sequence,\n                simulation_folder = tempdir())\n\n            @test build!(sim) == PSI.SimulationBuildStatus.BUILT\n\n            @test execute!(sim) ==\n                  IS.Simulation.RunStatusModule.RunStatus.SUCCESSFULLY_FINALIZED\n\n            psi_constraint_test(ps_model, constraint_keys)\n\n            moi_tests(\n                ps_model,\n                test_results...,\n                false,\n            )\n            psi_checkobjfun_test(ps_model, objfuncs[1])\n\n            results = SimulationResults(sim)\n            res = get_decision_problem_results(results, \"UC\")\n            check_dlr_branch_flows!(\n                res,\n                sys,\n                branches_dlr,\n                dlr_factors,\n                add_parallel_line_name,\n            )\n        end\n    end\nend\n"
  },
  {
    "path": "test/test_parallel_branch_parameter_multipliers.jl",
    "content": "@testset \"Parallel-branch multiplier rows are populated per-branch (no NaN)\" begin\n    # Regression coverage for the multiplier-axis bug where parallel branches\n    # sharing a time-series UUID had their multipliers written to the wrong row\n    # of the device-name-keyed multiplier array, leaving the other branch's row\n    # at the construction-time NaN fill.\n    line_device_model = DeviceModel(\n        Line,\n        StaticBranch;\n        time_series_names = Dict(\n            DynamicBranchRatingTimeSeriesParameter => \"dynamic_line_ratings\",\n        ),\n    )\n\n    branches_dlr = [\"1\", \"2\", \"6\"]\n    dlr_factors = vcat([fill(x, 6) for x in [0.99, 0.98, 1.0, 0.95]]...)\n\n    for parallel_line_name in [\"1\", \"2\", \"3\"]\n        sys = PSB.build_system(PSITestSystems, \"c_sys5\")\n        line_to_add_parallel = get_component(Line, sys, parallel_line_name)\n        add_equivalent_ac_transmission_with_parallel_circuits!(\n            sys,\n            line_to_add_parallel,\n            PSY.Line,\n        )\n        add_dlr_to_system_branches!(\n            sys,\n            branches_dlr,\n            2,\n            dlr_factors;\n            initial_date = \"2024-01-01\",\n        )\n\n        template = get_thermal_dispatch_template_network(\n            NetworkModel(PTDFPowerModel; PTDF_matrix = PTDF(sys)),\n        )\n        set_device_model!(template, line_device_model)\n        ps_model = DecisionModel(template, sys; optimizer = HiGHS_optimizer)\n\n        @test build!(ps_model; output_dir = mktempdir(; cleanup = true)) ==\n              PSI.ModelBuildStatus.BUILT\n\n        container = PSI.get_optimization_container(ps_model)\n        param_key = PSI.ParameterKey(DynamicBranchRatingTimeSeriesParameter, Line)\n        param_container = PSI.get_parameter(container, param_key)\n        mult_array = PSI.get_multiplier_array(param_container)\n        device_name_axis = axes(mult_array)[1]\n\n        # Every device row in the multiplier array must be fully populated for\n        # branches that actually carry the time series — no rows left at the\n        # NaN sentinel from the fill! at construction time.\n        for name in device_name_axis\n            row = mult_array[name, :]\n            @test !any(isnan, row)\n        end\n    end\nend\n"
  },
  {
    "path": "test/test_power_flow_in_the_loop.jl",
    "content": "@testset \"AC Power Flow in the loop for PhaseShiftingTransformer\" begin\n    system = build_system(PSITestSystems, \"c_sys5_uc\")\n\n    line = get_component(Line, system, \"1\")\n    arc = get_arc(line)\n    remove_component!(system, line)\n\n    ps = PhaseShiftingTransformer(;\n        name = get_name(line),\n        available = true,\n        active_power_flow = 0.0,\n        reactive_power_flow = 0.0,\n        r = get_r(line),\n        x = get_x(line),\n        primary_shunt = 0.0,\n        tap = 1.0,\n        α = 0.0,\n        rating = get_rating(line),\n        arc = arc,\n        base_power = get_base_power(system),\n    )\n\n    add_component!(system, ps)\n\n    template = get_template_dispatch_with_network(\n        NetworkModel(\n            PTDFPowerModel;\n            PTDF_matrix = PTDF(system),\n            power_flow_evaluation = ACPowerFlow(),\n        ),\n    )\n    set_device_model!(template, DeviceModel(PhaseShiftingTransformer, PhaseAngleControl))\n    model_m = DecisionModel(template, system; optimizer = HiGHS_optimizer)\n    @test build!(model_m; output_dir = mktempdir(; cleanup = true)) ==\n          PSI.ModelBuildStatus.BUILT\n\n    @test solve!(model_m) == PSI.RunStatus.SUCCESSFULLY_FINALIZED\n\n    results = OptimizationProblemResults(model_m)\n    vd = read_variables(results)\n\n    data = PSI.get_power_flow_data(\n        only(PSI.get_power_flow_evaluation_data(PSI.get_optimization_container(model_m))),\n    )\n    base_power = get_base_power(system)\n    phase_results = vd[\"FlowActivePowerVariable__PhaseShiftingTransformer\"]\n\n    # cannot easily test for the \"from\" bus because of the generators \"Park City\" and \"Alta\"\n    bus_lookup = PFS.get_bus_lookup(data)\n    @test isapprox(\n        data.bus_active_power_injections[bus_lookup[get_number(get_to(arc))], :] *\n        base_power,\n        filter(row -> row[:name] == get_name(line), phase_results)[!, :value],\n        atol = 1e-9,\n        rtol = 0,\n    )\nend\n\n@testset \"AC Power Flow in the loop with parallel lines\" begin\n    original_line_flow, parallel_line_flow = zero(ComplexF64), zero(ComplexF64)\n    for replace_line in (true, false)\n        system = build_system(PSITestSystems, \"c_sys5_uc\")\n\n        line = get_component(Line, system, \"1\")\n        # split line into 2 parallel lines.\n        if replace_line\n            original_impedance = get_r(line) + im * get_x(line)\n            original_shunt = get_b(line)\n            remove_component!(system, line)\n            split_impedance = original_impedance * 2\n            split_shunt = (from = 0.5 * original_shunt.from, to = 0.5 * original_shunt.to)\n            for i in 1:2\n                l = Line(;\n                    name = get_name(line) * \"_$i\",\n                    available = true,\n                    active_power_flow = 0.0,\n                    reactive_power_flow = 0.0,\n                    arc = get_arc(line),\n                    r = real(split_impedance),\n                    x = imag(split_impedance),\n                    b = split_shunt,\n                    angle_limits = get_angle_limits(line),\n                    rating = get_rating(line),\n                )\n                add_component!(system, l)\n            end\n        end\n        template = get_template_dispatch_with_network(\n            NetworkModel(\n                PTDFPowerModel;\n                PTDF_matrix = PTDF(system),\n                power_flow_evaluation = ACPowerFlow(),\n            ),\n        )\n        model_m = DecisionModel(template, system; optimizer = HiGHS_optimizer)\n        @test build!(model_m; output_dir = mktempdir(; cleanup = true)) ==\n              PSI.ModelBuildStatus.BUILT\n\n        @test solve!(model_m) == PSI.RunStatus.SUCCESSFULLY_FINALIZED\n        results = OptimizationProblemResults(model_m)\n        vd = read_aux_variables(results)\n        active_power_ft = vd[\"PowerFlowBranchActivePowerFromTo__Line\"]\n        reactive_power_ft = vd[\"PowerFlowBranchReactivePowerFromTo__Line\"]\n        if replace_line\n            name = \"$(get_name(line))_1\"\n            parallel_line_flow =\n                filter(row -> row[:name] == name, active_power_ft)[1, :value][1] +\n                im * filter(row -> row[:name] == name, reactive_power_ft)[1, :value][1]\n        else\n            name = get_name(line)\n            original_line_flow =\n                filter(row -> row[:name] == name, active_power_ft)[1, :value][1] +\n                im * filter(row -> row[:name] == name, reactive_power_ft)[1, :value][1]\n        end\n    end\n\n    @test isapprox(\n        2 * parallel_line_flow,\n        original_line_flow,\n        atol = 1e-3,\n    )\nend\n\n@testset \"AC Power Flow in the loop with a breaker-switch\" begin\n    system = build_system(PSITestSystems, \"c_sys5_uc\")\n    # we choose a line to replace such that the arc lookup of a different line changes.\n    line = get_component(Line, system, \"2\")\n    remove_component!(system, line)\n    bs = PSY.DiscreteControlledACBranch(\n        ;\n        name = get_name(line),\n        available = true,\n        active_power_flow = 0.0,\n        reactive_power_flow = 0.0,\n        arc = get_arc(line),\n        r = 0.0,\n        x = 0.0,\n        rating = get_rating(line),\n        discrete_branch_type = PSY.DiscreteControlledBranchType.BREAKER,\n        branch_status = PSY.DiscreteControlledBranchStatus.CLOSED,\n    )\n    add_component!(system, bs)\n    # these lines end up being parallel, so we set their impedances to be the same\n    line3 = get_component(Line, system, \"3\")\n    line6 = get_component(Line, system, \"6\")\n    PSY.set_r!(line3, PSY.get_r(line6))\n    PSY.set_x!(line3, PSY.get_x(line6))\n    template = get_template_dispatch_with_network(\n        NetworkModel(\n            PTDFPowerModel;\n            PTDF_matrix = PTDF(system),\n            power_flow_evaluation = ACPowerFlow(),\n        ),\n    )\n    model_m = DecisionModel(template, system; optimizer = HiGHS_optimizer)\n\n    @test build!(model_m; output_dir = mktempdir(; cleanup = true)) ==\n          PSI.ModelBuildStatus.BUILT\n    @test solve!(model_m) == PSI.RunStatus.SUCCESSFULLY_FINALIZED\n    # the interface currently doesn't allow for power flow in-the-loop on networks with\n    # reductions. we'd have to pass kwargs all the way down to add_power_flow_data!.\nend\n\n# already has a TwoTerminalGenericHVDCLine\nreplace_hvdc!(::PSY.System, ::Type{TwoTerminalGenericHVDCLine}) = nothing\n\nfunction replace_hvdc!(sys::PSY.System, ::Type{TwoTerminalVSCLine})\n    # required fields for constructor:\n    # name, available, arc, active_power_flow, rating, active_power_limits_from,\n    # active_power_limits_to\n    old_hvdc = only(get_components(TwoTerminalGenericHVDCLine, sys))\n    remove_component!(sys, old_hvdc)\n    hvdc = TwoTerminalVSCLine(;\n        name = get_name(old_hvdc),\n        available = true,\n        arc = get_arc(old_hvdc),\n        active_power_flow = get_active_power_flow(old_hvdc),\n        rating = 100.0, # arbitrary\n        active_power_limits_from = get_active_power_limits_from(old_hvdc),\n        active_power_limits_to = get_active_power_limits_to(old_hvdc),\n    )\n    add_component!(sys, hvdc)\nend\n\nfunction replace_hvdc!(sys::PSY.System, ::Type{TwoTerminalLCCLine})\n    # required fields for constructor (yikes):\n    # name, available, arc, active_power_flow, r, transfer_setpoint, scheduled_dc_voltage,\n    # rectifier_bridges, rectifier_delay_angle_limits, rectifier_rc, rectifier_xc, \n    #rectifier_base_voltage, inverter_bridges, inverter_extinction_angle_limits, \n    # inverter_rc, inverter_xc, inverter_base_voltage, \n    old_hvdc = only(get_components(TwoTerminalGenericHVDCLine, sys))\n    remove_component!(sys, old_hvdc)\n    # hand-tuned parameters.\n    r = 0.01\n    xr = 0.01\n    xi = 0.01\n    hvdc = TwoTerminalLCCLine(;\n        name = get_name(old_hvdc),\n        available = true,\n        arc = get_arc(old_hvdc),\n        active_power_flow = get_active_power_flow(old_hvdc),\n        r = r,\n        transfer_setpoint = 50,\n        scheduled_dc_voltage = 200.0,\n        rectifier_bridges = 1,\n        rectifier_delay_angle_limits = (min = 0.0, max = π / 2),\n        rectifier_rc = 0.0,\n        rectifier_xc = xr,\n        rectifier_base_voltage = 100.0,\n        inverter_bridges = 1,\n        inverter_extinction_angle_limits = (min = 0, max = π / 2),\n        inverter_rc = 0.0,\n        inverter_xc = xi,\n        inverter_base_voltage = 100.0,\n        # rest are optional.\n        #=power_mode = true,\n        switch_mode_voltage = 0.0,\n        compounding_resistance = 0.0,\n        min_compounding_voltage = 0.0,\n        rectifier_transformer_ratio = 1.0,\n        rectifier_tap_setting = 1.0,\n        rectifier_tap_limits = (min = 0.5, max = 1.5),\n        rectifier_tap_step = 0.05,\n        rectifier_delay_angle = 0.01,\n        rectifier_capacitor_reactance = 0.0,\n        inverter_transformer_ratio = 1.0,\n        inverter_tap_setting = 1.0,\n        inverter_tap_limits = (min = 0.5, max = 1.5),\n        inverter_tap_step = 0.05,\n        inverter_extinction_angle = 0.0,\n        inverter_capacitor_reactance = 0.0,\n        active_power_limits_from = (min = 0.0, max = 0.0),\n        active_power_limits_to = (min = 0.0, max = 0.0),\n        reactive_power_limits_from = (min = 0.0, max = 0.0),\n        reactive_power_limits_to = (min = 0.0, max = 0.0),=#\n    )\n    add_component!(sys, hvdc)\nend\n\n@testset \"HVDCs with DC PF in the loop\" begin\n    for hvdc_type in (TwoTerminalGenericHVDCLine, TwoTerminalLCCLine, TwoTerminalVSCLine)\n        sys = build_system(PSISystems, \"2Area 5 Bus System\")\n        replace_hvdc!(sys, hvdc_type)\n\n        template_uc =\n            ProblemTemplate(\n                NetworkModel(PTDFPowerModel; power_flow_evaluation = DCPowerFlow()),\n            )\n\n        set_device_model!(template_uc, ThermalStandard, ThermalBasicUnitCommitment)\n        set_device_model!(template_uc, RenewableDispatch, RenewableFullDispatch)\n        set_device_model!(template_uc, PowerLoad, StaticPowerLoad)\n        set_device_model!(template_uc, DeviceModel(Line, StaticBranch))\n\n        if hvdc_type == TwoTerminalVSCLine\n            set_device_model!(\n                template_uc,\n                # regardless of formulation, PowerFlows.jl always takes losses into account...\n                DeviceModel(hvdc_type, HVDCTwoTerminalLossless),\n            )\n        else\n            set_device_model!(\n                template_uc,\n                DeviceModel(hvdc_type, HVDCTwoTerminalDispatch),\n            )\n        end\n\n        model = DecisionModel(template_uc, sys; name = \"UC\", optimizer = HiGHS_optimizer)\n\n        @test build!(model; output_dir = mktempdir()) == PSI.ModelBuildStatus.BUILT\n        @test solve!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED\n    end\nend\n\n@testset \"LCC HVDC with AC PF in the loop\" begin\n    sys5 = build_system(PSISystems, \"2Area 5 Bus System\")\n    hvdc = first(get_components(TwoTerminalGenericHVDCLine, sys5))\n    lcc = TwoTerminalLCCLine(;\n        name = \"lcc\",\n        available = true,\n        arc = hvdc.arc,\n        active_power_flow = 0.1,\n        r = 0.000189,\n        transfer_setpoint = -100.0,\n        scheduled_dc_voltage = 7.5,\n        rectifier_bridges = 2,\n        rectifier_delay_angle_limits = (min = 0.31590, max = 1.570),\n        rectifier_rc = 2.6465e-5,\n        rectifier_xc = 0.001092,\n        rectifier_base_voltage = 230.0,\n        inverter_bridges = 2,\n        inverter_extinction_angle_limits = (min = 0.3037, max = 1.57076),\n        inverter_rc = 2.6465e-5,\n        inverter_xc = 0.001072,\n        inverter_base_voltage = 230.0,\n        power_mode = true,\n        switch_mode_voltage = 0.0,\n        compounding_resistance = 0.0,\n        min_compounding_voltage = 0.0,\n        rectifier_transformer_ratio = 0.09772,\n        rectifier_tap_setting = 1.0,\n        rectifier_tap_limits = (min = 1, max = 1),\n        rectifier_tap_step = 0.00624,\n        rectifier_delay_angle = 0.31590,\n        rectifier_capacitor_reactance = 0.1,\n        inverter_transformer_ratio = 0.07134,\n        inverter_tap_setting = 1.0,\n        inverter_tap_limits = (min = 1, max = 1),\n        inverter_tap_step = 0.00625,\n        inverter_extinction_angle = 0.31416,\n        inverter_capacitor_reactance = 0.0,\n        active_power_limits_from = (min = -3.0, max = 3.0),\n        active_power_limits_to = (min = -3.0, max = 3.0),\n        reactive_power_limits_from = (min = -3.0, max = 3.0),\n        reactive_power_limits_to = (min = -3.0, max = 3.0),\n    )\n\n    add_component!(sys5, lcc)\n    remove_component!(sys5, hvdc)\n\n    template = get_thermal_dispatch_template_network(\n        NetworkModel(\n            ACPPowerModel;\n            use_slacks = false,\n            power_flow_evaluation = ACPowerFlow(),\n        ),\n    )\n\n    set_device_model!(template, TwoTerminalLCCLine, PSI.HVDCTwoTerminalLCC)\n    set_device_model!(template, ThermalStandard, ThermalDispatchNoMin)\n\n    model = DecisionModel(\n        template,\n        sys5;\n        optimizer = optimizer_with_attributes(Ipopt.Optimizer),\n        horizon = Hour(2),\n    )\n    @test build!(model; output_dir = mktempdir(; cleanup = true)) ==\n          PSI.ModelBuildStatus.BUILT\n    @test solve!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED\nend\n\n@testset \"generic HVDC with AC PF in the loop\" begin\n    # TODO replace RTS with something smaller, so this test case doesn't take so long.\n    sys = build_system(PSISystems, \"RTS_GMLC_DA_sys\")\n\n    hvdc = only(get_components(TwoTerminalGenericHVDCLine, sys))\n    from = get_from(get_arc(hvdc))\n    to = get_to(get_arc(hvdc))\n\n    # remove components that impact total bus power at the HVDC line buses.\n    components = collect(\n        get_components(\n            x -> get_number(get_bus(x)) ∈ (get_number(from), get_number(to)),\n            StaticInjection,\n            sys,\n        ),\n    )\n    foreach(x -> remove_component!(sys, x), components)\n    change_to_PQ = [\"Chifa\", \"Arne\"]\n    for bus_name in change_to_PQ\n        bus = get_component(PSY.ACBus, sys, bus_name)\n        @assert !isnothing(bus) \"bus does not exist\"\n        set_bustype!(bus, PSY.ACBusTypes.PQ)\n    end\n\n    set_bustype!(get_component(ACBus, sys, \"Arthur\"), ACBusTypes.REF)\n\n    template_uc =\n        ProblemTemplate(NetworkModel(PTDFPowerModel; power_flow_evaluation = ACPowerFlow()))\n\n    set_device_model!(template_uc, ThermalStandard, ThermalBasicUnitCommitment)\n    set_device_model!(template_uc, RenewableDispatch, RenewableFullDispatch)\n    set_device_model!(template_uc, PowerLoad, StaticPowerLoad)\n    set_device_model!(template_uc, DeviceModel(Line, StaticBranch))\n    set_device_model!(\n        template_uc,\n        DeviceModel(TwoTerminalGenericHVDCLine, HVDCTwoTerminalDispatch),\n    )\n\n    model = DecisionModel(template_uc, sys; name = \"UC\", optimizer = HiGHS_optimizer)\n\n    @test build!(model; output_dir = mktempdir()) == PSI.ModelBuildStatus.BUILT\n    @test solve!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED\n\n    results = OptimizationProblemResults(model)\n    vd = read_variables(results)\n    ad = read_aux_variables(results)\n\n    data = PSI.get_power_flow_data(\n        only(PSI.get_power_flow_evaluation_data(PSI.get_optimization_container(model))),\n    )\n    base_power = get_base_power(sys)\n\n    # test that the power flow results for the HVDC buses match the HVDC power transfer from the simulation\n    bus_lookup = PFS.get_bus_lookup(data)\n\n    from_to = vd[\"FlowActivePowerFromToVariable__TwoTerminalGenericHVDCLine\"][:, :value]\n    to_from = vd[\"FlowActivePowerToFromVariable__TwoTerminalGenericHVDCLine\"][:, :value]\n\n    @test isapprox(\n        data.bus_active_power_injections[bus_lookup[get_number(from)], :] * base_power,\n        -1 .* from_to,\n        atol = 1e-9,\n        rtol = 0,\n    )\n    # verify the line loss curve is exactly 10% so the loss-ratio check below is meaningful\n    hvdc_loss_curve = get_loss(hvdc)\n    @assert hvdc_loss_curve isa PSY.LinearCurve\n    @assert get_proportional_term(hvdc_loss_curve) == 0.1\n    @assert get_constant_term(hvdc_loss_curve) == 0.0\n    nonzeros = (abs.(from_to) .> 1e-9) .| (abs.(to_from) .> 1e-9)\n    loss_ratios = (from_to .+ to_from) ./ maximum.(zip(abs.(from_to), abs.(to_from)))\n    ten_percent_loss = abs.(loss_ratios .- 0.1) .< 1e-9\n    @test all(ten_percent_loss[nonzeros])\n\n    @test isapprox(\n        data.bus_active_power_injections[bus_lookup[get_number(to)], :] * base_power,\n        -1 .* to_from,\n        atol = 1e-9,\n        rtol = 0,\n    )\nend\n\n@testset \"Test AC power flow in the loop: small system UCED, PSS/E export\" for calculate_loss_factors in\n                                                                               (true, false)\n    for calculate_voltage_stability_factors in (true, false)\n        file_path = mktempdir(; cleanup = true)\n        export_path = mktempdir(; cleanup = true)\n        pf_path = mktempdir(; cleanup = true)\n        c_sys5_hy_uc = PSB.build_system(PSITestSystems, \"c_sys5_hy_uc\")\n        c_sys5_hy_ed = PSB.build_system(PSITestSystems, \"c_sys5_hy_ed\")\n        sim = run_simulation(\n            c_sys5_hy_uc,\n            c_sys5_hy_ed,\n            file_path,\n            export_path;\n            ed_network_model = NetworkModel(\n                CopperPlatePowerModel;\n                duals = [CopperPlateBalanceConstraint],\n                use_slacks = true,\n                power_flow_evaluation =\n                ACPowerFlow(;\n                    exporter = PSSEExportPowerFlow(:v33, pf_path; write_comments = true),\n                    calculate_loss_factors = calculate_loss_factors,\n                    calculate_voltage_stability_factors = calculate_voltage_stability_factors,\n                ),\n            ),\n        )\n        results = SimulationResults(sim)\n        results_ed = get_decision_problem_results(results, \"ED\")\n        thermal_results = first(\n            values(\n                PSI.read_results_with_keys(results_ed,\n                    [PSI.VariableKey(ActivePowerVariable, ThermalStandard)]),\n            ),\n        )\n        min_time = minimum(thermal_results.DateTime)\n        max_time = maximum(thermal_results.DateTime)\n        first_result = filter(row -> row[:DateTime] == min_time, thermal_results)\n        last_result = filter(row -> row[:DateTime] == max_time, thermal_results)\n\n        available_aux_variables = list_aux_variable_keys(results_ed)\n        loss_factors_aux_var_key = PSI.AuxVarKey(PowerFlowLossFactors, ACBus)\n        voltage_stability_aux_var_key =\n            PSI.AuxVarKey(PowerFlowVoltageStabilityFactors, ACBus)\n\n        # here we check if the loss factors are stored in the results, the values are tested in PowerFlows.jl\n        if calculate_loss_factors\n            @test loss_factors_aux_var_key ∈ available_aux_variables\n            loss_factors = first(\n                values(\n                    PSI.read_results_with_keys(results_ed,\n                        [loss_factors_aux_var_key]),\n                ),\n            )\n            @test !isnothing(loss_factors)\n            # count distinct time periods\n            @test length(unique(loss_factors.DateTime)) == 48 * 12\n        else\n            @test loss_factors_aux_var_key ∉ available_aux_variables\n        end\n\n        if calculate_voltage_stability_factors\n            @test voltage_stability_aux_var_key ∈ available_aux_variables\n            voltage_stability = first(\n                values(\n                    PSI.read_results_with_keys(results_ed,\n                        [voltage_stability_aux_var_key];\n                        table_format = TableFormat.LONG),\n                ),\n            )\n            @test !isnothing(voltage_stability)\n            @test length(unique(voltage_stability.DateTime)) == 48 * 12\n        else\n            @test voltage_stability_aux_var_key ∉ available_aux_variables\n        end\n\n        @test length(filter(x -> isdir(joinpath(pf_path, x)), readdir(pf_path))) == 48 * 12\n        # this now returns a system?!\n        first_export = load_pf_export(pf_path, \"export_1_1\")\n        last_export = load_pf_export(pf_path, \"export_48_12\")\n\n        # Test that the active powers written to the first and last exports line up with the real simulation results\n        for gen_name in get_name.(get_components(ThermalStandard, c_sys5_hy_ed))\n            this_first_result =\n                filter(row -> row[:name] == gen_name, first_result)[1, :value]\n            this_first_exported =\n                get_active_power(get_component(ThermalStandard, first_export, gen_name))\n            @test isapprox(this_first_result, this_first_exported)\n\n            this_last_result = filter(row -> row[:name] == gen_name, last_result)[1, :value]\n            this_last_exported =\n                get_active_power(get_component(ThermalStandard, last_export, gen_name))\n            @test isapprox(this_last_result, this_last_exported)\n        end\n    end\nend\n\n@testset \"AC Power Flow line active power loss auxiliary variable\" begin\n    system = build_system(PSITestSystems, \"c_sys5_uc\")\n\n    template = get_template_dispatch_with_network(\n        NetworkModel(\n            PTDFPowerModel;\n            PTDF_matrix = PTDF(system),\n            power_flow_evaluation = ACPowerFlow(),\n        ),\n    )\n    model_m = DecisionModel(template, system; optimizer = HiGHS_optimizer)\n    @test build!(model_m; output_dir = mktempdir(; cleanup = true)) ==\n          PSI.ModelBuildStatus.BUILT\n    @test solve!(model_m) == PSI.RunStatus.SUCCESSFULLY_FINALIZED\n\n    results = OptimizationProblemResults(model_m)\n    ad = read_aux_variables(results)\n\n    active_power_ft = ad[\"PowerFlowBranchActivePowerFromTo__Line\"]\n    active_power_tf = ad[\"PowerFlowBranchActivePowerToFrom__Line\"]\n    active_power_loss = ad[\"PowerFlowBranchActivePowerLoss__Line\"]\n\n    for line_name in unique(active_power_loss.name)\n        ft_vals = filter(row -> row[:name] == line_name, active_power_ft)[!, :value]\n        tf_vals = filter(row -> row[:name] == line_name, active_power_tf)[!, :value]\n        loss_vals = filter(row -> row[:name] == line_name, active_power_loss)[!, :value]\n        @test isapprox(loss_vals, ft_vals .+ tf_vals; atol = 1e-9)\n    end\nend\n\n@testset \"AC Power Flow in the loop with headroom-proportional slack\" begin\n    system = build_system(PSITestSystems, \"c_sys5_uc\")\n\n    template = get_template_dispatch_with_network(\n        NetworkModel(\n            PTDFPowerModel;\n            PTDF_matrix = PTDF(system),\n            power_flow_evaluation = ACPowerFlow(;\n                distribute_slack_proportional_to_headroom = true,\n                correct_bustypes = true,\n            ),\n        ),\n    )\n    model = DecisionModel(template, system; optimizer = HiGHS_optimizer)\n    @test build!(model; output_dir = mktempdir(; cleanup = true)) ==\n          PSI.ModelBuildStatus.BUILT\n    @test solve!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED\n\n    container = PSI.get_optimization_container(model)\n    pf_e_data = only(PSI.get_power_flow_evaluation_data(container))\n    data = PSI.get_power_flow_data(pf_e_data)\n\n    computed_gspf = PFS.get_computed_gspf(data)\n    n_time_steps = length(PSI.get_time_steps(container))\n    bus_lookup = PFS.get_bus_lookup(data)\n    base_power = get_base_power(system)\n\n    # Headroom factors should be populated for every time step\n    @test length(computed_gspf) == n_time_steps\n    @test all(!isempty(d) for d in computed_gspf)\n    @test all(all(v > 0.0 for v in values(d)) for d in computed_gspf)\n\n    # bus_active_power_range should equal the sum of generator headroom per bus per time step\n    for t in 1:n_time_steps\n        bus_headroom_check = zeros(size(data.bus_active_power_range, 1))\n        for ((comp_type, comp_name), headroom) in computed_gspf[t]\n            comp = get_component(comp_type, system, comp_name)\n            bus_number = get_number(get_bus(comp))\n            bus_ix = bus_lookup[bus_number]\n            bus_headroom_check[bus_ix] += headroom\n        end\n        @test isapprox(\n            data.bus_active_power_range[:, t],\n            bus_headroom_check;\n            atol = 1e-10,\n        )\n    end\n\n    # bus_slack_participation_factors should match bus_active_power_range at participating buses\n    bus_slack_pf = PFS.get_bus_slack_participation_factors(data)\n    for t in 1:n_time_steps\n        for bus_ix in axes(data.bus_active_power_range, 1)\n            R_k = data.bus_active_power_range[bus_ix, t]\n            if R_k > 0.0\n                @test bus_slack_pf[bus_ix, t] == R_k\n            end\n        end\n    end\n\n    # Independently recompute expected headroom from optimization results and system limits,\n    # then verify it matches computed_gspf exactly.\n    # NOTE: c_sys5_uc thermals have fixed active power limits (no ActivePowerTimeSeriesParameter),\n    # so this exercises the static P_max path. The time-varying P_max path (using\n    # min(static_limit, ts_param_value)) would need a system with renewable time series at a\n    # REF/PV bus to be exercised.\n    for t in 1:n_time_steps\n        for ((comp_type, comp_name), headroom) in computed_gspf[t]\n            comp = get_component(comp_type, system, comp_name)\n            p_max_sys = PFS.get_active_power_limits_for_power_flow(comp).max\n\n            # Look up the optimization set point for this generator at this time step\n            var_key = PSI.VariableKey(PSI.ActivePowerVariable, comp_type)\n            result_data = PSI.lookup_value(container, var_key)\n            p_setpoint = JuMP.value(result_data[comp_name, t])\n\n            expected_headroom = p_max_sys - p_setpoint\n            @test expected_headroom > 0.0\n            @test isapprox(headroom, expected_headroom; atol = 1e-10)\n        end\n    end\nend\n\n@testset \"Headroom proportional slack excludes FixedOutput generators\" begin\n    # c_sys5_uc_re has renewables at PV/REF buses, so they would normally participate\n    # in headroom slack. Setting them to FixedOutput should exclude them.\n    system = build_system(PSITestSystems, \"c_sys5_uc_re\")\n\n    template = get_template_dispatch_with_network(\n        NetworkModel(\n            PTDFPowerModel;\n            PTDF_matrix = PTDF(system),\n            use_slacks = true,\n            power_flow_evaluation = ACPowerFlow(;\n                distribute_slack_proportional_to_headroom = true,\n                correct_bustypes = true,\n            ),\n        ),\n    )\n    set_device_model!(template, RenewableDispatch, FixedOutput)\n\n    model = DecisionModel(template, system; optimizer = HiGHS_optimizer)\n    @test build!(model; output_dir = mktempdir(; cleanup = true)) ==\n          PSI.ModelBuildStatus.BUILT\n    @test solve!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED\n\n    container = PSI.get_optimization_container(model)\n    pf_e_data = only(PSI.get_power_flow_evaluation_data(container))\n    data = PSI.get_power_flow_data(pf_e_data)\n    computed_gspf = PFS.get_computed_gspf(data)\n\n    # The renewable is at a PV bus but uses FixedOutput, so it must not appear in the\n    # headroom factors — only ThermalStandard generators should participate.\n    for t in 1:length(PSI.get_time_steps(container))\n        for ((comp_type, _), _) in computed_gspf[t]\n            @test comp_type <: ThermalStandard\n        end\n    end\nend\n\n@testset \"Headroom proportional slack with time-varying active power limits\" begin\n    # c_sys5_uc_re has renewables at PV/REF buses with time series data.\n    system = build_system(PSITestSystems, \"c_sys5_uc_re\")\n    re_gen = first(get_components(RenewableDispatch, system))\n    re_name = get_name(re_gen)\n    # Force device_base != system_base so the unit-handling path is exercised. If headroom\n    # ever silently re-introduces a `* device_base / system_base` factor, the recomputed\n    # expected_headroom below will mismatch by 0.5×, failing the assertion.\n    PSY.set_units_base_system!(system, \"SYSTEM_BASE\")\n    PSY.set_base_power!(re_gen, get_base_power(re_gen) / 2)\n\n    template = get_template_dispatch_with_network(\n        NetworkModel(\n            PTDFPowerModel;\n            PTDF_matrix = PTDF(system),\n            use_slacks = true,\n            power_flow_evaluation = ACPowerFlow(;\n                distribute_slack_proportional_to_headroom = true,\n                correct_bustypes = true,\n            ),\n        ),\n    )\n    set_device_model!(template, RenewableDispatch, RenewableFullDispatch)\n\n    model = DecisionModel(template, system; optimizer = HiGHS_optimizer)\n    @test build!(model; output_dir = mktempdir(; cleanup = true)) ==\n          PSI.ModelBuildStatus.BUILT\n    @test solve!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED\n\n    container = PSI.get_optimization_container(model)\n    pf_e_data = only(PSI.get_power_flow_evaluation_data(container))\n    data = PSI.get_power_flow_data(pf_e_data)\n    computed_gspf = PFS.get_computed_gspf(data)\n    n_time_steps = length(PSI.get_time_steps(container))\n\n    # Verify the renewable's headroom uses min(static_limit, ts_param) at each time step\n    re_type = typeof(re_gen)\n    p_max_static = PFS.get_active_power_limits_for_power_flow(re_gen).max\n\n    var_key = PSI.VariableKey(PSI.ActivePowerVariable, re_type)\n    var_values = PSI.lookup_value(container, var_key)\n    ts_key = PSI.ParameterKey(PSI.ActivePowerTimeSeriesParameter, re_type)\n    ts_values = PSI.lookup_value(container, ts_key)\n\n    for t in 1:n_time_steps\n        p_setpoint = JuMP.value(var_values[re_name, t])\n        p_max_ts = ts_values[re_name, t]\n        p_max_t = min(p_max_static, p_max_ts)\n        expected_headroom = p_max_t - p_setpoint\n\n        entry = get(computed_gspf[t], (re_type, re_name), nothing)\n        if expected_headroom > 0.0\n            @test entry !== nothing\n            @test isapprox(entry, expected_headroom; atol = 1e-10)\n        else\n            @test entry === nothing\n        end\n    end\n\n    # The time series should cause P_max to vary, producing different headroom across steps\n    re_ts_vals = [ts_values[re_name, t] for t in 1:n_time_steps]\n    @test !all(isapprox.(re_ts_vals, re_ts_vals[1]; atol = 1e-10))\nend\n\n@testset \"Power Flow in the loop with separate in/out active power variables\" begin\n    # Build a system with a Source component that uses ImportExportSourceModel,\n    # which creates separate ActivePowerInVariable and ActivePowerOutVariable.\n    sys = make_5_bus_with_import_export(; add_single_time_series = false)\n\n    template = get_template_dispatch_with_network(\n        NetworkModel(\n            PTDFPowerModel;\n            PTDF_matrix = PTDF(sys),\n            power_flow_evaluation = PTDFDCPowerFlow(),\n        ),\n    )\n    set_device_model!(\n        template,\n        DeviceModel(\n            Source,\n            ImportExportSourceModel;\n            attributes = Dict(\"reservation\" => false),\n        ),\n    )\n    model = DecisionModel(template, sys; optimizer = HiGHS_optimizer)\n    @test build!(model; output_dir = mktempdir(; cleanup = true)) ==\n          PSI.ModelBuildStatus.BUILT\n    @test solve!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED\n\n    container = PSI.get_optimization_container(model)\n    pf_e_data =\n        only(PSI.get_power_flow_evaluation_data(container))\n    input_key_map = PSI.get_input_key_map(pf_e_data)\n\n    # Verify that active_power_in and active_power_out categories are present in the map\n    @test haskey(input_key_map, :active_power_in)\n    @test haskey(input_key_map, :active_power_out)\n\n    # Check that ActivePowerInVariable and ActivePowerOutVariable for Source are mapped\n    in_keys = collect(keys(input_key_map[:active_power_in]))\n    out_keys = collect(keys(input_key_map[:active_power_out]))\n    @test any(\n        k ->\n            PSI.get_entry_type(k) == PSI.ActivePowerInVariable &&\n                PSI.get_component_type(k) == Source,\n        in_keys,\n    )\n    @test any(\n        k ->\n            PSI.get_entry_type(k) == PSI.ActivePowerOutVariable &&\n                PSI.get_component_type(k) == Source,\n        out_keys,\n    )\n\n    # Verify the PowerFlowData bus injections reflect the net power (out - in)\n    data = PSI.get_power_flow_data(pf_e_data)\n    base_power = get_base_power(sys)\n    bus_lookup = PFS.get_bus_lookup(data)\n\n    source = get_component(Source, sys, \"source\")\n    source_bus_ix = bus_lookup[get_number(get_bus(source))]\n\n    results = OptimizationProblemResults(model)\n    vd = read_variables(results)\n    p_out_results = vd[\"ActivePowerOutVariable__Source\"]\n    p_in_results = vd[\"ActivePowerInVariable__Source\"]\n\n    source_p_out = filter(row -> row[:name] == \"source\", p_out_results)[!, :value]\n    source_p_in = filter(row -> row[:name] == \"source\", p_in_results)[!, :value]\n\n    @test length(source_p_out) > 0\n    @test length(source_p_in) > 0\n\n    # The net bus injection (= injections − withdrawals) at the source bus must equal\n    # the sum of every `:active_power` contribution at that bus plus the source's\n    # (out − in). Loads are routed to `bus_active_power_withdrawals` under the same\n    # category, so subtracting withdrawals folds them back into the comparison.\n    other_injection_at_bus = zeros(length(source_p_out))\n    for (key, comp_map) in input_key_map[:active_power]\n        result_data = PSI.lookup_value(container, key)\n        for (dev_name, bus_ix) in comp_map\n            bus_ix == source_bus_ix || continue\n            for t in eachindex(other_injection_at_bus)\n                other_injection_at_bus[t] += PSI.jump_value(result_data[dev_name, t])\n            end\n        end\n    end\n    source_net_pu = (source_p_out .- source_p_in) ./ base_power\n    @test isapprox(\n        data.bus_active_power_injections[source_bus_ix, :] .-\n        data.bus_active_power_withdrawals[source_bus_ix, :],\n        other_injection_at_bus .+ source_net_pu;\n        atol = 1e-9,\n    )\nend\n\n@testset \"Headroom proportional slack with in/out active power variables (Source)\" begin\n    # nodeC is a PV bus in c_sys5_uc, so a Source there can participate in headroom slack.\n    sys = make_5_bus_with_import_export(; add_single_time_series = false)\n\n    template = get_template_dispatch_with_network(\n        NetworkModel(\n            PTDFPowerModel;\n            PTDF_matrix = PTDF(sys),\n            power_flow_evaluation = ACPowerFlow(;\n                distribute_slack_proportional_to_headroom = true,\n                correct_bustypes = true,\n            ),\n        ),\n    )\n    set_device_model!(\n        template,\n        DeviceModel(\n            Source,\n            ImportExportSourceModel;\n            attributes = Dict(\"reservation\" => false),\n        ),\n    )\n    model = DecisionModel(template, sys; optimizer = HiGHS_optimizer)\n    @test build!(model; output_dir = mktempdir(; cleanup = true)) ==\n          PSI.ModelBuildStatus.BUILT\n    @test solve!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED\n\n    container = PSI.get_optimization_container(model)\n    pf_e_data = only(PSI.get_power_flow_evaluation_data(container))\n    data = PSI.get_power_flow_data(pf_e_data)\n    computed_gspf = PFS.get_computed_gspf(data)\n    n_time_steps = length(PSI.get_time_steps(container))\n\n    source = get_component(Source, sys, \"source\")\n    p_max_out = PSY.get_active_power_limits(source).max\n\n    in_key = PSI.VariableKey(PSI.ActivePowerInVariable, Source)\n    out_key = PSI.VariableKey(PSI.ActivePowerOutVariable, Source)\n    p_in_data = PSI.lookup_value(container, in_key)\n    p_out_data = PSI.lookup_value(container, out_key)\n\n    # When net = p_out - p_in is negative the source is charging and per spec gets zero\n    # headroom (omitted from `computed_gspf`); when net >= 0 the headroom is p_max_out - net.\n    for t in 1:n_time_steps\n        net = JuMP.value(p_out_data[\"source\", t]) - JuMP.value(p_in_data[\"source\", t])\n        if net < 0.0\n            @test !haskey(computed_gspf[t], (Source, \"source\"))\n        else\n            @test isapprox(\n                computed_gspf[t][(Source, \"source\")],\n                p_max_out - net;\n                atol = 1e-10,\n            )\n        end\n    end\n    # Guard against a regression that silently drops the in/out accumulation path.\n    @test any(haskey(d, (Source, \"source\")) for d in computed_gspf)\nend\n\n@testset \"Power Flow in the loop with Source FixedOutput (parameter path)\" begin\n    # Source under FixedOutput emits ActivePowerIn/OutTimeSeriesParameter and\n    # no In/Out Variables. Verifies that the parameter keys are picked up by\n    # PF_INPUT_KEY_PRECEDENCES (Luke Kiernan, PR #1612), and that the resulting\n    # bus injection follows the same out − in allocation rule as the variable path.\n    sys = make_5_bus_with_import_export(; add_single_time_series = true)\n    source = get_component(Source, sys, \"source\")\n\n    load = first(get_components(PowerLoad, sys))\n    tstamp = TimeSeries.timestamp(\n        get_time_series_array(SingleTimeSeries, load, \"max_active_power\"),\n    )\n    day_data = [\n        0.9, 0.85, 0.95, 0.2, 0.0, 0.0,\n        0.9, 0.85, 0.95, 0.2, 0.0, 0.0,\n        0.9, 0.85, 0.95, 0.2, 0.0, 0.0,\n        0.9, 0.85, 0.95, 0.2, 0.0, 0.0,\n    ]\n    ts_data = repeat(day_data, 2)\n    ts_out = SingleTimeSeries(\n        \"max_active_power_out\",\n        TimeArray(tstamp, ts_data);\n        scaling_factor_multiplier = get_max_active_power,\n    )\n    ts_in = SingleTimeSeries(\n        \"max_active_power_in\",\n        TimeArray(tstamp, ts_data);\n        scaling_factor_multiplier = get_max_active_power,\n    )\n    add_time_series!(sys, source, ts_out)\n    add_time_series!(sys, source, ts_in)\n    transform_single_time_series!(sys, Hour(24), Hour(24))\n\n    template = get_template_dispatch_with_network(\n        NetworkModel(\n            PTDFPowerModel;\n            PTDF_matrix = PTDF(sys),\n            power_flow_evaluation = PTDFDCPowerFlow(),\n        ),\n    )\n    set_device_model!(template, DeviceModel(Source, FixedOutput))\n    model = DecisionModel(template, sys; optimizer = HiGHS_optimizer)\n    @test build!(model; output_dir = mktempdir(; cleanup = true)) ==\n          PSI.ModelBuildStatus.BUILT\n    @test solve!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED\n\n    container = PSI.get_optimization_container(model)\n    pf_e_data = only(PSI.get_power_flow_evaluation_data(container))\n    input_key_map = PSI.get_input_key_map(pf_e_data)\n\n    @test haskey(input_key_map, :active_power_in)\n    @test haskey(input_key_map, :active_power_out)\n\n    in_keys = collect(keys(input_key_map[:active_power_in]))\n    out_keys = collect(keys(input_key_map[:active_power_out]))\n    @test any(\n        k ->\n            PSI.get_entry_type(k) == PSI.ActivePowerInTimeSeriesParameter &&\n                PSI.get_component_type(k) == Source,\n        in_keys,\n    )\n    @test any(\n        k ->\n            PSI.get_entry_type(k) == PSI.ActivePowerOutTimeSeriesParameter &&\n                PSI.get_component_type(k) == Source,\n        out_keys,\n    )\n\n    # PF data injections at the source bus should equal the sum of every other\n    # device's contribution at that bus plus (out_param − in_param) from the\n    # FixedOutput source — same allocation rule as the in/out variable path.\n    data = PSI.get_power_flow_data(pf_e_data)\n    bus_lookup = PFS.get_bus_lookup(data)\n    source_bus_ix = bus_lookup[get_number(get_bus(source))]\n    n_time_steps = length(PSI.get_time_steps(container))\n\n    in_param = PSI.lookup_value(\n        container, PSI.ParameterKey(PSI.ActivePowerInTimeSeriesParameter, Source),\n    )\n    out_param = PSI.lookup_value(\n        container, PSI.ParameterKey(PSI.ActivePowerOutTimeSeriesParameter, Source),\n    )\n\n    other_injection_at_bus = zeros(n_time_steps)\n    for (key, comp_map) in input_key_map[:active_power]\n        result_data = PSI.lookup_value(container, key)\n        for (dev_name, bus_ix) in comp_map\n            bus_ix == source_bus_ix || continue\n            for t in 1:n_time_steps\n                other_injection_at_bus[t] += PSI.jump_value(result_data[dev_name, t])\n            end\n        end\n    end\n\n    source_net = [\n        PSI.jump_value(out_param[\"source\", t]) - PSI.jump_value(in_param[\"source\", t])\n        for t in 1:n_time_steps\n    ]\n    # Net bus injection = injections − withdrawals; loads route to withdrawals under\n    # the same `:active_power` category and the difference folds them back in.\n    @test isapprox(\n        data.bus_active_power_injections[source_bus_ix, 1:n_time_steps] .-\n        data.bus_active_power_withdrawals[source_bus_ix, 1:n_time_steps],\n        other_injection_at_bus .+ source_net;\n        atol = 1e-9,\n    )\n\n    # Guard against a regression that drives both parameters to zero — the test\n    # above would then pass trivially without exercising the parameter path.\n    @test !all(isapprox.(source_net, 0.0; atol = 1e-10))\nend\n"
  },
  {
    "path": "test/test_print.jl",
    "content": "function _test_plain_print_methods(list::Array)\n    for object in list\n        normal = repr(object)\n        io = IOBuffer()\n        show(io, \"text/plain\", object)\n        grabbed = String(take!(io))\n        @test grabbed !== nothing\n    end\nend\n\nfunction _test_html_print_methods(list::Array)\n    for object in list\n        normal = repr(object)\n        io = IOBuffer()\n        show(io, \"text/html\", object)\n        grabbed = String(take!(io))\n        @test grabbed !== nothing\n    end\nend\n\n@testset \"Test Model Print Methods\" begin\n    template = get_thermal_dispatch_template_network()\n    c_sys5 = PSB.build_system(PSITestSystems, \"c_sys5\")\n\n    dm_model = DecisionModel(template, c_sys5; optimizer = HiGHS_optimizer)\n    @test build!(dm_model; output_dir = mktempdir(; cleanup = true)) ==\n          PSI.ModelBuildStatus.BUILT\n    @test solve!(dm_model; optimizer = HiGHS_optimizer) ==\n          PSI.RunStatus.SUCCESSFULLY_FINALIZED\n    results = OptimizationProblemResults(dm_model)\n    variables = read_variables(results)\n\n    list = [\n        template,\n        dm_model,\n        PSI.get_model(template, ThermalStandard),\n        PSI.get_network_model(template),\n        results,\n    ]\n\n    _test_plain_print_methods(list)\n    _test_html_print_methods(list)\nend\n\n@testset \"Test Simulation Print Methods\" begin\n    template_uc = get_template_basic_uc_simulation()\n    template_ed = get_template_nomin_ed_simulation()\n    set_device_model!(template_ed, InterruptiblePowerLoad, StaticPowerLoad)\n    set_network_model!(template_uc, NetworkModel(\n        CopperPlatePowerModel,\n        # MILP \"duals\" not supported with free solvers\n        # duals = [CopperPlateBalanceConstraint],\n    ))\n    set_network_model!(\n        template_ed,\n        NetworkModel(\n            CopperPlatePowerModel;\n            duals = [CopperPlateBalanceConstraint],\n            use_slacks = true,\n        ),\n    )\n    c_sys5_hy_uc = PSB.build_system(PSITestSystems, \"c_sys5_hy_uc\")\n    c_sys5_hy_ed = PSB.build_system(PSITestSystems, \"c_sys5_hy_ed\")\n\n    models = SimulationModels(;\n        decision_models = [\n            DecisionModel(\n                template_uc,\n                c_sys5_hy_uc;\n                name = \"UC\",\n                optimizer = HiGHS_optimizer,\n            ),\n            DecisionModel(\n                template_ed,\n                c_sys5_hy_ed;\n                name = \"ED\",\n                optimizer = HiGHS_optimizer,\n            ),\n        ],\n    )\n\n    sequence = SimulationSequence(;\n        models = models,\n        feedforwards = Dict(\n            \"ED\" => [\n                SemiContinuousFeedforward(;\n                    component_type = ThermalStandard,\n                    source = OnVariable,\n                    affected_values = [ActivePowerVariable],\n                ),\n            ],\n        ),\n        ini_cond_chronology = InterProblemChronology(),\n    )\n\n    sim_not_built = Simulation(;\n        name = \"printing_sim\",\n        steps = 2,\n        models = models,\n        sequence = sequence,\n        simulation_folder = mktempdir(; cleanup = true),\n    )\n\n    sim = Simulation(;\n        name = \"printing_sim\",\n        steps = 2,\n        models = models,\n        sequence = sequence,\n        simulation_folder = mktempdir(; cleanup = true),\n    )\n\n    build!(sim)\n    execute!(sim)\n    results = SimulationResults(sim)\n    results_uc = get_decision_problem_results(results, \"UC\")\n    list = [models, sequence, template_uc, template_ed, sim, sim_not_built, results_uc]\n    _test_plain_print_methods(list)\n    _test_html_print_methods(list)\nend\n"
  },
  {
    "path": "test/test_problem_template.jl",
    "content": "# This file is WIP while the interface for templates is finalized\n@testset \"Manual Operations Template\" begin\n    template = ProblemTemplate(CopperPlatePowerModel)\n    set_device_model!(template, PowerLoad, StaticPowerLoad)\n    set_device_model!(template, ThermalStandard, ThermalStandardUnitCommitment)\n    set_device_model!(template, Line, StaticBranchUnbounded)\n    @test !isempty(template.devices)\n    @test !isempty(template.branches)\n    @test isempty(template.services)\nend\n\n@testset \"Operations Template Overwrite\" begin\n    template = ProblemTemplate(CopperPlatePowerModel)\n    set_device_model!(template, PowerLoad, StaticPowerLoad)\n    set_device_model!(template, ThermalStandard, ThermalStandardUnitCommitment)\n    @test_logs (:warn, \"Overwriting ThermalStandard existing model\") set_device_model!(\n        template,\n        DeviceModel(ThermalStandard, ThermalBasicUnitCommitment),\n    )\n    @test PSI.get_formulation(template.devices[:ThermalStandard]) ==\n          ThermalBasicUnitCommitment\nend\n\n@testset \"Provided Templates Tests\" begin\n    uc_template = template_unit_commitment()\n    @test !isempty(uc_template.devices)\n    @test PSI.get_formulation(uc_template.devices[:ThermalStandard]) ==\n          ThermalBasicUnitCommitment\n    uc_template = template_unit_commitment(; network = DCPPowerModel)\n    @test get_network_formulation(uc_template) == DCPPowerModel\n    @test !isempty(uc_template.branches)\n    @test !isempty(uc_template.services)\n\n    ed_template = template_economic_dispatch()\n    @test !isempty(ed_template.devices)\n    @test PSI.get_formulation(ed_template.devices[:ThermalStandard]) == ThermalBasicDispatch\n    ed_template = template_economic_dispatch(; network = ACPPowerModel)\n    @test get_network_formulation(ed_template) == ACPPowerModel\n    @test !isempty(ed_template.branches)\n    @test !isempty(ed_template.services)\nend\n"
  },
  {
    "path": "test/test_recorder_events.jl",
    "content": "@testset \"Show recorder events in EmulationModel\" begin\n    template = get_thermal_standard_uc_template()\n    c_sys5_uc_re = PSB.build_system(\n        PSITestSystems,\n        \"c_sys5_uc_re\";\n        add_single_time_series = true,\n        force_build = true,\n    )\n    set_device_model!(template, RenewableDispatch, RenewableFullDispatch)\n    model = EmulationModel(template, c_sys5_uc_re; optimizer = HiGHS_optimizer)\n\n    @test build!(model; executions = 10, output_dir = mktempdir(; cleanup = true)) ==\n          PSI.ModelBuildStatus.BUILT\n    @test run!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED\n\n    recorder_log = joinpath(PSI.get_recorder_dir(model), \"execution.log\")\n    events = list_recorder_events(PSI.ParameterUpdateEvent, recorder_log)\n    @test !isempty(events)\n    events = list_recorder_events(PSI.InitialConditionUpdateEvent, recorder_log)\n    @test !isempty(events)\n    for wall_time in (true, false)\n        show_recorder_events(\n            devnull,\n            PSI.InitialConditionUpdateEvent,\n            recorder_log;\n            wall_time = wall_time,\n        )\n    end\nend\n"
  },
  {
    "path": "test/test_services_constructor.jl",
    "content": "@testset \"Test Reserves from Thermal Dispatch\" begin\n    template = get_thermal_dispatch_template_network(CopperPlatePowerModel)\n    set_service_model!(\n        template,\n        ServiceModel(VariableReserve{ReserveUp}, RangeReserve, \"Reserve1\"),\n    )\n    set_service_model!(\n        template,\n        ServiceModel(VariableReserve{ReserveUp}, RangeReserve, \"Reserve11\"),\n    )\n    set_service_model!(\n        template,\n        ServiceModel(VariableReserve{ReserveDown}, RangeReserve, \"Reserve2\"),\n    )\n    set_service_model!(\n        template,\n        ServiceModel(ReserveDemandCurve{ReserveUp}, StepwiseCostReserve, \"ORDC1\"),\n    )\n\n    c_sys5_uc = PSB.build_system(PSITestSystems, \"c_sys5_uc\"; add_reserves = true)\n    model = DecisionModel(template, c_sys5_uc)\n    @test build!(model; output_dir = mktempdir(; cleanup = true)) ==\n          PSI.ModelBuildStatus.BUILT\n    moi_tests(model, 624, 0, 216, 216, 48, false)\n    reserve_variables = [\n        :ActivePowerReserveVariable__VariableReserve__ReserveUp__Reserve1\n        :ActivePowerReserveVariable__ReserveDemandCurve__ReserveUp__ORDC1\n        :ActivePowerReserveVariable__VariableReserve__ReserveDown__Reserve2\n        :ActivePowerReserveVariable__VariableReserve__ReserveUp__Reserve11\n    ]\n    found_vars = 0\n    for (k, var_array) in PSI.get_optimization_container(model).variables\n        if ISOPT.encode_key(k) in reserve_variables\n            for var in var_array\n                @test JuMP.has_lower_bound(var)\n                @test JuMP.lower_bound(var) == 0.0\n            end\n            found_vars += 1\n        end\n    end\n    @test found_vars == 4\nend\n\n@testset \"Test Ramp Reserves from Thermal Dispatch\" begin\n    template = get_thermal_dispatch_template_network(CopperPlatePowerModel)\n    set_service_model!(\n        template,\n        ServiceModel(VariableReserve{ReserveUp}, RampReserve, \"Reserve1\"),\n    )\n    set_service_model!(\n        template,\n        ServiceModel(VariableReserve{ReserveUp}, RampReserve, \"Reserve11\"),\n    )\n    set_service_model!(\n        template,\n        ServiceModel(VariableReserve{ReserveDown}, RampReserve, \"Reserve2\"),\n    )\n\n    c_sys5_uc = PSB.build_system(PSITestSystems, \"c_sys5_uc\"; add_reserves = true)\n    model = DecisionModel(template, c_sys5_uc)\n    @test build!(model; output_dir = mktempdir(; cleanup = true)) ==\n          PSI.ModelBuildStatus.BUILT\n    moi_tests(model, 384, 0, 336, 192, 24, false)\n    reserve_variables = [\n        :ActivePowerReserveVariable__VariableReserve_ReserveDown_Reserve2,\n        :ActivePowerReserveVariable__VariableReserve_ReserveUp_Reserve1,\n        :ActivePowerReserveVariable__VariableReserve_ReserveUp_Reserve11,\n    ]\n    for (k, var_array) in PSI.get_optimization_container(model).variables\n        if ISOPT.encode_key(k) in reserve_variables\n            for var in var_array\n                @test JuMP.has_lower_bound(var)\n                @test JuMP.lower_bound(var) == 0.0\n            end\n        end\n    end\nend\n\n@testset \"Test Reserves from Thermal Standard UC\" begin\n    template = get_thermal_standard_uc_template()\n    set_service_model!(\n        template,\n        ServiceModel(VariableReserve{ReserveUp}, RangeReserve, \"Reserve1\"),\n    )\n    set_service_model!(\n        template,\n        ServiceModel(VariableReserve{ReserveUp}, RangeReserve, \"Reserve11\"),\n    )\n    set_service_model!(\n        template,\n        ServiceModel(VariableReserve{ReserveDown}, RangeReserve, \"Reserve2\"),\n    )\n    set_service_model!(\n        template,\n        ServiceModel(ReserveDemandCurve{ReserveUp}, StepwiseCostReserve, \"ORDC1\"),\n    )\n    c_sys5_uc = PSB.build_system(PSITestSystems, \"c_sys5_uc\"; add_reserves = true)\n\n    model = DecisionModel(template, c_sys5_uc; optimizer = HiGHS_optimizer)\n    @test build!(model; output_dir = mktempdir(; cleanup = true)) ==\n          PSI.ModelBuildStatus.BUILT\n    moi_tests(model, 984, 0, 576, 216, 168, true)\nend\n\n@testset \"Test Reserves from Thermal Standard UC with NonSpinningReserve\" begin\n    template = get_thermal_standard_uc_template()\n    set_device_model!(\n        template,\n        DeviceModel(ThermalMultiStart, ThermalStandardUnitCommitment),\n    )\n    set_service_model!(\n        template,\n        ServiceModel(VariableReserveNonSpinning, NonSpinningReserve, \"NonSpinningReserve\"),\n    )\n\n    c_sys5_uc = PSB.build_system(PSITestSystems, \"c_sys5_uc_non_spin\"; add_reserves = true)\n    model = DecisionModel(template, c_sys5_uc; optimizer = HiGHS_optimizer)\n    @test build!(model; output_dir = mktempdir(; cleanup = true)) ==\n          PSI.ModelBuildStatus.BUILT\n    moi_tests(model, 1032, 0, 888, 192, 288, true)\nend\n\n@testset \"Test Upwards Reserves from Renewable Dispatch\" begin\n    template = ProblemTemplate(CopperPlatePowerModel)\n    set_device_model!(template, PowerLoad, StaticPowerLoad)\n    set_device_model!(template, RenewableDispatch, RenewableFullDispatch)\n    set_service_model!(\n        template,\n        ServiceModel(VariableReserve{ReserveUp}, RangeReserve, \"Reserve3\"),\n    )\n    set_service_model!(\n        template,\n        ServiceModel(ReserveDemandCurve{ReserveUp}, StepwiseCostReserve, \"ORDC1\"),\n    )\n\n    c_sys5_re = PSB.build_system(PSITestSystems, \"c_sys5_re\"; add_reserves = true)\n    model = DecisionModel(template, c_sys5_re)\n    @test build!(model; output_dir = mktempdir(; cleanup = true)) ==\n          PSI.ModelBuildStatus.BUILT\n    moi_tests(model, 336, 0, 168, 120, 48, false)\nend\n\n@testset \"Test Reserves from Hydro\" begin\n    template = ProblemTemplate(CopperPlatePowerModel)\n    set_device_model!(template, PowerLoad, StaticPowerLoad)\n    set_device_model!(template, HydroTurbine, HydroTurbineEnergyDispatch)\n    set_device_model!(template, HydroReservoir, HydroEnergyModelReservoir)\n    set_service_model!(\n        template,\n        ServiceModel(VariableReserve{ReserveUp}, RangeReserve, \"Reserve5\"),\n    )\n    set_service_model!(\n        template,\n        ServiceModel(VariableReserve{ReserveDown}, RangeReserve, \"Reserve6\"),\n    )\n    set_service_model!(\n        template,\n        ServiceModel(ReserveDemandCurve{ReserveUp}, StepwiseCostReserve, \"ORDC1\"),\n    )\n\n    c_sys5_hyd = PSB.build_system(PSITestSystems, \"c_sys5_hyd\"; add_reserves = true)\n    model = DecisionModel(template, c_sys5_hyd)\n    @test build!(model; output_dir = mktempdir(; cleanup = true)) ==\n          PSI.ModelBuildStatus.BUILT\n    moi_tests(model, 312, 0, 120, 96, 72, false)\nend\n\n@testset \"Test Reserves from with slack variables\" begin\n    template = get_thermal_dispatch_template_network(\n        NetworkModel(CopperPlatePowerModel; use_slacks = true),\n    )\n    set_service_model!(\n        template,\n        ServiceModel(\n            VariableReserve{ReserveUp},\n            RangeReserve,\n            \"Reserve1\";\n            use_slacks = true,\n        ),\n    )\n    set_service_model!(\n        template,\n        ServiceModel(\n            VariableReserve{ReserveUp},\n            RangeReserve,\n            \"Reserve11\";\n            use_slacks = true,\n        ),\n    )\n    set_service_model!(\n        template,\n        ServiceModel(\n            VariableReserve{ReserveDown},\n            RangeReserve,\n            \"Reserve2\";\n            use_slacks = true,\n        ),\n    )\n\n    c_sys5_uc = PSB.build_system(PSITestSystems, \"c_sys5_uc\"; add_reserves = true)\n    model = DecisionModel(template, c_sys5_uc;)\n    @test build!(model; output_dir = mktempdir(; cleanup = true)) ==\n          PSI.ModelBuildStatus.BUILT\n    moi_tests(model, 504, 0, 120, 192, 24, false)\nend\n\n#=\n@testset \"Test AGC\" begin\n    c_sys5_reg = PSB.build_system(PSITestSystems, \"c_sys5_reg\")\n    @test_throws ArgumentError template_agc_reserve_deployment(; dummy_arg = 0.0)\n\n    template_agc = template_agc_reserve_deployment()\n    set_service_model!(template_agc, ServiceModel(PSY.AGC, PIDSmoothACE, \"AGC_Area1\"))\n    agc_problem = DecisionModel(AGCReserveDeployment, template_agc, c_sys5_reg)\n    @test build!(agc_problem; output_dir = mktempdir(; cleanup = true)) ==\n          PSI.ModelBuildStatus.BUILT\n    # These values might change as the AGC model is refined\n    moi_tests(agc_problem, 696, 0, 480, 0, 384, false)\nend\n\n@testset \"Test GroupReserve from Thermal Dispatch\" begin\n    template = get_thermal_dispatch_template_network()\n    set_service_model!(\n        template,\n        ServiceModel(VariableReserve{ReserveUp}, RangeReserve, \"Reserve1\"),\n    )\n    set_service_model!(\n        template,\n        ServiceModel(VariableReserve{ReserveUp}, RangeReserve, \"Reserve11\"),\n    )\n    set_service_model!(\n        template,\n        ServiceModel(VariableReserve{ReserveDown}, RangeReserve, \"Reserve2\"),\n    )\n    set_service_model!(\n        template,\n        ServiceModel(ReserveDemandCurve{ReserveUp}, StepwiseCostReserve, \"ORDC1\"),\n    )\n    set_service_model!(\n        template,\n        ServiceModel(ConstantReserveGroup{ReserveDown}, GroupReserve, \"init\"),\n    )\n\n    c_sys5_uc = PSB.build_system(PSITestSystems, \"c_sys5_uc\"; add_reserves = true)\n    services = get_components(Service, c_sys5_uc)\n    contributing_services = Vector{Service}()\n    for service in services\n        if !(typeof(service) <: PSY.ReserveDemandCurve)\n            push!(contributing_services, service)\n        end\n    end\n    groupservice = ConstantReserveGroup{ReserveDown}(;\n        name = \"init\",\n        available = true,\n        requirement = 0.0,\n        ext = Dict{String, Any}(),\n    )\n    add_service!(c_sys5_uc, groupservice, contributing_services)\n\n    model = DecisionModel(template, c_sys5_uc)\n    @test build!(model; output_dir = mktempdir(; cleanup = true)) ==\n          PSI.ModelBuildStatus.BUILT\n    moi_tests(model, 624, 0, 216, 240, 48, false)\nend\n\n@testset \"Test GroupReserve Errors\" begin\n    template = get_thermal_dispatch_template_network()\n    set_service_model!(template, ServiceModel(VariableReserve{ReserveUp}, RangeReserve))\n    set_service_model!(template, ServiceModel(VariableReserve{ReserveDown}, RangeReserve))\n    set_service_model!(\n        template,\n        ServiceModel(ReserveDemandCurve{ReserveUp}, StepwiseCostReserve),\n    )\n    set_service_model!(\n        template,\n        ServiceModel(ConstantReserveGroup{ReserveDown}, GroupReserve),\n    )\n\n    c_sys5_uc = PSB.build_system(PSITestSystems, \"c_sys5_uc\"; add_reserves = true)\n    services = get_components(Service, c_sys5_uc)\n    contributing_services = Vector{Service}()\n    for service in services\n        if !(typeof(service) <: PSY.ReserveDemandCurve)\n            push!(contributing_services, service)\n        end\n    end\n    groupservice = ConstantReserveGroup{ReserveDown}(;\n        name = \"init\",\n        available = true,\n        requirement = 0.0,\n        ext = Dict{String, Any}(),\n    )\n    add_service!(c_sys5_uc, groupservice, contributing_services)\n\n    off_service = VariableReserve{ReserveUp}(\"Reserveoff\", true, 0.6, 10)\n    push!(groupservice.contributing_services, off_service)\n\n    model = DecisionModel(template, c_sys5_uc)\n    @test build!(\n        model;\n        output_dir = mktempdir(; cleanup = true),\n        console_level = Logging.AboveMaxLevel,\n    ) == PSI.ModelBuildStatus.FAILED\nend\n\n@testset \"Test ConstantReserve\" begin\n    template = get_thermal_dispatch_template_network()\n    set_service_model!(\n        template,\n        ServiceModel(ConstantReserve{ReserveUp}, RangeReserve, \"Reserve3\"),\n    )\n\n    c_sys5_uc = PSB.build_system(PSITestSystems, \"c_sys5_uc\")\n    static_reserve = ConstantReserve{ReserveUp}(\"Reserve3\", true, 30, 100)\n    add_service!(c_sys5_uc, static_reserve, get_components(ThermalGen, c_sys5_uc))\n    model = DecisionModel(template, c_sys5_uc)\n    @test build!(model; output_dir = mktempdir(; cleanup = true)) ==\n          PSI.ModelBuildStatus.BUILT\n    @test typeof(model) <: DecisionModel{<:PSI.DecisionProblem}\nend\n=#\n\n@testset \"Test Reserves with Feedforwards\" begin\n    template = get_thermal_dispatch_template_network()\n    service_model = ServiceModel(VariableReserve{ReserveUp}, RangeReserve, \"Reserve1\")\n    ff_lb = LowerBoundFeedforward(;\n        component_type = VariableReserve{ReserveUp},\n        source = ActivePowerReserveVariable,\n        affected_values = [ActivePowerReserveVariable],\n        meta = \"Reserve1\",\n    )\n    PSI.attach_feedforward!(service_model, ff_lb)\n\n    set_service_model!(template, service_model)\n\n    c_sys5_uc = PSB.build_system(PSITestSystems, \"c_sys5_uc\"; add_reserves = true)\n    model = DecisionModel(template, c_sys5_uc; optimizer = HiGHS_optimizer)\n    # set manually to test cases for simulation\n    PSI.get_optimization_container(model).built_for_recurrent_solves = true\n    @test build!(model; output_dir = mktempdir(; cleanup = true)) ==\n          PSI.ModelBuildStatus.BUILT\n    moi_tests(model, 456, 0, 120, 264, 24, false)\nend\n\n@testset \"Test Reserves with Participation factor limits\" begin\n    c_sys5_uc = PSB.build_system(PSITestSystems, \"c_sys5_uc\"; add_reserves = true)\n    for service in get_components(Reserve, c_sys5_uc)\n        set_max_participation_factor!(service, 0.8)\n    end\n\n    template = get_thermal_dispatch_template_network(CopperPlatePowerModel)\n    set_service_model!(\n        template,\n        ServiceModel(VariableReserve{ReserveUp}, RangeReserve, \"Reserve1\"),\n    )\n    set_service_model!(\n        template,\n        ServiceModel(VariableReserve{ReserveUp}, RangeReserve, \"Reserve11\"),\n    )\n    set_service_model!(\n        template,\n        ServiceModel(VariableReserve{ReserveDown}, RangeReserve, \"Reserve2\"),\n    )\n    set_service_model!(\n        template,\n        ServiceModel(ReserveDemandCurve{ReserveUp}, StepwiseCostReserve, \"ORDC1\"),\n    )\n\n    model = DecisionModel(template, c_sys5_uc)\n    @test build!(model; output_dir = mktempdir(; cleanup = true)) ==\n          PSI.ModelBuildStatus.BUILT\n    moi_tests(model, 624, 0, 480, 216, 48, false)\n    reserve_variables = [\n        :ActivePowerReserveVariable__VariableReserve__ReserveUp__Reserve1\n        :ActivePowerReserveVariable__ReserveDemandCurve__ReserveUp__ORDC1\n        :ActivePowerReserveVariable__VariableReserve__ReserveDown__Reserve2\n        :ActivePowerReserveVariable__VariableReserve__ReserveUp__Reserve11\n    ]\n    found_vars = 0\n    for (k, var_array) in PSI.get_optimization_container(model).variables\n        if ISOPT.encode_key(k) in reserve_variables\n            for var in var_array\n                @test JuMP.has_lower_bound(var)\n                @test JuMP.lower_bound(var) == 0.0\n            end\n            found_vars += 1\n        end\n    end\n    @test found_vars == 4\n\n    participation_constraints = [\n        :ParticipationFractionConstraint__VariableReserve__ReserveUp__Reserve11,\n        :ParticipationFractionConstraint__VariableReserve__ReserveDown__Reserve2,\n    ]\n\n    found_constraints = 0\n\n    for (k, _) in PSI.get_optimization_container(model).constraints\n        if ISOPT.encode_key(k) in participation_constraints\n            found_constraints += 1\n        end\n    end\n\n    @test found_constraints == 2\nend\n\n@testset \"Test Transmission Interface\" begin\n    c_sys5_uc = PSB.build_system(PSITestSystems, \"c_sys5_uc\"; add_reserves = true)\n    interface = TransmissionInterface(;\n        name = \"west_east\",\n        available = true,\n        active_power_flow_limits = (min = 0.0, max = 400.0),\n    )\n    interface_lines = [\n        get_component(Line, c_sys5_uc, \"1\"),\n        get_component(Line, c_sys5_uc, \"2\"),\n        get_component(Line, c_sys5_uc, \"6\"),\n    ]\n    add_service!(c_sys5_uc, interface, interface_lines)\n\n    template = get_thermal_dispatch_template_network(DCPPowerModel)\n    set_service_model!(\n        template,\n        ServiceModel(TransmissionInterface, ConstantMaxInterfaceFlow; use_slacks = true),\n    )\n\n    model = DecisionModel(template, c_sys5_uc)\n    @test build!(model; output_dir = mktempdir(; cleanup = true)) ==\n          PSI.ModelBuildStatus.BUILT\n    moi_tests(model, 432, 144, 288, 288, 288, false)\n\n    template = get_thermal_dispatch_template_network(PTDFPowerModel)\n    set_service_model!(\n        template,\n        ServiceModel(TransmissionInterface, ConstantMaxInterfaceFlow; use_slacks = true),\n    )\n    model = DecisionModel(template, c_sys5_uc)\n    @test build!(model; output_dir = mktempdir(; cleanup = true)) ==\n          PSI.ModelBuildStatus.BUILT\n    moi_tests(model, 168, 0, 288, 288, 24, false)\n\n    #= TODO: Implement Interfaces in AC\n    template = get_thermal_dispatch_template_network(ACPPowerModel)\n    set_service_model!(\n        template,\n        ServiceModel(TransmissionInterface, ConstantMaxInterfaceFlow; use_slacks = true),\n    )\n    model = DecisionModel(template, c_sys5_uc)\n    @test build!(model; output_dir = mktempdir(; cleanup = true)) == PSI.BuildStatus.BUILT\n    moi_tests(model, 312, 0, 288, 288, 168, false)\n    =#\nend\n\n@testset \"Test Transmission Interface with TimeSeries\" begin\n    c_sys5_uc = PSB.build_system(PSITestSystems, \"c_sys5_uc\"; add_reserves = true)\n    interface = TransmissionInterface(;\n        name = \"west_east\",\n        available = true,\n        active_power_flow_limits = (min = 0.0, max = 400.0),\n    )\n    interface_lines = [\n        get_component(Line, c_sys5_uc, \"1\"),\n        get_component(Line, c_sys5_uc, \"2\"),\n        get_component(Line, c_sys5_uc, \"6\"),\n    ]\n    add_service!(c_sys5_uc, interface, interface_lines)\n    # Add TimeSeries Data\n    data_minflow = Dict(\n        DateTime(\"2024-01-01T00:00:00\") => zeros(24),\n        DateTime(\"2024-01-02T00:00:00\") => zeros(24),\n    )\n\n    forecast_minflow = Deterministic(\n        \"min_active_power_flow_limit\",\n        data_minflow,\n        Hour(1);\n        scaling_factor_multiplier = get_min_active_power_flow_limit,\n    )\n\n    data_maxflow = Dict(\n        DateTime(\"2024-01-01T00:00:00\") => [\n            0.9, 0.85, 0.95, 0.2, 0.15, 0.2,\n            0.9, 0.85, 0.95, 0.2, 0.15, 0.2,\n            0.9, 0.85, 0.95, 0.2, 0.5, 0.5,\n            0.9, 0.85, 0.95, 0.2, 0.6, 0.6,\n        ],\n        DateTime(\"2024-01-02T00:00:00\") => [\n            0.9, 0.85, 0.95, 0.2, 0.15, 0.2,\n            0.9, 0.85, 0.95, 0.2, 0.15, 0.2,\n            0.9, 0.85, 0.95, 0.2, 0.5, 0.5,\n            0.9, 0.85, 0.95, 0.2, 0.6, 0.6,\n        ],\n    )\n\n    forecast_maxflow = Deterministic(\n        \"max_active_power_flow_limit\",\n        data_maxflow,\n        Hour(1);\n        scaling_factor_multiplier = get_max_active_power_flow_limit,\n    )\n\n    add_time_series!(c_sys5_uc, interface, forecast_minflow)\n    add_time_series!(c_sys5_uc, interface, forecast_maxflow)\n\n    template = get_thermal_dispatch_template_network(DCPPowerModel)\n    set_service_model!(\n        template,\n        ServiceModel(TransmissionInterface, VariableMaxInterfaceFlow; use_slacks = true),\n    )\n\n    model = DecisionModel(template, c_sys5_uc)\n    @test build!(model; output_dir = mktempdir(; cleanup = true)) ==\n          PSI.ModelBuildStatus.BUILT\n    moi_tests(model, 432, 144, 288, 288, 288, false)\n\n    template = get_thermal_dispatch_template_network(PTDFPowerModel)\n    set_service_model!(\n        template,\n        ServiceModel(TransmissionInterface, VariableMaxInterfaceFlow; use_slacks = true),\n    )\n    model = DecisionModel(template, c_sys5_uc)\n    @test build!(model; output_dir = mktempdir(; cleanup = true)) ==\n          PSI.ModelBuildStatus.BUILT\n    moi_tests(model, 168, 0, 288, 288, 24, false)\nend\n\n@testset \"Test Transmission Interface with Feedforwards\" begin\n    c_sys5_uc = PSB.build_system(PSITestSystems, \"c_sys5_uc\"; add_reserves = true)\n    interface = TransmissionInterface(;\n        name = \"west_east\",\n        available = true,\n        active_power_flow_limits = (min = 0.0, max = 400.0),\n    )\n    interface_lines = [\n        get_component(Line, c_sys5_uc, \"1\"),\n        get_component(Line, c_sys5_uc, \"2\"),\n        get_component(Line, c_sys5_uc, \"6\"),\n    ]\n    add_service!(c_sys5_uc, interface, interface_lines)\n    c_sys5_uc2 = PSB.build_system(PSITestSystems, \"c_sys5_uc\"; add_reserves = true)\n    interface2 = TransmissionInterface(;\n        name = \"west_east\",\n        available = true,\n        active_power_flow_limits = (min = 0.0, max = 400.0),\n    )\n    interface_lines2 = [\n        get_component(Line, c_sys5_uc2, \"1\"),\n        get_component(Line, c_sys5_uc2, \"2\"),\n        get_component(Line, c_sys5_uc2, \"6\"),\n    ]\n    add_service!(c_sys5_uc2, interface2, interface_lines2)\n\n    template = get_thermal_dispatch_template_network(DCPPowerModel)\n    set_service_model!(\n        template,\n        ServiceModel(TransmissionInterface, ConstantMaxInterfaceFlow; use_slacks = true),\n    )\n    models = SimulationModels(;\n        decision_models = [\n            DecisionModel(template, c_sys5_uc; optimizer = HiGHS_optimizer, name = \"Sys1\"),\n            DecisionModel(template, c_sys5_uc2; optimizer = HiGHS_optimizer, name = \"Sys2\"),\n        ],\n    )\n\n    feedforward = Dict(\n        \"Sys2\" => [\n            FixValueFeedforward(;\n                component_type = TransmissionInterface,\n                source = PSI.FlowActivePowerVariable,\n                affected_values = [PSI.FlowActivePowerVariable],\n            ),\n        ],\n    )\n\n    sequence = SimulationSequence(;\n        models = models,\n        ini_cond_chronology = InterProblemChronology(),\n        feedforwards = feedforward,\n    )\n\n    sim = Simulation(;\n        name = \"interface-fail\",\n        steps = 2,\n        models = models,\n        sequence = sequence,\n        simulation_folder = mktempdir(; cleanup = true),\n    )\n    @test_throws ArgumentError build!(sim; console_level = Logging.AboveMaxLevel)\nend\n\n@testset \"2 Areas AreaBalance With Transmission Interface\" begin\n    c_sys = PSB.build_system(PSISystems, \"two_area_pjm_DA\")\n    transform_single_time_series!(c_sys, Hour(24), Hour(1))\n    template = get_thermal_dispatch_template_network(NetworkModel(AreaBalancePowerModel))\n    set_device_model!(template, AreaInterchange, StaticBranch)\n    ps_model =\n        DecisionModel(template, c_sys; resolution = Hour(1), optimizer = HiGHS_optimizer)\n\n    @test build!(ps_model; output_dir = mktempdir(; cleanup = true)) ==\n          PSI.ModelBuildStatus.BUILT\n    @test solve!(ps_model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED\n\n    moi_tests(ps_model, 264, 0, 264, 264, 48, false)\n\n    opt_container = PSI.get_optimization_container(ps_model)\n    copper_plate_constraints =\n        PSI.get_constraint(opt_container, CopperPlateBalanceConstraint(), PSY.Area)\n    @test size(copper_plate_constraints) == (2, 24)\n\n    psi_checksolve_test(ps_model, [MOI.OPTIMAL], 482055, 1)\n\n    results = OptimizationProblemResults(ps_model)\n    interarea_flow = read_variable(\n        results,\n        \"FlowActivePowerVariable__AreaInterchange\";\n        table_format = TableFormat.WIDE,\n    )\n    # The values for these tests come from the data\n    @test all(interarea_flow[!, \"1_2\"] .<= 150)\n    @test all(interarea_flow[!, \"1_2\"] .>= -150)\n\n    load = read_parameter(\n        results,\n        \"ActivePowerTimeSeriesParameter__PowerLoad\";\n        table_format = TableFormat.WIDE,\n    )\n    thermal_gen = read_variable(\n        results,\n        \"ActivePowerVariable__ThermalStandard\";\n        table_format = TableFormat.WIDE,\n    )\n\n    zone_1_load = sum(eachcol(load[!, [\"Bus4_1\", \"Bus3_1\", \"Bus2_1\"]]))\n    zone_1_gen = sum(\n        eachcol(\n            thermal_gen[\n                !,\n                [\"Solitude_1\", \"Park City_1\", \"Sundance_1\", \"Brighton_1\", \"Alta_1\"],\n            ],\n        ),\n    )\n    @test all(\n        isapprox.(\n            sum(zone_1_gen .+ zone_1_load .- interarea_flow[!, \"1_2\"]; dims = 2),\n            0.0;\n            atol = 1e-3,\n        ),\n    )\n\n    zone_2_load = sum(eachcol(load[!, [\"Bus4_2\", \"Bus3_2\", \"Bus2_2\"]]))\n    zone_2_gen = sum(\n        eachcol(\n            thermal_gen[\n                !,\n                [\"Solitude_2\", \"Park City_2\", \"Sundance_2\", \"Brighton_2\", \"Alta_2\"],\n            ],\n        ),\n    )\n    @test all(\n        isapprox.(\n            sum(zone_2_gen .+ zone_2_load .+ interarea_flow[!, \"1_2\"]; dims = 2),\n            0.0;\n            atol = 1e-3,\n        ),\n    )\nend\n\n@testset \"Test Interfaces on Interchanges with AreaBalance\" begin\n    sys_rts_da = build_system(PSISystems, \"modified_RTS_GMLC_DA_sys\")\n    transform_single_time_series!(sys_rts_da, Hour(24), Hour(1))\n    interchange1 = AreaInterchange(;\n        name = \"interchange1_2\",\n        available = true,\n        active_power_flow = 100.0,\n        flow_limits = (from_to = 1.0, to_from = 1.0),\n        from_area = get_component(Area, sys_rts_da, \"1\"),\n        to_area = get_component(Area, sys_rts_da, \"2\"),\n    )\n    interchange2 = AreaInterchange(;\n        name = \"interchange1_3\",\n        available = true,\n        active_power_flow = 100.0,\n        flow_limits = (from_to = 1.0, to_from = 1.0),\n        from_area = get_component(Area, sys_rts_da, \"1\"),\n        to_area = get_component(Area, sys_rts_da, \"3\"),\n    )\n    interchange3 = AreaInterchange(;\n        name = \"interchange3_2\",\n        available = true,\n        active_power_flow = 100.0,\n        flow_limits = (from_to = 1.0, to_from = 1.0),\n        from_area = get_component(Area, sys_rts_da, \"3\"),\n        to_area = get_component(Area, sys_rts_da, \"2\"),\n    )\n    add_components!(\n        sys_rts_da,\n        [interchange1, interchange2, interchange3],\n    )\n    # This interface is limiting all the flows into 1\n    interface = TransmissionInterface(;\n        name = \"interface1_2_3\",\n        available = true,\n        active_power_flow_limits = (min = 0.0, max = 1.0),\n        violation_penalty = 1000.0,\n        direction_mapping = Dict(\"interchange1_2\" => 1,\n            \"interchange1_3\" => -1,\n        ),\n    )\n    add_service!(\n        sys_rts_da,\n        interface,\n        [interchange1, interchange2],\n    )\n    template = ProblemTemplate(NetworkModel(AreaBalancePowerModel))\n    set_device_model!(template, ThermalStandard, ThermalDispatchNoMin)\n    set_device_model!(template, RenewableDispatch, RenewableFullDispatch)\n    set_device_model!(template, PowerLoad, StaticPowerLoad)\n    set_device_model!(template, RenewableNonDispatch, FixedOutput)\n    set_device_model!(template, HydroDispatch, HydroDispatchRunOfRiver)\n    set_device_model!(template, AreaInterchange, StaticBranch)\n    set_service_model!(\n        template,\n        ServiceModel(TransmissionInterface, ConstantMaxInterfaceFlow),\n    )\n    ps_model =\n        DecisionModel(\n            template,\n            sys_rts_da;\n            resolution = Hour(1),\n            optimizer = HiGHS_optimizer,\n            store_variable_names = true,\n        )\n\n    @test build!(ps_model; output_dir = mktempdir(; cleanup = true)) ==\n          PSI.ModelBuildStatus.BUILT\n    @test solve!(ps_model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED\n\n    moi_tests(ps_model, 8568, 0, 2136, 1416, 2664, false)\n\n    opt_container = PSI.get_optimization_container(ps_model)\n    copper_plate_constraints =\n        PSI.get_constraint(opt_container, CopperPlateBalanceConstraint(), PSY.Area)\n    @test size(copper_plate_constraints) == (3, 24)\n\n    interchange_constraints_ub =\n        PSI.get_constraint(opt_container, InterfaceFlowLimit(), TransmissionInterface, \"ub\")\n    interchange_constraints_lb =\n        PSI.get_constraint(opt_container, InterfaceFlowLimit(), TransmissionInterface, \"lb\")\n    @test size(interchange_constraints_ub) == (1, 24)\n    @test size(interchange_constraints_lb) == (1, 24)\n\n    interchange_constraints_ub[\"interface1_2_3\", 1]\n    # Obj Function test is disabled because with this constraints\n    # the model goes into the slacks and the cost is larger than 1e6\n    # psi_checksolve_test(ps_model, [MOI.OPTIMAL], 482055, 1)\n\n    results = OptimizationProblemResults(ps_model)\n    interface_results =\n        read_expression(\n            results,\n            \"InterfaceTotalFlow__TransmissionInterface\";\n            table_format = TableFormat.WIDE,\n        )\n    for i in 1:24\n        @test interface_results[!, \"interface1_2_3\"][i] <= 100.0 + PSI.ABSOLUTE_TOLERANCE\n    end\nend\n\n@testset \"Test Interfaces on Interchanges and Double Circuits with AreaPTDFPowerModel\" begin\n    sys_rts_da = build_system(PSISystems, \"modified_RTS_GMLC_DA_sys\")\n    transform_single_time_series!(sys_rts_da, Hour(24), Hour(1))\n    interchange1 = AreaInterchange(;\n        name = \"interchange1_2\",\n        available = true,\n        active_power_flow = 100.0,\n        flow_limits = (from_to = 1.0, to_from = 1.0),\n        from_area = get_component(Area, sys_rts_da, \"1\"),\n        to_area = get_component(Area, sys_rts_da, \"2\"),\n    )\n    interchange2 = AreaInterchange(;\n        name = \"interchange1_3\",\n        available = true,\n        active_power_flow = 100.0,\n        flow_limits = (from_to = 1.0, to_from = 1.0),\n        from_area = get_component(Area, sys_rts_da, \"1\"),\n        to_area = get_component(Area, sys_rts_da, \"3\"),\n    )\n    interchange3 = AreaInterchange(;\n        name = \"interchange3_2\",\n        available = true,\n        active_power_flow = 100.0,\n        flow_limits = (from_to = 1.0, to_from = 1.0),\n        from_area = get_component(Area, sys_rts_da, \"3\"),\n        to_area = get_component(Area, sys_rts_da, \"2\"),\n    )\n    add_components!(\n        sys_rts_da,\n        [interchange1, interchange2, interchange3],\n    )\n    # This interface is limiting all the flows into 1\n    interface1 = TransmissionInterface(;\n        name = \"interface1_2_3\",\n        available = true,\n        active_power_flow_limits = (min = 0.0, max = 1.0),\n        violation_penalty = 1000.0,\n        direction_mapping = Dict(\"interchange1_2\" => 1,\n            \"interchange1_3\" => -1,\n        ),\n    )\n    add_service!(\n        sys_rts_da,\n        interface1,\n        [interchange1, interchange2],\n    )\n\n    #Add an interface on a double circuit:\n    double_circuit_1 = get_component(Line, sys_rts_da, \"A33-1\")\n    double_circuit_2 = get_component(Line, sys_rts_da, \"A33-2\")\n    interface2 = TransmissionInterface(;\n        name = \"interface_double_circuit\",\n        available = true,\n        active_power_flow_limits = (min = 0.0, max = 1.0),\n        violation_penalty = 1000.0,\n        direction_mapping = Dict(\"A33-1\" => 1, \"A33-2\" => 1),\n    )\n    add_service!(\n        sys_rts_da,\n        interface2,\n        [double_circuit_1, double_circuit_2],\n    )\n\n    template = ProblemTemplate(NetworkModel(AreaPTDFPowerModel; use_slacks = true))\n    set_device_model!(template, ThermalStandard, ThermalDispatchNoMin)\n    set_device_model!(template, RenewableDispatch, RenewableFullDispatch)\n    set_device_model!(template, PowerLoad, StaticPowerLoad)\n    set_device_model!(template, RenewableNonDispatch, FixedOutput)\n    set_device_model!(template, HydroDispatch, HydroDispatchRunOfRiver)\n    set_device_model!(template, Line, StaticBranchUnbounded)\n    set_device_model!(\n        template,\n        DeviceModel(AreaInterchange, StaticBranchUnbounded; use_slacks = false),\n    )\n    set_service_model!(\n        template,\n        ServiceModel(TransmissionInterface, ConstantMaxInterfaceFlow),\n    )\n    ps_model =\n        DecisionModel(\n            template,\n            sys_rts_da;\n            resolution = Hour(1),\n            optimizer = HiGHS_optimizer,\n            store_variable_names = true,\n            optimizer_solve_log_print = true,\n        )\n\n    @test build!(ps_model; output_dir = mktempdir(; cleanup = true)) ==\n          PSI.ModelBuildStatus.BUILT\n    @test solve!(ps_model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED\n\n    moi_tests(ps_model, 8712, 0, 2160, 1440, 2664, false)\n\n    opt_container = PSI.get_optimization_container(ps_model)\n    copper_plate_constraints =\n        PSI.get_constraint(opt_container, CopperPlateBalanceConstraint(), PSY.Area)\n    @test size(copper_plate_constraints) == (3, 24)\n\n    interchange_constraints_ub =\n        PSI.get_constraint(opt_container, InterfaceFlowLimit(), TransmissionInterface, \"ub\")\n    interchange_constraints_lb =\n        PSI.get_constraint(opt_container, InterfaceFlowLimit(), TransmissionInterface, \"lb\")\n    @test size(interchange_constraints_ub) == (2, 24)\n    @test size(interchange_constraints_lb) == (2, 24)\n\n    interchange_constraints_ub[\"interface1_2_3\", 1]\n    # Obj Function test is disabled because with this constraints\n    # the model goes into the slacks and the cost is larger than 1e6\n    # psi_checksolve_test(ps_model, [MOI.OPTIMAL], 482055, 1)\n\n    results = OptimizationProblemResults(ps_model)\n    interface_results =\n        read_expression(\n            results,\n            \"InterfaceTotalFlow__TransmissionInterface\";\n            table_format = TableFormat.WIDE,\n        )\n    for i in 1:24\n        @test interface_results[!, \"interface1_2_3\"][i] <= 100.0 + PSI.ABSOLUTE_TOLERANCE\n    end\nend\n\n@testset \"Test bad data for interfaces with reductions\" begin\n    sys_rts_da = build_system(PSISystems, \"modified_RTS_GMLC_DA_sys\")\n    transform_single_time_series!(sys_rts_da, Hour(24), Hour(1))\n\n    # Add an interface on a double circuit:\n    double_circuit_1 = get_component(Line, sys_rts_da, \"A33-1\")\n    double_circuit_2 = get_component(Line, sys_rts_da, \"A33-2\")\n    interface_double_circuit = TransmissionInterface(;\n        name = \"interface_double_circuit\",\n        available = true,\n        active_power_flow_limits = (min = 0.0, max = 1.0),\n        violation_penalty = 1000.0,\n        direction_mapping = Dict(\"A33-1\" => 1, \"A33-2\" => 1),\n    )\n    add_service!(\n        sys_rts_da,\n        interface_double_circuit,\n        [double_circuit_1, double_circuit_2],\n    )\n\n    # Add interface on a series chain:\n    series_chain_1 = get_component(Line, sys_rts_da, \"CA-1\")\n    series_chain_2 = get_component(Line, sys_rts_da, \"C35\")\n    interface_series_chain = TransmissionInterface(;\n        name = \"interface_series_chain\",\n        available = true,\n        active_power_flow_limits = (min = 0.0, max = 1.0),\n        violation_penalty = 1000.0,\n        direction_mapping = Dict(\"CA-1\" => -1, \"C35\" => -1),\n    )\n    # Order matters here; must compute the ptdf before adding the service for the lines that \n    # are included in the interface to be reduced (in order to test the bad data checking)\n    ptdf = PTDF(sys_rts_da; network_reductions = NetworkReduction[DegreeTwoReduction()])\n    add_service!(\n        sys_rts_da,\n        interface_series_chain,\n        [series_chain_1, series_chain_2],\n    )\n    template = ProblemTemplate(\n        NetworkModel(\n            AreaPTDFPowerModel;\n            PTDF_matrix = ptdf,\n            reduce_degree_two_branches = true,\n            use_slacks = true,\n        ),\n    )\n    set_device_model!(template, ThermalStandard, ThermalDispatchNoMin)\n    set_device_model!(template, RenewableDispatch, RenewableFullDispatch)\n    set_device_model!(template, PowerLoad, StaticPowerLoad)\n    set_device_model!(template, RenewableNonDispatch, FixedOutput)\n    set_device_model!(template, HydroDispatch, HydroDispatchRunOfRiver)\n    set_device_model!(template, Line, StaticBranchUnbounded)\n    set_device_model!(\n        template,\n        DeviceModel(AreaInterchange, StaticBranchUnbounded; use_slacks = false),\n    )\n    set_service_model!(\n        template,\n        ServiceModel(TransmissionInterface, ConstantMaxInterfaceFlow),\n    )\n    ps_model =\n        DecisionModel(\n            template,\n            sys_rts_da;\n            resolution = Hour(1),\n            optimizer = HiGHS_optimizer,\n            store_variable_names = true,\n            optimizer_solve_log_print = true,\n        )\n\n    @test build!(ps_model; output_dir = mktempdir(; cleanup = true)) ==\n          PSI.ModelBuildStatus.BUILT\n\n    # Test bad direction data for interface on series chain :\n    set_direction_mapping!(interface_series_chain, Dict(\"CA-1\" => 1, \"C35\" => -1))\n    ps_model =\n        DecisionModel(\n            template,\n            sys_rts_da;\n            resolution = Hour(1),\n            optimizer = HiGHS_optimizer,\n            store_variable_names = true,\n        )\n    @test build!(\n        ps_model;\n        console_level = Logging.AboveMaxLevel,  # Ignore expected errors.\n        output_dir = mktempdir(; cleanup = true),\n    ) == PSI.ModelBuildStatus.FAILED\n\n    # Test bad direction data for interface on double circuit:\n    set_direction_mapping!(interface_series_chain, Dict(\"CA-1\" => 1, \"C35\" => 1))\n    set_direction_mapping!(interface_double_circuit, Dict(\"A33-1\" => 1, \"A33-2\" => -1))\n    ps_model =\n        DecisionModel(\n            template,\n            sys_rts_da;\n            resolution = Hour(1),\n            optimizer = HiGHS_optimizer,\n            store_variable_names = true,\n            optimizer_solve_log_print = true,\n        )\n    @test build!(\n        ps_model;\n        console_level = Logging.AboveMaxLevel,  # Ignore expected errors.\n        output_dir = mktempdir(; cleanup = true),\n    ) == PSI.ModelBuildStatus.FAILED\n    set_direction_mapping!(interface_double_circuit, Dict(\"A33-1\" => 1, \"A33-2\" => 1))\n\n    # Test only including part of a double circuit in an interface:\n    pop!(get_services(double_circuit_1))\n    ps_model =\n        DecisionModel(\n            template,\n            sys_rts_da;\n            resolution = Hour(1),\n            optimizer = HiGHS_optimizer,\n            store_variable_names = true,\n        )\n    @test build!(\n        ps_model;\n        console_level = Logging.AboveMaxLevel,  # Ignore expected errors.\n        output_dir = mktempdir(; cleanup = true),\n    ) == PSI.ModelBuildStatus.FAILED\n\n    # Test only including part of a series chain in an interface:\n    push!(get_services(double_circuit_1), interface_double_circuit)\n    pop!(get_services(series_chain_1))\n    ps_model =\n        DecisionModel(\n            template,\n            sys_rts_da;\n            resolution = Hour(1),\n            optimizer = HiGHS_optimizer,\n            store_variable_names = true,\n        )\n    @test build!(\n        ps_model;\n        console_level = Logging.AboveMaxLevel,  # Ignore expected errors.\n        output_dir = mktempdir(; cleanup = true),\n    ) == PSI.ModelBuildStatus.FAILED\nend\n"
  },
  {
    "path": "test/test_simulation_build.jl",
    "content": "@testset \"Simulation Build Tests\" begin\n    models = create_simulation_build_test_problems(get_template_basic_uc_simulation())\n    sequence = SimulationSequence(;\n        models = models,\n        feedforwards = Dict(\n            \"ED\" => [\n                SemiContinuousFeedforward(;\n                    component_type = ThermalStandard,\n                    source = OnVariable,\n                    affected_values = [ActivePowerVariable],\n                ),\n            ],\n        ),\n        ini_cond_chronology = InterProblemChronology(),\n    )\n    sim = Simulation(;\n        name = \"test\",\n        steps = 1,\n        models = models,\n        sequence = sequence,\n        simulation_folder = mktempdir(; cleanup = true),\n    )\n\n    build_out = build!(sim)\n    @test build_out == PSI.SimulationBuildStatus.BUILT\n\n    for field in fieldnames(SimulationSequence)\n        if fieldtype(SimulationSequence, field) == Union{Dates.DateTime, Nothing}\n            @test getfield(sim.sequence, field) !== nothing\n        end\n    end\n\n    @test length(findall(x -> x == 2, sequence.execution_order)) == 24\n    @test length(findall(x -> x == 1, sequence.execution_order)) == 1\n\n    state = PSI.get_simulation_state(sim)\n\n    uc_vars = [OnVariable, StartVariable, StopVariable]\n    ed_vars = [ActivePowerVariable]\n    for (key, data) in state.decision_states.variables\n        if PSI.get_entry_type(key) ∈ uc_vars\n            _, count = size(data.values)\n            @test count == 24\n        elseif PSI.get_entry_type(key) ∈ ed_vars\n            _, count = size(data.values)\n            @test count == 288\n        end\n    end\nend\n\n@testset \"Simulation with provided initial time\" begin\n    models = create_simulation_build_test_problems(get_template_basic_uc_simulation())\n    sequence = SimulationSequence(;\n        models = models,\n        feedforwards = Dict(\n            \"ED\" => [\n                SemiContinuousFeedforward(;\n                    component_type = ThermalStandard,\n                    source = OnVariable,\n                    affected_values = [ActivePowerVariable],\n                ),\n            ],\n        ),\n        ini_cond_chronology = InterProblemChronology(),\n    )\n    second_day = DateTime(\"1/1/2024  23:00:00\", \"d/m/y  H:M:S\") + Hour(1)\n    sim = Simulation(;\n        name = \"test\",\n        steps = 1,\n        models = models,\n        sequence = sequence,\n        simulation_folder = mktempdir(; cleanup = true),\n        initial_time = second_day,\n    )\n    build_out = build!(sim)\n    @test build_out == PSI.SimulationBuildStatus.BUILT\n\n    for model in PSI.get_decision_models(PSI.get_models(sim))\n        @test PSI.get_initial_time(model) == second_day\n    end\n\n    for field in fieldnames(SimulationSequence)\n        if fieldtype(SimulationSequence, field) == Union{Dates.DateTime, Nothing}\n            @test getfield(sim.sequence, field) !== nothing\n        end\n    end\n\n    @test length(findall(x -> x == 2, sequence.execution_order)) == 24\n    @test length(findall(x -> x == 1, sequence.execution_order)) == 1\nend\n\n@testset \"Negative Tests (Bad Parametrization)\" begin\n    models = create_simulation_build_test_problems(get_template_basic_uc_simulation())\n    sequence = SimulationSequence(;\n        models = models,\n        feedforwards = Dict(\n            \"ED\" => [\n                SemiContinuousFeedforward(;\n                    component_type = ThermalStandard,\n                    source = OnVariable,\n                    affected_values = [ActivePowerVariable],\n                ),\n            ],\n        ),\n        ini_cond_chronology = InterProblemChronology(),\n    )\n\n    @test_throws UndefKeywordError sim = Simulation(; name = \"test\", steps = 1)\n\n    sim = Simulation(;\n        name = \"test\",\n        steps = 1,\n        models = models,\n        sequence = sequence,\n        simulation_folder = mktempdir(; cleanup = true),\n        initial_time = Dates.now(),\n    )\n\n    @test_throws IS.ConflictingInputsError build!(\n        sim,\n        console_level = Logging.AboveMaxLevel,\n    )\n\n    sim = Simulation(;\n        name = \"fake_path\",\n        steps = 1,\n        models = models,\n        sequence = sequence,\n        simulation_folder = \"fake_path\",\n    )\n\n    @test_throws IS.ConflictingInputsError PSI._check_folder(sim)\nend\n\n@testset \"Test SemiContinuous Feedforward with Active and Reactive Power variables\" begin\n    template_uc = get_template_basic_uc_simulation()\n    set_device_model!(template_uc, Line, StaticBranchUnbounded)\n    set_network_model!(template_uc, NetworkModel(DCPPowerModel; use_slacks = true))\n    # network slacks added because of data issues\n    template_ed =\n        get_template_nomin_ed_simulation(NetworkModel(ACPPowerModel; use_slacks = true))\n    set_device_model!(template_ed, Line, StaticBranchUnbounded)\n    c_sys5_hy_uc = PSB.build_system(PSITestSystems, \"c_sys5_hy_uc\")\n    c_sys5_hy_ed = PSB.build_system(PSITestSystems, \"c_sys5_hy_ed\")\n    models = SimulationModels(;\n        decision_models = [\n            DecisionModel(\n                template_uc,\n                c_sys5_hy_uc;\n                name = \"UC\",\n                optimizer = HiGHS_optimizer,\n                initialize_model = false,\n            ),\n            DecisionModel(\n                template_ed,\n                c_sys5_hy_ed;\n                name = \"ED\",\n                optimizer = ipopt_optimizer,\n                initialize_model = false,\n            ),\n        ],\n    )\n\n    sequence = SimulationSequence(;\n        models = models,\n        feedforwards = Dict(\n            \"ED\" => [\n                SemiContinuousFeedforward(;\n                    component_type = ThermalStandard,\n                    source = OnVariable,\n                    affected_values = [ActivePowerVariable, ReactivePowerVariable],\n                ),\n            ],\n        ),\n        ini_cond_chronology = InterProblemChronology(),\n    )\n\n    sim = Simulation(;\n        name = \"reactive_feedforward\",\n        steps = 2,\n        models = models,\n        sequence = sequence,\n        simulation_folder = mktempdir(; cleanup = true),\n    )\n    build_out = build!(sim)\n    @test build_out == PSI.SimulationBuildStatus.BUILT\n    ac_power_model = PSI.get_simulation_model(PSI.get_models(sim), :ED)\n    c = PSI.get_constraint(\n        PSI.get_optimization_container(ac_power_model),\n        FeedforwardSemiContinuousConstraint(),\n        ThermalStandard,\n        \"ActivePowerVariable_ub\",\n    )\n    @test !isempty(c)\n    c = PSI.get_constraint(\n        PSI.get_optimization_container(ac_power_model),\n        FeedforwardSemiContinuousConstraint(),\n        ThermalStandard,\n        \"ActivePowerVariable_lb\",\n    )\n    @test !isempty(c)\n    c = PSI.get_constraint(\n        PSI.get_optimization_container(ac_power_model),\n        FeedforwardSemiContinuousConstraint(),\n        ThermalStandard,\n        \"ReactivePowerVariable_ub\",\n    )\n    @test !isempty(c)\n    c = PSI.get_constraint(\n        PSI.get_optimization_container(ac_power_model),\n        FeedforwardSemiContinuousConstraint(),\n        ThermalStandard,\n        \"ReactivePowerVariable_lb\",\n    )\n    @test !isempty(c)\nend\n\n@testset \"Test Upper/Lower Bound Feedforwards\" begin\n    template_uc = get_template_basic_uc_simulation()\n    set_network_model!(template_uc, NetworkModel(PTDFPowerModel; use_slacks = true))\n    set_device_model!(template_uc, DeviceModel(Line, StaticBranchBounds))\n    template_ed =\n        get_template_nomin_ed_simulation(NetworkModel(PTDFPowerModel; use_slacks = true))\n    set_device_model!(template_ed, DeviceModel(Line, StaticBranchBounds))\n    c_sys5_hy_uc = PSB.build_system(PSITestSystems, \"c_sys5_hy_uc\")\n    c_sys5_hy_ed = PSB.build_system(PSITestSystems, \"c_sys5_hy_ed\")\n    models = SimulationModels(;\n        decision_models = [\n            DecisionModel(\n                template_uc,\n                c_sys5_hy_uc;\n                name = \"UC\",\n                optimizer = HiGHS_optimizer,\n                initialize_model = false,\n            ),\n            DecisionModel(\n                template_ed,\n                c_sys5_hy_ed;\n                name = \"ED\",\n                optimizer = ipopt_optimizer,\n                initialize_model = false,\n            ),\n        ],\n    )\n\n    sequence = SimulationSequence(;\n        models = models,\n        feedforwards = Dict(\n            \"ED\" => [\n                SemiContinuousFeedforward(;\n                    component_type = ThermalStandard,\n                    source = OnVariable,\n                    affected_values = [ActivePowerVariable],\n                ),\n                LowerBoundFeedforward(;\n                    component_type = Line,\n                    source = FlowActivePowerVariable,\n                    affected_values = [FlowActivePowerVariable],\n                    add_slacks = true,\n                ),\n                UpperBoundFeedforward(;\n                    component_type = Line,\n                    source = FlowActivePowerVariable,\n                    affected_values = [FlowActivePowerVariable],\n                    add_slacks = true,\n                ),\n            ],\n        ),\n        ini_cond_chronology = InterProblemChronology(),\n    )\n\n    sim = Simulation(;\n        name = \"reactive_feedforward\",\n        steps = 2,\n        models = models,\n        sequence = sequence,\n        simulation_folder = mktempdir(; cleanup = true),\n    )\n    build_out = build!(sim)\n    @test build_out == PSI.SimulationBuildStatus.BUILT\n    ed_power_model = PSI.get_simulation_model(PSI.get_models(sim), :ED)\n    c = PSI.get_constraint(\n        PSI.get_optimization_container(ed_power_model),\n        FeedforwardSemiContinuousConstraint(),\n        ThermalStandard,\n        \"ActivePowerVariable_ub\",\n    )\n    @test !isempty(c)\n    c = PSI.get_constraint(\n        PSI.get_optimization_container(ed_power_model),\n        FeedforwardSemiContinuousConstraint(),\n        ThermalStandard,\n        \"ActivePowerVariable_lb\",\n    )\n    @test !isempty(c)\n    c = PSI.get_constraint(\n        PSI.get_optimization_container(ed_power_model),\n        FeedforwardLowerBoundConstraint(),\n        Line,\n        \"FlowActivePowerVariablelb\",\n    )\n    @test !isempty(c)\n    c = PSI.get_constraint(\n        PSI.get_optimization_container(ed_power_model),\n        FeedforwardUpperBoundConstraint(),\n        Line,\n        \"FlowActivePowerVariableub\",\n    )\n    @test !isempty(c)\n    c = PSI.get_variable(\n        PSI.get_optimization_container(ed_power_model),\n        UpperBoundFeedForwardSlack(),\n        Line,\n        \"FlowActivePowerVariable\",\n    )\n    @test !isempty(c)\n    c = PSI.get_variable(\n        PSI.get_optimization_container(ed_power_model),\n        LowerBoundFeedForwardSlack(),\n        Line,\n        \"FlowActivePowerVariable\",\n    )\n    @test !isempty(c)\nend\n\n@testset \"Test FixValue Feedforwards\" begin\n    template_uc = get_template_basic_uc_simulation()\n    set_network_model!(template_uc, NetworkModel(PTDFPowerModel; use_slacks = true))\n    set_device_model!(template_uc, DeviceModel(Line, StaticBranchUnbounded))\n    set_service_model!(template_uc, ServiceModel(VariableReserve{ReserveUp}, RangeReserve))\n    template_ed =\n        get_template_nomin_ed_simulation(NetworkModel(PTDFPowerModel; use_slacks = true))\n    set_device_model!(template_ed, DeviceModel(Line, StaticBranchUnbounded))\n    set_service_model!(template_ed, ServiceModel(VariableReserve{ReserveUp}, RangeReserve))\n    c_sys5_hy_uc = PSB.build_system(PSITestSystems, \"c_sys5_uc\"; add_reserves = true)\n    c_sys5_hy_ed = PSB.build_system(PSITestSystems, \"c_sys5_ed\"; add_reserves = true)\n    models = SimulationModels(;\n        decision_models = [\n            DecisionModel(\n                template_uc,\n                c_sys5_hy_uc;\n                name = \"UC\",\n                optimizer = HiGHS_optimizer,\n                initialize_model = false,\n            ),\n            DecisionModel(\n                template_ed,\n                c_sys5_hy_ed;\n                name = \"ED\",\n                optimizer = ipopt_optimizer,\n                initialize_model = false,\n            ),\n        ],\n    )\n\n    sequence = SimulationSequence(;\n        models = models,\n        feedforwards = Dict(\n            \"ED\" => [\n                SemiContinuousFeedforward(;\n                    component_type = ThermalStandard,\n                    source = OnVariable,\n                    affected_values = [ActivePowerVariable],\n                ),\n                FixValueFeedforward(;\n                    component_type = VariableReserve{ReserveUp},\n                    source = ActivePowerReserveVariable,\n                    affected_values = [ActivePowerReserveVariable],\n                ),\n            ],\n        ),\n        ini_cond_chronology = InterProblemChronology(),\n    )\n\n    sim = Simulation(;\n        name = \"reserve_feedforward\",\n        steps = 2,\n        models = models,\n        sequence = sequence,\n        simulation_folder = mktempdir(; cleanup = true),\n    )\n    build_out = build!(sim)\n    @test build_out == PSI.SimulationBuildStatus.BUILT\n    ed_power_model = PSI.get_simulation_model(PSI.get_models(sim), :ED)\n    c = PSI.get_parameter(\n        PSI.get_optimization_container(ed_power_model),\n        FixValueParameter(),\n        VariableReserve{ReserveUp},\n        \"Reserve1\",\n    )\n    @test !isempty(c.multiplier_array)\n    @test !isempty(c.parameter_array)\n    c = PSI.get_parameter(\n        PSI.get_optimization_container(ed_power_model),\n        FixValueParameter(),\n        VariableReserve{ReserveUp},\n        \"Reserve11\",\n    )\n    @test !isempty(c.multiplier_array)\n    @test !isempty(c.parameter_array)\nend\n\n@testset \"Build with store_systems_in_results option\" begin\n    models = create_simulation_build_test_problems(get_template_basic_uc_simulation())\n    sequence = SimulationSequence(;\n        models = models,\n        feedforwards = Dict(\n            \"ED\" => [\n                SemiContinuousFeedforward(;\n                    component_type = ThermalStandard,\n                    source = OnVariable,\n                    affected_values = [ActivePowerVariable],\n                ),\n            ],\n        ),\n        ini_cond_chronology = InterProblemChronology(),\n    )\n\n    # Test store_systems_in_results = true (default)\n    sim_with = Simulation(;\n        name = \"test_with_systems\",\n        steps = 1,\n        models = models,\n        sequence = sequence,\n        simulation_folder = mktempdir(; cleanup = true),\n    )\n    build_out = build!(sim_with; store_systems_in_results = true)\n    @test build_out == PSI.SimulationBuildStatus.BUILT\n    PSI.open_store(PSI.HdfSimulationStore, PSI.get_store_dir(sim_with), \"r\") do store\n        root = store.file[\"simulation\"]\n        @test haskey(root, \"systems\")\n        @test length(keys(root[\"systems\"])) > 0\n    end\n\n    # Test store_systems_in_results = false\n    models2 = create_simulation_build_test_problems(get_template_basic_uc_simulation())\n    sequence2 = SimulationSequence(;\n        models = models2,\n        feedforwards = Dict(\n            \"ED\" => [\n                SemiContinuousFeedforward(;\n                    component_type = ThermalStandard,\n                    source = OnVariable,\n                    affected_values = [ActivePowerVariable],\n                ),\n            ],\n        ),\n        ini_cond_chronology = InterProblemChronology(),\n    )\n    sim_without = Simulation(;\n        name = \"test_without_systems\",\n        steps = 1,\n        models = models2,\n        sequence = sequence2,\n        simulation_folder = mktempdir(; cleanup = true),\n    )\n    build_out = build!(sim_without; store_systems_in_results = false)\n    @test build_out == PSI.SimulationBuildStatus.BUILT\n    PSI.open_store(PSI.HdfSimulationStore, PSI.get_store_dir(sim_without), \"r\") do store\n        root = store.file[\"simulation\"]\n        @test !haskey(root, \"systems\")\n    end\nend\n"
  },
  {
    "path": "test/test_simulation_execute.jl",
    "content": "function test_single_stage_sequential(in_memory, rebuild, export_model)\n    tmp_dir = mktempdir(; cleanup = true)\n    template_ed = get_template_nomin_ed_simulation()\n    c_sys = PSB.build_system(PSITestSystems, \"c_sys5_uc\")\n    models = SimulationModels([\n        DecisionModel(\n            template_ed,\n            c_sys;\n            name = \"ED\",\n            optimizer = ipopt_optimizer,\n            rebuild_model = rebuild,\n            export_optimization_model = export_model,\n        ),\n    ])\n    test_sequence =\n        SimulationSequence(;\n            models = models,\n            ini_cond_chronology = InterProblemChronology(),\n        )\n    sim_single = Simulation(;\n        name = \"consecutive\",\n        steps = 2,\n        models = models,\n        sequence = test_sequence,\n        simulation_folder = tmp_dir,\n    )\n    build_out = build!(sim_single)\n    @test build_out == PSI.SimulationBuildStatus.BUILT\n    execute_out = execute!(sim_single; in_memory = in_memory)\n    @test execute_out == PSI.RunStatus.SUCCESSFULLY_FINALIZED\n    return tmp_dir\nend\n\n@testset \"Single stage sequential tests\" begin\n    for in_memory in (true, false), rebuild in (true, false)\n        test_single_stage_sequential(in_memory, rebuild, false)\n    end\nend\n\n@testset \"Test model export at each solve\" begin\n    folder = test_single_stage_sequential(true, false, true)\n    test_path =\n        joinpath(folder, \"consecutive\", \"problems\", \"ED\", \"optimization_model_exports\")\n    @test ispath(test_path)\n    @test length(readdir(test_path)) == 4\nend\n\nfunction test_2_stage_decision_models_with_feedforwards(in_memory)\n    template_uc = get_template_basic_uc_simulation()\n    template_ed = get_template_nomin_ed_simulation()\n    set_device_model!(template_ed, InterruptiblePowerLoad, StaticPowerLoad)\n    set_network_model!(\n        template_uc,\n        NetworkModel(\n            CopperPlatePowerModel;\n            duals = [CopperPlateBalanceConstraint],\n        ),\n    )\n    set_network_model!(\n        template_ed,\n        NetworkModel(\n            CopperPlatePowerModel;\n            duals = [CopperPlateBalanceConstraint],\n            use_slacks = true,\n        ),\n    )\n    c_sys5_hy_uc = PSB.build_system(PSITestSystems, \"c_sys5_hy_uc\")\n    c_sys5_hy_ed = PSB.build_system(PSITestSystems, \"c_sys5_hy_ed\")\n    models = SimulationModels(;\n        decision_models = [\n            DecisionModel(\n                template_uc,\n                c_sys5_hy_uc;\n                name = \"UC\",\n                optimizer = HiGHS_optimizer),\n            DecisionModel(\n                template_ed,\n                c_sys5_hy_ed;\n                name = \"ED\",\n                optimizer = ipopt_optimizer,\n            ),\n        ],\n    )\n\n    sequence = SimulationSequence(;\n        models = models,\n        feedforwards = Dict(\n            \"ED\" => [\n                SemiContinuousFeedforward(;\n                    component_type = ThermalStandard,\n                    source = OnVariable,\n                    affected_values = [ActivePowerVariable],\n                ),\n            ],\n        ),\n        ini_cond_chronology = InterProblemChronology(),\n    )\n    sim = Simulation(;\n        name = \"no_cache\",\n        steps = 2,\n        models = models,\n        sequence = sequence,\n        simulation_folder = mktempdir(; cleanup = true),\n    )\n\n    build_out = build!(sim; console_level = Logging.Error)\n    @test build_out == PSI.SimulationBuildStatus.BUILT\n    execute_out = execute!(sim; in_memory = in_memory)\n    @test execute_out == PSI.RunStatus.SUCCESSFULLY_FINALIZED\nend\n\n@testset \"2-Stage Decision Models with FeedForwards\" begin\n    for in_memory in (true, false)\n        test_2_stage_decision_models_with_feedforwards(in_memory)\n    end\nend\n\n@testset \"Test Simulation Utils\" begin\n    template_uc = get_template_basic_uc_simulation()\n    set_device_model!(template_uc, ThermalStandard, ThermalStandardUnitCommitment)\n    set_network_model!(\n        template_uc,\n        NetworkModel(\n            CopperPlatePowerModel;\n            duals = [CopperPlateBalanceConstraint],\n        ),\n    )\n\n    template_ed = get_template_nomin_ed_simulation(\n        NetworkModel(\n            CopperPlatePowerModel;\n            # Added because of data issues\n            use_slacks = true,\n            duals = [CopperPlateBalanceConstraint],\n        ),\n    )\n    set_device_model!(template_ed, InterruptiblePowerLoad, StaticPowerLoad)\n    c_sys5_hy_uc = PSB.build_system(PSITestSystems, \"c_sys5_hy_uc\")\n    c_sys5_hy_ed = PSB.build_system(PSITestSystems, \"c_sys5_hy_ed\")\n    models = SimulationModels(;\n        decision_models = [\n            DecisionModel(\n                template_uc,\n                c_sys5_hy_uc;\n                name = \"UC\",\n                optimizer = HiGHS_optimizer,\n            ),\n            DecisionModel(\n                template_ed,\n                c_sys5_hy_ed;\n                name = \"ED\",\n                optimizer = ipopt_optimizer,\n            ),\n        ],\n    )\n\n    sequence = SimulationSequence(;\n        models = models,\n        feedforwards = Dict(\n            \"ED\" => [\n                SemiContinuousFeedforward(;\n                    component_type = ThermalStandard,\n                    source = OnVariable,\n                    affected_values = [ActivePowerVariable],\n                ),\n            ],\n        ),\n        ini_cond_chronology = InterProblemChronology(),\n    )\n    sim = Simulation(;\n        name = \"aggregation\",\n        steps = 2,\n        models = models,\n        sequence = sequence,\n        simulation_folder = mktempdir(; cleanup = false),\n    )\n\n    build_out = build!(sim; console_level = Logging.Error)\n    @test build_out == PSI.SimulationBuildStatus.BUILT\n    execute_out = execute!(sim)\n    @test execute_out == PSI.RunStatus.SUCCESSFULLY_FINALIZED\n\n    @testset \"Verify simulation events\" begin\n        file = joinpath(PSI.get_simulation_dir(sim), \"recorder\", \"simulation_status.log\")\n        @test isfile(file)\n        events = PSI.list_simulation_events(\n            PSI.InitialConditionUpdateEvent,\n            PSI.get_simulation_dir(sim);\n            step = 1,\n        )\n        @test length(events) == 23\n        events = PSI.list_simulation_events(\n            PSI.InitialConditionUpdateEvent,\n            PSI.get_simulation_dir(sim);\n            step = 2,\n        )\n        # This value needs to be checked\n        @test length(events) == 46\n\n        PSI.show_simulation_events(\n            devnull,\n            PSI.InitialConditionUpdateEvent,\n            PSI.get_simulation_dir(sim),\n            ;\n            step = 2,\n        )\n        events = PSI.list_simulation_events(\n            PSI.InitialConditionUpdateEvent,\n            PSI.get_simulation_dir(sim);\n            step = 1,\n            model_name = \"UC\",\n        )\n        @test length(events) == 0\n        events = PSI.list_simulation_events(\n            PSI.InitialConditionUpdateEvent,\n            PSI.get_simulation_dir(sim),\n            ;\n            step = 2,\n            model_name = \"UC\",\n        )\n        @test length(events) == 22\n        PSI.show_simulation_events(\n            devnull,\n            PSI.InitialConditionUpdateEvent,\n            PSI.get_simulation_dir(sim),\n            ;\n            step = 2,\n            model_name = \"UC\",\n        )\n    end\nend\n\n@testset \"Test simulation with VariableReserve\" begin\n    template_uc = get_template_basic_uc_simulation()\n    set_network_model!(template_uc, NetworkModel(PTDFPowerModel; use_slacks = true))\n    set_device_model!(template_uc, DeviceModel(Line, StaticBranchUnbounded))\n    set_service_model!(template_uc, ServiceModel(VariableReserve{ReserveUp}, RangeReserve))\n    template_ed =\n        get_template_nomin_ed_simulation(NetworkModel(PTDFPowerModel; use_slacks = true))\n    set_device_model!(template_ed, DeviceModel(Line, StaticBranchUnbounded))\n    set_service_model!(template_ed, ServiceModel(VariableReserve{ReserveUp}, RangeReserve))\n    c_sys5_hy_uc = PSB.build_system(PSITestSystems, \"c_sys5_uc\"; add_reserves = true)\n    c_sys5_hy_ed = PSB.build_system(PSITestSystems, \"c_sys5_ed\"; add_reserves = true)\n    models = SimulationModels(;\n        decision_models = [\n            DecisionModel(\n                template_uc,\n                c_sys5_hy_uc;\n                name = \"UC\",\n                optimizer = HiGHS_optimizer,\n                initialize_model = false,\n            ),\n            DecisionModel(\n                template_ed,\n                c_sys5_hy_ed;\n                name = \"ED\",\n                optimizer = ipopt_optimizer,\n                initialize_model = false,\n            ),\n        ],\n    )\n\n    sequence = SimulationSequence(;\n        models = models,\n        feedforwards = Dict(\n            \"ED\" => [\n                SemiContinuousFeedforward(;\n                    component_type = ThermalStandard,\n                    source = OnVariable,\n                    affected_values = [ActivePowerVariable],\n                ),\n                FixValueFeedforward(;\n                    component_type = VariableReserve{ReserveUp},\n                    source = ActivePowerReserveVariable,\n                    affected_values = [ActivePowerReserveVariable],\n                ),\n            ],\n        ),\n        ini_cond_chronology = InterProblemChronology(),\n    )\n\n    sim = Simulation(;\n        name = \"reserve_feedforward\",\n        steps = 2,\n        models = models,\n        sequence = sequence,\n        simulation_folder = mktempdir(; cleanup = true),\n    )\n    build_out = build!(sim)\n    @test build_out == PSI.SimulationBuildStatus.BUILT\n    execute_out = execute!(sim)\n    @test execute_out == PSI.RunStatus.SUCCESSFULLY_FINALIZED\n\n    results = SimulationResults(sim)\n    for name in list_decision_problems(results)\n        res = get_decision_problem_results(results, name)\n        parameters = read_realized_parameters(res)\n        @test !isempty(parameters)\n        for (key, df1) in parameters\n            df2 = read_realized_parameter(res, key)\n            @test df1 == df2\n        end\n    end\nend\n\nfunction test_3_stage_simulation_with_feedforwards(in_memory)\n    sys_rts_da = PSB.build_system(PSISystems, \"modified_RTS_GMLC_DA_sys\")\n    sys_rts_rt = PSB.build_system(PSISystems, \"modified_RTS_GMLC_RT_sys\")\n    sys_rts_ha = deepcopy(sys_rts_rt)\n\n    PSY.transform_single_time_series!(sys_rts_da, Hour(36), Hour(24))\n    PSY.transform_single_time_series!(sys_rts_ha, Hour(2), Hour(1))\n    PSY.transform_single_time_series!(sys_rts_rt, Hour(1), Hour(1))\n\n    template_uc = get_template_standard_uc_simulation()\n    set_network_model!(template_uc, NetworkModel(CopperPlatePowerModel))\n    template_ha = deepcopy(template_uc)\n    # network slacks added because of data issues\n    template_ed = get_thermal_dispatch_template_network(\n        NetworkModel(CopperPlatePowerModel; use_slacks = true),\n    )\n\n    models = SimulationModels(;\n        decision_models = [\n            DecisionModel(\n                template_uc,\n                sys_rts_da;\n                name = \"UC\",\n                optimizer = HiGHS_optimizer,\n                initialize_model = true,\n            ),\n            DecisionModel(\n                template_ha,\n                sys_rts_ha;\n                name = \"HA\",\n                optimizer = HiGHS_optimizer,\n                initialize_model = true,\n            ),\n            DecisionModel(\n                template_ed,\n                sys_rts_rt;\n                name = \"ED\",\n                optimizer = HiGHS_optimizer,\n                initialize_model = true,\n            ),\n        ],\n    )\n\n    sequence = SimulationSequence(;\n        models = models,\n        feedforwards = Dict(\n            \"ED\" => [\n                SemiContinuousFeedforward(;\n                    component_type = ThermalStandard,\n                    source = OnVariable,\n                    affected_values = [ActivePowerVariable],\n                ),\n            ],\n        ),\n        ini_cond_chronology = InterProblemChronology(),\n    )\n\n    sim = Simulation(;\n        name = \"3stage_feedforward\",\n        steps = 1,\n        models = models,\n        sequence = sequence,\n        simulation_folder = mktempdir(; cleanup = true),\n    )\n    build_out = build!(sim)\n    @test build_out == PSI.SimulationBuildStatus.BUILT\n    execute_out = execute!(sim; in_memory = in_memory)\n    @test execute_out == PSI.RunStatus.SUCCESSFULLY_FINALIZED\nend\n\n@testset \"Test 3 stage simulation with FeedForwards\" begin\n    for in_memory in (true, false)\n        test_3_stage_simulation_with_feedforwards(in_memory)\n    end\nend\n"
  },
  {
    "path": "test/test_simulation_models.jl",
    "content": "@testset \"Test Simulation Models\" begin\n    models = SimulationModels(\n        [\n            DecisionModel(\n                MockOperationProblem;\n                horizon = Hour(48),\n                interval = Hour(24),\n                steps = 2,\n                name = \"DAUC\",\n            ),\n            DecisionModel(\n                MockOperationProblem;\n                horizon = Hour(24),\n                interval = Hour(1),\n                steps = 2 * 24,\n                name = \"HAUC\",\n            ),\n            DecisionModel(\n                MockOperationProblem;\n                horizon = Hour(12),\n                interval = Minute(5),\n                steps = 2 * 24 * 12,\n                name = \"ED\",\n            ),\n        ],\n        EmulationModel(MockEmulationProblem; resolution = Minute(1), name = \"AGC\"),\n    )\n\n    @test length(PSI.get_decision_models(models)) == 3\n    @test PSI.get_emulation_model(models) !== nothing\n\n    @test_throws ErrorException SimulationModels(\n        [\n            DecisionModel(\n                MockOperationProblem;\n                horizon = Hour(48),\n                interval = Hour(24),\n                steps = 2,\n                name = \"DAUC\",\n            ),\n            DecisionModel(\n                MockOperationProblem;\n                horizon = Hour(24),\n                interval = Hour(1),\n                steps = 2 * 24,\n                name = \"DAUC\",\n            ),\n            DecisionModel(\n                MockOperationProblem;\n                horizon = Hour(12),\n                interval = Minute(5),\n                steps = 2 * 24 * 12,\n                name = \"ED\",\n            ),\n        ],\n        EmulationModel(MockEmulationProblem; resolution = Minute(1), name = \"AGC\"),\n    )\nend\n"
  },
  {
    "path": "test/test_simulation_partitions.jl",
    "content": "@testset \"Test partitions and step ranges\" begin\n    partitions = SimulationPartitions(2, 1, 0)\n    @test PSI.get_absolute_step_range(partitions, 1) == 1:1\n    @test PSI.get_valid_step_offset(partitions, 1) == 1\n    @test PSI.get_valid_step_length(partitions, 1) == 1\n    @test PSI.get_absolute_step_range(partitions, 2) == 2:2\n    @test PSI.get_valid_step_offset(partitions, 2) == 1\n    @test PSI.get_valid_step_length(partitions, 2) == 1\n\n    partitions = SimulationPartitions(365, 7, 1)\n    @test get_num_partitions(partitions) == 53\n    @test PSI.get_absolute_step_range(partitions, 1) == 1:7\n    @test PSI.get_valid_step_offset(partitions, 1) == 1\n    @test PSI.get_valid_step_length(partitions, 1) == 7\n    @test PSI.get_absolute_step_range(partitions, 2) == 7:14\n    @test PSI.get_valid_step_offset(partitions, 2) == 2\n    @test PSI.get_valid_step_length(partitions, 2) == 7\n    @test PSI.get_absolute_step_range(partitions, 52) == 357:364\n    @test PSI.get_valid_step_offset(partitions, 52) == 2\n    @test PSI.get_valid_step_length(partitions, 52) == 7\n    @test PSI.get_absolute_step_range(partitions, 53) == 364:365\n    @test PSI.get_valid_step_offset(partitions, 53) == 2\n    @test PSI.get_valid_step_length(partitions, 53) == 1\n\n    @test_throws ErrorException PSI.get_absolute_step_range(partitions, -1)\n    @test_throws ErrorException PSI.get_absolute_step_range(partitions, 54)\nend\n\n@testset \"Test simulation partitions\" begin\n    sim_dir = mktempdir()\n    script = joinpath(BASE_DIR, \"test\", \"run_partitioned_simulation.jl\")\n    include(script)\n\n    partition_name = \"partitioned\"\n    run_parallel_simulation(\n        build_simulation,\n        execute_simulation;\n        script = script,\n        output_dir = sim_dir,\n        name = partition_name,\n        num_steps = 3,\n        period = 1,\n        num_overlap_steps = 1,\n        # Running multiple processes in CI can kill the VM.\n        num_parallel_processes = haskey(ENV, \"CI\") ? 1 : 3,\n        exeflags = \"--project=test\",\n        force = true,\n    )\n\n    regular_name = \"regular\"\n    regular_sim = build_simulation(\n        sim_dir,\n        regular_name;\n        initial_time = DateTime(\"2024-01-02T00:00:00\"),\n        num_steps = 1,\n        HiGHS_optimizer = HiGHS_optimizer,\n    )\n    @test execute_simulation(regular_sim) == PSI.RunStatus.SUCCESSFULLY_FINALIZED\n\n    regular_results = SimulationResults(sim_dir, regular_name)\n    partitioned_results = SimulationResults(sim_dir, partition_name)\n\n    functions = (\n        read_realized_aux_variables,\n        read_realized_expressions,\n        read_realized_parameters,\n        read_realized_variables,\n    )\n    key_strings_to_skip = (\"Flow\", \"On\", \"Off\", \"Shut\", \"Start\", \"Stop\")\n    for name in (\"ED\", \"UC\")\n        regular_model_results = get_decision_problem_results(regular_results, name)\n        partitioned_model_results = get_decision_problem_results(partitioned_results, name)\n\n        for func in functions\n            regular = func(regular_model_results; table_format = TableFormat.WIDE)\n            partitioned = func(partitioned_model_results; table_format = TableFormat.WIDE)\n            @test sort(collect(keys(regular))) == sort(collect(keys(partitioned)))\n            for key in keys(regular)\n                t_start = regular[key][1, 1]\n                t_end = regular[key][end, 1]\n                rdf = regular[key]\n                pdf = partitioned[key]\n                pdf = pdf[(pdf.DateTime .>= t_start) .& (pdf.DateTime .<= t_end), :]\n                @test nrow(rdf) == nrow(pdf)\n                @test ncol(rdf) == ncol(pdf)\n                skip = false\n                for key_string_to_skip in key_strings_to_skip\n                    if occursin(key_string_to_skip, key)\n                        skip = true\n                        break\n                    end\n                end\n                skip && continue\n                r_sum = 0\n                p_sum = 0\n                atol =\n                    if (\n                        occursin(\"ProductionCostExpression\", key) ||\n                        occursin(\"FuelCostExpression__ThermalStandard\", key)\n                    )\n                        11000\n                    else\n                        1e-6\n                    end\n                for i in 2:ncol(rdf)\n                    r_sum += sum(rdf[!, i])\n                    p_sum += sum(pdf[!, i])\n                end\n                if !isapprox(r_sum, p_sum; atol = atol)\n                    @error \"Mismatch\" r_sum p_sum key\n                end\n                @test isapprox(r_sum, p_sum, atol = atol)\n            end\n        end\n    end\n\n    # TODO: Can emulation model results be validated?\nend\n"
  },
  {
    "path": "test/test_simulation_results.jl",
    "content": "using Base: COMPILETIME_PREFERENCES\n# Read the actual data of a result to see what the timestamps are\nactual_timestamps(result) = result |> values |> first |> x -> x.data |> keys |> collect\n\n# Test that a particular call to _read_results reads from outside the cache; pass through the results\nmacro test_no_cache(expr)\n    :(@test_logs(\n        match_mode = :any,\n        (:debug, r\"reading results from data store\"),\n        min_level = Logging.Debug,\n        $(esc(expr))))\nend\n@test_no_cache((@debug \"reading results from data store\"; @debug \"msg 2\"))\n\n# Test that a particular call to _read_results reads from the cache; pass through the results\nmacro test_yes_cache(expr)\n    :(@test_logs(\n        match_mode = :any,\n        (:debug, r\"reading results from SimulationsResults cache\"),\n        min_level = Logging.Debug,\n        $(esc(expr))))\nend\n@test_yes_cache((@debug \"reading results from SimulationsResults cache\"; @debug \"msg 2\"))\n\nED_EXPECTED_VARS = [\n    \"ActivePowerVariable__HydroTurbine\",\n    \"EnergyVariable__HydroReservoir\",\n    \"WaterSpillageVariable__HydroReservoir\",\n    \"HydroEnergySurplusVariable__HydroReservoir\",\n    \"HydroEnergyShortageVariable__HydroReservoir\",\n    \"ActivePowerVariable__RenewableDispatch\",\n    \"ActivePowerVariable__ThermalStandard\",\n    \"SystemBalanceSlackDown__System\",\n    \"SystemBalanceSlackUp__System\",\n]\n\nUC_EXPECTED_VARS = [\n    \"ActivePowerVariable__HydroTurbine\",\n    \"EnergyVariable__HydroReservoir\",\n    \"WaterSpillageVariable__HydroReservoir\",\n    \"HydroEnergySurplusVariable__HydroReservoir\",\n    \"HydroEnergyShortageVariable__HydroReservoir\",\n    \"ActivePowerVariable__RenewableDispatch\",\n    \"ActivePowerVariable__ThermalStandard\",\n    \"OnVariable__ThermalStandard\",\n    \"StartVariable__ThermalStandard\",\n    \"StopVariable__ThermalStandard\",\n]\n\nfunction verify_export_results(results, export_path)\n    exports = SimulationResultsExport(\n        make_export_all(keys(results.decision_problem_results)),\n        results.params,\n    )\n    export_results(results, exports)\n\n    for problem_results in values(results.decision_problem_results)\n        rpath = problem_results.results_output_folder\n        problem = problem_results.problem\n        for timestamp in get_timestamps(problem_results)\n            for name in list_dual_names(problem_results)\n                @test compare_results(rpath, export_path, problem, \"duals\", name, timestamp)\n            end\n            for name in list_parameter_names(problem_results)\n                @test compare_results(\n                    rpath,\n                    export_path,\n                    problem,\n                    \"parameters\",\n                    name,\n                    timestamp,\n                )\n            end\n            for name in list_variable_names(problem_results)\n                @test compare_results(\n                    rpath,\n                    export_path,\n                    problem,\n                    \"variables\",\n                    name,\n                    timestamp,\n                )\n            end\n\n            for name in list_aux_variable_names(problem_results)\n                @test compare_results(\n                    rpath,\n                    export_path,\n                    problem,\n                    \"aux_variables\",\n                    name,\n                    timestamp,\n                )\n            end\n        end\n\n        # This file is not currently exported during the simulation.\n        @test isfile(\n            joinpath(\n                problem_results.results_output_folder,\n                problem_results.problem,\n                \"optimizer_stats.csv\",\n            ),\n        )\n    end\nend\n\nNATURAL_UNITS_VALUES = [\n    \"ActivePowerVariable__HydroTurbine\",\n    \"ActivePowerVariable__RenewableDispatch\",\n    \"ActivePowerVariable__ThermalStandard\",\n    \"EnergyVariable__HydroReservoir\",\n    \"ActivePowerTimeSeriesParameter__PowerLoad\",\n    \"ActivePowerTimeSeriesParameter__HydroTurbine\",\n    \"ActivePowerTimeSeriesParameter__RenewableDispatch\",\n    \"ActivePowerTimeSeriesParameter__InterruptiblePowerLoad\",\n    \"SystemBalanceSlackDown__System\",\n    \"SystemBalanceSlackUp__System\",\n]\n\nfunction compare_results(rpath, epath, model, field, name, timestamp)\n    filename = string(name) * \"_\" * IS.convert_for_path(timestamp) * \".csv\"\n    rp = joinpath(rpath, model, field, filename)\n    ep = joinpath(epath, model, field, filename)\n    df1 = PSI.read_dataframe(rp)\n    df2 = PSI.read_dataframe(ep)\n    # TODO: Store the tables in the same format.\n    measure_vars = [x for x in names(df2) if x != \"DateTime\"]\n    df2_long = DataFrames.stack(\n        df2,\n        measure_vars;\n        variable_name = :name,\n        value_name = :value,\n    )\n\n    if name ∈ NATURAL_UNITS_VALUES\n        df2_long[!, :value] .*= 100.0\n    end\n\n    names1 = names(df1)\n    names2 = names(df2_long)\n    names1 != names2 && return false\n    size(df1) != size(df2_long) && return false\n\n    if !isapprox(df1.value, df2_long.value)\n        @error \"File mismatch\" rp ep row1 row2\n        return false\n    end\n\n    return true\nend\n\nfunction make_export_all(problems)\n    return [\n        OptimizationProblemResultsExport(\n            x;\n            store_all_duals = true,\n            store_all_variables = true,\n            store_all_aux_variables = true,\n            store_all_parameters = true,\n        ) for x in problems\n    ]\nend\n\nfunction test_simulation_results(\n    file_path::String,\n    export_path;\n    in_memory = false,\n)\n    @testset \"Test simulation results in_memory = $in_memory\" begin\n        c_sys5_hy_uc = PSB.build_system(PSITestSystems, \"c_sys5_hy_uc\")\n        c_sys5_hy_ed = PSB.build_system(PSITestSystems, \"c_sys5_hy_ed\")\n        sim = run_simulation(\n            c_sys5_hy_uc,\n            c_sys5_hy_ed,\n            file_path,\n            export_path;\n            in_memory = in_memory,\n        )\n        results = SimulationResults(sim)\n        test_decision_problem_results(results, c_sys5_hy_ed, c_sys5_hy_uc, in_memory)\n        if !in_memory\n            test_decision_problem_results_kwargs_handling(\n                dirname(results.path),\n                c_sys5_hy_ed,\n                c_sys5_hy_uc,\n            )\n        end\n        test_emulation_problem_results(results, in_memory)\n\n        results_ed = get_decision_problem_results(results, \"ED\")\n        @test !isempty(results_ed)\n        @test !isempty(results)\n        empty!(results)\n        @test isempty(results_ed)\n        @test isempty(results)\n\n        verify_export_results(results, export_path)\n        exported = readdir(export_realized_results(results_ed))\n        @test length(exported) >= 22\n        @test any(contains.(exported, \"ProductionCostExpression\"))\n        @test any(contains.(exported, \"FuelCostExpression\"))\n        @test any(contains.(exported, \"StartUpCostExpression\"))\n\n        # Test that you can't read a failed simulation.\n        PSI.set_simulation_status!(sim, PSI.RunStatus.FAILED)\n        PSI.serialize_status(sim)\n        @test PSI.deserialize_status(sim) == PSI.RunStatus.FAILED\n        @test_throws ErrorException SimulationResults(sim)\n        @test_logs(\n            match_mode = :any,\n            (:warn, r\"Results may not be valid\"),\n            SimulationResults(sim, ignore_status = true),\n        )\n\n        if in_memory\n            @test !isempty(\n                sim.internal.store.dm_data[:ED].variables[PSI.VariableKey(\n                    ActivePowerVariable,\n                    ThermalStandard,\n                )],\n            )\n            @test !isempty(sim.internal.store.dm_data[:ED].optimizer_stats)\n            empty!(sim.internal.store)\n            @test isempty(sim.internal.store.dm_data[:ED].variables)\n            @test isempty(sim.internal.store.dm_data[:ED].optimizer_stats)\n        end\n    end\nend\n\nfunction test_decision_problem_results_values(\n    results_ed,\n    results_uc,\n    c_sys5_hy_ed,\n    c_sys5_hy_uc,\n)\n    @test IS.get_uuid(get_system(results_uc)) === IS.get_uuid(c_sys5_hy_uc)\n    @test IS.get_uuid(get_system(results_ed)) === IS.get_uuid(c_sys5_hy_ed)\n\n    # Temporarily mark some stuff unavailable\n    unav_uc = first(PSY.get_available_components(ThermalStandard, get_system(results_uc)))\n    PSY.set_available!(unav_uc, false)\n    unav_ed = first(PSY.get_available_components(ThermalStandard, get_system(results_ed)))\n    PSY.set_available!(unav_ed, false)\n    sel = PSY.make_selector(ThermalStandard; groupby = :each)\n    @test collect(get_components(ThermalStandard, results_uc)) ==\n          collect(get_available_components(ThermalStandard, get_system(results_uc)))\n    @test collect(get_components(ThermalStandard, results_ed)) ==\n          collect(get_available_components(ThermalStandard, get_system(results_ed)))\n    @test collect(get_groups(sel, results_uc)) ==\n          collect(get_available_groups(sel, get_system(results_uc)))\n    @test collect(get_groups(sel, results_ed)) ==\n          collect(get_available_groups(sel, get_system(results_ed)))\n    PSY.set_available!(unav_uc, true)\n    PSY.set_available!(unav_ed, true)\n\n    @test isempty(setdiff(UC_EXPECTED_VARS, list_variable_names(results_uc)))\n    @test isempty(setdiff(ED_EXPECTED_VARS, list_variable_names(results_ed)))\n\n    p_thermal_standard_ed = read_variable(results_ed, ActivePowerVariable, ThermalStandard)\n    @test length(keys(p_thermal_standard_ed)) == 48\n    for v in values(p_thermal_standard_ed)\n        @test length(unique(v.DateTime)) == 12\n        @test length(unique(v.name)) == 5\n    end\n    p_thermal_standard_ed_wide = read_variable(\n        results_ed,\n        ActivePowerVariable,\n        ThermalStandard;\n        table_format = TableFormat.WIDE,\n    )\n    @test length(keys(p_thermal_standard_ed)) == 48\n    for v in values(p_thermal_standard_ed_wide)\n        @test size(v) == (12, 6)\n    end\n\n    ren_dispatch_params =\n        read_parameter(results_ed, ActivePowerTimeSeriesParameter, RenewableDispatch)\n    @test length(keys(ren_dispatch_params)) == 48\n    for v in values(ren_dispatch_params)\n        @test length(unique(v.DateTime)) == 12\n        @test length(unique(v.name)) == 3\n    end\n\n    network_duals = read_dual(results_ed, CopperPlateBalanceConstraint, PSY.System)\n    @test length(keys(network_duals)) == 48\n    for v in values(network_duals)\n        @test length(unique(v.DateTime)) == 12\n        @test length(unique(v.name)) == 1\n    end\n\n    expression = read_expression(results_ed, PSI.ProductionCostExpression, ThermalStandard)\n    @test length(keys(expression)) == 48\n    for v in values(expression)\n        @test length(unique(v.DateTime)) == 12\n        @test length(unique(v.name)) == 5\n    end\n\n    for var_key in\n        ((ActivePowerVariable, RenewableDispatch), (ActivePowerVariable, ThermalStandard))\n        variable_by_initial_time = read_variable(results_uc, var_key...)\n        for df in values(variable_by_initial_time)\n            @test length(unique(df.DateTime)) == 24\n        end\n    end\n\n    realized_variable_uc = read_realized_variables(results_uc)\n    @test length(keys(realized_variable_uc)) == length(UC_EXPECTED_VARS)\n    for var in values(realized_variable_uc)\n        @test length(unique(var.DateTime)) == 48\n    end\n    compare_long_and_wide_realized_results_2d(read_realized_variables, results_uc)\n\n    realized_variable_uc =\n        read_realized_variables(results_uc, [(ActivePowerVariable, ThermalStandard)])\n    @test realized_variable_uc ==\n          read_realized_variables(results_uc, [\"ActivePowerVariable__ThermalStandard\"])\n    @test length(keys(realized_variable_uc)) == 1\n    for var in values(realized_variable_uc)\n        @test length(unique(var.DateTime)) == 48\n    end\n\n    # Test custom indexing.\n    realized_variable_uc2 =\n        read_realized_variables(\n            results_uc,\n            [(ActivePowerVariable, ThermalStandard)];\n            start_time = Dates.DateTime(\"2024-01-01T01:00:00\"),\n            len = 47,\n        )\n    @test unique(realized_variable_uc[\"ActivePowerVariable__ThermalStandard\"].DateTime)[2:end] ==\n          unique(realized_variable_uc2[\"ActivePowerVariable__ThermalStandard\"].DateTime)\n    @test realized_variable_uc2[\"ActivePowerVariable__ThermalStandard\"] ==\n          @rsubset(\n        realized_variable_uc[\"ActivePowerVariable__ThermalStandard\"],\n        :DateTime > Dates.DateTime(\"2024-01-01T00:00:00\")\n    )\n    compare_long_and_wide_realized_results_2d(\n        read_realized_variables,\n        results_uc,\n        [(ActivePowerVariable, ThermalStandard)];\n        start_time = Dates.DateTime(\"2024-01-01T01:00:00\"),\n        len = 47,\n    )\n    compare_long_and_wide_realized_results_2d(\n        read_realized_variables,\n        results_uc,\n        [(ActivePowerVariable, ThermalStandard)];\n        start_time = Dates.DateTime(\"2024-01-02T12:00:00\"),\n        len = 10,\n    )\n\n    realized_param_uc = read_realized_parameters(results_uc)\n    @test length(keys(realized_param_uc)) == 3\n    for param in values(realized_param_uc)\n        @test length(unique(param.DateTime)) == 48\n    end\n    compare_long_and_wide_realized_results_2d(read_realized_parameters, results_uc)\n\n    realized_param_uc = read_realized_parameters(\n        results_uc,\n        [(ActivePowerTimeSeriesParameter, RenewableDispatch)],\n    )\n    compare_long_and_wide_realized_results_2d(\n        read_realized_parameters,\n        results_uc,\n        [(ActivePowerTimeSeriesParameter, RenewableDispatch)],\n    )\n    @test realized_param_uc == read_realized_parameters(\n        results_uc,\n        [\"ActivePowerTimeSeriesParameter__RenewableDispatch\"],\n    )\n    @test length(keys(realized_param_uc)) == 1\n    for param in values(realized_param_uc)\n        @test length(unique(param.DateTime)) == 48\n    end\n\n    realized_duals_ed = read_realized_duals(results_ed)\n    @test length(keys(realized_duals_ed)) == 1\n    for param in values(realized_duals_ed)\n        @test size(param)[1] == 576\n    end\n\n    realized_duals_ed =\n        read_realized_duals(results_ed, [(CopperPlateBalanceConstraint, System)])\n    @test realized_duals_ed ==\n          read_realized_duals(results_ed, [\"CopperPlateBalanceConstraint__System\"])\n    @test length(keys(realized_duals_ed)) == 1\n    for param in values(realized_duals_ed)\n        @test size(param)[1] == 576\n    end\n\n    realized_duals_uc =\n        read_realized_duals(results_uc, [(CopperPlateBalanceConstraint, System)])\n    @test length(keys(realized_duals_uc)) == 1\n    for param in values(realized_duals_uc)\n        @test size(param)[1] == 48\n    end\n\n    realized_expressions = read_realized_expressions(\n        results_uc,\n        [(PSI.ProductionCostExpression, RenewableDispatch)],\n    )\n    @test realized_expressions == read_realized_expressions(\n        results_uc,\n        [\"ProductionCostExpression__RenewableDispatch\"],\n    )\n    @test length(keys(realized_expressions)) == 1\n    for exp in values(realized_expressions)\n        @test length(unique(exp.DateTime)) == 48\n        @test length(unique(exp.name)) == 3\n    end\n\n    #request non sync data\n    @test_logs(\n        (:error, r\"Requested time does not match available results\"),\n        match_mode = :any,\n        @test_throws IS.InvalidValue read_realized_variables(\n            results_ed,\n            [(ActivePowerVariable, ThermalStandard)];\n            start_time = DateTime(\"2024-01-01T02:12:00\"),\n            len = 3,\n        )\n    )\n\n    # request good window\n    @test length(\n        unique(\n            read_realized_variables(\n                results_ed,\n                [(ActivePowerVariable, ThermalStandard)];\n                start_time = DateTime(\"2024-01-02T23:10:00\"),\n                len = 10,\n            )[\"ActivePowerVariable__ThermalStandard\"].DateTime),\n    ) == 10\n\n    # request bad window\n    @test_logs(\n        (:error, r\"Requested time does not match available results\"),\n        (@test_throws IS.InvalidValue read_realized_variables(\n            results_ed,\n            [(ActivePowerVariable, ThermalStandard)];\n            start_time = DateTime(\"2024-01-02T23:10:00\"),\n            len = 11,\n        ))\n    )\n\n    # request bad window\n    @test_logs(\n        (:error, r\"Requested time does not match available results\"),\n        (@test_throws IS.InvalidValue read_realized_variables(\n            results_ed,\n            [(ActivePowerVariable, ThermalStandard)];\n            start_time = DateTime(\"2024-01-02T23:10:00\"),\n            len = 12,\n        ))\n    )\n\n    load_results!(\n        results_ed,\n        3;\n        initial_time = DateTime(\"2024-01-01T00:00:00\"),\n        variables = [(ActivePowerVariable, ThermalStandard)],\n    )\n\n    @test !isempty(\n        PSI.get_cached_variables(results_ed)[PSI.VariableKey(\n            ActivePowerVariable,\n            ThermalStandard,\n        )].data,\n    )\n    @test length(\n        PSI.get_cached_variables(results_ed)[PSI.VariableKey(\n            ActivePowerVariable,\n            ThermalStandard,\n        )].data,\n    ) == 3\n    @test length(results_ed) == 3\n\n    @test_throws(ErrorException, read_parameter(results_ed, \"invalid\"))\n    @test_throws(ErrorException, read_variable(results_ed, \"invalid\"))\n    @test_logs(\n        (:error, r\"not stored\"),\n        @test_throws(\n            IS.InvalidValue,\n            read_variable(\n                results_uc,\n                ActivePowerVariable,\n                ThermalStandard;\n                initial_time = now(),\n            )\n        )\n    )\n    @test_logs(\n        (:error, r\"not stored\"),\n        @test_throws(\n            IS.InvalidValue,\n            read_variable(results_uc, ActivePowerVariable, ThermalStandard; count = 25)\n        )\n    )\n\n    empty!(results_ed)\n    @test !haskey(\n        PSI.get_cached_variables(results_ed),\n        PSI.VariableKey(ActivePowerVariable, ThermalStandard),\n    )\n\n    initial_time = DateTime(\"2024-01-01T00:00:00\")\n    load_results!(\n        results_ed,\n        3;\n        initial_time = initial_time,\n        variables = [(ActivePowerVariable, ThermalStandard)],\n        duals = [(CopperPlateBalanceConstraint, System)],\n        parameters = [(ActivePowerTimeSeriesParameter, RenewableDispatch)],\n    )\n\n    @test !isempty(\n        PSI.get_cached_variables(results_ed)[PSI.VariableKey(\n            ActivePowerVariable,\n            ThermalStandard,\n        )].data,\n    )\n    @test !isempty(\n        PSI.get_cached_duals(results_ed)[PSI.ConstraintKey(\n            CopperPlateBalanceConstraint,\n            System,\n        )].data,\n    )\n    @test !isempty(\n        PSI.get_cached_parameters(results_ed)[PSI.ParameterKey{\n            ActivePowerTimeSeriesParameter,\n            RenewableDispatch,\n        }(\n            \"\",\n        )].data,\n    )\n\n    # Inspired by https://github.com/Sienna-Platform/PowerSimulations.jl/issues/1072\n    @testset \"Test cache behavior\" begin\n        myres = deepcopy(results_ed)\n        initial_time = DateTime(\"2024-01-01T00:00:00\")\n        timestamps = PSI._process_timestamps(myres, initial_time, 3)\n        variable_tuple = (ActivePowerVariable, ThermalStandard)\n        variable_key = PSI.VariableKey(variable_tuple...)\n\n        empty!(myres)\n        @test isempty(PSI.get_cached_variables(myres))\n\n        # With nothing cached, all reads should be from outside the cache\n        read = @test_no_cache PSI._read_results(myres, [variable_key], timestamps, nothing)\n        @test actual_timestamps(read) == timestamps\n\n        # With 2 result windows cached, reading 2 windows should come from cache and reading 3 should come from outside\n        load_results!(myres, 2; initial_time = initial_time, variables = [variable_tuple])\n        @test haskey(PSI.get_cached_variables(myres), variable_key)\n        read = @test_yes_cache PSI._read_results(\n            myres,\n            [variable_key],\n            timestamps[1:2],\n            nothing,\n        )\n        @test actual_timestamps(read) == timestamps[1:2]\n        read = @test_no_cache PSI._read_results(myres, [variable_key], timestamps, nothing)\n        @test actual_timestamps(read) == timestamps\n\n        # With 3 result windows cached, reading 2 and 3 windows should both come from cache\n        load_results!(myres, 3; initial_time = initial_time, variables = [variable_tuple])\n        read = @test_yes_cache PSI._read_results(\n            myres,\n            [variable_key],\n            timestamps[1:2],\n            nothing,\n        )\n        @test actual_timestamps(read) == timestamps[1:2]\n        read = @test_yes_cache PSI._read_results(myres, [variable_key], timestamps, nothing)\n        @test actual_timestamps(read) == timestamps\n\n        # Caching an additional variable should incur an additional read but not evict the old variable\n        @test_no_cache load_results!(\n            myres,\n            3;\n            initial_time = initial_time,\n            variables = [(ActivePowerVariable, RenewableDispatch)],\n        )\n        @test haskey(PSI.get_cached_variables(myres), variable_key)\n        @test haskey(\n            PSI.get_cached_variables(myres),\n            PSI.VariableKey(ActivePowerVariable, RenewableDispatch),\n        )\n\n        # Reset back down to 2 windows\n        empty!(myres)\n        @test_no_cache load_results!(\n            myres,\n            2;\n            initial_time = initial_time,\n            variables = [variable_tuple],\n        )\n\n        # Loading a subset of what has already been loaded should not incur additional reads from outside the cache\n        @test_yes_cache load_results!(\n            myres,\n            2;\n            initial_time = initial_time,\n            variables = [variable_tuple],\n        )\n        @test_yes_cache load_results!(\n            myres,\n            1;\n            initial_time = initial_time,\n            variables = [variable_tuple],\n        )\n        # But loading a superset should\n        @test_no_cache load_results!(\n            myres,\n            3;\n            initial_time = initial_time,\n            variables = [variable_tuple],\n        )\n        empty!(myres)\n\n        # With windows 2-3 cached, reading 2-3 and 3-3 should be from cache, reading 1-2 should be from outside cache\n        @test_no_cache load_results!(\n            myres,\n            2;\n            initial_time = timestamps[2],\n            variables = [variable_tuple],\n        )\n        read = @test_yes_cache PSI._read_results(\n            myres,\n            [variable_key],\n            timestamps[2:3],\n            nothing,\n        )\n        @test actual_timestamps(read) == timestamps[2:3]\n        read = @test_yes_cache PSI._read_results(\n            myres,\n            [variable_key],\n            timestamps[3:3],\n            nothing,\n        )\n        @test actual_timestamps(read) == timestamps[3:3]\n        read = @test_no_cache PSI._read_results(\n            myres,\n            [variable_key],\n            timestamps[1:2],\n            nothing,\n        )\n        @test actual_timestamps(read) == timestamps[1:2]\n\n        empty!(myres)\n        @test isempty(PSI.get_cached_variables(myres))\n    end\n\n    @testset \"Test read_results_with_keys\" begin\n        myres = deepcopy(results_ed)\n        initial_time = DateTime(\"2024-01-01T00:00:00\")\n        timestamps = PSI._process_timestamps(myres, initial_time, 3)\n        result_keys = [PSI.VariableKey(ActivePowerVariable, ThermalStandard)]\n\n        res1 =\n            PSI.read_results_with_keys(myres, result_keys; table_format = TableFormat.WIDE)\n        @test Set(keys(res1)) == Set(result_keys)\n        res1_df = res1[first(result_keys)]\n        @test size(res1_df) == (576, 6)\n        @test names(res1_df) ==\n              [\"DateTime\", \"Solitude\", \"Park City\", \"Alta\", \"Brighton\", \"Sundance\"]\n        @test first(eltype.(eachcol(res1_df))) === DateTime\n\n        res2 =\n            PSI.read_results_with_keys(\n                myres,\n                result_keys;\n                cols = [\"Park City\", \"Brighton\"],\n                table_format = TableFormat.WIDE,\n            )\n        @test Set(keys(res2)) == Set(result_keys)\n        res2_df = res2[first(result_keys)]\n        @test size(res2_df) == (576, 3)\n        @test names(res2_df) ==\n              [\"DateTime\", \"Park City\", \"Brighton\"]\n        @test first(eltype.(eachcol(res2_df))) === DateTime\n        compare_long_and_wide_realized_results_2d(\n            PSI.read_results_with_keys,\n            myres,\n            result_keys;\n            cols = [\"Park City\", \"Brighton\"],\n        )\n\n        res3_df =\n            PSI.read_results_with_keys(\n                myres,\n                result_keys;\n                start_time = timestamps[2],\n                table_format = TableFormat.WIDE,\n            )[first(\n                result_keys,\n            )]\n        @test res3_df[1, \"DateTime\"] == timestamps[2]\n\n        res4_df =\n            PSI.read_results_with_keys(\n                myres,\n                result_keys;\n                len = 2,\n                table_format = TableFormat.WIDE,\n            )[first(result_keys)]\n        @test size(res4_df) == (2, 6)\n    end\nend\n\nfunction test_decision_problem_results(\n    results::SimulationResults,\n    c_sys5_hy_ed,\n    c_sys5_hy_uc,\n    in_memory,\n)\n    @test list_decision_problems(results) == [\"ED\", \"UC\"]\n    results_uc = get_decision_problem_results(results, \"UC\")\n    results_ed = get_decision_problem_results(results, \"ED\")\n\n    test_decision_problem_results_values(results_ed, results_uc, c_sys5_hy_ed, c_sys5_hy_uc)\n    if !in_memory\n        test_simulation_results_from_file(dirname(results.path), c_sys5_hy_ed, c_sys5_hy_uc)\n    end\nend\n\nfunction test_emulation_problem_results(results::SimulationResults, in_memory)\n    results_em = get_emulation_problem_results(results)\n\n    read_realized_aux_variables(results_em)\n\n    duals_keys = collect(keys(read_realized_duals(results_em)))\n    @test length(duals_keys) == 1\n    @test duals_keys[1] == \"CopperPlateBalanceConstraint__System\"\n    duals_inputs =\n        ([\"CopperPlateBalanceConstraint__System\"], [(CopperPlateBalanceConstraint, System)])\n    for input in duals_inputs\n        duals_value = first(values(read_realized_duals(results_em, input)))\n        @test duals_value isa DataFrames.DataFrame\n        @test length(unique(duals_value.DateTime)) == 576\n    end\n\n    expressions_keys = collect(keys(read_realized_expressions(results_em)))\n    @test length(expressions_keys) == 12\n    expressions_inputs = (\n        [\n            # \"ProductionCostExpression__HydroEnergyReservoir\",\n            \"ProductionCostExpression__ThermalStandard\",\n        ],\n        [\n            # (ProductionCostExpression, HydroEnergyReservoir),\n            (ProductionCostExpression, ThermalStandard),\n        ],\n    )\n    for input in expressions_inputs\n        expressions_value = first(values(read_realized_expressions(results_em, input)))\n        @test expressions_value isa DataFrames.DataFrame\n        @test length(unique(expressions_value.DateTime)) == 576\n    end\n\n    @test length(\n        unique(\n            read_realized_expression(\n                results_em,\n                \"ProductionCostExpression__ThermalStandard\",\n            ).DateTime,\n        ),\n    ) == 576\n    @test length(\n        unique(\n            read_realized_expression(\n                results_em,\n                ProductionCostExpression,\n                ThermalStandard;\n                len = 10,\n            ).DateTime),\n    ) == 10\n\n    parameters_keys = collect(keys(read_realized_parameters(results_em)))\n    @test length(parameters_keys) == 5\n    parameters_inputs = (\n        [\n            \"ActivePowerTimeSeriesParameter__PowerLoad\",\n            \"ActivePowerTimeSeriesParameter__RenewableDispatch\",\n        ],\n        [\n            (ActivePowerTimeSeriesParameter, PowerLoad),\n            (ActivePowerTimeSeriesParameter, RenewableDispatch),\n        ],\n    )\n    for input in parameters_inputs\n        parameters_value = first(values(read_realized_parameters(results_em, input)))\n        @test parameters_value isa DataFrames.DataFrame\n        @test length(unique(parameters_value.DateTime)) == 576\n    end\n\n    @test length(\n        unique(\n            read_realized_parameter(\n                results_em,\n                \"ActivePowerTimeSeriesParameter__RenewableDispatch\";\n                len = 10,\n            ).DateTime),\n    ) == 10\n\n    expected_vars = union(Set(ED_EXPECTED_VARS), UC_EXPECTED_VARS)\n    @test isempty(setdiff(list_variable_names(results_em), expected_vars))\n    all_vars = Set(keys(read_realized_variables(results_em)))\n    @test isempty(setdiff(all_vars, expected_vars))\n\n    variables_inputs = (\n        [\"ActivePowerVariable__ThermalStandard\", \"ActivePowerVariable__RenewableDispatch\"],\n        [(ActivePowerVariable, ThermalStandard), (ActivePowerVariable, RenewableDispatch)],\n    )\n    for input in variables_inputs\n        vars = read_realized_variables(results_em, input; table_format = TableFormat.WIDE)\n        var_keys = collect(keys(vars))\n        @test length(var_keys) == 2\n        @test first(var_keys) == \"ActivePowerVariable__ThermalStandard\"\n        @test last(var_keys) == \"ActivePowerVariable__RenewableDispatch\"\n        for val in values(vars)\n            @test val isa DataFrames.DataFrame\n            @test DataFrames.nrow(val) == 576\n        end\n    end\n\n    start_time = first(results_em.timestamps) + Dates.Hour(1)\n    len = 12\n    @test DataFrames.nrow(\n        read_realized_variable(\n            results_em,\n            ActivePowerVariable,\n            ThermalStandard;\n            start_time = start_time,\n            len = len,\n            table_format = TableFormat.WIDE,\n        ),\n    ) == len\n\n    vars = read_realized_variables(\n        results_em,\n        variables_inputs[1];\n        start_time = start_time,\n        len = len,\n        table_format = TableFormat.WIDE,\n    )\n    df = first(values(vars))\n    @test length(unique(df.DateTime)) == len\n    @test df[!, \"DateTime\"][1] == start_time\n\n    @test_throws IS.InvalidValue read_realized_variables(\n        results_em,\n        variables_inputs[1],\n        start_time = start_time,\n        len = 100000,\n        table_format = TableFormat.WIDE,\n    )\n    @test_throws IS.InvalidValue read_realized_variables(\n        results_em,\n        variables_inputs[1],\n        start_time = start_time + Dates.Second(1),\n        table_format = TableFormat.WIDE,\n    )\n    @test_throws IS.InvalidValue read_realized_variables(\n        results_em,\n        variables_inputs[1],\n        start_time = start_time - Dates.Hour(1000),\n        table_format = TableFormat.WIDE,\n    )\n    @test_throws IS.InvalidValue read_realized_variables(\n        results_em,\n        variables_inputs[1],\n        len = 100000,\n        table_format = TableFormat.WIDE,\n    )\n\n    @test isempty(results_em)\n    load_results!(\n        results_em;\n        duals = duals_inputs[2],\n        expressions = expressions_inputs[2],\n        parameters = parameters_inputs[2],\n        variables = variables_inputs[2],\n    )\n    @test !isempty(results_em)\n    @test length(results_em) ==\n          length(duals_inputs[2]) +\n          length(expressions_inputs[2]) +\n          length(parameters_inputs[2]) +\n          length(variables_inputs[2])\n\n    # Test that table_format is applied when reading from cache (regression test for GH issue)\n    vars_wide_from_cache = read_realized_variables(\n        results_em,\n        variables_inputs[2];\n        table_format = TableFormat.WIDE,\n    )\n    for val in values(vars_wide_from_cache)\n        @test val isa DataFrames.DataFrame\n        @test DataFrames.nrow(val) == 576\n        @test :DateTime in propertynames(val)\n        @test :name ∉ propertynames(val)\n        @test :value ∉ propertynames(val)\n        @test DataFrames.ncol(val) > 1  # DateTime + at least one component column\n    end\n\n    empty!(results_em)\n    @test isempty(results_em)\n\n    export_path = mktempdir(; cleanup = true)\n    export_realized_results(results_em, export_path)\n\n    var_name = \"ActivePowerVariable__ThermalStandard\"\n    df = read_realized_variable(results_em, var_name)\n    export_active_power_file = joinpath(export_path, \"$(var_name).csv\")\n    export_df = PSI.read_dataframe(export_active_power_file)\n    # TODO: results A bug in the code produces NaN after index 48.\n    @test isapprox(df[48, :], export_df[48, :])\nend\n\nfunction test_simulation_results_from_file(path::AbstractString, c_sys5_hy_ed, c_sys5_hy_uc)\n    results = SimulationResults(path, \"no_cache\")\n    @test list_decision_problems(results) == [\"ED\", \"UC\"]\n    results_uc = get_decision_problem_results(results, \"UC\")\n    results_ed = get_decision_problem_results(results, \"ED\")\n\n    @test !isnothing(get_system(results_uc))\n    @test length(read_realized_variables(results_uc)) == length(UC_EXPECTED_VARS)\n\n    @test_throws IS.InvalidValue set_system!(results_uc, c_sys5_hy_ed)\n    set_system!(results_ed, c_sys5_hy_ed)\n    set_system!(results_uc, c_sys5_hy_uc)\n\n    test_decision_problem_results_values(results_ed, results_uc, c_sys5_hy_ed, c_sys5_hy_uc)\nend\n\nfunction test_decision_problem_results_kwargs_handling(\n    path::AbstractString,\n    c_sys5_hy_ed,\n    c_sys5_hy_uc,\n)\n    results = SimulationResults(path, \"no_cache\")\n    @test list_decision_problems(results) == [\"ED\", \"UC\"]\n    results_uc = get_decision_problem_results(results, \"UC\")\n    results_ed = get_decision_problem_results(results, \"ED\")\n\n    @test !isnothing(get_system(results_uc))\n    @test !isnothing(get_system(results_ed))\n\n    results_ed = get_decision_problem_results(results, \"ED\"; populate_system = true)\n    @test !isnothing(get_system(results_ed))\n    @test PSY.get_units_base(get_system(results_ed)) == \"NATURAL_UNITS\"\n\n    @test_throws IS.InvalidValue set_system!(results_uc, c_sys5_hy_ed)\n\n    set_system!(results_ed, c_sys5_hy_ed)\n    set_system!(results_uc, c_sys5_hy_uc)\n\n    results_ed = get_decision_problem_results(\n        results,\n        \"ED\";\n        populate_system = true,\n        populate_units = IS.UnitSystem.DEVICE_BASE,\n    )\n    @test !isnothing(PSI.get_system(results_ed))\n    @test PSY.get_units_base(get_system(results_ed)) == \"DEVICE_BASE\"\n\n    @test_throws ArgumentError get_decision_problem_results(\n        results,\n        \"ED\";\n        populate_system = false,\n        populate_units = IS.UnitSystem.DEVICE_BASE,\n    )\n\n    test_decision_problem_results_values(results_ed, results_uc, c_sys5_hy_ed, c_sys5_hy_uc)\nend\n\nfunction compare_long_and_wide_realized_results_2d(func, args...; kwargs...)\n    long_results = func(args...; table_format = TableFormat.LONG, kwargs...)\n    wide_results = func(args...; table_format = TableFormat.WIDE, kwargs...)\n    @test sort!(collect(keys(long_results))) == sort!(collect(keys(wide_results)))\n    for (key, long_value) in long_results\n        wide_value = wide_results[key]\n        measure_vars = [x for x in names(wide_value) if x != \"DateTime\"]\n        wide_converted = DataFrames.stack(\n            wide_value,\n            measure_vars;\n            variable_name = :name,\n            value_name = :value,\n        )\n        @test @orderby(wide_converted, :DateTime, :name) ==\n              @orderby(long_value, :DateTime, :name)\n    end\nend\n\n@testset \"Test simulation results\" begin\n    for in_memory in (false, true)\n        file_path = mktempdir(; cleanup = true)\n        export_path = mktempdir(; cleanup = true)\n        test_simulation_results(file_path, export_path; in_memory = in_memory)\n    end\nend\n\n@testset \"Test simulation results with system from store\" begin\n    file_path = mktempdir(; cleanup = true)\n    export_path = mktempdir(; cleanup = true)\n    c_sys5_hy_uc = PSB.build_system(PSITestSystems, \"c_sys5_hy_uc\")\n    c_sys5_hy_ed = PSB.build_system(PSITestSystems, \"c_sys5_hy_ed\")\n    in_memory = false\n    sim = run_simulation(\n        c_sys5_hy_uc,\n        c_sys5_hy_ed,\n        file_path,\n        export_path;\n        in_memory = in_memory,\n    )\n    results = SimulationResults(PSI.get_simulation_folder(sim))\n    uc = get_decision_problem_results(results, \"UC\")\n    ed = get_decision_problem_results(results, \"ED\")\n    sys_uc = get_system!(uc)\n    sys_ed = get_system!(ed)\n    test_decision_problem_results(results, sys_ed, sys_uc, in_memory)\n    test_emulation_problem_results(results, in_memory)\nend\n\nfunction read_result_names(results, key::PSI.OptimizationContainerKey)\n    result_data = PSI.read_results_with_keys(\n        results,\n        [key];\n        table_format = TableFormat.LONG,\n    )\n    first_result = only(values(result_data))\n    columns_without_datetime = first_result[!, Not(:DateTime)]\n    return Set(names(columns_without_datetime))\nend\n\n@testset \"Test system is automatically populated from HDF5 store on file deserialization\" begin\n    file_path = mktempdir(; cleanup = true)\n    export_path = mktempdir(; cleanup = true)\n    c_sys5_hy_uc = PSB.build_system(PSITestSystems, \"c_sys5_hy_uc\")\n    c_sys5_hy_ed = PSB.build_system(PSITestSystems, \"c_sys5_hy_ed\")\n    sim = run_simulation(\n        c_sys5_hy_uc,\n        c_sys5_hy_ed,\n        file_path,\n        export_path;\n        in_memory = false,\n    )\n\n    results = SimulationResults(PSI.get_simulation_folder(sim))\n    results_uc = get_decision_problem_results(results, \"UC\")\n    results_ed = get_decision_problem_results(results, \"ED\")\n    results_em = get_emulation_problem_results(results)\n\n    sys_uc = get_system(results_uc)\n    sys_ed = get_system(results_ed)\n    sys_em = get_system(results_em)\n    @test !isnothing(sys_uc)\n    @test !isnothing(sys_ed)\n    @test !isnothing(sys_em)\n\n    @test IS.get_uuid(sys_uc) == IS.get_uuid(c_sys5_hy_uc)\n    @test IS.get_uuid(sys_ed) == IS.get_uuid(c_sys5_hy_ed)\n\n    # Note: the system is serialized as a JSON string into the HDF5 store and does not include time series data\n    ts_counts = PSY.get_time_series_counts(sys_uc)\n    @test ts_counts.forecast_count == 0\nend\n\n@testset \"Test system is automatically populated from HDF5 store on file deserialization\" begin\n    file_path = mktempdir(; cleanup = true)\n    export_path = mktempdir(; cleanup = true)\n    c_sys5_hy_uc = PSB.build_system(PSITestSystems, \"c_sys5_hy_uc\")\n    c_sys5_hy_ed = PSB.build_system(PSITestSystems, \"c_sys5_hy_ed\")\n    sim = run_simulation(\n        c_sys5_hy_uc,\n        c_sys5_hy_ed,\n        file_path,\n        export_path;\n        in_memory = false,\n        store_systems_in_results = false,\n    )\n\n    results = SimulationResults(PSI.get_simulation_folder(sim))\n    @test results isa SimulationResults\nend\n"
  },
  {
    "path": "test/test_simulation_results_export.jl",
    "content": "\nimport PowerSimulations:\n    SimulationStoreParams,\n    ModelStoreParams,\n    get_problem_exports,\n    should_export_dual,\n    should_export_parameter,\n    should_export_variable,\n    ISOPT.OptimizationContainerMetadata\n\nfunction _make_params()\n    sim = Dict(\n        \"initial_time\" => Dates.DateTime(\"2020-01-01T00:00:00\"),\n        \"step_resolution\" => Dates.Hour(24),\n        \"num_steps\" => 2,\n    )\n    problem_defs = OrderedDict(\n        :ED => Dict(\n            \"execution_count\" => 24,\n            \"horizon\" => Dates.Hour(12),\n            \"interval\" => Dates.Hour(1),\n            \"resolution\" => Dates.Hour(1),\n            \"base_power\" => 100.0,\n            \"system_uuid\" => Base.UUID(\"4076af6c-e467-56ae-b986-b466b2749572\"),\n        ),\n        :UC => Dict(\n            \"execution_count\" => 1,\n            \"horizon\" => Dates.Hour(24),\n            \"interval\" => Dates.Hour(1),\n            \"resolution\" => Dates.Hour(24),\n            \"base_power\" => 100.0,\n            \"system_uuid\" => Base.UUID(\"4076af6c-e467-56ae-b986-b466b2749572\"),\n        ),\n    )\n    container_metadata = ISOPT.OptimizationContainerMetadata(\n        Dict(\n            \"ActivePowerVariable__ThermalStandard\" =>\n                PSI.VariableKey(ActivePowerVariable, ThermalStandard),\n            #\"EnergyVariable__HydroEnergyReservoir\" =>\n            #    PSI.VariableKey(EnergyVariable, HydroEnergyReservoir),\n            \"OnVariable__ThermalStandard\" =>\n                PSI.VariableKey(OnVariable, ThermalStandard),\n        ),\n    )\n    problems = OrderedDict{Symbol, ModelStoreParams}()\n    for problem in keys(problem_defs)\n        problem_params = ModelStoreParams(\n            problem_defs[problem][\"execution_count\"],\n            IS.time_period_conversion(problem_defs[problem][\"horizon\"]),\n            IS.time_period_conversion(problem_defs[problem][\"interval\"]),\n            IS.time_period_conversion(problem_defs[problem][\"resolution\"]),\n            problem_defs[problem][\"base_power\"],\n            problem_defs[problem][\"system_uuid\"],\n            container_metadata,\n        )\n\n        problems[problem] = problem_params\n    end\n\n    return SimulationStoreParams(\n        sim[\"initial_time\"],\n        sim[\"step_resolution\"],\n        sim[\"num_steps\"],\n        problems,\n        # Emulation Problem Params. Export not implemented yet\n        OrderedDict{Symbol, ModelStoreParams}(),\n    )\nend\n\n@testset \"Test export from JSON\" begin\n    params = _make_params()\n    exports = SimulationResultsExport(joinpath(DATA_DIR, \"results_export.json\"), params)\n\n    valid = Dates.DateTime(\"2020-01-01T06:00:00\")\n    valid2 = Dates.DateTime(\"2020-01-02T23:00:00\")\n    invalid = Dates.DateTime(\"2020-01-01T02:00:00\")\n    invalid2 = Dates.DateTime(\"2020-01-03T00:00:00\")\n\n    @test should_export_variable(\n        exports,\n        valid,\n        :ED,\n        PSI.VariableKey(ActivePowerVariable, ThermalStandard),\n    )\n    @test should_export_variable(\n        exports,\n        valid2,\n        :ED,\n        PSI.VariableKey(ActivePowerVariable, ThermalStandard),\n    )\n    @test !should_export_variable(\n        exports,\n        invalid,\n        :ED,\n        PSI.VariableKey(ActivePowerVariable, ThermalStandard),\n    )\n    @test !should_export_variable(\n        exports,\n        invalid2,\n        :ED,\n        PSI.VariableKey(ActivePowerVariable, ThermalStandard),\n    )\n    @test !should_export_variable(\n        exports,\n        valid,\n        :ED,\n        PSI.VariableKey(ActivePowerVariable, RenewableNonDispatch),\n    )\n    @test should_export_parameter(\n        exports,\n        valid,\n        :ED,\n        PSI.ParameterKey(ActivePowerTimeSeriesParameter, ThermalStandard),\n    )\n    @test !should_export_dual(\n        exports,\n        valid,\n        :ED,\n        PSI.ConstraintKey(ActivePowerVariableLimitsConstraint, RenewableNonDispatch),\n    )\n\n    @test should_export_variable(\n        exports,\n        valid,\n        :UC,\n        PSI.VariableKey(OnVariable, ThermalStandard),\n    )\n    @test !should_export_variable(\n        exports,\n        valid,\n        :UC,\n        PSI.VariableKey(ActivePowerVariable, RenewableNonDispatch),\n    )\n    @test should_export_parameter(\n        exports,\n        valid,\n        :UC,\n        PSI.ParameterKey(ActivePowerTimeSeriesParameter, ThermalStandard),\n    )\n    @test should_export_dual(\n        exports,\n        valid,\n        :UC,\n        PSI.ConstraintKey(ActivePowerVariableLimitsConstraint, RenewableNonDispatch),\n    )\n\n    @test exports.path == \"export_path\"\n    @test exports.format == \"csv\"\n    @test \"csv\" in list_supported_formats(SimulationResultsExport)\nend\n\n@testset \"Invalid exports\" begin\n    params = _make_params()\n    valid = Dates.DateTime(\"2020-01-01T00:00:00\")\n    invalid = Dates.DateTime(\"2020-01-03T00:00:00\")\n\n    # Invalid start_time\n    @test_throws IS.InvalidValue SimulationResultsExport(\n        Dict(\"start_time\" => invalid, \"models\" => [Dict(\"name\" => \"ED\")]),\n        params,\n    )\n\n    # Invalid end_time\n    @test_throws IS.InvalidValue SimulationResultsExport(\n        Dict(\"end_time\" => invalid, \"models\" => [Dict(\"name\" => \"ED\")]),\n        params,\n    )\n\n    # Invalid format\n    @test_throws IS.InvalidValue SimulationResultsExport(\n        Dict(\"format\" => \"invalid\", \"models\" => [Dict(\"name\" => \"ED\")]),\n        params,\n    )\n\n    # Missing name\n    @test_throws IS.InvalidValue SimulationResultsExport(\n        Dict(\"models\" => [Dict(\"variables\" => [\"ActivePowerVariable__ThermalStandard\"])]),\n        params,\n    )\nend\n"
  },
  {
    "path": "test/test_simulation_sequence.jl",
    "content": "@testset \"Simulation Sequence Correct Execution Order\" begin\n    models_array = [\n        DecisionModel(\n            MockOperationProblem;\n            horizon = Hour(48),\n            interval = Hour(24),\n            resolution = Hour(1),\n            steps = 2,\n            name = \"DAUC\",\n        ),\n        DecisionModel(\n            MockOperationProblem;\n            horizon = Hour(24),\n            resolution = Minute(5),\n            interval = Hour(1),\n            steps = 2 * 24,\n            name = \"HAUC\",\n        ),\n        DecisionModel(\n            MockOperationProblem;\n            horizon = Hour(12),\n            resolution = Minute(5),\n            interval = Minute(5),\n            steps = 2 * 24 * 12,\n            name = \"ED\",\n        ),\n    ]\n    set_device_model!(\n        PSI.get_template(models_array[3]),\n        ThermalStandard,\n        ThermalBasicDispatch,\n    )\n    models = SimulationModels(\n        models_array,\n        EmulationModel(\n            MockEmulationProblem;\n            interval = Minute(1),\n            resolution = Minute(1),\n            name = \"AGC\",\n        ),\n    )\n\n    test_sequence = SimulationSequence(;\n        models = models,\n        feedforwards = Dict(\n            \"ED\" => [\n                SemiContinuousFeedforward(;\n                    component_type = ThermalStandard,\n                    source = OnVariable,\n                    affected_values = [ActivePowerVariable],\n                ),\n            ],\n        ),\n        ini_cond_chronology = InterProblemChronology(),\n    )\n\n    @test !isempty(\n        PSI.get_model(PSI.get_template(models_array[3]), ThermalStandard).feedforwards,\n    )\n\n    @test length(findall(x -> x == 4, test_sequence.execution_order)) == 24 * 60\n    @test length(findall(x -> x == 3, test_sequence.execution_order)) == 24 * 12\n    @test length(findall(x -> x == 2, test_sequence.execution_order)) == 24\n    @test length(findall(x -> x == 1, test_sequence.execution_order)) == 1\n\n    for model in PSI.get_decision_models(models)\n        @test PSI.get_sequence_uuid(model) == test_sequence.uuid\n    end\n\n    # Test single stage sequence\n    test_sequence = SimulationSequence(;\n        models = SimulationModels(\n        # TODO: support passing one model without making a vector\n            [DecisionModel(MockOperationProblem; horizon = Hour(48), name = \"DAUC\")]),\n        ini_cond_chronology = InterProblemChronology(),\n    )\n\n    # Disabled temporarily\n    # @test isa(test_sequence.ini_cond_chronology, IntraProblemChronology)\n    @test test_sequence.execution_order == [1]\nend\n\n@testset \"Simulation Sequence invalid sequences\" begin\n    models = SimulationModels([\n        DecisionModel(\n            MockOperationProblem;\n            horizon = Hour(48),\n            interval = Hour(24),\n            resolution = Hour(1),\n            steps = 2,\n            name = \"DAUC\",\n        ),\n        DecisionModel(\n            MockOperationProblem;\n            horizon = Hour(24),\n            interval = Hour(5),\n            resolution = Minute(5),\n            steps = 2 * 24,\n            name = \"HAUC\",\n        ),\n    ])\n\n    @test_throws IS.ConflictingInputsError SimulationSequence(models = models)\n\n    models = SimulationModels([\n        DecisionModel(\n            MockOperationProblem;\n            horizon = Hour(2),\n            interval = Hour(1),\n            resolution = Hour(1),\n            steps = 2,\n            name = \"DAUC\",\n        ),\n        DecisionModel(\n            MockOperationProblem;\n            horizon = Hour(24),\n            interval = Hour(1),\n            resolution = Minute(5),\n            steps = 2 * 24,\n            name = \"HAUC\",\n        ),\n    ])\n\n    @test_throws IS.ConflictingInputsError SimulationSequence(models = models)\n\n    models = SimulationModels([\n        DecisionModel(\n            MockOperationProblem;\n            horizon = Hour(24),\n            interval = Hour(1),\n            resolution = Hour(1),\n            steps = 2,\n            name = \"DAUC\",\n        ),\n        DecisionModel(\n            MockOperationProblem;\n            horizon = Hour(24),\n            interval = Minute(22),\n            resolution = Hour(1),\n            steps = 2 * 24,\n            name = \"HAUC\",\n        ),\n    ])\n\n    @test_throws IS.ConflictingInputsError SimulationSequence(models = models)\nend\n"
  },
  {
    "path": "test/test_simulation_store.jl",
    "content": "import PowerSimulations:\n    open_store,\n    HdfSimulationStore,\n    HDF_FILENAME,\n    SimulationStoreParams,\n    ModelStoreParams,\n    SimulationModelStoreRequirements,\n    CacheFlushRules,\n    KiB,\n    MiB,\n    GiB,\n    STORE_CONTAINER_VARIABLES,\n    initialize_problem_storage!,\n    add_rule!,\n    write_result!,\n    read_result,\n    has_dirty,\n    get_cache_hit_percentage\n\nfunction _initialize!(store, sim, variables, model_defs, cache_rules)\n    models = OrderedDict{Symbol, ModelStoreParams}()\n    model_reqs = Dict{Symbol, SimulationModelStoreRequirements}()\n    num_param_containers = 0\n    for model in keys(model_defs)\n        execution_count = model_defs[model][\"execution_count\"]\n        horizon = model_defs[model][\"horizon\"]\n        num_rows = execution_count * sim[\"num_steps\"]\n        resolution = model_defs[model][\"resolution\"]\n        interval = model_defs[model][\"interval\"]\n\n        model_params = ModelStoreParams(\n            execution_count,\n            IS.time_period_conversion(horizon),\n            IS.time_period_conversion(interval),\n            IS.time_period_conversion(resolution),\n            model_defs[model][\"base_power\"],\n            model_defs[model][\"system_uuid\"],\n        )\n        reqs = SimulationModelStoreRequirements()\n        horizon_count = horizon ÷ resolution\n        for (key, array) in model_defs[model][\"variables\"]\n            reqs.variables[key] = Dict(\n                \"columns\" => model_defs[model][\"names\"],\n                \"dims\" =>\n                    (horizon_count, length(model_defs[model][\"names\"][1]), num_rows),\n            )\n            keep_in_cache = variables[key][\"keep_in_cache\"]\n            add_rule!(cache_rules, model, key, keep_in_cache)\n        end\n\n        models[model] = model_params\n        model_reqs[model] = reqs\n        num_param_containers += length(reqs.variables)\n    end\n\n    params = SimulationStoreParams(\n        sim[\"initial_time\"],\n        sim[\"step_resolution\"],\n        sim[\"num_steps\"],\n        models,\n        # Emulation Model Store requirements. No tests yet\n        OrderedDict(\n            :Emulator => ModelStoreParams(\n                100, # Num Executions\n                IS.time_period_conversion(Hour(1)),\n                IS.time_period_conversion(Minute(5)), # Interval\n                IS.time_period_conversion(Minute(5)), # Resolution\n                100.0,\n                Base.UUID(\"4076af6c-e467-56ae-b986-b466b2749572\"),\n            ),\n        ),\n    )\n    em_reqs = SimulationModelStoreRequirements()\n    initialize_problem_storage!(store, params, model_reqs, em_reqs, cache_rules)\n    return\nend\n\nfunction _run_sim_test(path, sim, variables, model_defs, cache_rules, seed)\n    rng = MersenneTwister(seed)\n    open_store(HdfSimulationStore, path, \"w\") do store\n        sim_time = sim[\"initial_time\"]\n        _initialize!(store, sim, variables, model_defs, cache_rules)\n        for _ in 1:sim[\"num_steps\"]\n            for model in keys(model_defs)\n                model_time = sim_time\n                for i in 1:model_defs[model][\"execution_count\"]\n                    for key in keys(variables)\n                        data = rand(rng, size(model_defs[model][\"variables\"][key])...)\n                        columns = model_defs[model][\"names\"]\n                        write_result!(\n                            store,\n                            model,\n                            key,\n                            model_time,\n                            model_time,\n                            Containers.DenseAxisArray(\n                                permutedims(data),\n                                columns,\n                                1:size(data)[1],\n                            ),\n                        )\n                        _verify_data(data, store, model, key, model_time, columns)\n                    end\n\n                    model_time += model_defs[model][\"resolution\"]\n                end\n            end\n\n            sim_time += sim[\"step_resolution\"]\n        end\n\n        for output_cache in values(store.cache.data)\n            if PSI.should_keep_in_cache(output_cache)\n                @test get_cache_hit_percentage(output_cache) == 100.0\n            else\n                @test get_cache_hit_percentage(output_cache) < 100.0\n            end\n        end\n\n        flush(store)\n        @test !has_dirty(store.cache)\n    end\nend\n\nfunction _verify_read_results(path, sim, variables, model_defs, seed)\n    rng = MersenneTwister(seed)\n    open_store(HdfSimulationStore, path, \"r\") do store\n        sim_time = sim[\"initial_time\"]\n        for _ in 1:sim[\"num_steps\"]\n            for model in keys(model_defs)\n                model_time = sim_time\n                for i in 1:model_defs[model][\"execution_count\"]\n                    for key in keys(variables)\n                        data = rand(rng, size(model_defs[model][\"variables\"][key])...)\n                        columns = model_defs[model][\"names\"]\n                        _verify_data(data, store, model, key, model_time, columns)\n                    end\n\n                    model_time += model_defs[model][\"resolution\"]\n                end\n            end\n\n            sim_time += sim[\"step_resolution\"]\n        end\n\n        for output_cache in values(store.cache.data)\n            @test get_cache_hit_percentage(output_cache) == 0.0\n        end\n    end\nend\n\nfunction _verify_data(expected, store, model, name, time, columns::Tuple{Vector{Symbol}})\n    expected_df = DataFrames.DataFrame(expected, columns[1])\n    df = read_result(DataFrames.DataFrame, store, model, name, time)\n    @test expected_df == df\nend\n\n@testset \"Test SimulationStore 2-d arrays\" begin\n    sim = Dict(\n        \"initial_time\" => Dates.DateTime(\"2020-01-01T00:00:00\"),\n        \"step_resolution\" => Dates.Hour(24),\n        \"num_steps\" => 50,\n    )\n    variables = Dict(\n        PSI.VariableKey(ActivePowerVariable, ThermalStandard) =>\n            Dict(\"keep_in_cache\" => true),\n        PSI.VariableKey(ActivePowerVariable, RenewableDispatch) =>\n            Dict(\"keep_in_cache\" => true),\n        PSI.VariableKey(ActivePowerVariable, InterruptiblePowerLoad) =>\n            Dict(\"keep_in_cache\" => false),\n        PSI.VariableKey(ActivePowerVariable, RenewableNonDispatch) =>\n            Dict(\"keep_in_cache\" => false),\n    )\n    model_defs = OrderedDict(\n        :ED => Dict(\n            \"execution_count\" => 24,\n            \"horizon\" => Hour(12),\n            \"names\" => ([:dev1, :dev2, :dev3, :dev4, :dev5],),\n            \"variables\" => Dict(x => ones(12, 5) for x in keys(variables)),\n            \"interval\" => Dates.Hour(1),\n            \"resolution\" => Dates.Hour(1),\n            \"base_power\" => 100.0,\n            \"system_uuid\" => Base.UUID(\"4076af6c-e467-56ae-b986-b466b2749572\"),\n        ),\n        :UC => Dict(\n            \"execution_count\" => 1,\n            \"horizon\" => Hour(24),\n            \"names\" => ([:dev1, :dev2, :dev3],),\n            \"variables\" => Dict(x => ones(24, 3) for x in keys(variables)),\n            \"interval\" => Dates.Hour(1),\n            \"resolution\" => Dates.Hour(1),\n            \"base_power\" => 100.0,\n            \"system_uuid\" => Base.UUID(\"4076af6c-e467-56ae-b986-b466b2749572\"),\n        ),\n    )\n    cache_rules = CacheFlushRules(; max_size = 1 * MiB, min_flush_size = 4 * KiB)\n\n    path = mktempdir()\n    # Use this seed to produce the same randomly generated arrays for write and verify.\n    seed = 1234\n    _run_sim_test(path, sim, variables, model_defs, cache_rules, seed)\n    _verify_read_results(path, sim, variables, model_defs, seed)\nend\n\n@testset \"Test OptimizationOutputCache\" begin\n    key = PSI.OptimizationResultCacheKey(\n        :ED,\n        PSI.VariableKey(ActivePowerVariable, InterruptiblePowerLoad),\n    )\n    cache = PSI.OptimizationOutputCache(key, PSI.CacheFlushRule(true))\n    @test !PSI.has_clean(cache)\n    @test !PSI.is_dirty(cache, Dates.now())\n\n    timestamp1 = Dates.DateTime(\"2020-01-01T00:00:00\")\n    timestamp2 = Dates.DateTime(\"2020-01-01T01:00:00\")\n    timestamp3 = Dates.DateTime(\"2020-01-01T02:00:00\")\n    timestamp4 = Dates.DateTime(\"2020-01-01T03:00:00\")\n    PSI.add_result!(cache, timestamp1, ones(2), false)\n    @test PSI.is_dirty(cache, timestamp1)\n    PSI.add_result!(cache, timestamp2, ones(2), false)\n    @test PSI.is_dirty(cache, timestamp2)\n\n    @test_throws IS.InvalidValue PSI.add_result!(cache, timestamp2, ones(2), false)\n\n    @test length(cache.data) == 2\n    @test length(cache.dirty_timestamps) == 2\n\n    popfirst!(cache.dirty_timestamps)\n    @test !PSI.is_dirty(cache, timestamp1)\n    @test PSI.has_clean(cache)\n    @test length(cache.data) == 2\n    @test length(cache.dirty_timestamps) == 1\n\n    PSI.add_result!(cache, timestamp3, ones(2), false)\n    @test length(cache.data) == 3\n    @test length(cache.dirty_timestamps) == 2\n\n    PSI.add_result!(cache, timestamp4, ones(2), true)\n    @test length(cache.data) == 3\n    @test length(cache.dirty_timestamps) == 3\n\n    popfirst!(cache.dirty_timestamps)\n    @test PSI.has_clean(cache)\n\n    empty!(cache.dirty_timestamps)\n    empty!(cache)\n    @test isempty(cache.data)\n    @test isempty(cache.dirty_timestamps)\n\n    PSI.add_result!(cache, timestamp1, ones(2), false)\n    PSI.add_result!(cache, timestamp2, ones(2), false)\n    PSI.discard_results!(cache, [timestamp1, timestamp2])\n    @test isempty(cache.data)\nend\n\n# TODO: test optimizer stats\n# TODO: unit tests of individual functions, size checks\n# TODO: profiling of memory performance and GC\n"
  },
  {
    "path": "test/test_utils/add_components_to_system.jl",
    "content": "function get_copied_line(\n    line::PSY.Line,\n)\n    copied_line = Line(;\n        name = PSY.get_name(line) * \"_copy\",\n        available = PSY.get_available(line),\n        active_power_flow = PSY.get_active_power_flow(line),\n        reactive_power_flow = PSY.get_reactive_power_flow(line),\n        arc = PSY.get_arc(line),\n        r = PSY.get_r(line),\n        x = PSY.get_x(line),\n        b = PSY.get_b(line),\n        rating = PSY.get_rating(line),\n        angle_limits = PSY.get_angle_limits(line),\n        rating_b = PSY.get_rating_b(line),\n        rating_c = PSY.get_rating_c(line),\n        g = PSY.get_g(line),\n        services = PSY.get_services(line),\n        ext = PSY.get_ext(line),\n    )\n    return copied_line\nend\n\nfunction get_copied_bus(\n    bus::PSY.ACBus,\n)\n    copied_bus = ACBus(;\n        number = PSY.get_number(bus) + 1000, #Add 1000 to avoid name conflicts\n        name = PSY.get_name(bus) * \"_copy\",\n        available = PSY.get_available(bus),\n        bustype = ACBusTypes.PQ,\n        angle = PSY.get_angle(bus),\n        magnitude = PSY.get_magnitude(bus),\n        voltage_limits = PSY.get_voltage_limits(bus),\n        base_voltage = PSY.get_base_voltage(bus),\n        area = PSY.get_area(bus),\n        load_zone = PSY.get_load_zone(bus),\n    )\n    return copied_bus\nend\n\nfunction add_equivalent_ac_transmission_with_series_parallel_circuits!(\n    sys::System,\n    ac_transmission::PSY.Line,\n    ::Type{T},\n) where {T <: PSY.Line}\n\n    #Create intermediate Bus\n    old_arc = PSY.get_arc(ac_transmission)\n    original_bus_to = PSY.get_to(old_arc)\n    original_bus_from = PSY.get_from(old_arc)\n    intermediate_bus = get_copied_bus(original_bus_to)\n    add_component!(sys, intermediate_bus)\n\n    #Remove old arc\n    remove_component!(sys, old_arc)\n\n    #add new Arcs\n    arc1 = Arc(; from = original_bus_from, to = intermediate_bus)\n    arc2 = Arc(; from = intermediate_bus, to = original_bus_to)\n    add_component!(sys, arc1)\n    add_component!(sys, arc2)\n    #Update Arc original Line\n    set_arc!(ac_transmission, arc1)\n\n    #make Parallel circuits\n    original_rating = PSY.get_rating(ac_transmission)\n    original_r = PSY.get_r(ac_transmission)\n    original_x = PSY.get_x(ac_transmission)\n    rating_new_parallel = PSY.get_rating(ac_transmission) / 2\n    new_r_parallel = PSY.get_r(ac_transmission) * 2\n    new_x_parallel = PSY.get_x(ac_transmission) * 2\n    ac_transmission_copy_parallel = get_copied_line(ac_transmission)\n    ac_transmission_copy_series = get_copied_line(ac_transmission_copy_parallel)\n\n    set_rating!(ac_transmission, rating_new_parallel)\n    set_rating!(ac_transmission_copy_parallel, rating_new_parallel)\n    set_r!(ac_transmission, original_r)\n    set_r!(ac_transmission_copy_parallel, new_r_parallel)\n    set_x!(ac_transmission, new_x_parallel)\n    set_x!(ac_transmission_copy_parallel, new_x_parallel)\n    add_component!(sys, ac_transmission_copy_parallel)\n\n    #Add new series Line with same parameters\n    set_arc!(ac_transmission_copy_series, arc2)\n    add_component!(sys, ac_transmission_copy_series)\n    set_x!(ac_transmission_copy_series, 1e-9)\n    set_r!(ac_transmission_copy_series, 1e-9)\nend\n\nfunction add_equivalent_ac_transmission_with_parallel_circuits!(\n    sys::System,\n    ac_transmission::PSY.Line,\n    ::Type{T},\n) where {T <: PSY.Line}\n    rating_new = PSY.get_rating(ac_transmission) / 2\n    x_new = PSY.get_x(ac_transmission) * 2\n    r_new = PSY.get_r(ac_transmission) * 2\n    ac_transmission_copy_parallel = get_copied_line(ac_transmission)\n\n    #Set ratings the half so the case remains equivalent to the original\n    set_rating!(ac_transmission, rating_new)\n    set_rating!(ac_transmission_copy_parallel, rating_new)\n    set_x!(ac_transmission, x_new)\n    set_x!(ac_transmission_copy_parallel, x_new)\n    set_r!(ac_transmission, r_new)\n    set_r!(ac_transmission_copy_parallel, r_new)\n    add_component!(sys, ac_transmission_copy_parallel)\nend\n\nfunction add_equivalent_ac_transmission_with_parallel_circuits!(\n    sys::System,\n    ac_transmission::PSY.ACTransmission,\n    ::Type{T},\n    ::Type{PSY.MonitoredLine},\n) where {T <: PSY.Line}\n    rating_new = PSY.get_rating(ac_transmission) / 2\n    x_new = PSY.get_x(ac_transmission) * 2\n    r_new = PSY.get_r(ac_transmission) * 2\n    ac_transmission_copy = MonitoredLine(;\n        name = PSY.get_name(ac_transmission) * \"_copy\",\n        available = PSY.get_available(ac_transmission),\n        active_power_flow = PSY.get_active_power_flow(ac_transmission),\n        reactive_power_flow = PSY.get_reactive_power_flow(ac_transmission),\n        arc = PSY.get_arc(ac_transmission),\n        r = PSY.get_r(ac_transmission),\n        x = PSY.get_x(ac_transmission),\n        b = PSY.get_b(ac_transmission),\n        flow_limits = (\n            from_to = rating_new,\n            to_from = rating_new,\n        ),\n        rating = rating_new,\n        angle_limits = PSY.get_angle_limits(ac_transmission),\n        rating_b = PSY.get_rating_b(ac_transmission),\n        rating_c = PSY.get_rating_c(ac_transmission),\n        g = PSY.get_g(ac_transmission),\n        services = PSY.get_services(ac_transmission),\n        ext = PSY.get_ext(ac_transmission))\n\n    #Set ratings the half so the case remains equivalent to the original\n    set_rating!(ac_transmission, rating_new)\n    set_x!(ac_transmission, x_new)\n    set_r!(ac_transmission, r_new)\n    add_component!(sys, ac_transmission_copy)\nend\n\nfunction add_reserve_product_without_requirement_time_series!(\n    sys::PSY.System,\n    name::String,\n    direction::String,\n    contributing_devices::Union{\n        IS.FlattenIteratorWrapper{<:PSY.Generator},\n        Vector{<:PSY.Generator},\n    },\n)\n    AS_DIRECTION_MAP = Dict(\n        \"Up\" => ReserveUp,\n        \"Down\" => ReserveDown,\n    )\n    as_direction = AS_DIRECTION_MAP[direction]\n    reserve_instance = VariableReserve{as_direction}(;\n        name = name,\n        available = true,\n        time_frame = 0.0,\n        requirement = 0.0,\n        sustained_time = 3600,\n        max_output_fraction = 1.0,\n        max_participation_factor = 0.25,\n        deployed_fraction = 0.0,\n    )\n    add_service!(sys, reserve_instance, contributing_devices)\nend\n"
  },
  {
    "path": "test/test_utils/add_dlr_ts.jl",
    "content": "function add_dlr_to_system_branches!(\n    sys::System,\n    branches_dlr::Vector{String},\n    n_steps::Int,\n    dlr_factors::Vector{Float64};\n    initial_date::String = \"2020-01-01\",\n)\n    # Add dynamic line ratings to the system\n    for branch_name in branches_dlr\n        branch = get_component(ACTransmission, sys, branch_name)\n        dlr_data = SortedDict{Dates.DateTime, TimeSeries.TimeArray}()\n        data_ts = collect(\n            DateTime(\"$initial_date 0:00:00\", \"y-m-d H:M:S\"):Hour(1):(\n                DateTime(\"$initial_date 23:00:00\", \"y-m-d H:M:S\")\n            ),\n        )\n        for t in 1:n_steps\n            ini_time = data_ts[1] + Day(t - 1)\n            dlr_data[ini_time] =\n                TimeArray(\n                    data_ts + Day(t - 1),\n                    dlr_factors,\n                )\n        end\n\n        PowerSystems.add_time_series!(\n            sys,\n            branch,\n            PowerSystems.Deterministic(\n                \"dynamic_line_ratings\",\n                dlr_data;\n                scaling_factor_multiplier = get_rating,\n            ),\n        )\n    end\nend\n"
  },
  {
    "path": "test/test_utils/add_market_bid_cost.jl",
    "content": "# WARNING: included in HydroPowerSimulations's tests as well.\n# If you make changes, run those tests too!\n\"\"\"\nAdd a MarketBidCost object to the selected components, with specified incremental and/or decremental cost curves.\n\"\"\"\nfunction add_mbc_inner!(\n    sys::PSY.System,\n    active_components::ComponentSelector;\n    incr_curve::Union{Nothing, PiecewiseIncrementalCurve} = nothing,\n    decr_curve::Union{Nothing, PiecewiseIncrementalCurve} = nothing,\n)\n    @assert !isempty(get_components(active_components, sys)) \"No components selected\"\n    if isnothing(incr_curve) && isnothing(decr_curve)\n        error(\"At least one of incr_curve or decr_curve must be provided\")\n    end\n    mbc = MarketBidCost(;\n        no_load_cost = 0.0,\n        start_up = (hot = 0.0, warm = 0.0, cold = 0.0),\n        shut_down = 0.0,\n    )\n    if !isnothing(decr_curve)\n        set_decremental_offer_curves!(mbc, CostCurve(decr_curve))\n    end\n    if !isnothing(incr_curve)\n        set_incremental_offer_curves!(mbc, CostCurve(incr_curve))\n    end\n    for comp in get_components(active_components, sys)\n        set_operation_cost!(comp, mbc)\n    end\nend\n\n\"\"\"\nAdd a MarketBidCost object to the selected components, with an incremental cost curve and/or\na decremental cost curve defined by hard-coded values.\n\"\"\"\nfunction add_mbc!(\n    sys::PSY.System,\n    active_components::ComponentSelector;\n    incremental::Bool = true,\n    decremental::Bool = false,\n)\n    incr_slopes = 100 .* [0.3, 0.5, 0.7]\n    decr_slopes = 100 .* [0.7, 0.5, 0.3]\n    x_coords = [10.0, 30.0, 50.0, 100.0]\n    initial_input = 20.0\n\n    if !incremental && !decremental\n        error(\"At least one of incremental or decremental must be true\")\n    end\n    if incremental\n        incr_curve =\n            PiecewiseIncrementalCurve(initial_input, x_coords, incr_slopes)\n    else\n        incr_curve = nothing\n    end\n\n    if decremental\n        decr_curve =\n            PiecewiseIncrementalCurve(initial_input, x_coords, decr_slopes)\n    else\n        decr_curve = nothing\n    end\n    add_mbc_inner!(sys, active_components; incr_curve = incr_curve, decr_curve = decr_curve)\nend\n\n\"\"\"\nGet a deterministic or DeterministicSingleTimeSeries time series from the system.\n\"\"\"\nfunction get_deterministic_ts(sys::PSY.System)\n    for device in get_components(PSY.Device, sys)\n        if has_time_series(device, Union{DeterministicSingleTimeSeries, Deterministic})\n            for key in PSY.get_time_series_keys(device)\n                ts = get_time_series(device, key)\n                if ts isa DeterministicSingleTimeSeries || ts isa Deterministic\n                    return ts\n                end\n            end\n        end\n    end\n    @assert false \"No Deterministic or DeterministicSingleTimeSeries found in system\"\n    return DeterministicSingleTimeSeries(nothing)\nend\n\n\"\"\"\nExtend the MarketBidCost objects attached to the selected components such that they're determined by a time series.\n\n# Arguments:\n\n  - `initial_varies`: whether the initial input time series should have values that vary\n    over time (as opposed to a time series with constant values over time)\n  - `breakpoints_vary`: whether the breakpoints in the variable cost time series should vary\n    over time\n  - `slopes_vary`: whether the slopes of the variable cost time series should vary over time\n  - `active_components`: a `ComponentSelector` specifying which components should get time\n    series\n  - `initial_input_names_vary`: whether the initial input time series names should vary over\n    components\n  - `variable_cost_names_vary`: whether the variable cost time series names should vary over\n    components\n\"\"\"\nfunction extend_mbc!(\n    sys::PSY.System,\n    active_components::ComponentSelector;\n    modify_baseline_pwl = nothing,\n    initial_varies::Bool = false,\n    breakpoints_vary::Bool = false,\n    slopes_vary::Bool = false,\n    initial_input_names_vary::Bool = false,\n    variable_cost_names_vary::Bool = false,\n    zero_cost_at_min::Bool = false,\n    create_extra_tranches::Bool = false,\n    do_override_min_x::Bool = false,\n)\n    @assert !isempty(get_components(active_components, sys)) \"No components selected\"\n    # incremental_initial_input is cost at minimum generation, NOT cost at zero generation\n    for comp in get_components(active_components, sys)\n        op_cost = get_operation_cost(comp)\n        if do_override_min_x && :active_power_limits in fieldnames(typeof(comp))\n            min_power = with_units_base(sys, UnitSystem.NATURAL_UNITS) do\n                get_active_power_limits(comp).min\n            end\n        else\n            min_power = nothing\n        end\n\n        @assert op_cost isa MarketBidCost\n        for (getter, setter_initial, setter_curves, incr_or_decr) in (\n            (\n                get_incremental_offer_curves,\n                set_incremental_initial_input!,\n                set_incremental_offer_curves!,\n                \"incremental\",\n            ),\n            (\n                get_decremental_offer_curves,\n                set_decremental_initial_input!,\n                set_decremental_offer_curves!,\n                \"decremental\",\n            ),\n        )\n            cost_curve = getter(op_cost)\n            isnothing(cost_curve) && continue\n\n            baseline = get_value_curve(cost_curve)::PiecewiseIncrementalCurve\n            baseline_initial = get_initial_input(baseline)\n            if zero_cost_at_min\n                baseline_initial = 0.0\n            end\n            baseline_pwl = get_function_data(baseline)\n            if do_override_min_x && isnothing(min_power)\n                min_power = first(get_x_coords(baseline_pwl))\n            end\n\n            !isnothing(modify_baseline_pwl) &&\n                (baseline_pwl = modify_baseline_pwl(baseline_pwl))\n            # primes for easier attribution\n            incr_initial = initial_varies ? (0.11, 0.05) : (0.0, 0.0)\n            incr_x = breakpoints_vary ? (0.02, 0.07, 0.03) : (0.0, 0.0, 0.0)\n            incr_y = slopes_vary ? (0.02, 0.07, 0.03) : (0.0, 0.0, 0.0)\n\n            name_modifier = \"_$(replace(get_name(comp), \" \" => \"_\"))_\"\n\n            initial_name =\n                \"initial_input $(incr_or_decr)\" *\n                (initial_input_names_vary ? name_modifier : \"\")\n            my_initial_ts = make_deterministic_ts(\n                sys,\n                initial_name,\n                baseline_initial,\n                incr_initial...;\n            )\n            variable_name =\n                \"variable_cost $(incr_or_decr)\" *\n                (variable_cost_names_vary ? name_modifier : \"\")\n            my_pwl_ts = make_deterministic_ts(\n                sys,\n                variable_name,\n                baseline_pwl,\n                incr_x,\n                incr_y;\n                create_extra_tranches = create_extra_tranches,\n                override_min_x = do_override_min_x ? min_power : nothing,\n            )\n            initial_key = add_time_series!(sys, comp, my_initial_ts)\n            curve_key = add_time_series!(sys, comp, my_pwl_ts)\n            setter_initial(op_cost, initial_key)\n            setter_curves(op_cost, curve_key)\n        end\n    end\nend\n\n\"\"\"\nMake a deterministic time series from a tuple or a float value. See below function for\ndetails about the arguments.\n\"\"\"\nfunction make_deterministic_ts(\n    name::String,\n    ini_val::T,\n    res_incr::Number,\n    interval_incr::Number,\n    init_time::DateTime,\n    horizon::Period,\n    interval::Period,\n    window_count::Int,\n    resolution::Period,\n) where {T <: Union{Number, Tuple}}\n    horizon_count = IS.get_horizon_count(horizon, resolution)\n    ts_data = OrderedDict{DateTime, Vector{T}}()\n    for i in 0:(window_count - 1)\n        if ini_val isa Tuple\n            series = [\n                ini_val .+ (res_incr * j + i * interval_incr) for\n                j in 0:(horizon_count - 1)\n            ]\n        else\n            series = ini_val .+ res_incr .* (0:(horizon_count - 1)) .+ i * interval_incr\n        end\n        ts_data[init_time + i * interval] = series\n    end\n    return Deterministic(;\n        name = name,\n        data = ts_data,\n        resolution = resolution,\n        interval = interval,\n    )\nend\n\n\"\"\"\nCreate a deterministic time series with increments to the initial values, breakpoints, and slopes.\nHere, the elements of `incrs_x` and `incrs_y` are tuples of three values, corresponding to:\n\n`tranche_incr`: increment between tranche breakpoints.\n`res_incr`: increment within the forecast horizon window.\n`interval_incr`: increment in baseline, between horizon windows.\n\n`override_min_x`: if provided, overrides the minimum x value in all piecewise curves.\n`create_extra_tranches`: if true, split the first tranche of the first timestep into two;\nsplit the last tranche of the last timestep of into three.\n\"\"\"\nfunction make_deterministic_ts(\n    name::String,\n    ini_val::PiecewiseStepData,\n    incrs_x::NTuple{3, Float64},\n    incrs_y::NTuple{3, Float64},\n    init_time::DateTime,\n    horizon::Period,\n    interval::Period,\n    count::Int,\n    resolution::Period;\n    override_min_x = nothing,\n    override_max_x = nothing,\n    create_extra_tranches = false,\n)\n    (tranche_incr_x, res_incr_x, interval_incr_x) = incrs_x\n    (tranche_incr_y, res_incr_y, interval_incr_y) = incrs_y\n\n    horizon_count = IS.get_horizon_count(horizon, resolution)\n\n    # Perturb the baseline curves by the tranche increments\n    xs1, ys1 = deepcopy(get_x_coords(ini_val)), deepcopy(get_y_coords(ini_val))\n    xs1 .+= [i * tranche_incr_x for i in 0:(length(xs1) - 1)]\n    ys1 .+= [i * tranche_incr_y for i in 0:(length(ys1) - 1)]\n\n    ts_data = OrderedDict{DateTime, Vector{PiecewiseStepData}}()\n    for i in 0:(count - 1)\n        xs = [deepcopy(xs1) .+ i * interval_incr_x for _ in 1:horizon_count]\n        ys = [deepcopy(ys1) .+ i * interval_incr_y for _ in 1:horizon_count]\n        for j in 1:horizon_count\n            xs[j] .+= (j - 1) * res_incr_x\n            ys[j] .+= (j - 1) * res_incr_y\n        end\n        if !isnothing(override_min_x)\n            for j in 1:horizon_count\n                xs[j][1] = override_min_x\n            end\n        end\n        if !isnothing(override_max_x)\n            for j in 1:horizon_count\n                xs[j][end] = override_max_x\n            end\n        end\n        if i == 0 && create_extra_tranches\n            xs[1] = [xs[1][1], (xs[1][1] + xs[1][2]) / 2, xs[1][2:end]...]\n            ys[1] = [ys[1][1], ys[1][1], ys[1][2:end]...]\n        elseif i == count - 1 && create_extra_tranches\n            xs[end] = [\n                xs[end][1:(end - 1)]...,\n                (2 * xs[end][end - 1] + xs[end][end]) / 3,\n                (xs[end][end - 1] + 2 * xs[end][end]) / 3,\n                xs[end][end],\n            ]\n            ys[end] = [ys[end][1:(end - 1)]..., ys[end][end], ys[end][end], ys[end][end]]\n        end\n        ts_data[init_time + i * interval] = PiecewiseStepData.(xs, ys)\n    end\n\n    return Deterministic(;\n        name = name,\n        data = ts_data,\n        resolution = resolution,\n        interval = interval,\n    )\nend\n\n\"\"\"\nCreate a deterministic time series as above, with the same horizon, count, and interval as an existing time series.\n\"\"\"\nfunction make_deterministic_ts(\n    sys::PSY.System,\n    args...;\n    kwargs...,\n)\n    @assert all(\n        PSY.get_time_series_resolutions(sys) .==\n        first(PSY.get_time_series_resolutions(sys)),\n    )\n    return make_deterministic_ts(\n        args...,\n        first(PSY.get_forecast_initial_times(sys)),\n        PSY.get_forecast_horizon(sys),\n        PSY.get_forecast_interval(sys),\n        PSY.get_forecast_window_count(sys),\n        first(PSY.get_time_series_resolutions(sys));\n        kwargs...,\n    )\nend\n"
  },
  {
    "path": "test/test_utils/common_operation_model.jl",
    "content": "const _DESERIALIZE_MESSAGE = \"Deserialized initial_conditions_data\"\nconst _MAKE_IC_MESSAGE = \"Make Initial Conditions Model\"\nconst _SKIP_IC_MESSAGE = \"Skip build of initial conditions\"\n\nfunction test_ic_serialization_outputs(model::PSI.OperationModel; ic_file_exists, message)\n    ic_file = PSI.get_initial_conditions_file(model)\n    log_file = PSI.get_log_file(model)\n\n    @test isfile(ic_file) == ic_file_exists\n    if ic_file_exists\n        @test Serialization.deserialize(ic_file) isa PSI.InitialConditionsData\n    end\n\n    make = false\n    deserialize = false\n    skip = false\n    if message == \"make\"\n        make = true\n    elseif message == \"deserialize\"\n        deserialize = true\n    elseif message == \"skip\"\n        skip = true\n    else\n        error(\"invalid: $message\")\n    end\n\n    text = read(log_file, String)\n    @test make == occursin(_MAKE_IC_MESSAGE, text)\n    @test deserialize == occursin(_DESERIALIZE_MESSAGE, text)\n    @test skip == occursin(_SKIP_IC_MESSAGE, text)\nend\n"
  },
  {
    "path": "test/test_utils/events_simulation_utils.jl",
    "content": "# Note: this function is used in HydroPowerSimulations.jl and StorageSystemsSimulations.jl as well for testing of events\nfunction run_fixed_forced_outage_sim_with_timeseries(;\n    sys,\n    networks,\n    optimizers,\n    outage_status_timeseries,\n    device_type,\n    device_names,\n    renewable_formulation,\n)\n    sys_em = deepcopy(sys)\n    sys_d1 = deepcopy(sys)\n    sys_d2 = deepcopy(sys)\n    transform_single_time_series!(sys_d1, Day(2), Day(1))\n    transform_single_time_series!(sys_d2, Hour(4), Hour(1))\n    event_model = EventModel(\n        FixedForcedOutage,\n        PSI.ContinuousCondition();\n        timeseries_mapping = Dict(\n            :outage_status => \"outage_profile_1\",\n        ),\n    )\n    template_d1 = get_template_basic_uc_simulation()\n    set_network_model!(template_d1, NetworkModel(networks[1]))\n    template_d2 = get_template_basic_uc_simulation()\n    set_network_model!(template_d2, NetworkModel(networks[2]))\n    template_em = get_template_nomin_ed_simulation(networks[3])\n\n    set_device_model!(template_d1, RenewableDispatch, renewable_formulation)\n    set_device_model!(template_d2, RenewableDispatch, renewable_formulation)\n    set_device_model!(template_em, RenewableDispatch, renewable_formulation)\n    set_device_model!(template_em, ThermalStandard, ThermalBasicDispatch)\n    set_service_model!(template_d1, ServiceModel(ConstantReserve{ReserveUp}, RangeReserve))\n    set_service_model!(template_d2, ServiceModel(ConstantReserve{ReserveUp}, RangeReserve))\n    set_device_model!(template_em, InterruptiblePowerLoad, PowerLoadDispatch)\n    set_device_model!(template_d1, InterruptiblePowerLoad, PowerLoadDispatch)\n    set_device_model!(template_d2, InterruptiblePowerLoad, PowerLoadDispatch)\n    set_device_model!(template_d1, Line, StaticBranch)\n    set_device_model!(template_d2, Line, StaticBranch)\n    set_device_model!(template_em, Line, StaticBranch)\n\n    for sys in [sys_d1, sys_d2, sys_em]\n        for name in device_names\n            g = get_component(device_type, sys, name)\n            transition_data = PSY.FixedForcedOutage(;\n                outage_status = 0.0,\n            )\n            add_supplemental_attribute!(sys, g, transition_data)\n            PSY.add_time_series!(\n                sys,\n                transition_data,\n                PSY.SingleTimeSeries(\"outage_profile_1\", outage_status_timeseries),\n            )\n        end\n    end\n\n    models = SimulationModels(;\n        decision_models = [\n            DecisionModel(\n                template_d1,\n                sys_d1;\n                name = \"D1\",\n                initialize_model = false,\n                optimizer = optimizers[1],\n            ),\n            DecisionModel(\n                template_d2,\n                sys_d2;\n                name = \"D2\",\n                initialize_model = false,\n                optimizer = optimizers[2],\n                store_variable_names = true,\n            ),\n        ],\n        emulation_model = EmulationModel(\n            template_em,\n            sys_em;\n            name = \"EM\",\n            optimizer = optimizers[3],\n            calculate_conflict = true,\n            store_variable_names = true,\n        ),\n    )\n    sequence = SimulationSequence(;\n        models = models,\n        ini_cond_chronology = InterProblemChronology(),\n        feedforwards = Dict(\n            \"EM\" => [# This FeedForward will force the commitment to be kept in the emulator\n                SemiContinuousFeedforward(;\n                    component_type = ThermalStandard,\n                    source = OnVariable,\n                    affected_values = [ActivePowerVariable],\n                ),\n            ],\n        ),\n        events = [event_model],\n    )\n\n    sim = Simulation(;\n        name = \"no_cache\",\n        steps = 1,\n        models = models,\n        sequence = sequence,\n        simulation_folder = mktempdir(; cleanup = true),\n    )\n    build_out = build!(sim; console_level = Logging.Error)\n    @test build_out == PSI.SimulationBuildStatus.BUILT\n    execute_out = execute!(sim; in_memory = true)\n    @test execute_out == PSI.RunStatus.SUCCESSFULLY_FINALIZED\n    results = SimulationResults(sim; ignore_status = true)\n    return results\nend\n\nfunction run_events_simulation(;\n    sys_emulator,\n    networks,\n    optimizers,\n    outage_time,   #DateTime\n    outage_length, #hrs\n    uc_formulation,   #\n    ed_formulation,\n    feedforward,\n    in_memory,\n)\n    sys_em = deepcopy(sys_emulator)\n    sys_d1 = build_system(PSITestSystems, \"c_sys5_events\")\n    transform_single_time_series!(sys_d1, Day(2), Day(1))\n    sys_d2 = build_system(PSITestSystems, \"c_sys5_events\")\n    transform_single_time_series!(sys_d2, Hour(4), Hour(1))\n\n    event_model = EventModel(\n        GeometricDistributionForcedOutage,\n        PSI.PresetTimeCondition([outage_time]),\n    )\n    if uc_formulation == \"basic\"\n        template_d1 = get_template_basic_uc_simulation()\n        set_network_model!(template_d1, NetworkModel(networks[1]))\n        template_d2 = get_template_basic_uc_simulation()\n        set_network_model!(template_d2, NetworkModel(networks[2]))\n    elseif uc_formulation == \"standard\"\n        template_d1 = get_template_standard_uc_simulation()\n        set_network_model!(template_d1, NetworkModel(networks[1]))\n        template_d2 = get_template_standard_uc_simulation()\n        set_network_model!(template_d2, NetworkModel(networks[2]))\n    else\n        @error \"invalid uc formulation: $(uc_formulation). Must be basic or standard\"\n    end\n    template_em = get_template_nomin_ed_simulation(networks[3])\n    if ed_formulation == \"basic\"\n        set_device_model!(template_em, ThermalStandard, ThermalBasicDispatch)\n    elseif ed_formulation == \"nomin\"\n    else\n        @error \"invalid ed formulation: $(ed). Must be basic or nomin\"\n    end\n    set_device_model!(template_d1, Line, StaticBranch)\n    set_device_model!(template_d2, Line, StaticBranch)\n    set_device_model!(template_em, Line, StaticBranch)\n\n    set_service_model!(template_d1, ServiceModel(ConstantReserve{ReserveUp}, RangeReserve))\n    set_service_model!(template_d2, ServiceModel(ConstantReserve{ReserveUp}, RangeReserve))\n\n    for sys in [sys_d1, sys_d2, sys_em]\n        outage_gens = [\"Alta\"]\n        for name in outage_gens\n            g = get_component(ThermalStandard, sys, name)\n            transition_data = PSY.GeometricDistributionForcedOutage(;\n                mean_time_to_recovery = outage_length,\n                outage_transition_probability = 1.0,\n            )\n            add_supplemental_attribute!(sys, g, transition_data)\n        end\n    end\n\n    models = SimulationModels(;\n        decision_models = [\n            DecisionModel(\n                template_d1,\n                sys_d1;\n                name = \"D1\",\n                initialize_model = false,\n                optimizer = optimizers[1],\n            ),\n            DecisionModel(\n                template_d2,\n                sys_d2;\n                name = \"D2\",\n                initialize_model = false,\n                optimizer = optimizers[2],\n                store_variable_names = true,\n            ),\n        ],\n        emulation_model = EmulationModel(\n            template_em,\n            sys_em;\n            name = \"EM\",\n            optimizer = optimizers[3],\n            calculate_conflict = true,\n            store_variable_names = true,\n        ),\n    )\n    if feedforward\n        sequence = SimulationSequence(;\n            models = models,\n            ini_cond_chronology = InterProblemChronology(),\n            feedforwards = Dict(\n                \"EM\" => [# This FeedForward will force the commitment to be kept in the emulator\n                    SemiContinuousFeedforward(;\n                        component_type = ThermalStandard,\n                        source = OnVariable,\n                        affected_values = [ActivePowerVariable],\n                        #   add_slacks = false,\n                    ),\n                ],\n            ),\n            events = [event_model],\n        )\n    else\n        sequence = SimulationSequence(;\n            models = models,\n            ini_cond_chronology = InterProblemChronology(),\n            events = [event_model],\n        )\n    end\n    sim = Simulation(;\n        name = \"no_cache\",\n        steps = 1,\n        models = models,\n        sequence = sequence,\n        simulation_folder = mktempdir(; cleanup = true),\n    )\n    build_out = build!(sim; console_level = Logging.Error)\n    @test build_out == PSI.SimulationBuildStatus.BUILT\n    execute_out = execute!(sim; in_memory = in_memory)\n    @test execute_out == PSI.RunStatus.SUCCESSFULLY_FINALIZED\n    results = SimulationResults(sim; ignore_status = true)\n    return results\nend\n\nfunction test_event_results(;\n    res,\n    outage_time,\n    outage_length,\n    expected_power_recovery,\n    expected_on_variable_recovery,\n    test_reactive_power = false,\n)\n    em = get_emulation_problem_results(res)\n    p = read_realized_variable(\n        em,\n        \"ActivePowerVariable__ThermalStandard\";\n        table_format = TableFormat.WIDE,\n    )\n    count = read_realized_variable(\n        em,\n        \"AvailableStatusChangeCountdownParameter__ThermalStandard\";\n        table_format = TableFormat.WIDE,\n    )\n    on = read_realized_variable(\n        em,\n        \"OnVariable__ThermalStandard\";\n        table_format = TableFormat.WIDE,\n    )\n    status = read_realized_variable(\n        em,\n        \"AvailableStatusParameter__ThermalStandard\";\n        table_format = TableFormat.WIDE,\n    )\n\n    outage_ix = indexin([outage_time], p[!, :DateTime])[1]\n    outage_length_ix = Int64((Hour(1) / em.resolution) * outage_length)\n    on_recover_ix = indexin([expected_on_variable_recovery], p[!, :DateTime])[1]\n    p_recover_ix = indexin([expected_power_recovery], p[!, :DateTime])[1]\n    #Test condition at time of outage\n    @test count[outage_ix, \"Alta\"] == 0.0\n    @test status[outage_ix, \"Alta\"] == 1.0\n    @test on[outage_ix, \"Alta\"] == 1.0\n    @test p[outage_ix, \"Alta\"] != 0.0\n    #Test condition during outage\n    @test count[(outage_ix + 1):(outage_ix + outage_length_ix), \"Alta\"] ==\n          outage_length_ix:-1.0:1.0\n    @test status[(outage_ix + 1):(outage_ix + outage_length_ix), \"Alta\"] ==\n          zeros(outage_length_ix)\n    @test on[(on_recover_ix - 1), \"Alta\"] == 0.0  #on variable not necessarily zero for full time; possibly updated later\n    length_p_zero = p_recover_ix - outage_ix - 1\n    @test isapprox(\n        p[(outage_ix + 1):(p_recover_ix - 1), \"Alta\"],\n        zeros(length_p_zero);\n        atol = 1e-5,\n    )\n    #Test condition after outage\n    @test count[(outage_ix + outage_length_ix + 1), \"Alta\"] == 0.0\n    @test status[(outage_ix + outage_length_ix + 1), \"Alta\"] == 1.0\n    @test isapprox(on[on_recover_ix, \"Alta\"], 1.0; atol = 1e-5)\n    @test !isapprox(p[p_recover_ix, \"Alta\"], 0.0; atol = 1e-5)\n    if test_reactive_power == true\n        q = read_realized_variable(\n            em,\n            \"ReactivePowerVariable__ThermalStandard\";\n            table_format = TableFormat.WIDE,\n        )\n        @test isapprox(\n            q[(outage_ix + 1):(p_recover_ix - 1), \"Alta\"],\n            zeros(length_p_zero);\n            atol = 5e-2,\n        )\n        @test q[p_recover_ix, \"Alta\"] != 0.0\n        @test !isapprox(q[p_recover_ix, \"Alta\"], 0.0; atol = 5e-2)\n    end\n    return\nend\n"
  },
  {
    "path": "test/test_utils/iec_simulation_utils.jl",
    "content": "const IECComponentType = Source\nconst IEC_COMPONENT_NAME = \"source\"\nconst SEL_IEC = make_selector(IECComponentType, IEC_COMPONENT_NAME)\n\nfunction make_5_bus_with_import_export(;\n    add_single_time_series::Bool = false,\n    name = nothing,\n)\n    sys = build_system(\n        PSITestSystems,\n        \"c_sys5_uc\";\n        add_single_time_series = add_single_time_series,\n    )\n\n    source = IECComponentType(;\n        name = IEC_COMPONENT_NAME,\n        available = true,\n        bus = get_component(ACBus, sys, \"nodeC\"),\n        active_power = 0.0,\n        reactive_power = 0.0,\n        active_power_limits = (min = -2.0, max = 2.0),\n        reactive_power_limits = (min = -2.0, max = 2.0),\n        R_th = 0.01,\n        X_th = 0.02,\n        internal_voltage = 1.0,\n        internal_angle = 0.0,\n        base_power = 100.0,\n    )\n\n    import_curve = make_import_curve(\n        [0.0, 100.0, 105.0, 120.0, 200.0],\n        [5.0, 10.0, 20.0, 40.0],\n    )\n\n    export_curve = make_export_curve(\n        [0.0, 100.0, 105.0, 120.0, 200.0],\n        [12.0, 8.0, 4.0, 1.0],  # elsewhere the final slope is 0.0 but that's problematic here\n    )\n\n    ie_cost = ImportExportCost(;\n        import_offer_curves = import_curve,\n        export_offer_curves = export_curve,\n        ancillary_service_offers = Vector{Service}(),\n        energy_import_weekly_limit = 1e6,\n        energy_export_weekly_limit = 1e6,\n    )\n\n    set_operation_cost!(source, ie_cost)\n    add_component!(sys, source)\n    @assert get_component(SEL_IEC, sys) == source\n\n    isnothing(name) || set_name!(sys, name)\n    return sys\nend\n\nfunction make_5_bus_with_ie_ts(\n    import_breakpoints_vary::Bool,\n    import_slopes_vary::Bool,\n    export_breakpoints_vary::Bool,\n    export_slopes_vary::Bool;\n    zero_min_power::Bool = true,\n    unperturb_max_power::Bool = false,\n    add_single_time_series::Bool = false,\n    import_scalar = 1.0,\n    export_scalar = 1.0,\n    name = nothing)\n    im_incr_x = import_breakpoints_vary ? (0.02, 0.11, 0.05) : (0.0, 0.0, 0.0)\n    im_incr_y = import_slopes_vary ? (0.02, 0.11, 0.05) : (0.0, 0.0, 0.0)\n\n    ex_incr_x = export_breakpoints_vary ? (0.03, 0.13, 0.07) : (0.0, 0.0, 0.0)\n    ex_incr_y = export_slopes_vary ? (0.03, 0.13, 0.07) : (0.0, 0.0, 0.0)\n\n    sys = make_5_bus_with_import_export(;\n        add_single_time_series = add_single_time_series,\n        name = name,\n    )\n\n    source = get_component(SEL_IEC, sys)\n    oc = get_operation_cost(source)::ImportExportCost\n    im_oc = get_import_offer_curves(oc)\n    ex_oc = get_export_offer_curves(oc)\n    im_fd = get_function_data(im_oc) * import_scalar\n    ex_fd = get_function_data(ex_oc) * export_scalar\n\n    im_ts = make_deterministic_ts(\n        sys,\n        \"variable_cost_import\",\n        im_fd,\n        im_incr_x,\n        im_incr_y;\n        override_min_x = zero_min_power ? 0.0 : nothing,\n        override_max_x = unperturb_max_power ? last(get_x_coords(im_fd)) : nothing,\n    )\n    ex_ts = make_deterministic_ts(\n        sys,\n        \"variable_cost_export\",\n        ex_fd,\n        ex_incr_x,\n        ex_incr_y;\n        override_min_x = zero_min_power ? 0.0 : nothing,\n        override_max_x = unperturb_max_power ? last(get_x_coords(ex_fd)) : nothing,\n    )\n\n    im_key = add_time_series!(sys, source, im_ts)\n    ex_key = add_time_series!(sys, source, ex_ts)\n\n    set_import_offer_curves!(oc, im_key)\n    set_export_offer_curves!(oc, ex_key)\n\n    return sys\nend\n\n# Analogous to run_mbc_obj_fun_test in test_utils/mbc_simulation_utils.jl\nfunction run_iec_obj_fun_test(sys1, sys2, comp_name::String, ::Type{T};\n    simulation = true, in_memory_store = false, reservation = false,\n) where {T <: PSY.Component}\n    _, res1, decisions1, nullable_decisions1 = run_iec_sim(sys1, comp_name, T;\n        simulation = simulation,\n        in_memory_store = in_memory_store,\n        reservation = reservation,\n    )\n    _, res2, decisions2, nullable_decisions2 = run_iec_sim(sys2, comp_name, T;\n        simulation = simulation,\n        in_memory_store = in_memory_store,\n        reservation = reservation,\n    )\n\n    all_decisions1 = (decisions1..., nullable_decisions1...)\n    all_decisions2 = (decisions2..., nullable_decisions2...)\n    if !all(isapprox.(all_decisions1, all_decisions2))\n        @error all_decisions1\n        @error all_decisions2\n    end\n    @assert all(isapprox.(all_decisions1, all_decisions2))\n\n    ground_truth_1 = cost_due_to_time_varying_iec(sys1, res1, T)\n    ground_truth_2 = cost_due_to_time_varying_iec(sys2, res2, T)\n\n    success = obj_fun_test_helper(ground_truth_1, ground_truth_2, res1, res2)\n    return decisions1, decisions2\nend\n\nfunction run_iec_sim(sys::System, comp_name::String, ::Type{T};\n    simulation = true, in_memory_store = false, reservation = false,\n) where {T <: PSY.Component}\n    device_to_formulation = FormulationDict(\n        Source => DeviceModel(\n            Source,\n            ImportExportSourceModel;\n            attributes = Dict(\"reservation\" => reservation),\n        ),\n    )\n    model, res = if simulation\n        run_generic_mbc_sim(\n            sys;\n            in_memory_store = in_memory_store,\n            device_to_formulation = device_to_formulation,\n        )\n    else\n        run_generic_mbc_prob(sys; device_to_formulation = device_to_formulation)\n    end\n\n    # Test that breakpoint and slope parameters read from results match the\n    # ground truth from the system's offer curve time series.\n    # We can compare raw PiecewiseStepData values directly because time-variant offer curve\n    # time series are always in natural units and the parameter multiplier is 1.0\n    # (see the analogous comment in mbc_simulation_utils.jl).\n    for (is_decremental, oc_getter) in (\n        (false, PSY.get_import_offer_curves),\n        (true, PSY.get_export_offer_curves),\n    )\n        bp_param_type = if is_decremental\n            PSI.DecrementalPiecewiseLinearBreakpointParameter\n        else\n            PSI.IncrementalPiecewiseLinearBreakpointParameter\n        end\n        sl_param_type = if is_decremental\n            PSI.DecrementalPiecewiseLinearSlopeParameter\n        else\n            PSI.IncrementalPiecewiseLinearSlopeParameter\n        end\n        bp_param = _maybe_upgrade_to_dict(read_parameter(res, bp_param_type, T))\n        sl_param = _maybe_upgrade_to_dict(read_parameter(res, sl_param_type, T))\n        for (step_dt, bp_step_df) in pairs(bp_param)\n            sl_step_df = sl_param[step_dt]\n            for gen_name in unique(bp_step_df.name)\n                comp = get_component(T, sys, gen_name)\n                cost = PSY.get_operation_cost(comp)\n                oc_ts = oc_getter(comp, cost; start_time = step_dt)\n                gen_bp = @rsubset(bp_step_df, :name == gen_name)\n                gen_sl = @rsubset(sl_step_df, :name == gen_name)\n                for (ts, psd) in\n                    zip(TimeSeries.timestamp(oc_ts), TimeSeries.values(oc_ts))\n                    expected_bp = get_x_coords(psd)\n                    expected_sl = get_y_coords(psd)\n                    actual_bp = sort(@rsubset(gen_bp, :DateTime == ts), :name2).value\n                    actual_sl = sort(@rsubset(gen_sl, :DateTime == ts), :name2).value\n                    # actual may be longer than expected due to padding (see _unwrap_for_param)\n                    @test length(actual_bp) >= length(expected_bp)\n                    @test length(actual_sl) >= length(expected_sl)\n                    @test all(isapprox.(actual_bp[1:length(expected_bp)], expected_bp))\n                    @test all(isapprox.(actual_sl[1:length(expected_sl)], expected_sl))\n                end\n            end\n        end\n    end\n\n    decisions = (\n        _read_one_value(res, PSI.ActivePowerOutVariable, T, comp_name),\n        _read_one_value(res, PSI.ActivePowerInVariable, T, comp_name),\n    )\n\n    output_var = read_variable_dict(res, PSI.ActivePowerOutVariable, T)\n    input_var = read_variable_dict(res, PSI.ActivePowerInVariable, T)\n\n    for key in keys(output_var)\n        output_on = output_var[key][!, \"value\"] .> PSI.COST_EPSILON\n        input_on = input_var[key][!, \"value\"] .> PSI.COST_EPSILON\n        if reservation\n            @test all(.~(output_on .& input_on))  # no simultaneous import/export\n        else\n            @test any(output_on .& input_on)  # some simultaneous import/export\n        end\n    end\n\n    return model, res, decisions, ()  # return format follows the MBC run_startup_shutdown_test convention\nend\n\n# Analogous to cost_due_to_time_varying_mbc in test_utils/mbc_simulation_utils.jl\n# TODO deduplicate after initial time-sensitive merge\nfunction cost_due_to_time_varying_iec(\n    sys::System,\n    res::IS.Results,\n    ::Type{T},\n) where {T <: PSY.Component}\n    power_in_vars = read_variable_dict(res, PSI.ActivePowerInVariable, T)\n    power_out_vars = read_variable_dict(res, PSI.ActivePowerOutVariable, T)\n    result = SortedDict{DateTime, DataFrame}()\n\n    for step_dt in keys(power_in_vars)\n        power_in_df = power_in_vars[step_dt]\n        step_df = DataFrame(:DateTime => unique(power_in_df.DateTime))\n        gen_names = unique(power_in_df.name)\n        @assert !isempty(gen_names)\n\n        power_out_df = power_out_vars[step_dt]\n        @assert names(power_in_df) == names(power_out_df)\n        @assert all(power_in_df.DateTime .== power_out_df.DateTime)\n\n        @assert any([\n            get_operation_cost(comp) isa ImportExportCost for\n            comp in get_components(T, sys)\n        ])\n        for gen_name in gen_names\n            comp = get_component(T, sys, gen_name)\n            cost = PSY.get_operation_cost(comp)\n            (cost isa ImportExportCost) || continue\n            step_df[!, gen_name] .= 0.0\n            # imports = addition of power = power flowing out of the device\n            # exports = reduction of power = power flowing into the device\n            for (multiplier, power_df, getter) in (\n                (1.0, power_out_df, PSY.get_import_offer_curves),\n                (-1.0, power_in_df, PSY.get_export_offer_curves),\n            )\n                offer_curves = getter(cost)\n                if PSI.is_time_variant(offer_curves)\n                    vc_ts = getter(comp, cost; start_time = step_dt)\n                    @assert all(unique(power_df.DateTime) .== TimeSeries.timestamp(vc_ts))\n                    step_df[!, gen_name] .+=\n                        multiplier *\n                        _calc_pwi_cost.(\n                            @rsubset(power_df, :name == gen_name).value,\n                            TimeSeries.values(vc_ts),\n                        )\n                end\n            end\n        end\n\n        measure_vars = [x for x in names(step_df) if x != \"DateTime\"]\n        # rows represent: [time, component, time-varying MBC cost for {component} at {time}]\n        result[step_dt] =\n            DataFrames.stack(\n                step_df,\n                measure_vars;\n                variable_name = :name,\n                value_name = :value,\n            )\n    end\n    return result\nend\n\nfunction iec_obj_fun_test_wrapper(sys_constant, sys_varying; reservation = false)\n    for use_simulation in (false, true)\n        for in_memory_store in (use_simulation ? (false, true) : (false,))\n            decisions1, decisions2 = run_iec_obj_fun_test(\n                sys_constant,\n                sys_varying,\n                IEC_COMPONENT_NAME,\n                IECComponentType;\n                simulation = use_simulation,\n                in_memory_store = in_memory_store,\n                reservation = reservation,\n            )\n\n            if !all(isapprox.(decisions1, decisions2))\n                @error decisions1\n                @error decisions2\n            end\n            @assert all(approx_geq_1.(decisions1))\n        end\n    end\nend\n"
  },
  {
    "path": "test/test_utils/mbc_simulation_utils.jl",
    "content": "# WARNING: included in HydroPowerSimulations's tests as well.\n# If you make changes, run those tests too!\nconst TIME1 = DateTime(\"2024-01-01T00:00:00\")\ntest_path = mktempdir()\nconst FormulationDict =\n    Dict{Type{<:PSY.Device}, Union{DeviceModel, Type{<:PSI.AbstractDeviceFormulation}}}\n# TODO could replace with PSI's defaults, template_unit_commitment\nconst DEFAULT_FORMULATIONS =\n    FormulationDict(\n        ThermalStandard => ThermalBasicUnitCommitment,\n        PowerLoad => StaticPowerLoad,\n        InterruptiblePowerLoad => PowerLoadInterruption,\n        RenewableDispatch => RenewableFullDispatch,\n        # I include this file in the tests of SSS and HPS, which error on these formulations.\n        # HydroDispatch => HydroCommitmentRunOfRiver,\n        # EnergyReservoirStorage => StorageDispatchWithReserves,\n    )\n\n# debugging code for inspecting objective functions -- ignore\nfunction format_objective_function_file(filepath::String)\n    if !isfile(filepath)\n        println(\"Error: File '$filepath' does not exist.\")\n        exit(1)\n    end\n\n    try\n        content = read(filepath, String)\n        content = replace(content, \"+\" => \"+\\n\")\n        content = replace(content, \"-\" => \"-\\n\")\n        write(filepath, content)\n    catch e\n        println(\"Error processing file '$filepath': $e\")\n        exit(1)\n    end\nend\n\nfunction save_objective_function(model::DecisionModel, filepath::String)\n    open(filepath, \"w\") do file\n        println(file, \"invariant_terms:\")\n        println(file, model.internal.container.objective_function.invariant_terms)\n        println(file, \"variant_terms:\")\n        println(file, model.internal.container.objective_function.variant_terms)\n    end\n    format_objective_function_file(filepath)\nend\n\nfunction save_constraints(model::DecisionModel, filepath::String)\n    open(filepath, \"w\") do file\n        for (k, v) in model.internal.container.constraints\n            println(file, \"Constraint Type: $(k)\")\n            println(file, v)\n        end\n    end\nend\n# end debugging code\n\nfunction set_formulations!(template::ProblemTemplate,\n    sys::PSY.System,\n    device_to_formulation::FormulationDict,\n)\n    for (device, formulation) in device_to_formulation\n        if !isempty(get_components(device, sys))\n            _set_formulations_helper(template, device, formulation)\n        end\n    end\n    for (device, formulation) in DEFAULT_FORMULATIONS\n        if !haskey(device_to_formulation, device) && !isempty(get_components(device, sys))\n            _set_formulations_helper(template, device, formulation)\n        end\n    end\nend\n\n_set_formulations_helper(\n    template::ProblemTemplate,\n    device::Type{<:PSY.Device},\n    formulation::Type{<:PSI.AbstractDeviceFormulation},\n) =\n    set_device_model!(template, device, formulation)\n_set_formulations_helper(template::ProblemTemplate, _, device_model::DeviceModel) =\n    set_device_model!(template, device_model)\n\n# Layer of indirection to upgrade problem results to look like simulation results\n_maybe_upgrade_to_dict(input::AbstractDict) = input\n_maybe_upgrade_to_dict(input::DataFrame) =\n    SortedDict{DateTime, DataFrame}(first(input[!, :DateTime]) => input)\n\nread_variable_dict(\n    res::IS.Results,\n    var_name::Type{<:PSI.VariableType},\n    comp_type::Type{<:PSY.Component},\n) =\n    _maybe_upgrade_to_dict(read_variable(res, var_name, comp_type))\nread_parameter_dict(\n    res::IS.Results,\n    par_name::Type{<:PSI.ParameterType},\n    comp_type::Type{<:PSY.Component},\n) =\n    _maybe_upgrade_to_dict(read_parameter(res, par_name, comp_type))\n\nfunction _read_one_value(res, var_name, gentype, unit_name)\n    df = @chain begin\n        vcat(values(read_variable_dict(res, var_name, gentype))...)\n        @rsubset(:name == unit_name)\n        @combine(:value = sum(:value))\n    end\n    return df[1, 1]\nend\n\nfunction build_generic_mbc_model(sys::System;\n    multistart::Bool = false,\n    standard::Bool = false,\n    device_to_formulation = FormulationDict(),\n)\n    template = ProblemTemplate(\n        NetworkModel(\n            CopperPlatePowerModel;\n            duals = [CopperPlateBalanceConstraint],\n        ),\n    )\n\n    set_formulations!(\n        template,\n        sys,\n        device_to_formulation,\n    )\n    if standard\n        set_device_model!(template, ThermalStandard, ThermalStandardUnitCommitment)\n    end\n    if multistart\n        set_device_model!(template, ThermalMultiStart, ThermalMultiStartUnitCommitment)\n    end\n\n    model = DecisionModel(\n        template,\n        sys;\n        name = \"UC\",\n        store_variable_names = true,\n        optimizer = HiGHS_optimizer_small_gap,\n    )\n    return model\nend\n\nfunction run_generic_mbc_prob(\n    sys::System;\n    multistart::Bool = false,\n    standard = false,\n    test_success = true,\n    filename::Union{String, Nothing} = nothing,\n    is_decremental::Bool = false,\n    device_to_formulation = FormulationDict(),\n)\n    model = build_generic_mbc_model(\n        sys;\n        multistart = multistart,\n        standard = standard,\n        device_to_formulation = device_to_formulation,\n    )\n    test_path = mktempdir()\n    build_result = build!(model; output_dir = test_path)\n    test_success && @test build_result == PSI.ModelBuildStatus.BUILT\n    solve_result = solve!(model)\n    test_success && @test solve_result == PSI.RunStatus.SUCCESSFULLY_FINALIZED\n    res = OptimizationProblemResults(model)\n    if !isnothing(filename)\n        adj = is_decremental ? \"decr\" : \"incr\"\n        save_objective_function(\n            model,\n            \"$(filename)_$(adj)_prob_objective_function.txt\",\n        )\n        save_constraints(\n            model,\n            \"$(filename)_$(adj)_prob_constraints.txt\",\n        )\n    end\n    return model, res\nend\n\nfunction run_generic_mbc_sim(\n    sys::System;\n    multistart::Bool = false,\n    in_memory_store::Bool = false,\n    standard::Bool = false,\n    test_success = true,\n    filename::Union{String, Nothing} = nothing,\n    is_decremental::Bool = false,\n    device_to_formulation = FormulationDict(),\n)\n    model = build_generic_mbc_model(\n        sys;\n        multistart = multistart,\n        standard = standard,\n        device_to_formulation = device_to_formulation,\n    )\n    models = SimulationModels(;\n        decision_models = [\n            model,\n        ],\n    )\n    sequence = SimulationSequence(;\n        models = models,\n        feedforwards = Dict(\n        ),\n        ini_cond_chronology = InterProblemChronology(),\n    )\n\n    sim = Simulation(;\n        name = \"compact_sim\",\n        steps = 2,\n        models = models,\n        sequence = sequence,\n        initial_time = TIME1,\n        simulation_folder = mktempdir(),\n    )\n\n    test_success && @test build!(sim) == PSI.SimulationBuildStatus.BUILT\n    test_success &&\n        @test execute!(sim; in_memory = in_memory_store) ==\n              PSI.RunStatus.SUCCESSFULLY_FINALIZED\n\n    sim_res = SimulationResults(sim)\n    res = get_decision_problem_results(sim_res, \"UC\")\n    if !isnothing(filename)\n        adj = is_decremental ? \"decr\" : \"incr\"\n        save_objective_function(\n            model,\n            \"$(filename)_$(adj)_sim_objective_function.txt\",\n        )\n        save_constraints(\n            model,\n            \"$(filename)_$(adj)_sim_constraints.txt\",\n        )\n    end\n    return model, res\nend\n\n\"\"\"\nRun a simple simulation with the system and return information useful for testing\ntime-varying startup and shutdown functionality.  Pass `simulation = false` to use a single\ndecision model, `true` for a full simulation.\n\"\"\"\nfunction run_mbc_sim(\n    sys::System,\n    comp_name::String,\n    ::Type{T};\n    has_initial_input::Bool = true,\n    is_decremental::Bool = false,\n    simulation = true,\n    in_memory_store = false,\n    standard = false,\n    filename::Union{String, Nothing} = nothing,\n    device_to_formulation = FormulationDict(),\n) where {T <: PSY.Component}\n    model, res = if simulation\n        run_generic_mbc_sim(\n            sys;\n            in_memory_store = in_memory_store,\n            standard = standard,\n            filename = filename,\n            is_decremental = is_decremental,\n            device_to_formulation = device_to_formulation,\n        )\n    else\n        run_generic_mbc_prob(\n            sys;\n            standard = standard,\n            filename = filename,\n            is_decremental = is_decremental,\n            device_to_formulation = device_to_formulation,\n        )\n    end\n\n    # TODO make this more general as to which variables we're reading.\n    # e.g. hydro.\n\n    # Test that breakpoint and slope parameters read from results match the\n    # ground truth from the system's offer curve time series.\n    # The PowerLoadDispatch device formulation doesn't have\n    # DecrementalCostAtMinParameter nor OnVariable.\n\n    if is_decremental\n        bp_param_type = PSI.DecrementalPiecewiseLinearBreakpointParameter\n        sl_param_type = PSI.DecrementalPiecewiseLinearSlopeParameter\n        oc_getter = get_decremental_offer_curves\n    else\n        bp_param_type = PSI.IncrementalPiecewiseLinearBreakpointParameter\n        sl_param_type = PSI.IncrementalPiecewiseLinearSlopeParameter\n        oc_getter = get_incremental_offer_curves\n    end\n    # Both bp_param and sl_param are SortedDict{DateTime, DataFrame}.\n    # Columns: :DateTime, :name (\"Test Unit1\"), :name2 (\"tranche_1\"),\n    # and :value (breakpoints for bp_param, slopes for sl_param).\n    bp_param = _maybe_upgrade_to_dict(read_parameter(res, bp_param_type, T))\n    sl_param = _maybe_upgrade_to_dict(read_parameter(res, sl_param_type, T))\n\n    # We can compare the raw PiecewiseStepData values directly against read_parameter\n    # results because: (1) time-variant offer curve time series are always in natural units\n    # (see PSY's cost_function_timeseries.jl), and (2) the parameter multiplier is 1.0 for\n    # both slopes and breakpoints (see default_interface_methods.jl). Unit conversion via\n    # get_piecewise_curve_per_system_unit only happens later when building expressions.\n    for (step_dt, bp_step_df) in pairs(bp_param)\n        sl_step_df = sl_param[step_dt]\n        for gen_name in unique(bp_step_df.name)\n            comp = get_component(T, sys, gen_name)\n            cost = PSY.get_operation_cost(comp)\n            oc_ts = oc_getter(comp, cost; start_time = step_dt)\n            gen_bp = @rsubset(bp_step_df, :name == gen_name)\n            gen_sl = @rsubset(sl_step_df, :name == gen_name)\n            for (ts, psd) in zip(TimeSeries.timestamp(oc_ts), TimeSeries.values(oc_ts))\n                expected_bp = get_x_coords(psd)\n                expected_sl = get_y_coords(psd)\n                actual_bp = sort(@rsubset(gen_bp, :DateTime == ts), :name2).value\n                actual_sl = sort(@rsubset(gen_sl, :DateTime == ts), :name2).value\n                # actual may be longer than expected due to padding (see _unwrap_for_param)\n                @test length(actual_bp) >= length(expected_bp)\n                @test length(actual_sl) >= length(expected_sl)\n                @test all(isapprox.(actual_bp[1:length(expected_bp)], expected_bp))\n                @test all(isapprox.(actual_sl[1:length(expected_sl)], expected_sl))\n            end\n        end\n    end\n\n    if has_initial_input\n        if is_decremental\n            param_type = PSI.DecrementalCostAtMinParameter\n            initial_getter = get_decremental_initial_input\n        else  # Default to incremental for ThermalStandard and other types\n            param_type = PSI.IncrementalCostAtMinParameter\n            initial_getter = get_incremental_initial_input\n        end\n        init_param = read_parameter_dict(res, param_type, T)\n        for (step_dt, step_df) in pairs(init_param)\n            for gen_name in unique(step_df.name)\n                comp = get_component(T, sys, gen_name)\n                ii_comp = initial_getter(\n                    comp,\n                    PSY.get_operation_cost(comp);\n                    start_time = step_dt,\n                )\n                @test all(step_df[!, :DateTime] .== TimeSeries.timestamp(ii_comp))\n                @test all(\n                    isapprox.(\n                        @rsubset(step_df, :name == gen_name).value,\n                        TimeSeries.values(ii_comp),\n                    ),\n                )\n            end\n        end\n    end\n    # NOTE this could be rewritten nicely using PowerAnalytics\n    # Select component based on comp_type - fallback to legacy behavior if needed\n    sel = make_selector(T, comp_name)\n    @assert !isnothing(first(get_components(sel, sys)))\n    if has_initial_input\n        decisions = (\n            _read_one_value(res, PSI.OnVariable, T, comp_name),\n            _read_one_value(res, PSI.ActivePowerVariable, T, comp_name),\n        )\n    else\n        decisions = (\n            1.0, # placeholder so return type is consistent.\n            _read_one_value(res, PSI.ActivePowerVariable, T, comp_name),\n        )\n    end\n    return model, res, decisions, ()\nend\n\nfunction cost_due_to_time_varying_mbc(\n    sys::System,\n    res::IS.Results,\n    ::Type{T};\n    is_decremental = false,\n    has_initial_input = true,\n    device_to_formulation::Any, #unused\n) where {T <: PSY.Device}\n    power_vars = read_variable_dict(res, PSI.ActivePowerVariable, T)\n    result = SortedDict{DateTime, DataFrame}()\n    if has_initial_input\n        on_vars = read_variable_dict(res, PSI.OnVariable, T)\n        @assert all(keys(on_vars) .== keys(power_vars))\n        @assert !isempty(keys(on_vars))\n    end\n    for step_dt in keys(power_vars)\n        power_df = power_vars[step_dt]\n        step_df = DataFrame(:DateTime => unique(power_df.DateTime))\n        gen_names = unique(power_df.name)\n        @assert !isempty(gen_names)\n        @assert any([\n            get_operation_cost(comp) isa MarketBidCost for\n            comp in get_components(T, sys)\n        ])\n        if has_initial_input\n            on_df = on_vars[step_dt]\n            @assert names(on_df) == names(power_df)\n            @assert on_df[!, :DateTime] == power_df[!, :DateTime]\n        else\n            # assumption: all devices are on.\n            on_df = DataFrame(\n                :DateTime => power_df.DateTime,\n                :name => power_df.name,\n                :value => ones(nrow(power_df)),\n            )\n        end\n        for gen_name in gen_names\n            comp = get_component(T, sys, gen_name)\n            cost = PSY.get_operation_cost(comp)\n            (cost isa MarketBidCost) || continue\n            step_df[!, gen_name] .= 0.0\n            ii_getter = if is_decremental\n                get_decremental_initial_input\n            else\n                get_incremental_initial_input\n            end\n            if PSI.is_time_variant(ii_getter(cost))\n                # initial cost: initial input time series multiplied by OnVariable value.\n                ii_ts = ii_getter(comp, cost; start_time = step_dt)\n                @assert all(unique(on_df.DateTime) .== TimeSeries.timestamp(ii_ts))\n                step_df[!, gen_name] .+=\n                    @rsubset(on_df, :name == gen_name).value .*\n                    TimeSeries.values(ii_ts)\n            end\n            oc_getter =\n                is_decremental ?\n                get_decremental_offer_curves :\n                get_incremental_offer_curves\n            if PSI.is_time_variant(oc_getter(cost))\n                vc_ts = oc_getter(comp, cost; start_time = step_dt)\n                @assert all(unique(power_df.DateTime) .== TimeSeries.timestamp(vc_ts))\n                # variable cost: cost function time series evaluated at ActivePowerVariable value.\n                step_df[!, gen_name] .+=\n                    _calc_pwi_cost.(\n                        @rsubset(power_df, :name == gen_name).value,\n                        TimeSeries.values(vc_ts),\n                    ) # could replace with direct evaluation, now that it is implemented in IS. (https://github.com/Sienna-Platform/PowerSimulations.jl/issues/1430)\n            end\n        end\n        measure_vars = [x for x in names(step_df) if x != \"DateTime\"]\n        # rows represent: [time, component, time-varying MBC cost for {component} at {time}]\n        result[step_dt] =\n            DataFrames.stack(\n                step_df,\n                measure_vars;\n                variable_name = :name,\n                value_name = :value,\n            )\n    end\n    return result\nend\n\n# See run_startup_shutdown_obj_fun_test for explanation\nfunction obj_fun_test_helper(\n    ground_truth_1,\n    ground_truth_2,\n    res1,\n    res2;\n    is_decremental = false,\n)\n    @assert all(keys(ground_truth_1) .== keys(ground_truth_2))\n    # total cost due to time-varying MBCs in each scenario\n    total1 =\n        [only(@combine(df, :total = sum(:value)).total) for df in values(ground_truth_1)]\n    total2 =\n        [only(@combine(df, :total = sum(:value)).total) for df in values(ground_truth_2)]\n    if !is_decremental\n        ground_truth_diff = total2 .- total1  # How much did the cost increase between simulation 1 and simulation 2 for each step\n    else\n        # objective = cost - benefit. higher load prices => more willing to pay, more benefit.\n        # so we get an extra negative sign, since we're increasing benefit, not cost.\n        ground_truth_diff = total1 .- total2\n    end\n\n    obj1 = PSI.read_optimizer_stats(res1)[!, \"objective_value\"]\n    obj2 = PSI.read_optimizer_stats(res2)[!, \"objective_value\"]\n    obj_diff = obj2 .- obj1\n\n    # Make sure there is some real difference between the two scenarios\n    @assert !any(isapprox.(ground_truth_diff, 0.0; atol = 0.0001))\n    # Make sure the difference is reflected correctly in the objective value\n    if !all(isapprox.(obj_diff, ground_truth_diff; atol = 0.0001))\n        @error obj_diff\n        @error ground_truth_diff\n    end\n\n    comparison_passes = all(isapprox.(obj_diff, ground_truth_diff; atol = 0.0001))\n\n    # An assumption in this line of testing is that our perturbations are small enough that\n    # they don't actually change the decisions, just slightly alter the cost. If the\n    # comparison isn't passing, one reason could be that this assumption is violated. In\n    # that case, we want an `AssertionError` rather than a test failure.\n    if !comparison_passes\n        @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.\"\n    end\n\n    # At this point, we've eliminiated many 'innocent' sources of error, so if the\n    # comparison isn't passing here we can have some confidence that it's actually pointing\n    # to a bug in the implementation\n    @test comparison_passes\n    return comparison_passes\nend\n\n# See run_startup_shutdown_obj_fun_test for explanation\nfunction run_mbc_obj_fun_test(\n    sys1,\n    sys2,\n    comp_name::String,\n    comp_type::Type{T};\n    is_decremental::Bool = false,\n    has_initial_input::Bool = true,\n    simulation = true,\n    in_memory_store = false,\n    filename::Union{String, Nothing} = nothing,\n    device_to_formulation = FormulationDict(),\n) where {T <: PSY.Component}\n    # at the moment, nullable_decisions are empty tuples, but keep them for future-proofing.\n    # look at run_startup_shutdown_test for explanation: non-nullable should be approx_geq_1.\n    kwargs = Dict(\n        :is_decremental => is_decremental,\n        :has_initial_input => has_initial_input,\n        :simulation => simulation,\n        :in_memory_store => in_memory_store,\n        :filename => filename,\n        :device_to_formulation => device_to_formulation,\n    )\n    filename_in = get(kwargs, :filename, nothing)\n    if !isnothing(filename_in)\n        kwargs[:filename] = filename_in * get_name(sys1)\n    end\n    _, res1, decisions1, nullable_decisions1 =\n        run_mbc_sim(\n            sys1,\n            comp_name,\n            comp_type;\n            kwargs...,\n        )\n    if !isnothing(filename_in)\n        kwargs[:filename] = filename_in * get_name(sys2)\n    end\n    _, res2, decisions2, nullable_decisions2 =\n        run_mbc_sim(\n            sys2,\n            comp_name,\n            comp_type;\n            kwargs...,\n        )\n    all_decisions1 = (decisions1..., nullable_decisions1...)\n    all_decisions2 = (decisions2..., nullable_decisions2...)\n    if !all(isapprox.(all_decisions1, all_decisions2))\n        @error all_decisions1\n        @error all_decisions2\n    end\n    @assert all(isapprox.(all_decisions1, all_decisions2))\n\n    ground_truth_1 =\n        cost_due_to_time_varying_mbc(sys1, res1, T; is_decremental = is_decremental,\n            has_initial_input = has_initial_input,\n            device_to_formulation = device_to_formulation)\n    ground_truth_2 =\n        cost_due_to_time_varying_mbc(sys2, res2, T; is_decremental = is_decremental,\n            has_initial_input = has_initial_input,\n            device_to_formulation = device_to_formulation)\n\n    success = obj_fun_test_helper(\n        ground_truth_1,\n        ground_truth_2,\n        res1,\n        res2;\n        is_decremental = is_decremental,\n    )\n    #=\n    if !success\n        @error ground_truth_1\n        @error ground_truth_2\n        obj1 = PSI.read_optimizer_stats(res1)[!, \"objective_value\"]\n        obj2 = PSI.read_optimizer_stats(res2)[!, \"objective_value\"]\n        @error obj1\n        @error obj2\n    end=#\n    return decisions1, decisions2\nend\n\n# TODO for https://github.com/Sienna-Platform/PowerSimulations.jl/issues/1430, reimplement this\n# by converting the implied IncrementalCurve into an InputOutputCurve and then evaluating\n# *its* `FunctionData`\nfunction _calc_pwi_cost(active_power::Float64, pwi::PiecewiseStepData)\n    isapprox(active_power, 0.0) && return 0.0\n    breakpoints = get_x_coords(pwi)\n    slopes = get_y_coords(pwi)\n    above_min =\n        isapprox(active_power, first(breakpoints)) || active_power > first(breakpoints)\n    below_max =\n        isapprox(active_power, last(breakpoints)) || active_power < last(breakpoints)\n    @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.\"\n    active_power = clamp(active_power, first(breakpoints), last(breakpoints))\n    i_leq = findlast(<=(active_power), breakpoints)\n    cost =\n        sum(slopes[1:(i_leq - 1)] .* (breakpoints[2:i_leq] .- breakpoints[1:(i_leq - 1)]))\n    (active_power > breakpoints[i_leq]) &&\n        (cost += slopes[i_leq] * (active_power - breakpoints[i_leq]))\n    return cost\nend\n\n\"Test that the two systems (typically one without time series and one with constant time series) simulate the same\"\nfunction test_generic_mbc_equivalence(sys0, sys1; kwargs...)\n    for runner in (run_generic_mbc_prob, run_generic_mbc_sim)  # test with both a single problem and a full simulation\n        filename_in = get(kwargs, :filename, nothing)\n        # Create a mutable copy of kwargs\n        kwargs_dict = Dict(kwargs)\n        if !isnothing(filename_in)\n            kwargs_dict[:filename] = filename_in * get_name(sys0)\n        end\n        _, res0 = runner(sys0; kwargs_dict...)\n        if !isnothing(filename_in)\n            kwargs_dict[:filename] = filename_in * get_name(sys1)\n        end\n        _, res1 = runner(sys1; kwargs_dict...)\n        obj_val_0 = PSI.read_optimizer_stats(res0)[!, \"objective_value\"]\n        obj_val_1 = PSI.read_optimizer_stats(res1)[!, \"objective_value\"]\n        @test isapprox(obj_val_0, obj_val_1; atol = 0.0001)\n    end\nend\n\napprox_geq_1(x; kwargs...) = (x >= 1.0) || isapprox(x, 1.0; kwargs...)\n"
  },
  {
    "path": "test/test_utils/mbc_system_utils.jl",
    "content": "# WARNING: included in HydroPowerSimulations's tests as well.\n# If you make changes, run those tests too!\nconst SEL_INCR = make_selector(ThermalStandard, \"Test Unit1\")\nconst SEL_DECR = make_selector(InterruptiblePowerLoad, \"Bus1_interruptible\")\nconst SEL_MULTISTART = make_selector(ThermalMultiStart, \"115_STEAM_1\")\n\n# functions for replacing components in the system\nfunction replace_with_renewable!(\n    sys::PSY.System,\n    unit1::PSY.Generator;\n    use_thermal_max_power = false,\n    magnitude = 1.0,\n    random_variation = 0.1,\n)\n    rg1 = PSY.RenewableDispatch(;\n        name = \"RG1\",\n        available = true,\n        bus = get_bus(unit1),\n        active_power = get_active_power(unit1),\n        reactive_power = get_reactive_power(unit1),\n        rating = get_rating(unit1),\n        prime_mover_type = PSY.PrimeMovers.PVe,\n        reactive_power_limits = get_reactive_power_limits(unit1),\n        power_factor = 0.9,\n        # the start up, shunt down, and no-load cost of renewables should be zero,\n        # but we'll use the unit's operation cost as-is for simplicity.\n        operation_cost = deepcopy(get_operation_cost(unit1)),\n        base_power = get_base_power(unit1),\n    )\n    add_component!(sys, rg1)\n    transfer_mbc!(rg1, unit1, sys)\n    remove_component!(sys, unit1)\n    zero_out_startup_shutdown_costs!(rg1)\n\n    # add a max_active_power time series to the component\n    load = first(PSY.get_components(PSY.PowerLoad, sys))\n    load_ts = get_time_series(Deterministic, load, \"max_active_power\")\n    num_windows = length(get_data(load_ts))\n    num_forecast_steps =\n        floor(Int, get_horizon(load_ts) / get_interval(load_ts))\n    total_steps = num_windows + num_forecast_steps - 1\n    dates = range(\n        get_initial_timestamp(load_ts);\n        step = get_interval(load_ts),\n        length = total_steps,\n    )\n    if use_thermal_max_power\n        rg_data = fill(get_active_power_limits(unit1).max, total_steps)\n    else\n        rg_data = magnitude .* ones(total_steps) .+ random_variation .* rand(total_steps)\n    end\n    rg_ts = SingleTimeSeries(\"max_active_power\", TimeArray(dates, rg_data))\n    add_time_series!(sys, rg1, rg_ts)\n    transform_single_time_series!(\n        sys,\n        get_horizon(load_ts),\n        get_interval(load_ts),\n    )\nend\n\nfunction replace_load_with_interruptible!(sys::System)\n    @assert !isempty(get_components(PSY.PowerLoad, sys))\n    load1 = first(get_components(PSY.PowerLoad, sys))\n    interruptible_load = PSY.InterruptiblePowerLoad(;\n        name = get_name(load1) * \"_interruptible\",\n        bus = get_bus(load1),\n        available = get_available(load1),\n        active_power = get_active_power(load1),\n        reactive_power = get_reactive_power(load1),\n        max_active_power = get_max_active_power(load1),\n        max_reactive_power = get_max_reactive_power(load1),\n        operation_cost = PSY.LoadCost(nothing),\n        base_power = get_base_power(load1),\n        conformity = get_conformity(load1),\n    )\n    add_component!(sys, interruptible_load)\n    for ts_key in get_time_series_keys(load1)\n        ts = get_time_series(load1, ts_key)\n        add_time_series!(\n            sys,\n            interruptible_load,\n            ts,\n        )\n    end\n    remove_component!(sys, load1)\nend\n\n# functions for adjusting power/cost curves and manipulating time series\n\"\"\"\nHelper function to tweak load powers, non-MBC generator powers, and non-MBC generator costs\nto exercise the generators we want to test.\n\nMultiplies {} for {} by {}:\n- max active power, all loads, load_pow_mult\n- active power limits, non-MBC ThermalStandard, therm_pow_mult\n- operational costs, non-MBC ThermalStandard, therm_price_mult\n\"\"\"\nfunction tweak_system!(sys::System, load_pow_mult, therm_pow_mult, therm_price_mult)\n    for load in get_components(PowerLoad, sys)\n        set_max_active_power!(load, get_max_active_power(load) * load_pow_mult)\n    end\n    # replace with type of component?\n    for therm in get_components(ThermalStandard, sys)\n        op_cost = get_operation_cost(therm)\n        op_cost isa MarketBidCost && continue\n        with_units_base(sys, UnitSystem.DEVICE_BASE) do\n            old_limits = get_active_power_limits(therm)\n            new_limits = (min = old_limits.min, max = old_limits.max * therm_pow_mult)\n            set_active_power_limits!(therm, new_limits)\n        end\n        if get_variable(op_cost) isa CostCurve{LinearCurve} ||\n           get_variable(op_cost) isa CostCurve{QuadraticCurve}\n            prop = get_proportional_term(get_value_curve(get_variable(op_cost)))\n            set_variable!(op_cost, CostCurve(LinearCurve(prop * therm_price_mult)))\n        elseif get_variable(op_cost) isa CostCurve{PiecewiseIncrementalCurve}\n            pwl = get_value_curve(get_variable(op_cost))\n            new_pwl = PiecewiseIncrementalCurve(\n                therm_price_mult * get_initial_input(pwl),\n                get_x_coords(pwl),\n                therm_price_mult * get_slopes(pwl),\n            )\n            set_variable!(op_cost, CostCurve(new_pwl))\n        else\n            error(\"Unhandled operation cost variable type $(typeof(get_variable(op_cost)))\")\n        end\n    end\nend\n\ntweak_for_startup_shutdown!(sys::System) = tweak_system!(sys::System, 0.8, 1.0, 1.0)\n\ntweak_for_decremental_initial!(sys::PSY.System) = tweak_system!(sys, 1.0, 1.2, 0.5)\n\n\"\"\"Transfer the market bid cost from old_comp to new_comp, copying any time series in the process.\"\"\"\nfunction transfer_mbc!(\n    new_comp::PSY.Device,\n    old_comp::PSY.Device,\n    new_sys::PSY.System,\n)\n    mbc = deepcopy(get_operation_cost(old_comp))\n    @assert mbc isa PSY.MarketBidCost\n    for field in fieldnames(PSY.MarketBidCost)\n        val = getfield(mbc, field)\n        if val isa IS.TimeSeriesKey\n            ts = PSY.get_time_series(old_comp, val)\n            new_ts_key = add_time_series!(new_sys, new_comp, deepcopy(ts))\n            setfield!(mbc, field, new_ts_key)\n        end\n    end\n    set_operation_cost!(new_comp, mbc)\n    return\nend\n\nfunction zero_out_startup_shutdown_costs!(comp::PSY.Device)\n    op_cost = get_operation_cost(comp)::MarketBidCost\n    set_start_up!(op_cost, (hot = 0.0, warm = 0.0, cold = 0.0))\n    set_shut_down!(op_cost, 0.0)\nend\n\n\"\"\"Set everything except the incremental_offer_curves to zero on the MarketBidCost attached to the unit.\"\"\"\nfunction zero_out_non_incremental_curve!(sys::PSY.System, unit::PSY.Component)\n    cost = deepcopy(get_operation_cost(unit)::MarketBidCost)\n    set_no_load_cost!(cost, 0.0)\n    set_start_up!(cost, (hot = 0.0, warm = 0.0, cold = 0.0))\n    set_shut_down!(cost, 0.0)\n    # set minimum generation cost (but not min gen power) to zero.\n    if get_incremental_offer_curves(cost) isa IS.TimeSeriesKey\n        zero_ts = make_deterministic_ts(sys, \"initial_input\", 0.0, 0.0, 0.0)\n        zero_ts_key = add_time_series!(sys, unit, zero_ts)\n        set_incremental_initial_input!(cost, zero_ts_key)\n    else\n        base_curve = get_value_curve(get_incremental_offer_curves(cost))\n        x_coords = get_x_coords(base_curve)\n        slopes = get_slopes(base_curve)\n        new_curve = PiecewiseIncrementalCurve(0.0, x_coords, slopes)\n        set_incremental_offer_curves!(cost, CostCurve(new_curve))\n    end\n    set_operation_cost!(unit, cost)\nend\n\n\"Set the no_load_cost to `nothing` and the initial_input to the old no_load_cost. Not designed for time series\"\nfunction no_load_to_initial_input!(comp::Generator)\n    cost = get_operation_cost(comp)::MarketBidCost\n    no_load = PSY.get_no_load_cost(cost)\n    old_fd = get_function_data(\n        get_value_curve(get_incremental_offer_curves(get_operation_cost(comp))),\n    )::IS.PiecewiseStepData\n    new_vc = PiecewiseIncrementalCurve(old_fd, no_load, nothing)\n    set_incremental_offer_curves!(get_operation_cost(comp), CostCurve(new_vc))\n    set_no_load_cost!(get_operation_cost(comp), nothing)\n    return\nend\n\nno_load_to_initial_input!(\n    sys::PSY.System,\n    sel = make_selector(x -> get_operation_cost(x) isa MarketBidCost, Generator),\n) = no_load_to_initial_input!.(get_components(sel, sys))\n\n\"Set all MBC thermal unit min active powers to their min breakpoints\"\nfunction adjust_min_power!(sys)\n    for comp in get_components(Union{ThermalStandard, ThermalMultiStart}, sys)\n        op_cost = get_operation_cost(comp)\n        op_cost isa MarketBidCost || continue\n        cost_curve = get_incremental_offer_curves(op_cost)::CostCurve\n        baseline = get_value_curve(cost_curve)::PiecewiseIncrementalCurve\n        x_coords = get_x_coords(get_function_data(baseline))\n        with_units_base(sys, UnitSystem.NATURAL_UNITS) do\n            set_active_power_limits!(comp, (min = first(x_coords), max = last(x_coords)))\n        end\n    end\nend\n\n\"\"\"\nAdd startup and shutdown time series to a certain component. `with_increments`: whether the\nelements should be increasing over time or constant. Version A: designed for\n`c_fixed_market_bid_cost`.\n\"\"\"\nfunction add_startup_shutdown_ts_a!(sys::System, with_increments::Bool)\n    res_incr = with_increments ? 0.05 : 0.0\n    interval_incr = with_increments ? 0.01 : 0.0\n    unit1 = get_component(ThermalStandard, sys, \"Test Unit1\")\n    @assert get_operation_cost(unit1) isa MarketBidCost\n    startup_ts_1 = make_deterministic_ts(\n        sys,\n        \"start_up\",\n        (1.0, 1.5, 2.0),\n        res_incr,\n        interval_incr,\n    )\n    set_start_up!(sys, unit1, startup_ts_1)\n    shutdown_ts_1 =\n        make_deterministic_ts(sys, \"shut_down\", 0.5, res_incr, interval_incr)\n    set_shut_down!(sys, unit1, shutdown_ts_1)\n    return startup_ts_1, shutdown_ts_1\nend\n\n\"\"\"\nAdd startup and shutdown time series to a certain component. `with_increments`: whether the\nelements should be increasing over time or constant. Version B: designed for `c_sys5_pglib`.\n\"\"\"\nfunction add_startup_shutdown_ts_b!(sys::System, with_increments::Bool)\n    res_incr = with_increments ? 0.05 : 0.0\n    interval_incr = with_increments ? 0.01 : 0.0\n    unit1 = get_component(ThermalMultiStart, sys, \"115_STEAM_1\")\n    base_startup = Tuple(get_start_up(get_operation_cost(unit1)))\n    base_shutdown = get_shut_down(get_operation_cost(unit1))\n    @assert get_operation_cost(unit1) isa MarketBidCost\n    startup_ts_1 = make_deterministic_ts(\n        sys,\n        \"start_up\",\n        base_startup,\n        res_incr,\n        interval_incr,\n    )\n    set_start_up!(sys, unit1, startup_ts_1)\n    shutdown_ts_1 =\n        make_deterministic_ts(\n            sys,\n            \"shut_down\",\n            base_shutdown,\n            res_incr,\n            interval_incr,\n        )\n    set_shut_down!(sys, unit1, shutdown_ts_1)\n    return startup_ts_1, shutdown_ts_1\nend\n\n# functions for building the systems: calls the above\n\nfunction load_and_fix_system(args...; kwargs...)\n    sys = Logging.with_logger(Logging.NullLogger()) do\n        build_system(args...; kwargs...)\n    end\n    no_load_to_initial_input!(sys)\n    adjust_min_power!(sys)\n    return sys\nend\n\n\"\"\"Create a system with for testing fixed market bid costs on thermal get_components.\"\"\"\nfunction load_sys_incr()\n    # NOTE we are using the fixed one so we can add time series ourselves\n    sys = load_and_fix_system(\n        PSITestSystems,\n        \"c_fixed_market_bid_cost\",\n    )\n    tweak_system!(sys, 1.05, 1.0, 1.0)\n    get_y_coords(\n        get_function_data(\n            get_value_curve(\n                get_incremental_offer_curves(\n                    get_operation_cost(get_component(ThermalStandard, sys, \"Test Unit2\")),\n                ),\n            ),\n        ),\n    )[1] *= 0.9\n    return sys\nend\n\n\"\"\"\nCreate a system with initial input and variable cost time series. Lots of options:\n\n# Arguments:\n  - `initial_varies`: whether the initial input time series should have values that vary\n    over time (as opposed to a time series with constant values over time)\n  - `breakpoints_vary`: whether the breakpoints in the variable cost time series should vary\n    over time\n  - `slopes_vary`: whether the slopes of the variable cost time series should vary over time\n  - `modify_baseline_pwl`: optional, a function to modify the baseline piecewise linear cost\n    `FunctionData` from which the variable cost time series is calculated\n  - `do_override_min_x`: whether to override the P1 to be equal to the minimum power in all\n    time steps\n  - `create_extra_tranches`: whether to create extra tranches in some time steps by\n    splitting one tranche into two\n  - `active_components`: a `ComponentSelector` specifying which components should get time\n    series\n  - `initial_input_names_vary`: whether the initial input time series names should vary over\n    components\n  - `variable_cost_names_vary`: whether the variable cost time series names should vary over\n    components\n\"\"\"\nfunction build_sys_incr(\n    initial_varies::Bool,\n    breakpoints_vary::Bool,\n    slopes_vary::Bool;\n    modify_baseline_pwl = nothing,\n    do_override_min_x = true,\n    create_extra_tranches = false,\n    active_components = SEL_INCR,\n    initial_input_names_vary = false,\n    variable_cost_names_vary = false,\n)\n    sys = load_sys_incr()\n    @assert !isempty(get_components(active_components, sys)) \"No components selected\"\n    extend_mbc!(\n        sys,\n        active_components;\n        initial_varies = initial_varies,\n        breakpoints_vary = breakpoints_vary,\n        slopes_vary = slopes_vary,\n        modify_baseline_pwl = modify_baseline_pwl,\n        do_override_min_x = do_override_min_x,\n        create_extra_tranches = create_extra_tranches,\n        initial_input_names_vary = initial_input_names_vary,\n        variable_cost_names_vary = variable_cost_names_vary,\n    )\n    return sys\nend\n\nfunction remove_thermal_mbcs!(sys::PSY.System)\n    for comp in get_components(ThermalStandard, sys)\n        old_cost = get_operation_cost(comp)\n        old_cost isa MarketBidCost || continue\n        new_op_cost = ThermalGenerationCost(;\n            variable = get_incremental_offer_curves(old_cost),\n            start_up = get_start_up(old_cost),\n            shut_down = get_shut_down(old_cost),\n            fixed = 0.0,\n        )\n        set_operation_cost!(comp, new_op_cost)\n    end\nend\n\nfunction zero_out_thermal_costs!(sys)\n    for comp in get_components(ThermalStandard, sys)\n        set_operation_cost!(\n            comp,\n            ThermalGenerationCost(;\n                variable = CostCurve(\n                    LinearCurve(0.0),\n                ),\n                start_up = (hot = 0.0, warm = 0.0, cold = 0.0),\n                shut_down = 0.0,\n                fixed = 0.0,\n            ),\n        )\n    end\nend\n\n\"\"\"Like `load_sys_incr` but for decremental MarketBidCost on ControllableLoad components.\"\"\"\nfunction load_sys_decr2()\n    sys = load_and_fix_system(\n        PSITestSystems,\n        \"c_fixed_market_bid_cost\",\n    )\n    replace_load_with_interruptible!(sys)\n    interruptible_load = first(get_components(PSY.InterruptiblePowerLoad, sys))\n    selector = make_selector(PSY.InterruptiblePowerLoad, get_name(interruptible_load))\n    add_mbc!(sys, selector; incremental = false, decremental = true)\n    # replace the MBCs on the thermals with ThermalCost objects.\n    remove_thermal_mbcs!(sys)\n    # makes the objective function/constraints simpler, easier to track down issues,\n    # but not actually needed.\n    zero_out_thermal_costs!(sys)\n    return sys\nend\n\n\"\"\"Like `build_sys_incr` but for decremental MarketBidCost on ControllableLoad components.\"\"\"\nfunction build_sys_decr2(\n    initial_varies::Bool,\n    breakpoints_vary::Bool,\n    slopes_vary::Bool;\n    modify_baseline_pwl = nothing,\n    do_override_min_x = true,\n    create_extra_tranches = false,\n    active_components = SEL_DECR,\n    initial_input_names_vary = false,\n    variable_cost_names_vary = false,\n)\n    sys = load_sys_decr2()\n    @assert !isempty(get_components(active_components, sys)) \"No components selected\"\n    extend_mbc!(\n        sys,\n        active_components;\n        initial_varies = initial_varies,\n        breakpoints_vary = breakpoints_vary,\n        slopes_vary = slopes_vary,\n        modify_baseline_pwl = modify_baseline_pwl,\n        do_override_min_x = do_override_min_x,\n        create_extra_tranches = create_extra_tranches,\n        initial_input_names_vary = initial_input_names_vary,\n        variable_cost_names_vary = variable_cost_names_vary,\n    )\n\n    # make the max_active_power time series constant.\n    il = first(get_components(PSY.InterruptiblePowerLoad, sys))\n    for ts_key in get_time_series_keys(il)\n        if get_name(ts_key) == \"max_active_power\"\n            max_active_power_ts = get_time_series(\n                first(get_components(PSY.InterruptiblePowerLoad, sys)),\n                ts_key,\n            )\n            max_max_active_power = maximum(maximum(values(max_active_power_ts.data)))\n            remove_time_series!(sys, Deterministic, il, \"max_active_power\")\n            new_ts = make_deterministic_ts(\n                sys,\n                \"max_active_power\",\n                max_max_active_power,\n                0.0,\n                0.0,\n            )\n            add_time_series!(sys, il, new_ts)\n            break\n        end\n    end\n    return sys\nend\n\nfunction create_multistart_sys(\n    with_increments::Bool,\n    load_pow_mult,\n    therm_pow_mult,\n    therm_price_mult;\n    add_ts = true,\n)\n    @assert add_ts || !with_increments\n    c_sys5_pglib = load_and_fix_system(PSITestSystems, \"c_sys5_pglib\")\n    tweak_system!(c_sys5_pglib, load_pow_mult, therm_pow_mult, therm_price_mult)\n    ms_comp = get_component(SEL_MULTISTART, c_sys5_pglib)\n    old_op = get_operation_cost(ms_comp)\n    old_ic = IncrementalCurve(get_value_curve(get_variable(old_op)))\n    new_ii = get_initial_input(old_ic) + get_fixed(old_op)\n    new_ic = IncrementalCurve(get_function_data(old_ic), new_ii, nothing)\n    set_operation_cost!(\n        ms_comp,\n        MarketBidCost(;\n            no_load_cost = nothing,\n            start_up = (hot = 300.0, warm = 450.0, cold = 500.0),\n            shut_down = 100.0,\n            incremental_offer_curves = CostCurve(new_ic),\n        ),\n    )\n\n    add_ts && add_startup_shutdown_ts_b!(c_sys5_pglib, with_increments)\n    return c_sys5_pglib\nend\n"
  },
  {
    "path": "test/test_utils/mock_operation_models.jl",
    "content": "# 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\n# models. Please refer to the documentation.\n\nstruct MockOperationProblem <: PSI.DefaultDecisionProblem end\nstruct MockEmulationProblem <: PSI.DefaultEmulationProblem end\n\nfunction PSI.DecisionModel(\n    ::Type{MockOperationProblem},\n    ::Type{T},\n    sys::PSY.System;\n    name = nothing,\n    kwargs...,\n) where {T <: PM.AbstractPowerModel}\n    settings = PSI.Settings(sys; kwargs...)\n    available_resolutions = PSY.get_time_series_resolutions(sys)\n    if length(available_resolutions) == 1\n        PSI.set_resolution!(settings, first(available_resolutions))\n    else\n        error(\"System has multiple resolutions MockOperationProblem won't work\")\n    end\n    return DecisionModel{MockOperationProblem}(\n        ProblemTemplate(T),\n        sys,\n        settings,\n        nothing;\n        name = name,\n    )\nend\n\nfunction make_mock_forecast(\n    horizon::Dates.TimePeriod,\n    resolution::Dates.TimePeriod,\n    interval::Dates.TimePeriod,\n    steps,\n)\n    init_time = DateTime(\"2024-01-01\")\n    timeseries_data = Dict{Dates.DateTime, Vector{Float64}}()\n    horizon_count = horizon ÷ resolution\n    for i in 1:steps\n        forecast_timestamps = init_time + interval * i\n        timeseries_data[forecast_timestamps] = rand(horizon_count)\n    end\n    return Deterministic(;\n        name = \"mock_forecast\",\n        data = timeseries_data,\n        resolution = resolution,\n    )\nend\n\nfunction make_mock_singletimeseries(horizon, resolution)\n    init_time = DateTime(\"2024-01-01\")\n    horizon_count = horizon ÷ resolution\n    tstamps = collect(range(init_time; length = horizon_count, step = resolution))\n    timeseries_data = TimeArray(tstamps, rand(horizon_count))\n    return SingleTimeSeries(; name = \"mock_timeseries\", data = timeseries_data)\nend\n\nfunction PSI.DecisionModel(::Type{MockOperationProblem}; name = nothing, kwargs...)\n    sys = System(100.0)\n    add_component!(sys, ACBus(nothing))\n    l = PowerLoad(nothing)\n    gen = ThermalStandard(nothing)\n    set_bus!(l, get_component(Bus, sys, \"init\"))\n    set_bus!(gen, get_component(Bus, sys, \"init\"))\n    add_component!(sys, l)\n    add_component!(sys, gen)\n    forecast = make_mock_forecast(\n        get(kwargs, :horizon, Hour(24)),\n        get(kwargs, :resolution, Hour(1)),\n        get(kwargs, :interval, Hour(1)),\n        get(kwargs, :steps, 2),\n    )\n    add_time_series!(sys, l, forecast)\n    settings = PSI.Settings(sys;\n        horizon = get(kwargs, :horizon, Hour(24)),\n        resolution = get(kwargs, :resolution, Hour(1)))\n    return DecisionModel{MockOperationProblem}(\n        ProblemTemplate(CopperPlatePowerModel),\n        sys,\n        settings,\n        nothing;\n        name = name,\n    )\nend\n\nfunction PSI.EmulationModel(::Type{MockEmulationProblem}; name = nothing, kwargs...)\n    sys = System(100.0)\n    add_component!(sys, ACBus(nothing))\n    l = PowerLoad(nothing)\n    gen = ThermalStandard(nothing)\n    set_bus!(l, get_component(Bus, sys, \"init\"))\n    set_bus!(gen, get_component(Bus, sys, \"init\"))\n    add_component!(sys, l)\n    add_component!(sys, gen)\n    single_ts = make_mock_singletimeseries(\n        get(kwargs, :horizon, Hour(24)),\n        get(kwargs, :resolution, Hour(1)),\n    )\n    add_time_series!(sys, l, single_ts)\n\n    settings = PSI.Settings(sys;\n        horizon = get(kwargs, :resolution, Hour(1)),\n        resolution = get(kwargs, :resolution, Hour(1)))\n    return EmulationModel{MockEmulationProblem}(\n        ProblemTemplate(CopperPlatePowerModel),\n        sys,\n        settings,\n        nothing;\n        name = name,\n    )\nend\n\n# Only used for testing\nfunction mock_construct_device!(\n    problem::PSI.DecisionModel{MockOperationProblem},\n    model;\n    built_for_recurrent_solves = false,\n    add_event_model = false,\n)\n    if add_event_model\n        device_type = typeof(model).parameters[1]\n        event_device = collect(get_components(device_type, PSI.get_system(problem)))[1]\n        transition_data = PSY.FixedForcedOutage(; outage_status = 0.0)\n        add_supplemental_attribute!(PSI.get_system(problem), event_device, transition_data)\n        mock_event_key = PowerSimulations.EventKey{FixedForcedOutage, device_type}(\"\")\n        mock_event_model = EventModel(\n            FixedForcedOutage,\n            PSI.ContinuousCondition(),\n        )\n        model.events = Dict(mock_event_key => mock_event_model)\n    end\n    set_device_model!(problem.template, model)\n    template = PSI.get_template(problem)\n    PSI.finalize_template!(template, PSI.get_system(problem))\n    PSI.validate_time_series!(problem)\n    #PSI.validate_template(problem)\n    PSI.init_optimization_container!(\n        PSI.get_optimization_container(problem),\n        PSI.get_network_model(template),\n        PSI.get_system(problem),\n    )\n    PSI.get_network_model(template).subnetworks =\n        PNM.find_subnetworks(PSI.get_system(problem))\n    PSI.get_optimization_container(problem).built_for_recurrent_solves =\n        built_for_recurrent_solves\n    PSI.initialize_system_expressions!(\n        PSI.get_optimization_container(problem),\n        PSI.get_network_model(template),\n        PSI.get_network_model(template).subnetworks,\n        PSI.get_branch_models(template),\n        PSI.get_system(problem),\n        Dict{Int64, Set{Int64}}(),\n    )\n    PSI.construct_device!(\n        PSI.get_optimization_container(problem),\n        PSI.get_system(problem),\n        PSI.ArgumentConstructStage(),\n        model,\n        PSI.get_network_model(template),\n    )\n    PSI.construct_device!(\n        PSI.get_optimization_container(problem),\n        PSI.get_system(problem),\n        PSI.ModelConstructStage(),\n        model,\n        PSI.get_network_model(template),\n    )\n\n    PSI.check_optimization_container(PSI.get_optimization_container(problem))\n\n    JuMP.@objective(\n        PSI.get_jump_model(problem),\n        MOI.MIN_SENSE,\n        PSI.get_objective_expression(\n            PSI.get_optimization_container(problem).objective_function,\n        )\n    )\n    return\nend\n\nfunction mock_construct_network!(problem::PSI.DecisionModel{MockOperationProblem}, model)\n    PSI.set_network_model!(problem.template, model)\n    PSI.construct_network!(\n        PSI.get_optimization_container(problem),\n        PSI.get_system(problem),\n        model,\n        problem.template.branches,\n    )\n    return\nend\n\nfunction mock_uc_ed_simulation_problems(uc_horizon, ed_horizon)\n    return SimulationModels([\n        DecisionModel(MockOperationProblem; horizon = uc_horizon, name = \"UC\"),\n        DecisionModel(\n            MockOperationProblem;\n            horizon = ed_horizon,\n            resolution = Minute(5),\n            name = \"ED\",\n        ),\n    ])\nend\n\nfunction create_simulation_build_test_problems(\n    template_uc = get_template_standard_uc_simulation(),\n    template_ed = get_template_nomin_ed_simulation(),\n    sys_uc = PSB.build_system(PSITestSystems, \"c_sys5_uc\"),\n    sys_ed = PSB.build_system(PSITestSystems, \"c_sys5_ed\"),\n)\n    return SimulationModels(;\n        decision_models = [\n            DecisionModel(template_uc, sys_uc; name = \"UC\", optimizer = HiGHS_optimizer),\n            DecisionModel(template_ed, sys_ed; name = \"ED\", optimizer = HiGHS_optimizer),\n        ],\n    )\nend\n\nstruct MockStagesStruct\n    stages::Dict{Int, Int}\nend\n\nfunction Base.show(io::IO, struct_stages::MockStagesStruct)\n    println(io, \"mock problem\")\n    return\nend\n\nfunction setup_ic_model_container!(model::DecisionModel)\n    # This function is only for testing purposes.\n    if !PSI.isempty(model)\n        PSI.reset!(model)\n    end\n\n    PSI.init_optimization_container!(\n        PSI.get_optimization_container(model),\n        PSI.get_network_model(PSI.get_template(model)),\n        PSI.get_system(model),\n    )\n\n    PSI.init_model_store_params!(model)\n\n    @info \"Make Initial Conditions Model\"\n    PSI.set_output_dir!(model, mktempdir(; cleanup = true))\n    PSI.build_initial_conditions!(model)\n    PSI.initialize!(model)\n    return\nend\n"
  },
  {
    "path": "test/test_utils/model_checks.jl",
    "content": "const GAEVF = JuMP.GenericAffExpr{Float64, VariableRef}\nconst GQEVF = JuMP.GenericQuadExpr{Float64, VariableRef}\n\nfunction moi_tests(\n    model::DecisionModel,\n    vars::Int,\n    interval::Int,\n    lessthan::Int,\n    greaterthan::Int,\n    equalto::Int,\n    binary::Bool,\n    lessthan_quadratic::Union{Int, Nothing} = nothing,\n)\n    JuMPmodel = PSI.get_jump_model(model)\n    @test JuMP.num_variables(JuMPmodel) == vars\n    @test JuMP.num_constraints(JuMPmodel, GAEVF, MOI.Interval{Float64}) == interval\n    @test JuMP.num_constraints(JuMPmodel, GAEVF, MOI.LessThan{Float64}) == lessthan\n    @test JuMP.num_constraints(JuMPmodel, GAEVF, MOI.GreaterThan{Float64}) == greaterthan\n    @test JuMP.num_constraints(JuMPmodel, GAEVF, MOI.EqualTo{Float64}) == equalto\n    @test ((JuMP.VariableRef, MOI.ZeroOne) in JuMP.list_of_constraint_types(JuMPmodel)) ==\n          binary\n    !isnothing(lessthan_quadratic) &&\n        @test JuMP.num_constraints(JuMPmodel, GQEVF, MOI.LessThan{Float64}) ==\n              lessthan_quadratic\n    return\nend\n\nfunction psi_constraint_test(\n    model::DecisionModel,\n    constraint_keys::Vector{<:PSI.ConstraintKey},\n)\n    constraints = PSI.get_constraints(model)\n    for con in constraint_keys\n        if get(constraints, con, nothing) !== nothing\n            # Ensure constraint container does not have undefined entries:\n            if typeof(constraints[con]) == DenseAxisArray\n                @test all(x -> isassigned(constraints[con], x), eachindex(constraints[con]))\n            else\n                @test true\n            end\n        else\n            @error con\n            @test false\n        end\n    end\n    return\nend\n\nfunction psi_aux_variable_test(\n    model::DecisionModel,\n    constraint_keys::Vector{<:PSI.AuxVarKey},\n)\n    op_container = PSI.get_optimization_container(model)\n    vars = PSI.get_aux_variables(op_container)\n    for key in constraint_keys\n        @test get(vars, key, nothing) !== nothing\n    end\n    return\nend\n\nfunction psi_checkbinvar_test(\n    model::DecisionModel,\n    bin_variable_keys::Vector{<:PSI.VariableKey},\n)\n    container = PSI.get_optimization_container(model)\n    for variable in bin_variable_keys\n        for v in PSI.get_variable(container, variable)\n            @test JuMP.is_binary(v)\n        end\n    end\n    return\nend\n\nfunction psi_checkobjfun_test(model::DecisionModel, exp_type)\n    model = PSI.get_jump_model(model)\n    @test JuMP.objective_function_type(model) == exp_type\n    return\nend\n\nfunction moi_lbvalue_test(\n    model::DecisionModel,\n    con_key::PSI.ConstraintKey,\n    value::Number,\n)\n    for con in PSI.get_constraints(model)[con_key]\n        @test JuMP.constraint_object(con).set.lower == value\n    end\n    return\nend\n\nfunction psi_checksolve_test(model::DecisionModel, status)\n    model = PSI.get_jump_model(model)\n    JuMP.optimize!(model)\n    @test termination_status(model) in status\nend\n\nfunction psi_checksolve_test(model::DecisionModel, status, expected_result, tol = 0.0)\n    res = solve!(model)\n    model = PSI.get_jump_model(model)\n    @test termination_status(model) in status\n    obj_value = JuMP.objective_value(model)\n    @test isapprox(obj_value, expected_result, atol = tol)\nend\n\nfunction psi_ptdf_lmps(res::OptimizationProblemResults, ptdf)\n    cp_duals = read_dual(\n        res,\n        PSI.ConstraintKey(CopperPlateBalanceConstraint, PSY.System);\n        table_format = TableFormat.WIDE,\n    )\n    λ = Matrix{Float64}(cp_duals[:, propertynames(cp_duals) .!= :DateTime])\n\n    flow_ub = read_dual(\n        res,\n        PSI.ConstraintKey(FlowRateConstraint, PSY.Line, \"ub\");\n        table_format = TableFormat.WIDE,\n    )\n    flow_lb = read_dual(\n        res,\n        PSI.ConstraintKey(FlowRateConstraint, PSY.Line, \"lb\");\n        table_format = TableFormat.WIDE,\n    )\n    arcs = PNM.get_arc_axis(ptdf)\n    nr = PNM.get_network_reduction_data(ptdf)\n    branch_names = [PSY.get_name(nr.direct_branch_map[arc]) for arc in arcs]\n    μ =\n        Matrix{Float64}(flow_ub[:, branch_names]) .+\n        Matrix{Float64}(flow_lb[:, branch_names])\n\n    buses = get_components(Bus, get_system(res))\n    lmps = OrderedDict()\n    for bus in buses\n        bus_number = get_number(bus)\n        ptdf_col = [ptdf[arc, bus_number] for arc in arcs]\n        lmps[get_name(bus)] = μ * ptdf_col\n    end\n    lmp = λ .+ DataFrames.DataFrame(lmps)\n    return lmp[!, sort(propertynames(lmp))]\nend\n\nfunction check_variable_unbounded(\n    model::DecisionModel,\n    ::Type{T},\n    ::Type{U},\n) where {T <: PSI.VariableType, U <: PSY.Component}\n    return check_variable_unbounded(model::DecisionModel, PSI.VariableKey(T, U))\nend\n\nfunction check_variable_unbounded(model::DecisionModel, var_key::PSI.VariableKey)\n    psi_cont = PSI.get_optimization_container(model)\n    variable = PSI.get_variable(psi_cont, var_key)\n    for var in variable\n        if JuMP.has_lower_bound(var) || JuMP.has_upper_bound(var)\n            return false\n        end\n    end\n    return true\nend\n\nfunction check_variable_bounded(\n    model::DecisionModel,\n    ::Type{T},\n    ::Type{U},\n) where {T <: PSI.VariableType, U <: PSY.Component}\n    return check_variable_bounded(model, PSI.VariableKey(T, U))\nend\n\nfunction check_variable_bounded(model::DecisionModel, var_key::PSI.VariableKey)\n    psi_cont = PSI.get_optimization_container(model)\n    variable = PSI.get_variable(psi_cont, var_key)\n    for var in variable\n        if !JuMP.has_lower_bound(var) || !JuMP.has_upper_bound(var)\n            return false\n        end\n    end\n    return true\nend\n\nfunction check_flow_variable_values(\n    model::DecisionModel,\n    ::Type{T},\n    ::Type{U},\n    device_name::String,\n    limit::Float64,\n) where {T <: PSI.VariableType, U <: PSY.Component}\n    psi_cont = PSI.get_optimization_container(model)\n    variable = PSI.get_variable(psi_cont, T(), U)\n    for var in variable[device_name, :]\n        if !(PSI.jump_value(var) <= (limit + 1e-2))\n            @error \"$device_name out of bounds $(PSI.jump_value(var))\"\n            return false\n        end\n    end\n    return true\nend\n\nfunction check_flow_variable_values(\n    model::DecisionModel,\n    ::Type{T},\n    ::Type{U},\n    device_name::String,\n    limit::Float64,\n) where {T <: PSI.FlowActivePowerVariable, U <: PSY.Component}\n    psi_cont = PSI.get_optimization_container(model)\n    template = model.template\n    device_model = PSI.get_model(template, U)\n    dev_formulation = PSI.get_formulation(device_model)\n    net_formulation = PSI.get_network_formulation(template)\n    if dev_formulation <: Union{PSI.StaticBranch, PSI.StaticBranchUnbounded} &&\n       net_formulation <: PSI.PTDFPowerModel\n        variable = PSI.get_expression(psi_cont, PSI.PTDFBranchFlow(), U)\n    else\n        variable = PSI.get_variable(psi_cont, T(), U)\n    end\n    for var in variable[device_name, :]\n        if !(PSI.jump_value(var) <= (limit + 1e-2))\n            @error \"$device_name out of bounds $(PSI.jump_value(var))\"\n            return false\n        end\n    end\n    return true\nend\n\nfunction check_flow_variable_values(\n    model::DecisionModel,\n    ::Type{T},\n    ::Type{U},\n    device_name::String,\n    limit_min::Float64,\n    limit_max::Float64,\n) where {T <: PSI.VariableType, U <: PSY.Component}\n    psi_cont = PSI.get_optimization_container(model)\n    variable = PSI.get_variable(psi_cont, T(), U)\n    for var in variable[device_name, :]\n        if !(PSI.jump_value(var) <= (limit_max + 1e-2)) ||\n           !(PSI.jump_value(var) >= (limit_min - 1e-2))\n            return false\n        end\n    end\n    return true\nend\n\nfunction check_flow_variable_values(\n    model::DecisionModel,\n    ::Type{T},\n    ::Type{U},\n    device_name::String,\n    limit_min::Float64,\n    limit_max::Float64,\n) where {T <: PSI.FlowActivePowerVariable, U <: PSY.Component}\n    psi_cont = PSI.get_optimization_container(model)\n    template = model.template\n    device_model = PSI.get_model(template, U)\n    dev_formulation = PSI.get_formulation(device_model)\n    net_formulation = PSI.get_network_formulation(template)\n    if dev_formulation <: Union{PSI.StaticBranch, PSI.StaticBranchUnbounded} &&\n       net_formulation <: PSI.PTDFPowerModel\n        variable = PSI.get_expression(psi_cont, PSI.PTDFBranchFlow(), U)\n    else\n        variable = PSI.get_variable(psi_cont, T(), U)\n    end\n    for var in variable[device_name, :]\n        if !(PSI.jump_value(var) <= (limit_max + 1e-2)) ||\n           !(PSI.jump_value(var) >= (limit_min - 1e-2))\n            return false\n        end\n    end\n    return true\nend\n\nfunction check_flow_variable_values(\n    model::DecisionModel,\n    ::Type{T},\n    ::Type{U},\n    ::Type{V},\n    device_name::String,\n    limit_min::Float64,\n    limit_max::Float64,\n) where {T <: PSI.VariableType, U <: PSI.VariableType, V <: PSY.Component}\n    psi_cont = PSI.get_optimization_container(model)\n    time_steps = PSI.get_time_steps(psi_cont)\n    pvariable = PSI.get_variable(psi_cont, T(), V)\n    qvariable = PSI.get_variable(psi_cont, U(), V)\n    for t in time_steps\n        fp = PSI.jump_value(pvariable[device_name, t])\n        fq = PSI.jump_value(qvariable[device_name, t])\n        flow = sqrt((fp)^2 + (fq)^2)\n        if !(flow <= (limit_max + 1e-2)^2) || !(flow >= (limit_min - 1e-2)^2)\n            return false\n        end\n    end\n    return true\nend\n\nfunction check_flow_variable_values(\n    model::DecisionModel,\n    ::Type{T},\n    ::Type{U},\n    ::Type{V},\n    device_name::String,\n    limit::Float64,\n) where {T <: PSI.VariableType, U <: PSI.VariableType, V <: PSY.Component}\n    psi_cont = PSI.get_optimization_container(model)\n    time_steps = PSI.get_time_steps(psi_cont)\n    pvariable = PSI.get_variable(psi_cont, T(), V)\n    qvariable = PSI.get_variable(psi_cont, U(), V)\n    for t in time_steps\n        fp = PSI.jump_value(pvariable[device_name, t])\n        fq = PSI.jump_value(qvariable[device_name, t])\n        flow = sqrt((fp)^2 + (fq)^2)\n        if !(flow <= (limit + 1e-2)^2)\n            return false\n        end\n    end\n    return true\nend\n\nfunction PSI.jump_value(int::Int)\n    @warn(\"This is for testing purposes only.\")\n    return int\nend\n\nfunction _check_constraint_bounds(bounds::PSI.ConstraintBounds, valid_bounds::NamedTuple)\n    @test bounds.coefficient.min == valid_bounds.coefficient.min\n    @test bounds.coefficient.max == valid_bounds.coefficient.max\n    @test bounds.rhs.min == valid_bounds.rhs.min\n    @test bounds.rhs.max == valid_bounds.rhs.max\nend\n\nfunction _check_variable_bounds(bounds::PSI.VariableBounds, valid_bounds::NamedTuple)\n    @test bounds.bounds.min == valid_bounds.min\n    @test bounds.bounds.max == valid_bounds.max\nend\n\nfunction check_duration_on_initial_conditions_values(\n    model,\n    ::Type{T},\n) where {T <: PSY.Component}\n    initial_conditions_data =\n        PSI.get_initial_conditions_data(PSI.get_optimization_container(model))\n    duration_on_data = PSI.get_initial_condition(\n        PSI.get_optimization_container(model),\n        InitialTimeDurationOn(),\n        T,\n    )\n    for ic in duration_on_data\n        name = PSY.get_name(ic.component)\n        on_var = PSI.get_initial_condition_value(initial_conditions_data, OnVariable(), T)[\n            name,\n            1,\n        ]\n        duration_on = PSI.jump_value(PSI.get_value(ic))\n        if on_var == 1.0 && PSY.get_status(ic.component)\n            @test duration_on == PSY.get_time_at_status(ic.component)\n        elseif on_var == 1.0 && !PSY.get_status(ic.component)\n            @test duration_on == 0.0\n        end\n    end\nend\n\nfunction check_duration_off_initial_conditions_values(\n    model,\n    ::Type{T},\n) where {T <: PSY.Component}\n    initial_conditions_data =\n        PSI.get_initial_conditions_data(PSI.get_optimization_container(model))\n    duration_off_data = PSI.get_initial_condition(\n        PSI.get_optimization_container(model),\n        InitialTimeDurationOff(),\n        T,\n    )\n    for ic in duration_off_data\n        name = PSY.get_name(ic.component)\n        on_var = PSI.get_initial_condition_value(initial_conditions_data, OnVariable(), T)[\n            name,\n            1,\n        ]\n        duration_off = PSI.jump_value(PSI.get_value(ic))\n        if on_var == 0.0 && !PSY.get_status(ic.component)\n            @test duration_off == PSY.get_time_at_status(ic.component)\n        elseif on_var == 0.0 && PSY.get_status(ic.component)\n            @test duration_off == 0.0\n        end\n    end\nend\n\nfunction check_energy_initial_conditions_values(model, ::Type{T}) where {T <: PSY.Component}\n    ic_data = PSI.get_initial_condition(\n        PSI.get_optimization_container(model),\n        InitialEnergyLevel(),\n        T,\n    )\n    for ic in ic_data\n        d = ic.component\n        name = PSY.get_name(ic.component)\n        e_value = PSI.jump_value(PSI.get_value(ic))\n        @test PSY.get_initial_storage_capacity_level(d) * PSY.get_storage_capacity(d) *\n              PSY.get_conversion_factor(d) == e_value\n    end\nend\n\nfunction check_energy_initial_conditions_values(model, ::Type{T}) where {T <: PSY.HydroGen}\n    ic_data = PSI.get_initial_condition(\n        PSI.get_optimization_container(model),\n        InitialEnergyLevel(),\n        T,\n    )\n    for ic in ic_data\n        name = PSY.get_name(ic.component)\n        e_value = PSI.jump_value(PSI.get_value(ic))\n        @test PSY.get_initial_storage(ic.component) == e_value\n    end\nend\n\nfunction check_status_initial_conditions_values(model, ::Type{T}) where {T <: PSY.Component}\n    initial_conditions =\n        PSI.get_initial_condition(PSI.get_optimization_container(model), DeviceStatus(), T)\n    initial_conditions_data =\n        PSI.get_initial_conditions_data(PSI.get_optimization_container(model))\n    for ic in initial_conditions\n        name = PSY.get_name(ic.component)\n        status = PSI.get_initial_condition_value(initial_conditions_data, OnVariable(), T)[\n            name,\n            1,\n        ]\n        @test PSI.jump_value(PSI.get_value(ic)) == status\n    end\nend\n\nfunction check_active_power_initial_condition_values(\n    model,\n    ::Type{T},\n) where {T <: PSY.Component}\n    initial_conditions =\n        PSI.get_initial_condition(PSI.get_optimization_container(model), DevicePower(), T)\n    initial_conditions_data =\n        PSI.get_initial_conditions_data(PSI.get_optimization_container(model))\n    for ic in initial_conditions\n        name = PSY.get_name(ic.component)\n        power = PSI.get_initial_condition_value(\n            initial_conditions_data,\n            ActivePowerVariable(),\n            T,\n        )[\n            name,\n            1,\n        ]\n        @test PSI.jump_value(PSI.get_value(ic)) == power\n    end\nend\n\nfunction check_active_power_abovemin_initial_condition_values(\n    model,\n    ::Type{T},\n) where {T <: PSY.Component}\n    initial_conditions = PSI.get_initial_condition(\n        PSI.get_optimization_container(model),\n        PSI.DeviceAboveMinPower(),\n        T,\n    )\n    initial_conditions_data =\n        PSI.get_initial_conditions_data(PSI.get_optimization_container(model))\n    for ic in initial_conditions\n        name = PSY.get_name(ic.component)\n        power = PSI.get_initial_condition_value(\n            initial_conditions_data,\n            PSI.PowerAboveMinimumVariable(),\n            T,\n        )[\n            name,\n            1,\n        ]\n        @test PSI.jump_value(PSI.get_value(ic)) == power\n    end\nend\n\nfunction check_initialization_variable_count(\n    model,\n    ::S,\n    ::Type{T},\n) where {S <: PSI.VariableType, T <: PSY.Component}\n    container = PSI.get_optimization_container(model)\n    initial_conditions_data = PSI.get_initial_conditions_data(container)\n    no_component = length(PSY.get_components(PSY.get_available, T, model.sys))\n    variable = PSI.get_initial_condition_value(initial_conditions_data, S(), T)\n    rows, cols = size(variable)\n    @test rows * cols == no_component * PSI.INITIALIZATION_PROBLEM_HORIZON_COUNT\nend\n\nfunction check_variable_count(\n    model,\n    ::S,\n    ::Type{T},\n) where {S <: PSI.VariableType, T <: PSY.Component}\n    no_component = length(PSY.get_components(PSY.get_available, T, model.sys))\n    time_steps = PSI.get_time_steps(PSI.get_optimization_container(model))[end]\n    variable = PSI.get_variable(PSI.get_optimization_container(model), S(), T)\n    @test length(variable) == no_component * time_steps\nend\n\nfunction check_initialization_constraint_count(\n    model,\n    ::S,\n    ::Type{T};\n    filter_func = PSY.get_available,\n    meta = PSI.ISOPT.CONTAINER_KEY_EMPTY_META,\n) where {S <: PSI.ConstraintType, T <: PSY.Component}\n    container =\n        ISOPT.get_initial_conditions_model_container(PSI.get_internal(model))\n    no_component = length(PSY.get_components(filter_func, T, model.sys))\n    time_steps = PSI.get_time_steps(container)[end]\n    constraint = PSI.get_constraint(container, S(), T, meta)\n    @test length(constraint) == no_component * time_steps\nend\n\nfunction check_constraint_count(\n    model,\n    ::S,\n    ::Type{T};\n    filter_func = PSY.get_available,\n    meta = PSI.ISOPT.CONTAINER_KEY_EMPTY_META,\n) where {S <: PSI.ConstraintType, T <: PSY.Component}\n    no_component = length(PSY.get_components(filter_func, T, model.sys))\n    time_steps = PSI.get_time_steps(PSI.get_optimization_container(model))[end]\n    constraint = PSI.get_constraint(PSI.get_optimization_container(model), S(), T, meta)\n    @test length(constraint) == no_component * time_steps\nend\n\nfunction check_constraint_count(\n    model,\n    ::PSI.RampConstraint,\n    ::Type{T},\n) where {T <: PSY.Component}\n    container = PSI.get_optimization_container(model)\n    device_name_set =\n        PSY.get_name.(\n            PSI._get_ramp_constraint_devices(\n                container,\n                get_components(PSY.get_available, T, model.sys),\n            ),\n        )\n    check_constraint_count(\n        model,\n        PSI.RampConstraint(),\n        T;\n        meta = \"up\",\n        filter_func = x -> x.name in device_name_set,\n    )\n    check_constraint_count(\n        model,\n        PSI.RampConstraint(),\n        T;\n        meta = \"dn\",\n        filter_func = x -> x.name in device_name_set,\n    )\n    return\nend\n\nfunction check_constraint_count(\n    model,\n    ::PSI.DurationConstraint,\n    ::Type{T},\n) where {T <: PSY.Component}\n    container = PSI.get_optimization_container(model)\n    resolution = PSI.get_resolution(container)\n    steps_per_hour = 60 / Dates.value(Dates.Minute(resolution))\n    fraction_of_hour = 1 / steps_per_hour\n    duration_devices = filter!(\n        x -> !(\n            PSY.get_time_limits(x).up <= fraction_of_hour &&\n            PSY.get_time_limits(x).down <= fraction_of_hour\n        ),\n        collect(get_components(PSY.get_available, T, model.sys)),\n    )\n    device_name_set = PSY.get_name.(duration_devices)\n    check_constraint_count(\n        model,\n        PSI.DurationConstraint(),\n        T;\n        meta = \"up\",\n        filter_func = x -> x.name in device_name_set,\n    )\n    return check_constraint_count(\n        model,\n        PSI.DurationConstraint(),\n        T;\n        meta = \"dn\",\n        filter_func = x -> x.name in device_name_set,\n    )\nend\n"
  },
  {
    "path": "test/test_utils/operations_problem_templates.jl",
    "content": "const NETWORKS_FOR_TESTING = [\n    (PM.ACPPowerModel, fast_ipopt_optimizer),\n    (PM.ACRPowerModel, fast_ipopt_optimizer),\n    (PM.ACTPowerModel, fast_ipopt_optimizer),\n    #(PM.IVRPowerModel, fast_ipopt_optimizer), #instantiate_ivp_expr_model not implemented\n    (PM.DCPPowerModel, fast_ipopt_optimizer),\n    (PM.DCMPPowerModel, fast_ipopt_optimizer),\n    (PM.NFAPowerModel, fast_ipopt_optimizer),\n    (PM.DCPLLPowerModel, fast_ipopt_optimizer),\n    (PM.LPACCPowerModel, fast_ipopt_optimizer),\n    (PM.SOCWRPowerModel, fast_ipopt_optimizer),\n    (PM.SOCWRConicPowerModel, scs_solver),\n    (PM.QCRMPowerModel, fast_ipopt_optimizer),\n    (PM.QCLSPowerModel, fast_ipopt_optimizer),\n    #(PM.SOCBFPowerModel, fast_ipopt_optimizer), # not implemented\n    (PM.BFAPowerModel, fast_ipopt_optimizer),\n    #(PM.SOCBFConicPowerModel, fast_ipopt_optimizer), # not implemented\n    (PM.SDPWRMPowerModel, scs_solver),\n]\n\nfunction get_thermal_standard_uc_template()\n    template = ProblemTemplate(CopperPlatePowerModel)\n    set_device_model!(template, PowerLoad, StaticPowerLoad)\n    set_device_model!(template, ThermalStandard, ThermalStandardUnitCommitment)\n    return template\nend\n\nfunction get_thermal_dispatch_template_network(network = CopperPlatePowerModel)\n    template = ProblemTemplate(network)\n    set_device_model!(template, ThermalStandard, ThermalBasicDispatch)\n    set_device_model!(template, PowerLoad, StaticPowerLoad)\n    set_device_model!(template, MonitoredLine, StaticBranchBounds)\n    set_device_model!(template, Line, StaticBranch)\n    set_device_model!(template, Transformer2W, StaticBranch)\n    set_device_model!(template, TapTransformer, StaticBranch)\n    set_device_model!(template, TwoTerminalGenericHVDCLine, HVDCTwoTerminalLossless)\n    return template\nend\n\nfunction get_template_basic_uc_simulation()\n    template = ProblemTemplate(CopperPlatePowerModel)\n    set_device_model!(template, ThermalStandard, ThermalBasicUnitCommitment)\n    set_device_model!(template, RenewableDispatch, RenewableFullDispatch)\n    set_device_model!(template, PowerLoad, StaticPowerLoad)\n    set_device_model!(template, InterruptiblePowerLoad, StaticPowerLoad)\n    set_device_model!(template, HydroTurbine, HydroTurbineEnergyDispatch)\n    set_device_model!(template, HydroReservoir, HydroEnergyModelReservoir)\n    return template\nend\n\nfunction get_template_standard_uc_simulation()\n    template = get_template_basic_uc_simulation()\n    set_device_model!(template, ThermalStandard, ThermalStandardUnitCommitment)\n    return template\nend\n\nfunction get_template_nomin_ed_simulation(network = CopperPlatePowerModel)\n    template = ProblemTemplate(network)\n    set_device_model!(template, ThermalStandard, ThermalDispatchNoMin)\n    set_device_model!(template, RenewableDispatch, RenewableFullDispatch)\n    set_device_model!(template, PowerLoad, StaticPowerLoad)\n    set_device_model!(template, InterruptiblePowerLoad, PowerLoadDispatch)\n    set_device_model!(template, HydroTurbine, HydroTurbineEnergyDispatch)\n    set_device_model!(template, HydroReservoir, HydroEnergyModelReservoir)\n    return template\nend\n\nfunction get_template_hydro_st_uc(network = CopperPlatePowerModel)\n    template = ProblemTemplate(network)\n    set_device_model!(template, ThermalStandard, ThermalStandardUnitCommitment),\n    set_device_model!(template, RenewableDispatch, RenewableFullDispatch),\n    set_device_model!(template, PowerLoad, StaticPowerLoad),\n    set_device_model!(template, InterruptiblePowerLoad, PowerLoadDispatch),\n    set_device_model!(template, HydroTurbine, HydroTurbineEnergyDispatch)\n    set_device_model!(template, HydroReservoir, HydroEnergyModelReservoir)\n    return template\nend\n\nfunction get_template_hydro_st_ed(network = CopperPlatePowerModel, duals = [])\n    template = ProblemTemplate(network)\n    set_device_model!(template, ThermalStandard, ThermalBasicDispatch)\n    set_device_model!(template, RenewableDispatch, RenewableFullDispatch)\n    set_device_model!(template, PowerLoad, StaticPowerLoad)\n    set_device_model!(template, InterruptiblePowerLoad, PowerLoadDispatch)\n    set_device_model!(template, HydroTurbine, HydroTurbineEnergyDispatch)\n    set_device_model!(template, HydroReservoir, HydroEnergyModelReservoir)\n    return template\nend\n\nfunction get_template_dispatch_with_network(network = PTDFPowerModel)\n    template = ProblemTemplate(network)\n    set_device_model!(template, PowerLoad, StaticPowerLoad)\n    set_device_model!(template, ThermalStandard, ThermalBasicDispatch)\n    set_device_model!(template, Line, StaticBranch)\n    set_device_model!(template, Transformer2W, StaticBranchBounds)\n    set_device_model!(template, TapTransformer, StaticBranchBounds)\n    set_device_model!(template, TwoTerminalGenericHVDCLine, HVDCTwoTerminalLossless)\n    return template\nend\n"
  },
  {
    "path": "test/test_utils/run_simulation.jl",
    "content": "function load_pf_export(root, export_subdir)\n    raw_path, md_path = get_psse_export_paths(export_subdir)\n    sys = System(joinpath(root, raw_path), JSON3.read(joinpath(root, md_path), Dict))\n    set_units_base_system!(sys, \"NATURAL_UNITS\")\n    return sys\nend\n\nfunction run_simulation(\n    c_sys5_hy_uc,\n    c_sys5_hy_ed,\n    file_path::String,\n    export_path;\n    in_memory = false,\n    uc_network_model = nothing,\n    ed_network_model = nothing,\n    store_systems_in_results = true,\n)\n    template_uc = get_template_basic_uc_simulation()\n    template_ed = get_template_nomin_ed_simulation()\n    isnothing(uc_network_model) && (\n        uc_network_model =\n            NetworkModel(CopperPlatePowerModel; duals = [CopperPlateBalanceConstraint])\n    )\n    isnothing(ed_network_model) && (\n        ed_network_model =\n            NetworkModel(\n                CopperPlatePowerModel;\n                duals = [CopperPlateBalanceConstraint],\n                use_slacks = true,\n            )\n    )\n    set_device_model!(template_ed, InterruptiblePowerLoad, StaticPowerLoad)\n    set_network_model!(\n        template_uc,\n        uc_network_model,\n    )\n    set_network_model!(\n        template_ed,\n        ed_network_model,\n    )\n    models = SimulationModels(;\n        decision_models = [\n            DecisionModel(\n                template_uc,\n                c_sys5_hy_uc;\n                name = \"UC\",\n                optimizer = HiGHS_optimizer,\n            ),\n            DecisionModel(\n                template_ed,\n                c_sys5_hy_ed;\n                name = \"ED\",\n                optimizer = ipopt_optimizer,\n            ),\n        ],\n    )\n\n    sequence = SimulationSequence(;\n        models = models,\n        feedforwards = Dict(\n            \"ED\" => [\n                SemiContinuousFeedforward(;\n                    component_type = ThermalStandard,\n                    source = OnVariable,\n                    affected_values = [ActivePowerVariable],\n                ),\n            ],\n        ),\n        ini_cond_chronology = InterProblemChronology(),\n    )\n    sim = Simulation(;\n        name = \"no_cache\",\n        steps = 2,\n        models = models,\n        sequence = sequence,\n        simulation_folder = file_path,\n    )\n\n    build_out = build!(\n        sim;\n        console_level = Logging.Error,\n        store_systems_in_results = store_systems_in_results,\n    )\n    @test build_out == PSI.SimulationBuildStatus.BUILT\n\n    exports = Dict(\n        \"models\" => [\n            Dict(\n                \"name\" => \"UC\",\n                \"store_all_variables\" => true,\n                \"store_all_parameters\" => true,\n                \"store_all_duals\" => true,\n                \"store_all_aux_variables\" => true,\n            ),\n            Dict(\n                \"name\" => \"ED\",\n                \"store_all_variables\" => true,\n                \"store_all_parameters\" => true,\n                \"store_all_duals\" => true,\n                \"store_all_aux_variables\" => true,\n            ),\n        ],\n        \"path\" => export_path,\n        \"optimizer_stats\" => true,\n    )\n    execute_out = execute!(sim; exports = exports, in_memory = in_memory)\n    @test execute_out == PSI.RunStatus.SUCCESSFULLY_FINALIZED\n\n    return sim\nend\n"
  },
  {
    "path": "test/test_utils/solver_definitions.jl",
    "content": "# Solvers\nusing Ipopt\nusing SCS\nusing HiGHS\n\nipopt_optimizer =\n    JuMP.optimizer_with_attributes(Ipopt.Optimizer, \"tol\" => 1e-6, \"print_level\" => 0)\nfast_ipopt_optimizer = JuMP.optimizer_with_attributes(\n    Ipopt.Optimizer,\n    \"print_level\" => 0,\n    \"max_cpu_time\" => 5.0,\n)\n# use default print_level = 5 # set to 0 to disable\nscs_solver = JuMP.optimizer_with_attributes(\n    SCS.Optimizer,\n    \"max_iters\" => 100000,\n    \"eps_infeas\" => 1e-4,\n    \"verbose\" => 0,\n)\n\nHiGHS_optimizer = JuMP.optimizer_with_attributes(\n    HiGHS.Optimizer,\n    \"time_limit\" => 100.0,\n    \"random_seed\" => 12345,\n    \"log_to_console\" => false,\n)\n\nHiGHS_optimizer_small_gap = JuMP.optimizer_with_attributes(\n    HiGHS.Optimizer,\n    \"time_limit\" => 100.0,\n    \"random_seed\" => 12345,\n    \"mip_rel_gap\" => 0.001,\n    \"log_to_console\" => false,\n)\n"
  },
  {
    "path": "test/test_utils.jl",
    "content": "@testset \"test key with value\" begin\n    d = Dict(\"foo\" => \"bar\")\n    @test_throws ErrorException PSI.find_key_with_value(d, \"fake\")\nend\n\n@testset \"Test ProgressMeter\" begin\n    @test !PSI.progress_meter_enabled()\nend\n\n@testset \"Axis Array to DataFrame\" begin\n    # The to_dataframe test the use of the `to_matrix` and `get_column_names` methods\n    one = PSI.DenseAxisArray{Float64}(undef, 1:2)\n    fill!(one, 1.0)\n    mock_key = PSI.VariableKey(ActivePowerVariable, ThermalStandard)\n    one_df = PSI.to_dataframe(one, mock_key)\n    test_df = DataFrames.DataFrame(ISOPT.encode_key(mock_key) => [1.0, 1.0])\n    @test one_df == test_df\n\n    two = PSI.DenseAxisArray{Float64}(undef, [\"a\"], 1:2)\n    fill!(two, 1.0)\n    two_df = PSI.to_dataframe(two, mock_key)\n    test_df = DataFrames.DataFrame(:a => [1.0, 1.0])\n    @test two_df == test_df\n\n    three = PSI.DenseAxisArray{Float64}(undef, [\"a\"], 1:2, 1:3)\n    fill!(three, 1.0)\n    @test_throws MethodError PSI.to_dataframe(three, mock_key)\n\n    four = PSI.DenseAxisArray{Float64}(undef, [\"a\"], 1:2, 1:3, 1:5)\n    fill!(three, 1.0)\n    @test_throws MethodError PSI.to_dataframe(four, mock_key)\n\n    sparse_num =\n        JuMP.Containers.@container([i = 1:10, j = (i + 1):10, t = 1:24], 0.0 + i + j + t)\n    @test_throws MethodError PSI.to_dataframe(sparse_num, mock_key)\n\n    i_num = 1:10\n    j_vals = Dict(\"$i\" => string.((i + 1):11) for i in i_num)\n    sparse_valid =\n        JuMP.Containers.@container([i = string.(i_num), j = j_vals[i], t = 1:24], rand())\n    df = PSI.to_dataframe(sparse_valid, mock_key)\n    @test size(df) == (24, 55)\nend\n\n@testset \"Test simulation output directory name\" begin\n    tmpdir = mktempdir()\n    name = \"simulation\"\n    dir1 = mkdir(joinpath(tmpdir, name))\n    @test PSI._get_output_dir_name(tmpdir, name) == joinpath(tmpdir, name * \"-2\")\n    dir2 = mkdir(joinpath(tmpdir, name * \"-2\"))\n    @test PSI._get_output_dir_name(tmpdir, name) == joinpath(tmpdir, name * \"-3\")\nend\n"
  }
]