From d4547f6a00a5e15854b4194fe2c28d2f8f51b3da Mon Sep 17 00:00:00 2001
From: kdayday
Date: Wed, 18 Feb 2026 21:13:10 -0700
Subject: [PATCH 01/46] Update documenter and formatter and add docstrings
---
docs/Project.toml | 3 +-
docs/make.jl | 47 ++++---
docs/src/api/public.md | 184 +++++++++++++++++++++++++++-
scripts/formatter/formatter_code.jl | 52 +++-----
src/core/constraints.jl | 126 ++++++++++++++++++-
src/core/decision_models.jl | 31 +++++
src/core/formulations.jl | 30 +++++
src/core/parameters.jl | 43 +++++++
src/core/variables.jl | 70 +++++++++++
src/feedforwards.jl | 18 +++
10 files changed, 549 insertions(+), 55 deletions(-)
diff --git a/docs/Project.toml b/docs/Project.toml
index aad3e873..775cd548 100644
--- a/docs/Project.toml
+++ b/docs/Project.toml
@@ -1,9 +1,10 @@
[deps]
DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8"
Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4"
+DocumenterInterLinks = "d12716ef-a0f6-4df4-a9f1-a5a34e75c656"
DocumenterTools = "35a29f4d-8980-5a13-9543-d66fff28ecb8"
HybridSystemsSimulations = "bed98974-b02a-5e2f-9ee0-a103f5c450dd"
[compat]
-Documenter = "0.27"
+Documenter = "1.0"
julia = "^1.6"
diff --git a/docs/make.jl b/docs/make.jl
index 0670e4bb..29c12801 100644
--- a/docs/make.jl
+++ b/docs/make.jl
@@ -1,5 +1,14 @@
-using Documenter, HybridSystemsSimulations
-import DataStructures: OrderedDict
+using Documenter
+using HybridSystemsSimulations
+using DataStructures
+using DocumenterInterLinks
+
+links = InterLinks(
+ "Julia" => "https://docs.julialang.org/en/v1/",
+ "InfrastructureSystems" => "https://nrel-sienna.github.io/InfrastructureSystems.jl/stable/",
+ "PowerSystems" => "https://nrel-sienna.github.io/PowerSystems.jl/stable/",
+ "PowerSimulations" => "https://nrel-sienna.github.io/PowerSimulations.jl/stable/",
+)
pages = OrderedDict(
"Welcome Page" => "index.md",
@@ -9,23 +18,25 @@ pages = OrderedDict(
"Internal API Reference" => "api/internal.md",
)
-makedocs(
- modules=[HybridSystemsSimulations],
- format=Documenter.HTML(;
- mathengine=Documenter.MathJax(),
- prettyurls=haskey(ENV, "GITHUB_ACTIONS"),
+makedocs(;
+ modules = [HybridSystemsSimulations],
+ format = Documenter.HTML(;
+ mathengine = Documenter.MathJax(),
+ prettyurls = haskey(ENV, "GITHUB_ACTIONS"),
+ size_threshold = nothing,
),
- sitename="HybridSystemsSimulations.jl",
- authors="Jose Daniel Lara, Rodrigo Henriquez-Auba",
- pages=Any[p for p in pages],
+ sitename = "HybridSystemsSimulations.jl",
+ authors = "Jose Daniel Lara, Rodrigo Henriquez-Auba",
+ pages = Any[p for p in pages],
+ plugins = [links],
)
-deploydocs(
- repo="github.com/NREL-Sienna/HybridSystemsSimulations.jl.git",
- target="build",
- branch="gh-pages",
- devbranch="main",
- devurl="dev",
- push_preview=true,
- versions=["stable" => "v^", "v#.#"],
+deploydocs(;
+ repo = "github.com/NREL-Sienna/HybridSystemsSimulations.jl.git",
+ target = "build",
+ branch = "gh-pages",
+ devbranch = "main",
+ devurl = "dev",
+ push_preview = true,
+ versions = ["stable" => "v^", "v#.#"],
)
diff --git a/docs/src/api/public.md b/docs/src/api/public.md
index 9aeb00f2..5387f3eb 100644
--- a/docs/src/api/public.md
+++ b/docs/src/api/public.md
@@ -1,6 +1,184 @@
+```@meta
+CurrentModule = HybridSystemsSimulations
+DocTestSetup = quote
+ using HybridSystemsSimulations
+end
+```
+
# Public API Reference
-```@autodocs
-Modules = [HybridSystemsSimulations]
-Public = true
+```@contents
+Pages = ["public.md"]
+Depth = 3
+```
+
+```@raw html
+
+
+```
+
+## Device Formulations
+
+Device formulations for hybrid systems (single PCC with renewable, thermal, and storage).
+Use with [`PowerSimulations.DeviceModel`](@extref PowerSimulations.DeviceModel) for unit
+commitment or economic dispatch.
+
+```@docs
+HybridDispatchWithReserves
+HybridEnergyOnlyDispatch
+HybridFixedDA
+```
+
+```@raw html
+
+
+```
+
+* * *
+
+## Decision Models
+
+Decision problem types for merchant hybrid participation in day-ahead and real-time markets.
+
+```@docs
+MerchantHybridEnergyCase
+MerchantHybridEnergyFixedDA
+MerchantHybridCooptimizerCase
+MerchantHybridBilevelCase
+```
+
+```@raw html
+
+
+```
+
+* * *
+
+## Variables
+
+### Energy Bids
+
+Day-ahead and real-time energy bid/offer variables at the PCC.
+
+```@docs
+EnergyDABidOut
+EnergyDABidIn
+EnergyRTBidOut
+EnergyRTBidIn
+```
+
+### Ancillary Service Bids
+
+Day-ahead ancillary service bid/offer variables at the PCC.
+
+```@docs
+BidReserveVariableOut
+BidReserveVariableIn
+```
+
+### Reserve Variables
+
+Reserve quantities allocated to the hybrid's internal assets and total reserve.
+
+```@docs
+ReserveVariableOut
+ReserveVariableIn
+TotalReserve
+```
+
+```@raw html
+
+
+```
+
+* * *
+
+## Feedforwards
+
+Feedforwards for hybrid storage cycle limits in recurrent simulations.
+
+```@docs
+CyclingChargeLimitFeedforward
+CyclingDischargeLimitFeedforward
+```
+
+```@raw html
+
+
+```
+
+* * *
+
+## Constraints
+
+### Dual Optimality Conditions
+
+KKT stationarity constraints for the merchant (lower-level) model; used in bilevel/MPEC formulations.
+
+```@docs
+OptConditionThermalPower
+OptConditionRenewablePower
+OptConditionBatteryCharge
+OptConditionBatteryDischarge
+OptConditionEnergyVariable
+```
+
+### Complementary Slackness
+
+Complementary slackness constraints for MPEC/bilevel reformulation. Each upper-bound (Ub)
+constraint has a corresponding lower-bound (Lb) variant.
+
+```@docs
+ComplementarySlacknessEnergyAssetBalanceUb
+ComplementarySlacknessEnergyAssetBalanceLb
+ComplementarySlacknessRenewableActivePowerLimitConstraintUb
+```
+```@docs; canonical=false
+ComplementarySlacknessRenewableActivePowerLimitConstraintLb
+```
+```@docs
+ComplementarySlacknessBatteryStatusDischargeOnUb
+ComplementarySlacknessBatteryStatusDischargeOnLb
+ComplementarySlacknessBatteryStatusChargeOnUb
+ComplementarySlacknessBatteryStatusChargeOnLb
+ComplementarySlacknessBatteryBalanceUb
+ComplementarySlacknessBatteryBalanceLb
+ComplentarySlacknessCyclingCharge
+ComplentarySlacknessCyclingDischarge
+ComplementarySlacknessEnergyLimitUb
+ComplementarySlacknessEnergyLimitLb
+```
+
+### Strong Duality
+
+```@docs
+StrongDualityCut
+```
+
+```@raw html
+
+
+```
+
+* * *
+
+## Parameters
+
+### Objective Function Parameters
+
+Price parameters used in the merchant objective (DA/RT energy and ancillary services).
+
+```@docs
+DayAheadEnergyPrice
+RealTimeEnergyPrice
+AncillaryServicePrice
+```
+
+### Variable Value Parameters
+
+Parameters for storage cycle limits (used with feedforwards in recurrent runs).
+
+```@docs
+CyclingChargeLimitParameter
+CyclingDischargeLimitParameter
```
diff --git a/scripts/formatter/formatter_code.jl b/scripts/formatter/formatter_code.jl
index 2caa2698..4221a325 100644
--- a/scripts/formatter/formatter_code.jl
+++ b/scripts/formatter/formatter_code.jl
@@ -1,41 +1,29 @@
using Pkg
Pkg.activate(@__DIR__)
Pkg.instantiate()
-using JuliaFormatter
+Pkg.update()
-main_paths = [".", "./docs/src"]
-for main_path in main_paths
- format(
- main_path;
- whitespace_ops_in_indices=true,
- remove_extra_newlines=true,
- verbose=true,
- always_for_in=true,
- whitespace_typedefs=true,
- whitespace_in_kwargs=false,
- format_docstrings=true,
- always_use_return=false, # removed since it has false positives.
- )
-end
+using JuliaFormatter
-# Documentation Formatter
-main_paths = ["./docs/src"]
+main_paths = ["."]
for main_path in main_paths
- for folder in readdir(main_path)
- @show folder_path = joinpath(main_path, folder)
- if isfile(folder_path)
- !occursin(".md", folder_path) && continue
+ for (root, dir, files) in walkdir(main_path)
+ for f in files
+ @show file_path = abspath(root, f)
+ !((occursin(".jl", f) || occursin(".md", f))) && continue
+ format(file_path;
+ whitespace_ops_in_indices = true,
+ remove_extra_newlines = true,
+ verbose = true,
+ always_for_in = true,
+ whitespace_typedefs = true,
+ conditional_to_if = true,
+ join_lines_based_on_source = true,
+ separate_kwargs_with_semicolon = true,
+ format_markdown = true,
+ # ignore = [ ],
+ # always_use_return = true. # Disabled since it throws a lot of false positives
+ )
end
- format(
- folder_path;
- format_markdown=true,
- whitespace_ops_in_indices=true,
- remove_extra_newlines=true,
- verbose=true,
- always_for_in=true,
- whitespace_typedefs=true,
- whitespace_in_kwargs=false,
- # always_use_return = true # removed since it has false positives.
- )
end
end
diff --git a/src/core/constraints.jl b/src/core/constraints.jl
index 11b38b1b..6e531cb5 100644
--- a/src/core/constraints.jl
+++ b/src/core/constraints.jl
@@ -79,31 +79,155 @@ struct FeedForwardCyclingDischargeConstraint <: PSI.ConstraintType end
### Dual Optimality Conditions Constraints ###
##############################################
# Names track the variable types in variables.jl
+"""
+ OptConditionThermalPower
+
+Constraint enforcing KKT stationarity for thermal power in the merchant (lower-level)
+model: links dual of thermal limits (``\\mu^{\\text{ThUb}}``, ``\\mu^{\\text{ThLb}}``) to the thermal power variable.
+Used in bilevel/MPEC formulations.
+"""
struct OptConditionThermalPower <: PSI.ConstraintType end
+
+"""
+ OptConditionRenewablePower
+
+Constraint enforcing KKT stationarity for renewable power (``p_{\\text{re},t}``) in the merchant
+model; ties duals of renewable limit (``\\mu^{\\text{ReUb}}``, ``\\mu^{\\text{ReLb}}``) to the renewable power variable.
+"""
struct OptConditionRenewablePower <: PSI.ConstraintType end
+
+"""
+ OptConditionBatteryCharge
+
+Constraint enforcing KKT stationarity for storage charging (``p_{\\text{ch},t}``) in the merchant
+model; involves duals ``\\mu^{\\text{ChUb}}``, ``\\mu^{\\text{ChLb}}`` and charge limits.
+"""
struct OptConditionBatteryCharge <: PSI.ConstraintType end
+
+"""
+ OptConditionBatteryDischarge
+
+Constraint enforcing KKT stationarity for storage discharging (``p_{\\text{ds},t}``) in the merchant
+model; involves duals ``\\mu^{\\text{DsUb}}``, ``\\mu^{\\text{DsLb}}``.
+"""
struct OptConditionBatteryDischarge <: PSI.ConstraintType end
-# EnergyVariable is defined in PSI
+
+"""
+ OptConditionEnergyVariable
+
+Constraint enforcing KKT stationarity for the energy variable at the PCC in the
+merchant model. #TODO DOCS
+"""
struct OptConditionEnergyVariable <: PSI.ConstraintType end
###############################################
##### Complementaty Slackness Constraints #####
###############################################
# Names track the constraint types and their Meta Ub and Lb
+"""
+ ComplementarySlacknessEnergyAssetBalanceUb
+
+Complementary slackness constraint (upper bound) for the energy asset balance
+equation in the merchant model; used in MPEC/bilevel reformulation.
+"""
struct ComplementarySlacknessEnergyAssetBalanceUb <: PSI.ConstraintType end
+
+"""
+ ComplementarySlacknessEnergyAssetBalanceLb
+
+Complementary slackness constraint (lower bound) for the energy asset balance.
+"""
struct ComplementarySlacknessEnergyAssetBalanceLb <: PSI.ConstraintType end
+
struct ComplementarySlacknessThermalOnVariableUb <: PSI.ConstraintType end
struct ComplementarySlacknessThermalOnVariableLb <: PSI.ConstraintType end
+
+"""
+ ComplementarySlacknessRenewableActivePowerLimitConstraintUb
+
+Complementary slackness (upper bound) for renewable active power limit (``p_{\\text{re},t} \\leq P^*_{\\text{re},t}``).
+"""
struct ComplementarySlacknessRenewableActivePowerLimitConstraintUb <: PSI.ConstraintType end
+
+"""
+ ComplementarySlacknessRenewableActivePowerLimitConstraintLb
+
+Complementary slackness (lower bound) for renewable active power limit.
+"""
struct ComplementarySlacknessRenewableActivePowerLimitConstraintLb <: PSI.ConstraintType end
+
+"""
+ ComplementarySlacknessBatteryStatusDischargeOnUb
+
+Complementary slackness (upper bound) for battery status discharge-on constraint (``ss_{\\text{st},t}``).
+"""
struct ComplementarySlacknessBatteryStatusDischargeOnUb <: PSI.ConstraintType end
+"""
+ ComplementarySlacknessBatteryStatusDischargeOnLb
+
+Complementary slackness (lower bound) for battery status discharge-on constraint.
+"""
struct ComplementarySlacknessBatteryStatusDischargeOnLb <: PSI.ConstraintType end
+
+"""
+ ComplementarySlacknessBatteryStatusChargeOnUb
+
+Complementary slackness (upper bound) for battery status charge-on constraint.
+"""
struct ComplementarySlacknessBatteryStatusChargeOnUb <: PSI.ConstraintType end
+"""
+ ComplementarySlacknessBatteryStatusChargeOnLb
+
+Complementary slackness (lower bound) for battery status charge-on constraint.
+"""
struct ComplementarySlacknessBatteryStatusChargeOnLb <: PSI.ConstraintType end
+
+"""
+ ComplementarySlacknessBatteryBalanceUb
+
+Complementary slackness (upper bound) for storage energy balance (``e_{\\text{st},t}``).
+"""
struct ComplementarySlacknessBatteryBalanceUb <: PSI.ConstraintType end
+"""
+ ComplementarySlacknessBatteryBalanceLb
+
+Complementary slackness (lower bound) for storage energy balance.
+"""
struct ComplementarySlacknessBatteryBalanceLb <: PSI.ConstraintType end
+
+"""
+ ComplentarySlacknessCyclingCharge
+
+Complementary slackness for the charging cycle limit (``c_{\\text{ch}}^-``); note spelling
+"Complentary" is kept for API compatibility.
+"""
struct ComplentarySlacknessCyclingCharge <: PSI.ConstraintType end
+
+"""
+ ComplentarySlacknessCyclingDischarge
+
+Complementary slackness for the discharging cycle limit (``c_{\\text{ds}}^-``).
+"""
struct ComplentarySlacknessCyclingDischarge <: PSI.ConstraintType end
+
+"""
+ ComplementarySlacknessEnergyLimitUb
+
+Complementary slackness (upper bound) for storage energy capacity (``e_{\\text{st},t} \\leq E_{\\max,\\text{st}}``).
+"""
struct ComplementarySlacknessEnergyLimitUb <: PSI.ConstraintType end
+"""
+ ComplementarySlacknessEnergyLimitLb
+
+Complementary slackness (lower bound) for storage energy capacity.
+"""
struct ComplementarySlacknessEnergyLimitLb <: PSI.ConstraintType end
+
+"""
+ StrongDualityCut
+
+Constraint that enforces strong duality for the merchant (lower-level) problem
+in a bilevel formulation: objective value equals dual objective (or equivalent
+cut), so that the lower level is replaced by its KKT conditions.
+"""
struct StrongDualityCut <: PSI.ConstraintType end
diff --git a/src/core/decision_models.jl b/src/core/decision_models.jl
index b0340802..1f1aabdb 100644
--- a/src/core/decision_models.jl
+++ b/src/core/decision_models.jl
@@ -1,6 +1,37 @@
abstract type HybridDecisionProblem <: PSI.DecisionProblem end
+"""
+ MerchantHybridEnergyCase
+
+Decision problem for a merchant hybrid resource that co-optimizes energy bids/offers
+in day-ahead and real-time markets only (no ancillary services). The hybrid optimizer
+maximizes profit from energy (e.g. DA/RT spread) subject to internal asset limits.
+"""
struct MerchantHybridEnergyCase <: HybridDecisionProblem end
+
+"""
+ MerchantHybridEnergyFixedDA
+
+Decision problem for a merchant hybrid with fixed day-ahead energy positions; used
+when solving the real-time subproblem with locked DA bids/offers.
+"""
struct MerchantHybridEnergyFixedDA <: HybridDecisionProblem end
+
+"""
+ MerchantHybridCooptimizerCase
+
+Decision problem for a merchant hybrid that co-optimizes energy and ancillary services
+in day-ahead and real-time markets. Maximizes ``d'y - c_h' x`` (revenue from bids/offers minus operating cost) subject to
+market and asset constraints; AS are committed in DA and fulfilled by internal asset
+allocation in RT.
+"""
struct MerchantHybridCooptimizerCase <: HybridDecisionProblem end
+
+"""
+ MerchantHybridBilevelCase
+
+Decision problem implementing a bilevel formulation for the merchant hybrid
+(e.g. upper level: bids/offers, lower level: internal dispatch); used for
+equilibrium or regulatory analysis. #TODO DOCS
+"""
struct MerchantHybridBilevelCase <: HybridDecisionProblem end
diff --git a/src/core/formulations.jl b/src/core/formulations.jl
index 44adee3e..6d19f59b 100644
--- a/src/core/formulations.jl
+++ b/src/core/formulations.jl
@@ -1,8 +1,38 @@
########################### Hybrid Generation Formulations ################################
abstract type AbstractHybridFormulation <: PSI.AbstractDeviceFormulation end
abstract type AbstractHybridFormulationWithReserves <: AbstractHybridFormulation end
+
+"""
+ HybridDispatchWithReserves
+
+Device formulation for a hybrid system (single PCC with renewable, thermal, and storage)
+that participates in both energy and ancillary service (AS) markets. Implements the
+centralized PCM model where the hybrid plant's net power at the PCC is constrained by
+``P_{\\max,\\text{pcc}}`` and AS allocations (``sb^{\\text{out}}_{p,t}``, ``sb^{\\text{in}}_{p,t}``) are assigned to internal assets
+(thermal, renewable, charge, discharge) per the four-quadrant AS model.
+
+Use with a hybrid system in a
+[`PowerSimulations.DeviceModel`](@extref PowerSimulations.DeviceModel) for unit commitment
+or economic dispatch.
+"""
struct HybridDispatchWithReserves <: AbstractHybridFormulationWithReserves end
+
+"""
+ HybridEnergyOnlyDispatch
+
+Device formulation for a hybrid system that participates in energy only (no ancillary
+services). Net power at the PCC is ``p^{\\text{out}}_t - p^{\\text{in}}_t`` from thermal, renewable, discharge,
+minus charge and load; subject to ``P_{\\max,\\text{pcc}}`` and asset limits.
+"""
struct HybridEnergyOnlyDispatch <: AbstractHybridFormulation end
+
+"""
+ HybridFixedDA
+
+Device formulation for a hybrid system with day-ahead (DA) energy bids/offers fixed;
+used in multi-step simulations when the real-time (RT) subproblem is solved with
+locked DA positions (e.g. merchant co-optimization with "then vs. now" RT adjustment).
+"""
struct HybridFixedDA <: AbstractHybridFormulation end
struct MerchantModelEnergyOnly <: AbstractHybridFormulation end
diff --git a/src/core/parameters.jl b/src/core/parameters.jl
index 63b0b4eb..71754243 100644
--- a/src/core/parameters.jl
+++ b/src/core/parameters.jl
@@ -5,12 +5,55 @@ const REG_COST = 0.001
struct RenewablePowerTimeSeries <: PSI.TimeSeriesParameter end
struct ElectricLoadTimeSeries <: PSI.TimeSeriesParameter end
+"""
+ DayAheadEnergyPrice
+
+Objective function parameter for day-ahead energy price.
+
+Docs abbreviation: ``\\Pi^*_{\\text{DA},t}`` (USD/MWh). Used in the merchant objective
+(e.g. ``f_{\\text{DA},t}`` term) when building the decision model.
+"""
struct DayAheadEnergyPrice <: PSI.ObjectiveFunctionParameter end
+
+"""
+ RealTimeEnergyPrice
+
+Objective function parameter for real-time energy price.
+
+Docs abbreviation: ``\\Pi^*_{\\text{RT},t}`` (USD/MWh). Used in the merchant profit
+expression for RT energy and DART spread.
+"""
struct RealTimeEnergyPrice <: PSI.ObjectiveFunctionParameter end
+
+"""
+ AncillaryServicePrice
+
+Objective function parameter for ancillary service price.
+
+Docs abbreviation: ``\\Pi^*_{p,t}`` (USD/MWh) for service ``p \\in P``. Used in the DA
+profit term for AS (``sb^{\\text{out}}`` + ``sb^{\\text{in}}``).
+"""
struct AncillaryServicePrice <: PSI.ObjectiveFunctionParameter end
struct EnergyTargetParameter <: PSI.VariableValueParameter end
+
+"""
+ CyclingChargeLimitParameter
+
+Variable-value parameter that provides the right-hand side for the storage charging
+cycle limit: ``\\eta_{\\text{ch}} \\Delta t \\sum_t p_{\\text{ch},t} - c_{\\text{ch}}^- \\leq C_{\\text{st}} E_{\\max,\\text{st}}``. Used with
+[`CyclingChargeLimitFeedforward`](@ref) in recurrent simulations to pass cumulative
+cycling from previous horizons.
+"""
struct CyclingChargeLimitParameter <: PSI.VariableValueParameter end
+
+"""
+ CyclingDischargeLimitParameter
+
+Variable-value parameter for the storage discharging cycle limit:
+``(\\Delta t/\\eta_{\\text{ds}}) \\sum_t p_{\\text{ds},t} - c_{\\text{ds}}^- \\leq C_{\\text{st}} E_{\\max,\\text{st}}``. Used with
+[`CyclingDischargeLimitFeedforward`](@ref).
+"""
struct CyclingDischargeLimitParameter <: PSI.VariableValueParameter end
PSI.should_write_resulting_value(::Type{DayAheadEnergyPrice}) = true
diff --git a/src/core/variables.jl b/src/core/variables.jl
index bdc9d8f8..8907f15c 100644
--- a/src/core/variables.jl
+++ b/src/core/variables.jl
@@ -1,8 +1,40 @@
### Define Variables using PSI.VariableType
# Energy Bids
+"""
+ EnergyDABidOut
+
+Variable type for day-ahead energy offer (generating power) at the PCC.
+
+Docs abbreviation: ``e^{\\text{out}}_{\\text{DA},t} \\in [0, P_{\\max,\\text{pcc}}]`` [MW].
+"""
struct EnergyDABidOut <: PSI.VariableType end
+
+"""
+ EnergyDABidIn
+
+Variable type for day-ahead energy bid (consuming power) at the PCC.
+
+Docs abbreviation: ``e^{\\text{in}}_{\\text{DA},t} \\in [0, P_{\\max,\\text{pcc}}]`` [MW].
+"""
struct EnergyDABidIn <: PSI.VariableType end
+
+"""
+ EnergyRTBidOut
+
+Variable type for real-time energy offer at the PCC.
+
+Docs abbreviation: ``e^{\\text{out}}_{\\text{RT},t}``. Net RT position with DA locked
+is used in the merchant profit expression (e.g. DART spread).
+"""
struct EnergyRTBidOut <: PSI.VariableType end
+
+"""
+ EnergyRTBidIn
+
+Variable type for real-time energy bid at the PCC.
+
+Docs abbreviation: ``e^{\\text{in}}_{\\text{RT},t}``.
+"""
struct EnergyRTBidIn <: PSI.VariableType end
# Energy Asset Bids
@@ -12,7 +44,24 @@ struct EnergyBatteryChargeBid <: PSI.VariableType end
struct EnergyBatteryDischargeBid <: PSI.VariableType end
# AS Total DA Bids
+"""
+ BidReserveVariableOut
+
+Variable type for day-ahead ancillary service offer (generation direction) for the
+hybrid at the PCC.
+
+Docs abbreviation: ``sb^{\\text{out}}_{p,t} \\in [0, F_p P_{\\max,\\text{pcc}}]`` for product ``p``.
+"""
struct BidReserveVariableOut <: PSI.VariableType end
+
+"""
+ BidReserveVariableIn
+
+Variable type for day-ahead ancillary service bid (consumption direction) for the
+hybrid at the PCC.
+
+Docs abbreviation: ``sb^{\\text{in}}_{p,t} \\in [0, F_p P_{\\max,\\text{pcc}}]`` for product ``p``.
+"""
struct BidReserveVariableIn <: PSI.VariableType end
# Component Variables
@@ -33,8 +82,29 @@ struct DischargeRegularizationVariable <: BatteryRegularizationVariable end
# AS Variable for Hybrid
abstract type ReserveVariableType <: PSI.VariableType end
abstract type AssetReserveVariableType <: ReserveVariableType end
+
+"""
+ ReserveVariableOut
+
+Variable type for ancillary service reserve quantity in the "out" (generation)
+direction allocated to the hybrid's internal assets (``sb^{\\text{th}}``, ``sb^{\\text{re}}``, ``sb^{\\text{ds}}``, ``sb^{\\text{ch}}``).
+"""
struct ReserveVariableOut <: AssetReserveVariableType end
+
+"""
+ ReserveVariableIn
+
+Variable type for ancillary service reserve quantity in the "in" (consumption)
+direction allocated to the hybrid's internal assets.
+"""
struct ReserveVariableIn <: AssetReserveVariableType end
+
+"""
+ TotalReserve
+
+Auxiliary variable type for the total reserve quantity (sum of component reserves)
+at the PCC. Used in reserve balance constraints; not written to results by default.
+"""
struct TotalReserve <: AssetReserveVariableType end
struct SlackReserveUp <: PSI.VariableType end
struct SlackReserveDown <: PSI.VariableType end
diff --git a/src/feedforwards.jl b/src/feedforwards.jl
index 20ea09a1..500aa895 100644
--- a/src/feedforwards.jl
+++ b/src/feedforwards.jl
@@ -1,3 +1,13 @@
+"""
+ CyclingChargeLimitFeedforward
+
+Feedforward that enforces a cumulative charging cycle limit on the hybrid's storage
+over the simulation. The constraint is ``\\eta_{\\text{ch}} \\Delta t \\sum (p_{\\text{ch}} + \\text{served\\_reg\\_down} - \\text{served\\_reg\\_up}) \\leq \\text{limit}``,
+where the limit is from [`CyclingChargeLimitParameter`](@ref) in recurrent solves or
+``\\text{cycles\\_in\\_horizon} \\times E_{\\max}`` otherwise. Use with PowerSimulations' `add_feedforward!` in a
+[`PowerSimulations.DeviceModel`](@extref PowerSimulations.DeviceModel) for
+[`HybridDispatchWithReserves`](@ref) or [`HybridEnergyOnlyDispatch`](@ref).
+"""
struct CyclingChargeLimitFeedforward <: PSI.AbstractAffectFeedforward
optimization_container_key::PSI.OptimizationContainerKey
affected_values::Vector{<:PSI.OptimizationContainerKey}
@@ -33,6 +43,14 @@ PSI.get_default_parameter_type(::CyclingChargeLimitFeedforward, _) =
PSI.get_optimization_container_key(ff::CyclingChargeLimitFeedforward) =
ff.optimization_container_key
+"""
+ CyclingDischargeLimitFeedforward
+
+Feedforward that enforces a cumulative discharging cycle limit on the hybrid's storage:
+``(1/\\eta_{\\text{ds}}) \\Delta t \\sum (p_{\\text{ds}} + \\text{served\\_reg\\_up} - \\text{served\\_reg\\_down}) \\leq \\text{limit}``. The limit comes from
+[`CyclingDischargeLimitParameter`](@ref) in recurrent runs. See
+[`CyclingChargeLimitFeedforward`](@ref) for usage pattern.
+"""
struct CyclingDischargeLimitFeedforward <: PSI.AbstractAffectFeedforward
optimization_container_key::PSI.OptimizationContainerKey
affected_values::Vector{<:PSI.OptimizationContainerKey}
From dbbbf3003ad4ef6c840adde21760dd05b67a1dbb Mon Sep 17 00:00:00 2001
From: kdayday
Date: Wed, 18 Feb 2026 21:14:36 -0700
Subject: [PATCH 02/46] Run formatter
---
CONTRIBUTING.md | 7 +-
docs/src/api/public.md | 2 +
src/add_aux_variables.jl | 4 +-
src/add_constraints.jl | 208 ++++++++++--------
src/add_parameters.jl | 14 +-
src/add_variables.jl | 6 +-
.../only_energy_decision_model.jl | 4 +-
src/feedforwards.jl | 14 +-
src/hybrid_system_decision_models.jl | 16 +-
src/utils.jl | 2 +-
test/runtests.jl | 6 +-
...t_device_hybrid_generation_constructors.jl | 6 +-
test/test_hybrid_device.jl | 20 +-
test/test_hybrid_simulations.jl | 72 +++---
test/test_merchant_cooptimizer.jl | 26 +--
test/test_merchant_only_energy.jl | 22 +-
test/test_utils/additional_templates.jl | 152 ++++++-------
test/test_utils/function_utils.jl | 90 ++++----
test/test_utils/price_generation_utils.jl | 90 ++++----
test/x_test_cooptimizer_with_build.jl | 10 +-
test/x_test_optimizer_sequence.jl | 14 +-
test/x_test_optimizer_with_build.jl | 12 +-
22 files changed, 426 insertions(+), 371 deletions(-)
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 91c9631e..65a8ddce 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,6 +1,7 @@
# Contributing
Community driven development of this package is encouraged. To maintain code quality standards, please adhere to the following guidlines when contributing:
- - To get started, sign the Contributor License Agreement.
- - Please do your best to adhere to our [coding style guide](docs/src/developer/style.md).
- - 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/).
+
+ - To get started, sign the Contributor License Agreement.
+ - Please do your best to adhere to our [coding style guide](docs/src/developer/style.md).
+ - 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/).
diff --git a/docs/src/api/public.md b/docs/src/api/public.md
index 5387f3eb..633a895a 100644
--- a/docs/src/api/public.md
+++ b/docs/src/api/public.md
@@ -133,9 +133,11 @@ ComplementarySlacknessEnergyAssetBalanceUb
ComplementarySlacknessEnergyAssetBalanceLb
ComplementarySlacknessRenewableActivePowerLimitConstraintUb
```
+
```@docs; canonical=false
ComplementarySlacknessRenewableActivePowerLimitConstraintLb
```
+
```@docs
ComplementarySlacknessBatteryStatusDischargeOnUb
ComplementarySlacknessBatteryStatusDischargeOnLb
diff --git a/src/add_aux_variables.jl b/src/add_aux_variables.jl
index 58f3f578..8fb47015 100644
--- a/src/add_aux_variables.jl
+++ b/src/add_aux_variables.jl
@@ -154,8 +154,8 @@ function PSI.update_decision_state!(
state_data_index = 1
state_data.timestamps[:] .= range(
simulation_time;
- step=state_resolution,
- length=PSI.get_num_rows(state_data),
+ step = state_resolution,
+ length = PSI.get_num_rows(state_data),
)
else
state_data_index = PSI.find_timestamp_index(state_timestamps, simulation_time)
diff --git a/src/add_constraints.jl b/src/add_constraints.jl
index f378846f..aba7eb1d 100644
--- a/src/add_constraints.jl
+++ b/src/add_constraints.jl
@@ -31,7 +31,8 @@ function _add_constraints_statusout!(
names = [PSY.get_name(d) for d in devices]
varon = PSI.get_variable(container, PSI.ReservationVariable(), D)
p_out = PSI.get_variable(container, PSI.ActivePowerOutVariable(), D)
- con_ub = PSI.add_constraints_container!(container, T(), D, names, time_steps, meta="ub")
+ con_ub =
+ PSI.add_constraints_container!(container, T(), D, names, time_steps; meta = "ub")
for device in devices, t in time_steps
ci_name = PSY.get_name(device)
@@ -75,8 +76,10 @@ function _add_constraints_statusout_withreserves!(
p_out = PSI.get_variable(container, PSI.ActivePowerOutVariable(), D)
res_out_up = PSI.get_expression(container, TotalReserveOutUpExpression(), D)
res_out_down = PSI.get_expression(container, TotalReserveOutDownExpression(), D)
- con_ub = PSI.add_constraints_container!(container, T(), D, names, time_steps, meta="ub")
- con_lb = PSI.add_constraints_container!(container, T(), D, names, time_steps, meta="lb")
+ con_ub =
+ PSI.add_constraints_container!(container, T(), D, names, time_steps; meta = "ub")
+ con_lb =
+ PSI.add_constraints_container!(container, T(), D, names, time_steps; meta = "lb")
for device in devices, t in time_steps
ci_name = PSY.get_name(device)
@@ -111,8 +114,10 @@ function _add_constraints_statusout_withreserves!(
res_out_down = PSI.get_expression(container, TotalReserveOutDownExpression(), D)
#serv_reg_out_up = PSI.get_expression(container, ServedReserveOutUpExpression(), D)
#serv_reg_out_down = PSI.get_expression(container, ServedReserveOutDownExpression(), D)
- con_ub = PSI.add_constraints_container!(container, T(), D, names, time_steps, meta="ub")
- con_lb = PSI.add_constraints_container!(container, T(), D, names, time_steps, meta="lb")
+ con_ub =
+ PSI.add_constraints_container!(container, T(), D, names, time_steps; meta = "ub")
+ con_lb =
+ PSI.add_constraints_container!(container, T(), D, names, time_steps; meta = "lb")
for device in devices, t in time_steps
ci_name = PSY.get_name(device)
@@ -162,7 +167,8 @@ function _add_constraints_statusin!(
names = [PSY.get_name(d) for d in devices]
varon = PSI.get_variable(container, PSI.ReservationVariable(), D)
p_in = PSI.get_variable(container, PSI.ActivePowerInVariable(), D)
- con_ub = PSI.add_constraints_container!(container, T(), D, names, time_steps, meta="ub")
+ con_ub =
+ PSI.add_constraints_container!(container, T(), D, names, time_steps; meta = "ub")
for device in devices, t in time_steps
ci_name = PSY.get_name(device)
@@ -206,8 +212,10 @@ function _add_constraints_statusin_withreserves!(
p_in = PSI.get_variable(container, PSI.ActivePowerInVariable(), D)
res_in_up = PSI.get_expression(container, TotalReserveInUpExpression(), D)
res_in_down = PSI.get_expression(container, TotalReserveInDownExpression(), D)
- con_ub = PSI.add_constraints_container!(container, T(), D, names, time_steps, meta="ub")
- con_lb = PSI.add_constraints_container!(container, T(), D, names, time_steps, meta="lb")
+ con_ub =
+ PSI.add_constraints_container!(container, T(), D, names, time_steps; meta = "ub")
+ con_lb =
+ PSI.add_constraints_container!(container, T(), D, names, time_steps; meta = "lb")
for device in devices, t in time_steps
ci_name = PSY.get_name(device)
@@ -243,8 +251,10 @@ function _add_constraints_statusin_withreserves!(
res_in_down = PSI.get_expression(container, TotalReserveInDownExpression(), D)
#serv_reg_in_up = PSI.get_expression(container, ServedReserveInUpExpression(), D)
#serv_reg_in_down = PSI.get_expression(container, ServedReserveInDownExpression(), D)
- con_ub = PSI.add_constraints_container!(container, T(), D, names, time_steps, meta="ub")
- con_lb = PSI.add_constraints_container!(container, T(), D, names, time_steps, meta="lb")
+ con_ub =
+ PSI.add_constraints_container!(container, T(), D, names, time_steps; meta = "ub")
+ con_lb =
+ PSI.add_constraints_container!(container, T(), D, names, time_steps; meta = "lb")
for device in devices, t in time_steps
ci_name = PSY.get_name(device)
@@ -535,7 +545,8 @@ function _add_constraints_thermalon_variableon!(
names = [PSY.get_name(d) for d in devices]
varon = PSI.get_variable(container, PSI.OnVariable(), D)
p_th = PSI.get_variable(container, ThermalPower(), D)
- con_ub = PSI.add_constraints_container!(container, T(), D, names, time_steps, meta="ub")
+ con_ub =
+ PSI.add_constraints_container!(container, T(), D, names, time_steps; meta = "ub")
for device in devices, t in time_steps
ci_name = PSY.get_name(device)
@@ -576,7 +587,8 @@ function _add_constraints_thermalon_variableoff!(
names = [PSY.get_name(d) for d in devices]
varon = PSI.get_variable(container, PSI.OnVariable(), D)
p_th = PSI.get_variable(container, ThermalPower(), D)
- con_lb = PSI.add_constraints_container!(container, T(), D, names, time_steps, meta="lb")
+ con_lb =
+ PSI.add_constraints_container!(container, T(), D, names, time_steps; meta = "lb")
for device in devices, t in time_steps
ci_name = PSY.get_name(device)
@@ -620,7 +632,7 @@ function _add_constraints_batterychargeon!(
status_st = PSI.get_variable(container, BatteryStatus(), D)
p_ch = PSI.get_variable(container, BatteryCharge(), D)
con_ub_ch =
- PSI.add_constraints_container!(container, T(), D, names, time_steps, meta="ub")
+ PSI.add_constraints_container!(container, T(), D, names, time_steps; meta = "ub")
for device in devices, t in time_steps
ci_name = PSY.get_name(device)
@@ -662,7 +674,7 @@ function _add_constraints_batterydischargeon!(
status_st = PSI.get_variable(container, BatteryStatus(), D)
p_ds = PSI.get_variable(container, BatteryDischarge(), D)
con_ub_ds =
- PSI.add_constraints_container!(container, T(), D, names, time_steps, meta="ub")
+ PSI.add_constraints_container!(container, T(), D, names, time_steps; meta = "ub")
for device in devices, t in time_steps
ci_name = PSY.get_name(device)
@@ -1263,8 +1275,8 @@ function PSI.add_constraints!(
ChargeRegularizationConstraint(),
V,
names,
- time_steps,
- meta="ub",
+ time_steps;
+ meta = "ub",
)
constraint_lb = PSI.add_constraints_container!(
@@ -1272,8 +1284,8 @@ function PSI.add_constraints!(
ChargeRegularizationConstraint(),
V,
names,
- time_steps,
- meta="lb",
+ time_steps;
+ meta = "lb",
)
if has_services
@@ -1356,8 +1368,8 @@ function PSI.add_constraints!(
DischargeRegularizationConstraint(),
V,
names,
- time_steps,
- meta="ub",
+ time_steps;
+ meta = "ub",
)
constraint_lb = PSI.add_constraints_container!(
@@ -1365,8 +1377,8 @@ function PSI.add_constraints!(
DischargeRegularizationConstraint(),
V,
names,
- time_steps,
- meta="lb",
+ time_steps;
+ meta = "lb",
)
if has_services
@@ -1442,7 +1454,7 @@ function _add_constraints_renewablelimit!(
p_re = PSI.get_variable(container, RenewablePower(), D)
names = [PSY.get_name(d) for d in devices]
con_ub_re =
- PSI.add_constraints_container!(container, T(), D, names, time_steps, meta="ub")
+ PSI.add_constraints_container!(container, T(), D, names, time_steps; meta = "ub")
param_container = PSI.get_parameter(container, RenewablePowerTimeSeries(), D)
for device in devices
ci_name = PSY.get_name(device)
@@ -1489,8 +1501,10 @@ function _add_thermallimit_withreserves!(
p_th = PSI.get_variable(container, ThermalPower(), D)
reg_th_up = PSI.get_expression(container, ThermalReserveUpExpression(), D)
reg_th_dn = PSI.get_expression(container, ThermalReserveDownExpression(), D)
- con_ub = PSI.add_constraints_container!(container, T(), D, names, time_steps, meta="ub")
- con_lb = PSI.add_constraints_container!(container, T(), D, names, time_steps, meta="lb")
+ con_ub =
+ PSI.add_constraints_container!(container, T(), D, names, time_steps; meta = "ub")
+ con_lb =
+ PSI.add_constraints_container!(container, T(), D, names, time_steps; meta = "lb")
for device in devices, t in time_steps
ci_name = PSY.get_name(device)
@@ -1549,8 +1563,8 @@ function _add_constraints_reservecoverage_withreserves!(
T(),
D,
names,
- time_steps,
- meta=service_name,
+ time_steps;
+ meta = service_name,
)
for ic in initial_conditions
device = PSI.get_component(ic)
@@ -1617,8 +1631,8 @@ function _add_constraints_reservecoverage_withreserves!(
T(),
D,
names,
- time_steps,
- meta=service_name,
+ time_steps;
+ meta = service_name,
)
for ic in initial_conditions
device = PSI.get_component(ic)
@@ -1686,8 +1700,8 @@ function _add_constraints_reservecoverage_withreserves_endofperiod!(
T(),
D,
names,
- time_steps,
- meta=service_name,
+ time_steps;
+ meta = service_name,
)
for device in devices, t in time_steps
ci_name = PSY.get_name(device)
@@ -1750,8 +1764,8 @@ function _add_constraints_reservecoverage_withreserves_endofperiod!(
T(),
D,
names,
- time_steps,
- meta=service_name,
+ time_steps;
+ meta = service_name,
)
for device in devices, t in time_steps
ci_name = PSY.get_name(device)
@@ -1805,8 +1819,10 @@ function _add_constraints_charging_reservelimit!(
p_ch = PSI.get_variable(container, BatteryCharge(), D)
reg_ch_up = PSI.get_expression(container, ChargeReserveUpExpression(), D)
reg_ch_dn = PSI.get_expression(container, ChargeReserveDownExpression(), D)
- con_ub = PSI.add_constraints_container!(container, T(), D, names, time_steps, meta="ub")
- con_lb = PSI.add_constraints_container!(container, T(), D, names, time_steps, meta="lb")
+ con_ub =
+ PSI.add_constraints_container!(container, T(), D, names, time_steps; meta = "ub")
+ con_lb =
+ PSI.add_constraints_container!(container, T(), D, names, time_steps; meta = "lb")
for device in devices, t in time_steps
ci_name = PSY.get_name(device)
@@ -1854,8 +1870,10 @@ function _add_constraints_discharging_reservelimit!(
p_ds = PSI.get_variable(container, BatteryDischarge(), D)
reg_ds_up = PSI.get_expression(container, DischargeReserveUpExpression(), D)
reg_ds_dn = PSI.get_expression(container, DischargeReserveDownExpression(), D)
- con_ub = PSI.add_constraints_container!(container, T(), D, names, time_steps, meta="ub")
- con_lb = PSI.add_constraints_container!(container, T(), D, names, time_steps, meta="lb")
+ con_ub =
+ PSI.add_constraints_container!(container, T(), D, names, time_steps; meta = "ub")
+ con_lb =
+ PSI.add_constraints_container!(container, T(), D, names, time_steps; meta = "lb")
for device in devices, t in time_steps
ci_name = PSY.get_name(device)
@@ -1902,8 +1920,10 @@ function _add_constraints_renewablereserve_limit!(
p_re = PSI.get_variable(container, RenewablePower(), D)
reg_re_up = PSI.get_expression(container, RenewableReserveUpExpression(), D)
reg_re_dn = PSI.get_expression(container, RenewableReserveDownExpression(), D)
- con_ub = PSI.add_constraints_container!(container, T(), D, names, time_steps, meta="ub")
- con_lb = PSI.add_constraints_container!(container, T(), D, names, time_steps, meta="lb")
+ con_ub =
+ PSI.add_constraints_container!(container, T(), D, names, time_steps; meta = "ub")
+ con_lb =
+ PSI.add_constraints_container!(container, T(), D, names, time_steps; meta = "lb")
param_container = PSI.get_parameter(container, P(), D)
for device in devices
ci_name = PSY.get_name(device)
@@ -1970,8 +1990,8 @@ function PSI.add_constraints!(
T(),
D,
names,
- time_steps,
- meta=service_name,
+ time_steps;
+ meta = service_name,
)
for device in devices, t in time_steps
ci_name = PSY.get_name(device)
@@ -2070,8 +2090,8 @@ function PSI.add_constraints!(
T(),
D,
names,
- time_steps,
- meta=service_name,
+ time_steps;
+ meta = service_name,
)
for device in devices, t in time_steps
ci_name = PSY.get_name(device)
@@ -2111,8 +2131,8 @@ function PSI.add_constraints!(
T(),
D,
names,
- time_steps,
- meta=service_name,
+ time_steps;
+ meta = service_name,
)
for device in devices
ci_name = PSY.get_name(device)
@@ -2184,8 +2204,10 @@ function add_constraints_dayaheadlimit_out_withreserves!(
bid_out = PSI.get_variable(container, EnergyDABidOut(), D)
res_out_up = PSI.get_expression(container, TotalReserveOutUpExpression(), D)
res_out_down = PSI.get_expression(container, TotalReserveOutDownExpression(), D)
- con_ub = PSI.add_constraints_container!(container, T(), D, names, time_steps, meta="ub")
- con_lb = PSI.add_constraints_container!(container, T(), D, names, time_steps, meta="lb")
+ con_ub =
+ PSI.add_constraints_container!(container, T(), D, names, time_steps; meta = "ub")
+ con_lb =
+ PSI.add_constraints_container!(container, T(), D, names, time_steps; meta = "lb")
for device in devices, t in time_steps
ci_name = PSY.get_name(device)
@@ -2218,8 +2240,10 @@ function add_constraints_dayaheadlimit_in_withreserves!(
bid_in = PSI.get_variable(container, EnergyDABidIn(), D)
res_in_up = PSI.get_expression(container, TotalReserveInUpExpression(), D)
res_in_down = PSI.get_expression(container, TotalReserveInDownExpression(), D)
- con_ub = PSI.add_constraints_container!(container, T(), D, names, time_steps, meta="ub")
- con_lb = PSI.add_constraints_container!(container, T(), D, names, time_steps, meta="lb")
+ con_ub =
+ PSI.add_constraints_container!(container, T(), D, names, time_steps; meta = "ub")
+ con_lb =
+ PSI.add_constraints_container!(container, T(), D, names, time_steps; meta = "lb")
for device in devices, t in time_steps
ci_name = PSY.get_name(device)
@@ -2252,8 +2276,10 @@ function add_constraints_realtimelimit_out_withreserves!(
bid_out = PSI.get_variable(container, EnergyRTBidOut(), D)
res_out_up = PSI.get_expression(container, TotalReserveOutUpExpression(), D)
res_out_down = PSI.get_expression(container, TotalReserveOutDownExpression(), D)
- con_ub = PSI.add_constraints_container!(container, T(), D, names, time_steps, meta="ub")
- con_lb = PSI.add_constraints_container!(container, T(), D, names, time_steps, meta="lb")
+ con_ub =
+ PSI.add_constraints_container!(container, T(), D, names, time_steps; meta = "ub")
+ con_lb =
+ PSI.add_constraints_container!(container, T(), D, names, time_steps; meta = "lb")
for device in devices, t in time_steps
tmap = PSY.get_ext(device)["tmap"]
@@ -2287,8 +2313,10 @@ function add_constraints_realtimelimit_in_withreserves!(
bid_in = PSI.get_variable(container, EnergyRTBidIn(), D)
res_in_up = PSI.get_expression(container, TotalReserveInUpExpression(), D)
res_in_down = PSI.get_expression(container, TotalReserveInDownExpression(), D)
- con_ub = PSI.add_constraints_container!(container, T(), D, names, time_steps, meta="ub")
- con_lb = PSI.add_constraints_container!(container, T(), D, names, time_steps, meta="lb")
+ con_ub =
+ PSI.add_constraints_container!(container, T(), D, names, time_steps; meta = "ub")
+ con_lb =
+ PSI.add_constraints_container!(container, T(), D, names, time_steps; meta = "lb")
for device in devices, t in time_steps
tmap = PSY.get_ext(device)["tmap"]
@@ -2323,8 +2351,10 @@ function _add_thermallimit_withreserves!(
p_th = PSI.get_variable(container, ThermalPower(), D)
reg_th_up = PSI.get_expression(container, ThermalReserveUpExpression(), D)
reg_th_dn = PSI.get_expression(container, ThermalReserveDownExpression(), D)
- con_ub = PSI.add_constraints_container!(container, T(), D, names, time_steps, meta="ub")
- con_lb = PSI.add_constraints_container!(container, T(), D, names, time_steps, meta="lb")
+ con_ub =
+ PSI.add_constraints_container!(container, T(), D, names, time_steps; meta = "ub")
+ con_lb =
+ PSI.add_constraints_container!(container, T(), D, names, time_steps; meta = "lb")
for device in devices, t in time_steps
tmap = PSY.get_ext(device)["tmap"]
@@ -2355,7 +2385,8 @@ function _add_constraints_thermalon_variableon!(
names = [PSY.get_name(d) for d in devices]
varon = PSI.get_variable(container, PSI.OnVariable(), D)
p_th = PSI.get_variable(container, ThermalPower(), D)
- con_ub = PSI.add_constraints_container!(container, T(), D, names, time_steps, meta="ub")
+ con_ub =
+ PSI.add_constraints_container!(container, T(), D, names, time_steps; meta = "ub")
for device in devices, t in time_steps
tmap = PSY.get_ext(device)["tmap"]
@@ -2383,7 +2414,8 @@ function _add_constraints_thermalon_variableoff!(
names = [PSY.get_name(d) for d in devices]
varon = PSI.get_variable(container, PSI.OnVariable(), D)
p_th = PSI.get_variable(container, ThermalPower(), D)
- con_lb = PSI.add_constraints_container!(container, T(), D, names, time_steps, meta="lb")
+ con_lb =
+ PSI.add_constraints_container!(container, T(), D, names, time_steps; meta = "lb")
for device in devices, t in time_steps
tmap = PSY.get_ext(device)["tmap"]
@@ -2495,8 +2527,8 @@ function _add_constraints_reservebalance!(
T(),
D,
names,
- time_steps,
- meta=service_name,
+ time_steps;
+ meta = service_name,
)
for device in devices
tmap = PSY.get_ext(device)["tmap"]
@@ -2811,9 +2843,9 @@ function add_constraints!(
primal_var = PSI.get_variable(container, PSI.EnergyVariable(), D)
k_variable = PSI.get_variable(container, ComplementarySlackVarEnergyLimitUb(), D)
assignment_constraint =
- PSI.add_constraints_container!(container, T(), D, names, time_steps, meta="eq")
+ PSI.add_constraints_container!(container, T(), D, names, time_steps; meta = "eq")
sos_constraint =
- PSI.add_constraints_container!(container, T(), D, names, time_steps, meta="sos")
+ PSI.add_constraints_container!(container, T(), D, names, time_steps; meta = "sos")
jm = PSI.get_jump_model(container)
for dev in devices
n = PSY.get_name(dev)
@@ -2845,7 +2877,7 @@ function add_constraints!(
dual_var = PSI.get_variable(container, νStLb(), D)
primal_var = PSI.get_variable(container, PSI.EnergyVariable(), D)
sos_constraint =
- PSI.add_constraints_container!(container, T(), D, names, time_steps, meta="sos")
+ PSI.add_constraints_container!(container, T(), D, names, time_steps; meta = "sos")
jm = PSI.get_jump_model(container)
for n in names, t in time_steps
#assignment_constraint[n, t] =
@@ -2872,9 +2904,9 @@ function add_constraints!(
variable = PSI.get_variable(container, ComplementarySlackVarEnergyAssetBalanceUb(), D)
dual_var = PSI.get_variable(container, λUb(), D)
assignment_constraint =
- PSI.add_constraints_container!(container, T(), D, names, time_steps, meta="eq")
+ PSI.add_constraints_container!(container, T(), D, names, time_steps; meta = "eq")
sos_constraint =
- PSI.add_constraints_container!(container, T(), D, names, time_steps, meta="sos")
+ PSI.add_constraints_container!(container, T(), D, names, time_steps; meta = "sos")
jm = PSI.get_jump_model(container)
for n in names, t in time_steps
assignment_constraint[n, t] =
@@ -2900,9 +2932,9 @@ function add_constraints!(
variable = PSI.get_variable(container, ComplementarySlackVarEnergyAssetBalanceLb(), D)
dual_var = PSI.get_variable(container, λLb(), D)
assignment_constraint =
- PSI.add_constraints_container!(container, T(), D, names, time_steps, meta="eq")
+ PSI.add_constraints_container!(container, T(), D, names, time_steps; meta = "eq")
sos_constraint =
- PSI.add_constraints_container!(container, T(), D, names, time_steps, meta="sos")
+ PSI.add_constraints_container!(container, T(), D, names, time_steps; meta = "sos")
jm = PSI.get_jump_model(container)
for n in names, t in time_steps
assignment_constraint[n, t] =
@@ -2930,9 +2962,9 @@ function add_constraints!(
varon = PSI.get_variable(container, PSI.OnVariable(), D)
k_variable = PSI.get_variable(container, ComplementarySlackVarThermalOnVariableUb(), D)
assignment_constraint =
- PSI.add_constraints_container!(container, T(), D, names, time_steps, meta="eq")
+ PSI.add_constraints_container!(container, T(), D, names, time_steps; meta = "eq")
sos_constraint =
- PSI.add_constraints_container!(container, T(), D, names, time_steps, meta="sos")
+ PSI.add_constraints_container!(container, T(), D, names, time_steps; meta = "sos")
jm = PSI.get_jump_model(container)
for dev in devices
tmap = PSY.get_ext(dev)["tmap"]
@@ -2968,9 +3000,9 @@ function add_constraints!(
varon = PSI.get_variable(container, PSI.OnVariable(), D)
k_variable = PSI.get_variable(container, ComplementarySlackVarThermalOnVariableLb(), D)
assignment_constraint =
- PSI.add_constraints_container!(container, T(), D, names, time_steps, meta="eq")
+ PSI.add_constraints_container!(container, T(), D, names, time_steps; meta = "eq")
sos_constraint =
- PSI.add_constraints_container!(container, T(), D, names, time_steps, meta="sos")
+ PSI.add_constraints_container!(container, T(), D, names, time_steps; meta = "sos")
jm = PSI.get_jump_model(container)
for dev in devices
tmap = PSY.get_ext(dev)["tmap"]
@@ -3009,9 +3041,9 @@ function add_constraints!(
primal_var = PSI.get_variable(container, RenewablePower(), D)
re_param_container = PSI.get_parameter(container, RenewablePowerTimeSeries(), D)
assignment_constraint =
- PSI.add_constraints_container!(container, T(), D, names, time_steps, meta="eq")
+ PSI.add_constraints_container!(container, T(), D, names, time_steps; meta = "eq")
sos_constraint =
- PSI.add_constraints_container!(container, T(), D, names, time_steps, meta="sos")
+ PSI.add_constraints_container!(container, T(), D, names, time_steps; meta = "sos")
jm = PSI.get_jump_model(container)
for d in devices
name = PSY.get_name(d)
@@ -3047,7 +3079,7 @@ function add_constraints!(
dual_var = PSI.get_variable(container, μReLb(), D)
primal_var = PSI.get_variable(container, RenewablePower(), D)
sos_constraint =
- PSI.add_constraints_container!(container, T(), D, names, time_steps, meta="sos")
+ PSI.add_constraints_container!(container, T(), D, names, time_steps; meta = "sos")
jm = PSI.get_jump_model(container)
for n in names, t in time_steps
#assignment_constraint[n, t] =
@@ -3075,9 +3107,9 @@ function add_constraints!(
k_variable =
PSI.get_variable(container, ComplementarySlackVarBatteryStatusDischargeOnUb(), D)
assignment_constraint =
- PSI.add_constraints_container!(container, T(), D, names, time_steps, meta="eq")
+ PSI.add_constraints_container!(container, T(), D, names, time_steps; meta = "eq")
sos_constraint =
- PSI.add_constraints_container!(container, T(), D, names, time_steps, meta="sos")
+ PSI.add_constraints_container!(container, T(), D, names, time_steps; meta = "sos")
jm = PSI.get_jump_model(container)
for dev in devices
n = PSY.get_name(dev)
@@ -3111,7 +3143,7 @@ function add_constraints!(
dual_var = PSI.get_variable(container, μDsLb(), D)
primal_var = PSI.get_variable(container, BatteryDischarge(), D)
sos_constraint =
- PSI.add_constraints_container!(container, T(), D, names, time_steps, meta="sos")
+ PSI.add_constraints_container!(container, T(), D, names, time_steps; meta = "sos")
jm = PSI.get_jump_model(container)
for n in names, t in time_steps
#assignment_constraint[n, t] =
@@ -3139,9 +3171,9 @@ function add_constraints!(
k_variable =
PSI.get_variable(container, ComplementarySlackVarBatteryStatusChargeOnUb(), D)
assignment_constraint =
- PSI.add_constraints_container!(container, T(), D, names, time_steps, meta="eq")
+ PSI.add_constraints_container!(container, T(), D, names, time_steps; meta = "eq")
sos_constraint =
- PSI.add_constraints_container!(container, T(), D, names, time_steps, meta="sos")
+ PSI.add_constraints_container!(container, T(), D, names, time_steps; meta = "sos")
jm = PSI.get_jump_model(container)
for dev in devices
n = PSY.get_name(dev)
@@ -3175,7 +3207,7 @@ function add_constraints!(
dual_var = PSI.get_variable(container, μChLb(), D)
primal_var = PSI.get_variable(container, BatteryCharge(), D)
sos_constraint =
- PSI.add_constraints_container!(container, T(), D, names, time_steps, meta="sos")
+ PSI.add_constraints_container!(container, T(), D, names, time_steps; meta = "sos")
jm = PSI.get_jump_model(container)
for n in names, t in time_steps
#assignment_constraint[n, t] =
@@ -3205,9 +3237,9 @@ function add_constraints!(
discharge_var = PSI.get_variable(container, BatteryDischarge(), D)
dual_var = PSI.get_variable(container, γStBalUb(), D)
assignment_constraint =
- PSI.add_constraints_container!(container, T(), D, names, time_steps, meta="eq")
+ PSI.add_constraints_container!(container, T(), D, names, time_steps; meta = "eq")
sos_constraint =
- PSI.add_constraints_container!(container, T(), D, names, time_steps, meta="sos")
+ PSI.add_constraints_container!(container, T(), D, names, time_steps; meta = "sos")
initial_conditions = PSI.get_initial_condition(container, PSI.InitialEnergyLevel(), D)
jm = PSI.get_jump_model(container)
for ic in initial_conditions
@@ -3267,9 +3299,9 @@ function add_constraints!(
discharge_var = PSI.get_variable(container, BatteryDischarge(), D)
dual_var = PSI.get_variable(container, γStBalLb(), D)
assignment_constraint =
- PSI.add_constraints_container!(container, T(), D, names, time_steps, meta="eq")
+ PSI.add_constraints_container!(container, T(), D, names, time_steps; meta = "eq")
sos_constraint =
- PSI.add_constraints_container!(container, T(), D, names, time_steps, meta="sos")
+ PSI.add_constraints_container!(container, T(), D, names, time_steps; meta = "sos")
initial_conditions = PSI.get_initial_condition(container, PSI.InitialEnergyLevel(), D)
jm = PSI.get_jump_model(container)
for ic in initial_conditions
@@ -3325,8 +3357,8 @@ function add_constraints!(
charge_var = PSI.get_variable(container, BatteryCharge(), D)
dual_var = PSI.get_variable(container, κStCh(), D)
assignment_constraint =
- PSI.add_constraints_container!(container, T(), D, names, meta="eq")
- sos_constraint = PSI.add_constraints_container!(container, T(), D, names, meta="sos")
+ PSI.add_constraints_container!(container, T(), D, names; meta = "eq")
+ sos_constraint = PSI.add_constraints_container!(container, T(), D, names; meta = "sos")
jm = PSI.get_jump_model(container)
resolution = PSI.get_resolution(container)
Δt_RT = Dates.value(Dates.Minute(resolution)) / PSI.MINUTES_IN_HOUR
@@ -3362,8 +3394,8 @@ function add_constraints!(
charge_var = PSI.get_variable(container, BatteryDischarge(), D)
dual_var = PSI.get_variable(container, κStDs(), D)
assignment_constraint =
- PSI.add_constraints_container!(container, T(), D, names, meta="eq")
- sos_constraint = PSI.add_constraints_container!(container, T(), D, names, meta="sos")
+ PSI.add_constraints_container!(container, T(), D, names; meta = "eq")
+ sos_constraint = PSI.add_constraints_container!(container, T(), D, names; meta = "sos")
jm = PSI.get_jump_model(container)
resolution = PSI.get_resolution(container)
Δt_RT = Dates.value(Dates.Minute(resolution)) / PSI.MINUTES_IN_HOUR
@@ -3424,7 +3456,7 @@ function PSI._add_parameters!(
key,
names,
time_steps;
- meta=PSI.get_service_name(model),
+ meta = PSI.get_service_name(model),
)
jump_model = PSI.get_jump_model(container)
for name in names, t in time_steps
diff --git a/src/add_parameters.jl b/src/add_parameters.jl
index db1ae133..4ded27c6 100644
--- a/src/add_parameters.jl
+++ b/src/add_parameters.jl
@@ -91,7 +91,7 @@ function _add_price_time_series_parameters(
Float64,
device_names,
time_steps;
- meta="$var",
+ meta = "$var",
)
for device in devices
@@ -149,7 +149,7 @@ function _add_price_time_series_parameters(
Float64,
device_names,
time_steps;
- meta="$(var)_$(service_name)",
+ meta = "$(var)_$(service_name)",
)
for device in devices
@@ -183,7 +183,7 @@ function add_time_series_parameters!(
container::PSI.OptimizationContainer,
param::RenewablePowerTimeSeries,
devices::Vector{PSY.HybridSystem},
- ts_name="RenewableDispatch__max_active_power",
+ ts_name = "RenewableDispatch__max_active_power",
)
_add_time_series_parameters(container, ts_name, param, devices)
end
@@ -192,7 +192,7 @@ function add_time_series_parameters!(
container::PSI.OptimizationContainer,
param::ElectricLoadTimeSeries,
devices::Vector{PSY.HybridSystem},
- ts_name="PowerLoad__max_active_power",
+ ts_name = "PowerLoad__max_active_power",
)
_add_time_series_parameters(container, ts_name, param, devices)
return
@@ -359,7 +359,7 @@ function PSI._update_parameter_values!(
t_step = model_resolution ÷ state_data.resolution
end
state_data_index = find_timestamp_index(state_timestamps, current_time)
- sim_timestamps = range(current_time; step=model_resolution, length=time[end])
+ sim_timestamps = range(current_time; step = model_resolution, length = time[end])
for t in time
timestamp_ix = min(max_state_index, state_data_index + t_step)
@debug "parameter horizon is over the step" max_state_index > state_data_index + 1
@@ -475,7 +475,7 @@ function PSI._add_parameters!(
device_names,
service_names,
time_steps;
- meta="$TotalReserve",
+ meta = "$TotalReserve",
)
jump_model = PSI.get_jump_model(container)
for d in devices
@@ -512,7 +512,7 @@ function PSI._fix_parameter_value!(
JuMP.fix(
variable[name, s_name, t],
parameter_array[name, s_name, t];
- force=true,
+ force = true,
)
end
end
diff --git a/src/add_variables.jl b/src/add_variables.jl
index f7b89b44..521d608b 100644
--- a/src/add_variables.jl
+++ b/src/add_variables.jl
@@ -88,7 +88,7 @@ function PSI.add_variables!(
typeof(service),
PSY.get_name.(devices),
time_steps;
- meta=PSY.get_name(service),
+ meta = PSY.get_name(service),
)
for d in devices, t in time_steps
@@ -126,7 +126,7 @@ function PSI.add_variables!(
typeof(service),
PSY.get_name.(devices),
time_steps;
- meta=PSY.get_name(service),
+ meta = PSY.get_name(service),
)
for d in devices, t in time_steps
@@ -205,7 +205,7 @@ function PSI.add_variables!(
typeof(service),
PSY.get_name.(devices),
time_steps;
- meta=PSY.get_name(service),
+ meta = PSY.get_name(service),
)
for d in devices, t in time_steps
diff --git a/src/decision_models/only_energy_decision_model.jl b/src/decision_models/only_energy_decision_model.jl
index 22a13c5f..e4f165d7 100644
--- a/src/decision_models/only_energy_decision_model.jl
+++ b/src/decision_models/only_energy_decision_model.jl
@@ -419,8 +419,8 @@ function PSI.build_impl!(decision_model::PSI.DecisionModel{MerchantHybridEnergyC
RenewableActivePowerLimitConstraint(),
PSY.HybridSystem,
h_names,
- T_rt,
- meta="ub",
+ T_rt;
+ meta = "ub",
)
re_param_container =
diff --git a/src/feedforwards.jl b/src/feedforwards.jl
index 500aa895..a7e72789 100644
--- a/src/feedforwards.jl
+++ b/src/feedforwards.jl
@@ -17,7 +17,7 @@ struct CyclingChargeLimitFeedforward <: PSI.AbstractAffectFeedforward
source::Type{T},
affected_values::Vector{DataType},
penalty_cost::Float64,
- meta=ISOPT.CONTAINER_KEY_EMPTY_META,
+ meta = ISOPT.CONTAINER_KEY_EMPTY_META,
) where {T}
values_vector = Vector{PSI.ParameterKey}(undef, length(affected_values))
for (ix, v) in enumerate(affected_values)
@@ -60,7 +60,7 @@ struct CyclingDischargeLimitFeedforward <: PSI.AbstractAffectFeedforward
source::Type{T},
affected_values::Vector{DataType},
penalty_cost::Float64,
- meta=ISOPT.CONTAINER_KEY_EMPTY_META,
+ meta = ISOPT.CONTAINER_KEY_EMPTY_META,
) where {T}
values_vector = Vector{PSI.ParameterKey}(undef, length(affected_values))
for (ix, v) in enumerate(affected_values)
@@ -221,7 +221,10 @@ function PSI.add_feedforward_constraints!(
cycles_per_day * fraction_of_hour * length(time_steps) / HOURS_IN_DAY
if PSI.built_for_recurrent_solves(container)
param_value =
- PSI.get_parameter_array(container, CyclingChargeLimitParameter(), D)[ci_name, time_steps[end]]
+ PSI.get_parameter_array(container, CyclingChargeLimitParameter(), D)[
+ ci_name,
+ time_steps[end],
+ ]
con_cycling_ch[ci_name] = JuMP.@constraint(
PSI.get_jump_model(container),
efficiency.in * fraction_of_hour * sum(charge_var[ci_name, :]) <=
@@ -325,7 +328,10 @@ function PSI.add_feedforward_constraints!(
cycles_per_day * fraction_of_hour * length(time_steps) / HOURS_IN_DAY
if PSI.built_for_recurrent_solves(container)
param_value =
- PSI.get_parameter_array(container, CyclingDischargeLimitParameter(), D)[ci_name, time_steps[end]]
+ PSI.get_parameter_array(container, CyclingDischargeLimitParameter(), D)[
+ ci_name,
+ time_steps[end],
+ ]
con_cycling_ds[ci_name] = JuMP.@constraint(
PSI.get_jump_model(container),
(1.0 / efficiency.out) *
diff --git a/src/hybrid_system_decision_models.jl b/src/hybrid_system_decision_models.jl
index 1a63296f..45941305 100644
--- a/src/hybrid_system_decision_models.jl
+++ b/src/hybrid_system_decision_models.jl
@@ -204,8 +204,8 @@ function PSI.update_decision_state!(
state_data_index = 1
state_data.timestamps[:] .= range(
simulation_time;
- step=state_resolution,
- length=PSI.get_num_rows(state_data),
+ step = state_resolution,
+ length = PSI.get_num_rows(state_data),
)
else
state_data_index = PSI.find_timestamp_index(state_timestamps, simulation_time)
@@ -245,7 +245,7 @@ function PSI._update_parameter_values!(
state_timestamps = state_data.timestamps
max_state_index = PSI.get_num_rows(state_data)
state_data_index = PSI.find_timestamp_index(state_timestamps, current_time)
- sim_timestamps = range(current_time; step=model_resolution, length=time[end])
+ sim_timestamps = range(current_time; step = model_resolution, length = time[end])
for t in time
timestamp_ix = min(max_state_index, state_data_index + t_step)
@debug "parameter horizon is over the step" max_state_index > state_data_index + 1
@@ -283,11 +283,11 @@ function PSI._fix_parameter_value!(
if time_var[end] < time[end]
for t in time_var, name in component_names
t_ = 1 + (t - 1) * time[end] ÷ time_var[end]
- JuMP.fix(variable[name, t], parameter_array[name, t_]; force=true)
+ JuMP.fix(variable[name, t], parameter_array[name, t_]; force = true)
end
elseif time_var[end] == time[end]
for t in time_var, name in component_names
- JuMP.fix(variable[name, t], parameter_array[name, t]; force=true)
+ JuMP.fix(variable[name, t], parameter_array[name, t]; force = true)
end
else
error("invalid condition")
@@ -319,8 +319,8 @@ function PSI.update_decision_state!(
state_data_index = 1
state_data.timestamps[:] .= range(
simulation_time;
- step=state_resolution,
- length=PSI.get_num_rows(state_data),
+ step = state_resolution,
+ length = PSI.get_num_rows(state_data),
)
else
state_data_index = PSI.find_timestamp_index(state_timestamps, simulation_time)
@@ -376,7 +376,7 @@ function PSI._update_parameter_values!(
@assert false
end
state_data_index = PSI.find_timestamp_index(state_timestamps, current_time)
- sim_timestamps = range(current_time; step=model_resolution, length=time[end])
+ sim_timestamps = range(current_time; step = model_resolution, length = time[end])
for t in time
timestamp_ix = min(max_state_index, state_data_index + t_step)
@debug "parameter horizon is over the step" max_state_index > state_data_index + 1
diff --git a/src/utils.jl b/src/utils.jl
index b8684fb8..958d4a74 100644
--- a/src/utils.jl
+++ b/src/utils.jl
@@ -18,7 +18,7 @@ function get_time_series(
subcomponent_type::Type{T},
parameter::TimeSeriesParameter,
# HSA - 10.02.2024 ---------------
- meta=ISOPT.CONTAINER_KEY_EMPTY_META,
+ meta = ISOPT.CONTAINER_KEY_EMPTY_META,
) where {S <: PSY.HybridSystem, T <: PSY.Component}
parameter_container = get_parameter(container, parameter, S, meta)
subcomponent = get_subcomponent(component, subcomponent_type)
diff --git a/test/runtests.jl b/test/runtests.jl
index f2478dc3..bf1fe9cc 100644
--- a/test/runtests.jl
+++ b/test/runtests.jl
@@ -110,9 +110,9 @@ function run_tests()
config = IS.LoggingConfiguration(logging_config_filename)
else
config = IS.LoggingConfiguration(;
- filename=LOG_FILE,
- file_level=Logging.Info,
- console_level=Logging.Error,
+ filename = LOG_FILE,
+ file_level = Logging.Info,
+ console_level = Logging.Error,
)
end
console_logger = ConsoleLogger(config.console_stream, config.console_level)
diff --git a/test/test_device_hybrid_generation_constructors.jl b/test/test_device_hybrid_generation_constructors.jl
index 2f23c88e..7aaf4352 100644
--- a/test/test_device_hybrid_generation_constructors.jl
+++ b/test/test_device_hybrid_generation_constructors.jl
@@ -2,13 +2,13 @@
device_model = DeviceModel(
PSY.HybridSystem,
HybridEnergyOnlyDispatch;
- attributes=Dict{String, Any}("cycling" => false),
+ attributes = Dict{String, Any}("cycling" => false),
)
sys = PSB.build_system(PSITestSystems, "c_sys5_hybrid")
# Parameters Testing
model =
- DecisionModel(MockOperationProblem, DCPPowerModel, sys; store_variable_names=true)
+ DecisionModel(MockOperationProblem, DCPPowerModel, sys; store_variable_names = true)
mock_construct_device!(model, device_model)
moi_tests(model, 816, 0, 720, 192, 192, true)
psi_checkobjfun_test(model, GAEVF)
@@ -18,7 +18,7 @@ end
device_model = DeviceModel(
PSY.HybridSystem,
HybridEnergyOnlyDispatch;
- attributes=Dict{String, Any}("cycling" => false),
+ attributes = Dict{String, Any}("cycling" => false),
)
sys = PSB.build_system(PSITestSystems, "c_sys5_hybrid")
diff --git a/test/test_hybrid_device.jl b/test/test_hybrid_device.jl
index a78c7ddd..70d5b9cb 100644
--- a/test/test_hybrid_device.jl
+++ b/test/test_hybrid_device.jl
@@ -18,18 +18,18 @@
DeviceModel(
PSY.HybridSystem,
HybridEnergyOnlyDispatch;
- attributes=Dict{String, Any}("cycling" => true),
+ attributes = Dict{String, Any}("cycling" => true),
),
)
m = DecisionModel(
template_uc_dcp,
- sys_rts_da,
- optimizer=HiGHS_optimizer,
- store_variable_names=true,
+ sys_rts_da;
+ optimizer = HiGHS_optimizer,
+ store_variable_names = true,
)
- build_out = PSI.build!(m, output_dir=mktempdir(cleanup=true))
+ build_out = PSI.build!(m; output_dir = mktempdir(; cleanup = true))
@test build_out == PSI.BuildStatus.BUILT
solve_out = PSI.solve!(m)
@test solve_out == PSI.RunStatus.SUCCESSFUL
@@ -74,18 +74,18 @@ end
DeviceModel(
PSY.HybridSystem,
HybridDispatchWithReserves;
- attributes=Dict{String, Any}("cycling" => true),
+ attributes = Dict{String, Any}("cycling" => true),
),
)
m = DecisionModel(
template_uc_dcp,
- sys_rts_da,
- optimizer=HiGHS_optimizer,
- store_variable_names=true,
+ sys_rts_da;
+ optimizer = HiGHS_optimizer,
+ store_variable_names = true,
)
- build_out = PSI.build!(m, output_dir=mktempdir(cleanup=true))
+ build_out = PSI.build!(m; output_dir = mktempdir(; cleanup = true))
@test build_out == PSI.BuildStatus.BUILT
solve_out = PSI.solve!(m)
@test solve_out == PSI.RunStatus.SUCCESSFUL
diff --git a/test/test_hybrid_simulations.jl b/test/test_hybrid_simulations.jl
index 55c217ea..db8f7498 100644
--- a/test/test_hybrid_simulations.jl
+++ b/test/test_hybrid_simulations.jl
@@ -7,32 +7,35 @@
DeviceModel(
PSY.HybridSystem,
HybridEnergyOnlyDispatch;
- attributes=Dict{String, Any}("cycling" => false),
+ attributes = Dict{String, Any}("cycling" => false),
),
)
- set_network_model!(template_uc, NetworkModel(CopperPlatePowerModel, use_slacks=true))
+ set_network_model!(template_uc, NetworkModel(CopperPlatePowerModel; use_slacks = true))
- models = SimulationModels(
- decision_models=[
+ models = SimulationModels(;
+ decision_models = [
DecisionModel(
template_uc,
sys_uc;
- name="UC",
- optimizer=HiGHS_optimizer,
- initialize_model=false,
+ name = "UC",
+ optimizer = HiGHS_optimizer,
+ initialize_model = false,
),
],
)
sequence =
- SimulationSequence(models=models, ini_cond_chronology=InterProblemChronology())
+ SimulationSequence(;
+ models = models,
+ ini_cond_chronology = InterProblemChronology(),
+ )
- sim = Simulation(
- name="hybrid_test",
- steps=2,
- models=models,
- sequence=sequence,
- simulation_folder=mktempdir(cleanup=true),
+ sim = Simulation(;
+ name = "hybrid_test",
+ steps = 2,
+ models = models,
+ sequence = sequence,
+ simulation_folder = mktempdir(; cleanup = true),
)
build_out = build!(sim)
@test build_out == PSI.BuildStatus.BUILT
@@ -48,50 +51,53 @@ end
DeviceModel(
PSY.HybridSystem,
HybridEnergyOnlyDispatch;
- attributes=Dict{String, Any}("cycling" => false),
+ attributes = Dict{String, Any}("cycling" => false),
),
)
- set_network_model!(template_uc, NetworkModel(CopperPlatePowerModel, use_slacks=true))
+ set_network_model!(template_uc, NetworkModel(CopperPlatePowerModel; use_slacks = true))
template_ed = get_thermal_dispatch_template_network(
- NetworkModel(CopperPlatePowerModel, use_slacks=true),
+ NetworkModel(CopperPlatePowerModel; use_slacks = true),
)
set_device_model!(
template_ed,
DeviceModel(
PSY.HybridSystem,
HybridEnergyOnlyDispatch;
- attributes=Dict{String, Any}("cycling" => false),
+ attributes = Dict{String, Any}("cycling" => false),
),
)
- models = SimulationModels(
- decision_models=[
+ models = SimulationModels(;
+ decision_models = [
DecisionModel(
template_uc,
sys_uc;
- name="UC",
- optimizer=HiGHS_optimizer,
- initialize_model=false,
+ name = "UC",
+ optimizer = HiGHS_optimizer,
+ initialize_model = false,
),
DecisionModel(
template_ed,
sys_ed;
- name="ED",
- optimizer=HiGHS_optimizer,
- initialize_model=false,
+ name = "ED",
+ optimizer = HiGHS_optimizer,
+ initialize_model = false,
),
],
)
sequence =
- SimulationSequence(models=models, ini_cond_chronology=InterProblemChronology())
+ SimulationSequence(;
+ models = models,
+ ini_cond_chronology = InterProblemChronology(),
+ )
- sim = Simulation(
- name="hybrid_test",
- steps=2,
- models=models,
- sequence=sequence,
- simulation_folder=mktempdir(cleanup=true),
+ sim = Simulation(;
+ name = "hybrid_test",
+ steps = 2,
+ models = models,
+ sequence = sequence,
+ simulation_folder = mktempdir(; cleanup = true),
)
build_out = build!(sim)
@test build_out == PSI.BuildStatus.BUILT
diff --git a/test/test_merchant_cooptimizer.jl b/test/test_merchant_cooptimizer.jl
index 22146978..04e02d11 100644
--- a/test/test_merchant_cooptimizer.jl
+++ b/test/test_merchant_cooptimizer.jl
@@ -2,12 +2,12 @@
#### Create Systems ####
horizon_merchant_rt = 288
horizon_merchant_da = 24
- sys_rts_merchant = PSB.build_RTS_GMLC_RT_sys(
- raw_data=PSB.RTS_DIR,
- horizon=horizon_merchant_rt,
- interval=Hour(24),
+ sys_rts_merchant = PSB.build_RTS_GMLC_RT_sys(;
+ raw_data = PSB.RTS_DIR,
+ horizon = horizon_merchant_rt,
+ interval = Hour(24),
)
- sys_rts_da = PSB.build_RTS_GMLC_DA_sys(raw_data=PSB.RTS_DIR, horizon=24)
+ sys_rts_da = PSB.build_RTS_GMLC_DA_sys(; raw_data = PSB.RTS_DIR, horizon = 24)
# There is no Wind + Thermal in a Single Bus.
# We will try to pick the Wind in 317 bus Chuhsi
@@ -55,16 +55,16 @@
decision_optimizer_DA = DecisionModel(
MerchantHybridCooptimizerCase,
ProblemTemplate(CopperPlatePowerModel),
- sys,
- optimizer=HiGHS_optimizer,
- calculate_conflict=true,
- optimizer_solve_log_print=true,
- store_variable_names=true,
- initial_time=DateTime("2020-10-03T00:00:00"),
- name="MerchantHybridCooptimizerCase_DA",
+ sys;
+ optimizer = HiGHS_optimizer,
+ calculate_conflict = true,
+ optimizer_solve_log_print = true,
+ store_variable_names = true,
+ initial_time = DateTime("2020-10-03T00:00:00"),
+ name = "MerchantHybridCooptimizerCase_DA",
)
- build!(decision_optimizer_DA; output_dir=mktempdir())
+ build!(decision_optimizer_DA; output_dir = mktempdir())
solve!(decision_optimizer_DA)
results = ProblemResults(decision_optimizer_DA)
diff --git a/test/test_merchant_only_energy.jl b/test/test_merchant_only_energy.jl
index c342afd3..c4985519 100644
--- a/test/test_merchant_only_energy.jl
+++ b/test/test_merchant_only_energy.jl
@@ -1,12 +1,12 @@
@testset "Test HybridSystem Merchant Decision Model Only Energy" begin
horizon_merchant_rt = 288
horizon_merchant_da = 24
- sys_rts_merchant = PSB.build_RTS_GMLC_RT_sys(
- raw_data=PSB.RTS_DIR,
- horizon=horizon_merchant_rt,
- interval=Hour(24),
+ sys_rts_merchant = PSB.build_RTS_GMLC_RT_sys(;
+ raw_data = PSB.RTS_DIR,
+ horizon = horizon_merchant_rt,
+ interval = Hour(24),
)
- sys_rts_da = PSB.build_RTS_GMLC_DA_sys(raw_data=PSB.RTS_DIR, horizon=24)
+ sys_rts_da = PSB.build_RTS_GMLC_DA_sys(; raw_data = PSB.RTS_DIR, horizon = 24)
# There is no Wind + Thermal in a Single Bus.
# We will try to pick the Wind in 317 bus Chuhsi
@@ -37,14 +37,14 @@
decision_optimizer_DA = DecisionModel(
MerchantHybridEnergyCase,
ProblemTemplate(CopperPlatePowerModel),
- sys,
- optimizer=HiGHS_optimizer,
- calculate_conflict=true,
- store_variable_names=true;
- name="MerchantHybridEnergyCase_DA",
+ sys;
+ optimizer = HiGHS_optimizer,
+ calculate_conflict = true,
+ store_variable_names = true,
+ name = "MerchantHybridEnergyCase_DA",
)
- build!(decision_optimizer_DA; output_dir=mktempdir())
+ build!(decision_optimizer_DA; output_dir = mktempdir())
solve!(decision_optimizer_DA)
results = ProblemResults(decision_optimizer_DA)
diff --git a/test/test_utils/additional_templates.jl b/test/test_utils/additional_templates.jl
index e07709b9..f9e956e8 100644
--- a/test/test_utils/additional_templates.jl
+++ b/test/test_utils/additional_templates.jl
@@ -18,7 +18,7 @@ function set_uc_models!(template_uc)
DeviceModel(
PSY.HybridSystem,
HybridEnergyOnlyDispatch;
- attributes=Dict{String, Any}("cycling" => false),
+ attributes = Dict{String, Any}("cycling" => false),
),
)
set_device_model!(template_uc, GenericBattery, BookKeeping)
@@ -51,7 +51,7 @@ end
function set_ptdf_line_template!(template_uc)
set_device_model!(
template_uc,
- DeviceModel(Line, StaticBranch, duals=[NetworkFlowConstraint]),
+ DeviceModel(Line, StaticBranch; duals = [NetworkFlowConstraint]),
)
return
end
@@ -75,10 +75,10 @@ end
function get_uc_ptdf_template(sys_rts_da)
template_uc = ProblemTemplate(
NetworkModel(
- PTDFPowerModel,
- use_slacks=true,
- PTDF_matrix=PTDF(sys_rts_da),
- duals=[CopperPlateBalanceConstraint],
+ PTDFPowerModel;
+ use_slacks = true,
+ PTDF_matrix = PTDF(sys_rts_da),
+ duals = [CopperPlateBalanceConstraint],
),
)
set_uc_models!(template_uc)
@@ -111,10 +111,10 @@ end
function get_uc_copperplate_template(sys_rts_da)
template_uc = ProblemTemplate(
NetworkModel(
- CopperPlatePowerModel,
- use_slacks=true,
- PTDF_matrix=PTDF(sys_rts_da),
- duals=[CopperPlateBalanceConstraint],
+ CopperPlatePowerModel;
+ use_slacks = true,
+ PTDF_matrix = PTDF(sys_rts_da),
+ duals = [CopperPlateBalanceConstraint],
),
)
set_uc_models!(template_uc)
@@ -132,7 +132,11 @@ end
function get_uc_dcp_template()
template_uc = ProblemTemplate(
- NetworkModel(DCPPowerModel, use_slacks=true, duals=[NodalBalanceActiveConstraint]),
+ NetworkModel(
+ DCPPowerModel;
+ use_slacks = true,
+ duals = [NodalBalanceActiveConstraint],
+ ),
)
set_uc_models!(template_uc)
set_dcp_line_template!(template_uc)
@@ -155,60 +159,60 @@ function build_simulation_case(
mipgap::Float64,
start_time,
)
- models = SimulationModels(
- decision_models=[
+ models = SimulationModels(;
+ decision_models = [
DecisionModel(
template_uc,
sys_da;
- name="UC",
- optimizer=HiGHS_optimizer,
- system_to_file=false,
- initialize_model=true,
- optimizer_solve_log_print=true,
- direct_mode_optimizer=true,
- rebuild_model=false,
- store_variable_names=true,
+ name = "UC",
+ optimizer = HiGHS_optimizer,
+ system_to_file = false,
+ initialize_model = true,
+ optimizer_solve_log_print = true,
+ direct_mode_optimizer = true,
+ rebuild_model = false,
+ store_variable_names = true,
#check_numerical_bounds=false,
),
DecisionModel(
template_ed,
sys_rt;
- name="ED",
- optimizer=optimizer_with_attributes(Xpress.Optimizer),
- system_to_file=false,
- initialize_model=true,
- optimizer_solve_log_print=false,
- check_numerical_bounds=false,
- rebuild_model=false,
- calculate_conflict=true,
- store_variable_names=true,
+ name = "ED",
+ optimizer = optimizer_with_attributes(Xpress.Optimizer),
+ system_to_file = false,
+ initialize_model = true,
+ optimizer_solve_log_print = false,
+ check_numerical_bounds = false,
+ rebuild_model = false,
+ calculate_conflict = true,
+ store_variable_names = true,
#export_pwl_vars = true,
),
],
)
# Set-up the sequence UC-ED
- sequence = SimulationSequence(
- models=models,
- feedforwards=Dict(
+ sequence = SimulationSequence(;
+ models = models,
+ feedforwards = Dict(
"ED" => [
- SemiContinuousFeedforward(
- component_type=ThermalStandard,
- source=OnVariable,
- affected_values=[ActivePowerVariable],
+ SemiContinuousFeedforward(;
+ component_type = ThermalStandard,
+ source = OnVariable,
+ affected_values = [ActivePowerVariable],
),
],
),
- ini_cond_chronology=InterProblemChronology(),
+ ini_cond_chronology = InterProblemChronology(),
)
- sim = Simulation(
- name="compact_sim",
- steps=num_steps,
- models=models,
- sequence=sequence,
- initial_time=start_time,
- simulation_folder=mktempdir(cleanup=true),
+ sim = Simulation(;
+ name = "compact_sim",
+ steps = num_steps,
+ models = models,
+ sequence = sequence,
+ initial_time = start_time,
+ simulation_folder = mktempdir(; cleanup = true),
)
return sim
@@ -224,52 +228,52 @@ function build_simulation_case_optimizer(
mipgap::Float64,
start_time,
)
- models = SimulationModels(
- decision_models=[
+ models = SimulationModels(;
+ decision_models = [
decision_optimizer,
DecisionModel(
template_uc,
sys_da;
- name="UC",
- optimizer=HiGHS_optimizer,
- system_to_file=false,
- initialize_model=true,
- optimizer_solve_log_print=false,
- direct_mode_optimizer=true,
- rebuild_model=false,
- store_variable_names=true,
+ name = "UC",
+ optimizer = HiGHS_optimizer,
+ system_to_file = false,
+ initialize_model = true,
+ optimizer_solve_log_print = false,
+ direct_mode_optimizer = true,
+ rebuild_model = false,
+ store_variable_names = true,
#check_numerical_bounds=false,
),
],
)
# Set-up the sequence Optimizer-UC
- sequence = SimulationSequence(
- models=models,
- feedforwards=Dict(
+ sequence = SimulationSequence(;
+ models = models,
+ feedforwards = Dict(
"UC" => [
- FixValueFeedforward(
- component_type=PSY.HybridSystem,
- source=EnergyDABidOut,
- affected_values=[ActivePowerOutVariable],
+ FixValueFeedforward(;
+ component_type = PSY.HybridSystem,
+ source = EnergyDABidOut,
+ affected_values = [ActivePowerOutVariable],
),
- FixValueFeedforward(
- component_type=PSY.HybridSystem,
- source=EnergyDABidIn,
- affected_values=[ActivePowerInVariable],
+ FixValueFeedforward(;
+ component_type = PSY.HybridSystem,
+ source = EnergyDABidIn,
+ affected_values = [ActivePowerInVariable],
),
],
),
- ini_cond_chronology=InterProblemChronology(),
+ ini_cond_chronology = InterProblemChronology(),
)
- sim = Simulation(
- name="compact_sim",
- steps=num_steps,
- models=models,
- sequence=sequence,
- initial_time=start_time,
- simulation_folder=mktempdir(cleanup=true),
+ sim = Simulation(;
+ name = "compact_sim",
+ steps = num_steps,
+ models = models,
+ sequence = sequence,
+ initial_time = start_time,
+ simulation_folder = mktempdir(; cleanup = true),
)
return sim
diff --git a/test/test_utils/function_utils.jl b/test/test_utils/function_utils.jl
index 41f6c751..50816664 100644
--- a/test/test_utils/function_utils.jl
+++ b/test/test_utils/function_utils.jl
@@ -4,22 +4,22 @@ function get_da_max_active_power_series(r_gen, starttime, steps::Int)
ta = get_time_series_array(
SingleTimeSeries,
r_gen,
- "max_active_power",
- start_time=starttime,
- len=24 * steps,
+ "max_active_power";
+ start_time = starttime,
+ len = 24 * steps,
)
- return DataFrame(DateTime=timestamp(ta), MaxPower=values(ta))
+ return DataFrame(; DateTime = timestamp(ta), MaxPower = values(ta))
end
function get_rt_max_active_power_series(r_gen, starttime, steps::Int)
ta = get_time_series_array(
SingleTimeSeries,
r_gen,
- "max_active_power",
- start_time=starttime,
- len=24 * 12 * steps,
+ "max_active_power";
+ start_time = starttime,
+ len = 24 * 12 * steps,
)
- return DataFrame(DateTime=timestamp(ta), MaxPower=values(ta))
+ return DataFrame(; DateTime = timestamp(ta), MaxPower = values(ta))
end
function get_battery_params(b_gen::GenericBattery)
@@ -49,7 +49,7 @@ function get_battery_params(b_gen::GenericBattery)
η_in,
η_out,
]
- return DataFrame(ParamName=battery_params_names, Value=battery_params_vals)
+ return DataFrame(; ParamName = battery_params_names, Value = battery_params_vals)
end
function get_thermal_params(t_gen)
@@ -60,9 +60,9 @@ function get_thermal_params(t_gen)
second_part = three_cost.variable[2]
slope = (second_part[1] - first_part[1]) / (second_part[2] - first_part[2]) # $/MWh
fix_cost = three_cost.fixed # $/h
- return DataFrame(
- ParamName=["P_min", "P_max", "C_var", "C_fix"],
- Value=[P_min, P_max, slope, fix_cost],
+ return DataFrame(;
+ ParamName = ["P_min", "P_max", "C_var", "C_fix"],
+ Value = [P_min, P_max, slope, fix_cost],
)
end
@@ -89,21 +89,21 @@ function _build_battery(
)
name = string(bus.number) * "_BATTERY"
device = GenericBattery(;
- name=name,
- available=true,
- bus=bus,
- prime_mover_type=PSY.PrimeMovers.BA,
- initial_energy=energy_capacity / 2,
- state_of_charge_limits=(min=energy_capacity * 0.05, max=energy_capacity),
- rating=rating,
- active_power=rating,
- input_active_power_limits=(min=0.0, max=rating),
- output_active_power_limits=(min=0.0, max=rating),
- efficiency=(in=efficiency_in, out=efficiency_out),
- reactive_power=0.0,
- reactive_power_limits=nothing,
- base_power=100.0,
- operation_cost=PSY.TwoPartCost(0.0, 0.0),
+ name = name,
+ available = true,
+ bus = bus,
+ prime_mover_type = PSY.PrimeMovers.BA,
+ initial_energy = energy_capacity / 2,
+ state_of_charge_limits = (min = energy_capacity * 0.05, max = energy_capacity),
+ rating = rating,
+ active_power = rating,
+ input_active_power_limits = (min = 0.0, max = rating),
+ output_active_power_limits = (min = 0.0, max = rating),
+ efficiency = (in = efficiency_in, out = efficiency_out),
+ reactive_power = 0.0,
+ reactive_power_limits = nothing,
+ base_power = 100.0,
+ operation_cost = PSY.TwoPartCost(0.0, 0.0),
)
return device
end
@@ -128,24 +128,24 @@ function add_hybrid_to_chuhsi_bus!(sys::System)
load = get_component(PowerLoad, sys, load_name)
# Create the Hybrid
hybrid_name = string(bus.number) * "_Hybrid"
- hybrid = PSY.HybridSystem(
- name=hybrid_name,
- available=true,
- status=true,
- bus=bus,
- active_power=1.0,
- reactive_power=0.0,
- base_power=100.0,
- operation_cost=TwoPartCost(nothing),
- thermal_unit=thermal, #new_th,
- electric_load=load, #new_load,
- storage=bat,
- renewable_unit=renewable, #new_ren,
- interconnection_impedance=0.0 + 0.0im,
- interconnection_rating=nothing,
- input_active_power_limits=(min=0.0, max=10.0),
- output_active_power_limits=(min=0.0, max=10.0),
- reactive_power_limits=nothing,
+ hybrid = PSY.HybridSystem(;
+ name = hybrid_name,
+ available = true,
+ status = true,
+ bus = bus,
+ active_power = 1.0,
+ reactive_power = 0.0,
+ base_power = 100.0,
+ operation_cost = TwoPartCost(nothing),
+ thermal_unit = thermal, #new_th,
+ electric_load = load, #new_load,
+ storage = bat,
+ renewable_unit = renewable, #new_ren,
+ interconnection_impedance = 0.0 + 0.0im,
+ interconnection_rating = nothing,
+ input_active_power_limits = (min = 0.0, max = 10.0),
+ output_active_power_limits = (min = 0.0, max = 10.0),
+ reactive_power_limits = nothing,
)
# Add Hybrid
add_component!(sys, hybrid)
diff --git a/test/test_utils/price_generation_utils.jl b/test/test_utils/price_generation_utils.jl
index 41f6c751..50816664 100644
--- a/test/test_utils/price_generation_utils.jl
+++ b/test/test_utils/price_generation_utils.jl
@@ -4,22 +4,22 @@ function get_da_max_active_power_series(r_gen, starttime, steps::Int)
ta = get_time_series_array(
SingleTimeSeries,
r_gen,
- "max_active_power",
- start_time=starttime,
- len=24 * steps,
+ "max_active_power";
+ start_time = starttime,
+ len = 24 * steps,
)
- return DataFrame(DateTime=timestamp(ta), MaxPower=values(ta))
+ return DataFrame(; DateTime = timestamp(ta), MaxPower = values(ta))
end
function get_rt_max_active_power_series(r_gen, starttime, steps::Int)
ta = get_time_series_array(
SingleTimeSeries,
r_gen,
- "max_active_power",
- start_time=starttime,
- len=24 * 12 * steps,
+ "max_active_power";
+ start_time = starttime,
+ len = 24 * 12 * steps,
)
- return DataFrame(DateTime=timestamp(ta), MaxPower=values(ta))
+ return DataFrame(; DateTime = timestamp(ta), MaxPower = values(ta))
end
function get_battery_params(b_gen::GenericBattery)
@@ -49,7 +49,7 @@ function get_battery_params(b_gen::GenericBattery)
η_in,
η_out,
]
- return DataFrame(ParamName=battery_params_names, Value=battery_params_vals)
+ return DataFrame(; ParamName = battery_params_names, Value = battery_params_vals)
end
function get_thermal_params(t_gen)
@@ -60,9 +60,9 @@ function get_thermal_params(t_gen)
second_part = three_cost.variable[2]
slope = (second_part[1] - first_part[1]) / (second_part[2] - first_part[2]) # $/MWh
fix_cost = three_cost.fixed # $/h
- return DataFrame(
- ParamName=["P_min", "P_max", "C_var", "C_fix"],
- Value=[P_min, P_max, slope, fix_cost],
+ return DataFrame(;
+ ParamName = ["P_min", "P_max", "C_var", "C_fix"],
+ Value = [P_min, P_max, slope, fix_cost],
)
end
@@ -89,21 +89,21 @@ function _build_battery(
)
name = string(bus.number) * "_BATTERY"
device = GenericBattery(;
- name=name,
- available=true,
- bus=bus,
- prime_mover_type=PSY.PrimeMovers.BA,
- initial_energy=energy_capacity / 2,
- state_of_charge_limits=(min=energy_capacity * 0.05, max=energy_capacity),
- rating=rating,
- active_power=rating,
- input_active_power_limits=(min=0.0, max=rating),
- output_active_power_limits=(min=0.0, max=rating),
- efficiency=(in=efficiency_in, out=efficiency_out),
- reactive_power=0.0,
- reactive_power_limits=nothing,
- base_power=100.0,
- operation_cost=PSY.TwoPartCost(0.0, 0.0),
+ name = name,
+ available = true,
+ bus = bus,
+ prime_mover_type = PSY.PrimeMovers.BA,
+ initial_energy = energy_capacity / 2,
+ state_of_charge_limits = (min = energy_capacity * 0.05, max = energy_capacity),
+ rating = rating,
+ active_power = rating,
+ input_active_power_limits = (min = 0.0, max = rating),
+ output_active_power_limits = (min = 0.0, max = rating),
+ efficiency = (in = efficiency_in, out = efficiency_out),
+ reactive_power = 0.0,
+ reactive_power_limits = nothing,
+ base_power = 100.0,
+ operation_cost = PSY.TwoPartCost(0.0, 0.0),
)
return device
end
@@ -128,24 +128,24 @@ function add_hybrid_to_chuhsi_bus!(sys::System)
load = get_component(PowerLoad, sys, load_name)
# Create the Hybrid
hybrid_name = string(bus.number) * "_Hybrid"
- hybrid = PSY.HybridSystem(
- name=hybrid_name,
- available=true,
- status=true,
- bus=bus,
- active_power=1.0,
- reactive_power=0.0,
- base_power=100.0,
- operation_cost=TwoPartCost(nothing),
- thermal_unit=thermal, #new_th,
- electric_load=load, #new_load,
- storage=bat,
- renewable_unit=renewable, #new_ren,
- interconnection_impedance=0.0 + 0.0im,
- interconnection_rating=nothing,
- input_active_power_limits=(min=0.0, max=10.0),
- output_active_power_limits=(min=0.0, max=10.0),
- reactive_power_limits=nothing,
+ hybrid = PSY.HybridSystem(;
+ name = hybrid_name,
+ available = true,
+ status = true,
+ bus = bus,
+ active_power = 1.0,
+ reactive_power = 0.0,
+ base_power = 100.0,
+ operation_cost = TwoPartCost(nothing),
+ thermal_unit = thermal, #new_th,
+ electric_load = load, #new_load,
+ storage = bat,
+ renewable_unit = renewable, #new_ren,
+ interconnection_impedance = 0.0 + 0.0im,
+ interconnection_rating = nothing,
+ input_active_power_limits = (min = 0.0, max = 10.0),
+ output_active_power_limits = (min = 0.0, max = 10.0),
+ reactive_power_limits = nothing,
)
# Add Hybrid
add_component!(sys, hybrid)
diff --git a/test/x_test_cooptimizer_with_build.jl b/test/x_test_cooptimizer_with_build.jl
index 07389aa5..84c15822 100644
--- a/test/x_test_cooptimizer_with_build.jl
+++ b/test/x_test_cooptimizer_with_build.jl
@@ -1,5 +1,5 @@
@testset "Test HybridSystem CoOptimizer DecisionModel" begin
- sys = PSB.build_RTS_GMLC_RT_sys(raw_data=PSB.RTS_DIR, horizon=864)
+ sys = PSB.build_RTS_GMLC_RT_sys(; raw_data = PSB.RTS_DIR, horizon = 864)
# Attach Data to System Ext
bus_name = "chuhsi"
@@ -31,11 +31,11 @@
m = DecisionModel(
MerchantHybridCooptimized,
ProblemTemplate(CopperPlatePowerModel),
- sys,
- optimizer=HiGHS_optimizer,
- store_variable_names=true,
+ sys;
+ optimizer = HiGHS_optimizer,
+ store_variable_names = true,
)
- build_out = PSI.build!(m, output_dir=mktempdir(cleanup=true))
+ build_out = PSI.build!(m; output_dir = mktempdir(; cleanup = true))
@test build_out == PSI.BuildStatus.BUILT
solve_out = PSI.solve!(m)
@test solve_out == PSI.RunStatus.SUCCESSFUL
diff --git a/test/x_test_optimizer_sequence.jl b/test/x_test_optimizer_sequence.jl
index afc20505..f851cc47 100644
--- a/test/x_test_optimizer_sequence.jl
+++ b/test/x_test_optimizer_sequence.jl
@@ -3,9 +3,13 @@
######## Load Systems #########
###############################
- sys_rts_da = PSB.build_RTS_GMLC_DA_sys(raw_data=PSB.RTS_DIR, horizon=48)
+ sys_rts_da = PSB.build_RTS_GMLC_DA_sys(; raw_data = PSB.RTS_DIR, horizon = 48)
sys_rts_rt =
- PSB.build_RTS_GMLC_RT_sys(raw_data=PSB.RTS_DIR, horizon=864, interval=Minute(1440))
+ PSB.build_RTS_GMLC_RT_sys(;
+ raw_data = PSB.RTS_DIR,
+ horizon = 864,
+ interval = Minute(1440),
+ )
# There is no Wind + Thermal in a Single Bus.
# We will try to pick the Wind in 317 bus Chuhsi
@@ -62,9 +66,9 @@
m = DecisionModel(
MerchantHybridEnergyOnly,
ProblemTemplate(CopperPlatePowerModel),
- sys_rts_rt,
- optimizer=HiGHS_optimizer,
- horizon=864,
+ sys_rts_rt;
+ optimizer = HiGHS_optimizer,
+ horizon = 864,
)
sim_optimizer = build_simulation_case_optimizer(
diff --git a/test/x_test_optimizer_with_build.jl b/test/x_test_optimizer_with_build.jl
index ea141090..f0f0ded2 100644
--- a/test/x_test_optimizer_with_build.jl
+++ b/test/x_test_optimizer_with_build.jl
@@ -1,5 +1,5 @@
@testset "Test HybridSystem CoOptimizer DecisionModel" begin
- sys = PSB.build_RTS_GMLC_RT_sys(raw_data=PSB.RTS_DIR, horizon=864)
+ sys = PSB.build_RTS_GMLC_RT_sys(; raw_data = PSB.RTS_DIR, horizon = 864)
# Attach Data to System Ext
bus_name = "chuhsi"
@@ -30,12 +30,12 @@
m = DecisionModel(
MerchantHybridEnergyOnly,
ProblemTemplate(CopperPlatePowerModel),
- sys,
- optimizer=HiGHS_optimizer,
- calculate_conflict=true,
- store_variable_names=true,
+ sys;
+ optimizer = HiGHS_optimizer,
+ calculate_conflict = true,
+ store_variable_names = true,
)
- build_out = PSI.build!(m, output_dir=mktempdir(cleanup=true))
+ build_out = PSI.build!(m; output_dir = mktempdir(; cleanup = true))
@test build_out == PSI.BuildStatus.BUILT
solve_out = PSI.solve!(m)
@test solve_out == PSI.RunStatus.SUCCESSFUL
From b3e27159a7bbdaa9eda07ef2f79309e6054d1138 Mon Sep 17 00:00:00 2001
From: kdayday
Date: Thu, 19 Feb 2026 13:16:21 -0700
Subject: [PATCH 03/46] Add formulations and add and clean docstrings
---
src/core/constraints.jl | 40 ++++-
src/core/decision_models.jl | 2 +-
src/core/formulations.jl | 349 +++++++++++++++++++++++++++++++++++-
src/core/parameters.jl | 2 +-
src/core/variables.jl | 18 +-
src/feedforwards.jl | 9 +-
6 files changed, 389 insertions(+), 31 deletions(-)
diff --git a/src/core/constraints.jl b/src/core/constraints.jl
index 6e531cb5..208166a4 100644
--- a/src/core/constraints.jl
+++ b/src/core/constraints.jl
@@ -13,6 +13,7 @@ struct RealTimeBidOutRangeLimit <: PSI.ConstraintType end
struct RealTimeBidInRangeLimit <: PSI.ConstraintType end
## Energy Market Asset Balance ##
+"""Links day-ahead energy bids to internal asset power (upper level)."""
struct EnergyBidAssetBalance <: PSI.ConstraintType end
## AS Market Convergence ##
@@ -35,37 +36,58 @@ struct BatteryDischargeBidDown <: PSI.ConstraintType end
## Across Markets Balance ##
struct BidBalanceOut <: PSI.ConstraintType end
struct BidBalanceIn <: PSI.ConstraintType end
+"""Binary status for hybrid output (generation) direction at the PCC."""
struct StatusOutOn <: PSI.ConstraintType end
+"""Binary status for hybrid input (consumption) direction at the PCC."""
struct StatusInOn <: PSI.ConstraintType end
## AS for Components
+"""Ensures storage has sufficient energy to meet ancillary service commitments."""
struct ReserveCoverageConstraint <: PSI.ConstraintType end
+"""End-of-period energy coverage for ancillary services."""
struct ReserveCoverageConstraintEndOfPeriod <: PSI.ConstraintType end
+"""Upper bound on charging power allocated to ancillary services."""
struct ChargingReservePowerLimit <: PSI.ConstraintType end
+"""Upper bound on discharging power allocated to ancillary services."""
struct DischargingReservePowerLimit <: PSI.ConstraintType end
+"""Upper bound on thermal power allocated to ancillary services."""
struct ThermalReserveLimit <: PSI.ConstraintType end
+"""Upper bound on renewable power allocated to ancillary services."""
struct RenewableReserveLimit <: PSI.ConstraintType end
## Auxiliary for Output
+"""Total reserve at PCC equals sum of component reserve allocations."""
struct ReserveBalance <: PSI.ConstraintType end
-# Used for DeviceModels inside UC/ED to equate with the ActivePowerReserveVariable
+"""Links component reserve variables to total reserve at the PCC."""
struct HybridReserveAssignmentConstraint <: PSI.ConstraintType end
###################
### Lower Level ###
###################
+"""Net internal power (thermal + renewable + discharge − charge − load) equals net PCC power (out − in)."""
struct EnergyAssetBalance <: PSI.ConstraintType end
+"""Thermal power upper bound: ``p^{\\text{th}}_t \\leq u^{\\text{th}}_t P_{\\max,\\text{th}}``."""
struct ThermalOnVariableUb <: PSI.ConstraintType end
+"""Thermal power lower bound: ``p^{\\text{th}}_t \\geq u^{\\text{th}}_t P_{\\min,\\text{th}}``."""
struct ThermalOnVariableLb <: PSI.ConstraintType end
+"""Charge power upper bound when not discharging: ``p^{\\text{ch}}_t \\leq (1 - ss^{\\text{st}}_t) P_{\\max,\\text{ch}}``."""
struct BatteryStatusChargeOn <: PSI.ConstraintType end
+"""Discharge power upper bound when discharging: ``p^{\\text{ds}}_t \\leq ss^{\\text{st}}_t P_{\\max,\\text{ds}}``."""
struct BatteryStatusDischargeOn <: PSI.ConstraintType end
+"""Storage energy balance: ``e^{\\text{st}}_t = e^{\\text{st}}_{t-1} + \\Delta t(\\eta_{\\text{ch}} p^{\\text{ch}}_t - p^{\\text{ds}}_t/\\eta_{\\text{ds}})``."""
struct BatteryBalance <: PSI.ConstraintType end
+"""Cumulative charging energy over horizon ≤ ``C_{\\text{st}} E_{\\max,\\text{st}}``."""
struct CyclingCharge <: PSI.ConstraintType end
+"""Cumulative discharging energy over horizon ≤ ``C_{\\text{st}} E_{\\max,\\text{st}}``."""
struct CyclingDischarge <: PSI.ConstraintType end
+"""Regularization on charge power changes (when `"regularization" => true`): penalizes ``|\\Delta p^{\\text{ch}}_t|``-style changes. See formulation docstrings for full constraint."""
struct ChargeRegularizationConstraint <: PSI.ConstraintType end
+"""Regularization on discharge power changes (when `"regularization" => true`): penalizes ``|\\Delta p^{\\text{ds}}_t|``-style changes. See formulation docstrings for full constraint."""
struct DischargeRegularizationConstraint <: PSI.ConstraintType end
+"""End-of-horizon storage energy target (when `"energy_target" => true`): ``e^{\\text{st}}_T = E^{\\text{st}}_T``."""
struct StateofChargeTargetConstraint <: PSI.ConstraintType end
+"""Renewable power upper bound: ``p^{\\text{re}}_t \\leq P^{*,\\text{re}}_t``."""
struct RenewableActivePowerLimitConstraint <: PSI.ConstraintType end
###################
@@ -82,16 +104,16 @@ struct FeedForwardCyclingDischargeConstraint <: PSI.ConstraintType end
"""
OptConditionThermalPower
-Constraint enforcing KKT stationarity for thermal power in the merchant (lower-level)
+Constraint enforcing Karush-Kuhn-Tucker (KKT) stationarity for thermal power in the merchant (lower-level)
model: links dual of thermal limits (``\\mu^{\\text{ThUb}}``, ``\\mu^{\\text{ThLb}}``) to the thermal power variable.
-Used in bilevel/MPEC formulations.
+Used in bilevel/mathematical program with equilibrium constraints (MPEC) formulations.
"""
struct OptConditionThermalPower <: PSI.ConstraintType end
"""
OptConditionRenewablePower
-Constraint enforcing KKT stationarity for renewable power (``p_{\\text{re},t}``) in the merchant
+Constraint enforcing Karush-Kuhn-Tucker (KKT) stationarity for renewable power (``p_{\\text{re},t}``) in the merchant
model; ties duals of renewable limit (``\\mu^{\\text{ReUb}}``, ``\\mu^{\\text{ReLb}}``) to the renewable power variable.
"""
struct OptConditionRenewablePower <: PSI.ConstraintType end
@@ -99,7 +121,7 @@ struct OptConditionRenewablePower <: PSI.ConstraintType end
"""
OptConditionBatteryCharge
-Constraint enforcing KKT stationarity for storage charging (``p_{\\text{ch},t}``) in the merchant
+Constraint enforcing Karush-Kuhn-Tucker (KKT) stationarity for storage charging (``p_{\\text{ch},t}``) in the merchant
model; involves duals ``\\mu^{\\text{ChUb}}``, ``\\mu^{\\text{ChLb}}`` and charge limits.
"""
struct OptConditionBatteryCharge <: PSI.ConstraintType end
@@ -107,7 +129,7 @@ struct OptConditionBatteryCharge <: PSI.ConstraintType end
"""
OptConditionBatteryDischarge
-Constraint enforcing KKT stationarity for storage discharging (``p_{\\text{ds},t}``) in the merchant
+Constraint enforcing Karush-Kuhn-Tucker (KKT) stationarity for storage discharging (``p_{\\text{ds},t}``) in the merchant
model; involves duals ``\\mu^{\\text{DsUb}}``, ``\\mu^{\\text{DsLb}}``.
"""
struct OptConditionBatteryDischarge <: PSI.ConstraintType end
@@ -115,7 +137,7 @@ struct OptConditionBatteryDischarge <: PSI.ConstraintType end
"""
OptConditionEnergyVariable
-Constraint enforcing KKT stationarity for the energy variable at the PCC in the
+Constraint enforcing Karush-Kuhn-Tucker (KKT) stationarity for the energy variable at the point of common coupling (PCC) in the
merchant model. #TODO DOCS
"""
struct OptConditionEnergyVariable <: PSI.ConstraintType end
@@ -128,7 +150,7 @@ struct OptConditionEnergyVariable <: PSI.ConstraintType end
ComplementarySlacknessEnergyAssetBalanceUb
Complementary slackness constraint (upper bound) for the energy asset balance
-equation in the merchant model; used in MPEC/bilevel reformulation.
+equation in the merchant model; used in mathematical program with equilibrium constraints (MPEC)/bilevel reformulation.
"""
struct ComplementarySlacknessEnergyAssetBalanceUb <: PSI.ConstraintType end
@@ -228,6 +250,6 @@ struct ComplementarySlacknessEnergyLimitLb <: PSI.ConstraintType end
Constraint that enforces strong duality for the merchant (lower-level) problem
in a bilevel formulation: objective value equals dual objective (or equivalent
-cut), so that the lower level is replaced by its KKT conditions.
+cut), so that the lower level is replaced by its Karush-Kuhn-Tucker (KKT) conditions.
"""
struct StrongDualityCut <: PSI.ConstraintType end
diff --git a/src/core/decision_models.jl b/src/core/decision_models.jl
index 1f1aabdb..aa27d764 100644
--- a/src/core/decision_models.jl
+++ b/src/core/decision_models.jl
@@ -22,7 +22,7 @@ struct MerchantHybridEnergyFixedDA <: HybridDecisionProblem end
Decision problem for a merchant hybrid that co-optimizes energy and ancillary services
in day-ahead and real-time markets. Maximizes ``d'y - c_h' x`` (revenue from bids/offers minus operating cost) subject to
-market and asset constraints; AS are committed in DA and fulfilled by internal asset
+market and asset constraints; ancillary services are committed in DA and fulfilled by internal asset
allocation in RT.
"""
struct MerchantHybridCooptimizerCase <: HybridDecisionProblem end
diff --git a/src/core/formulations.jl b/src/core/formulations.jl
index 6d19f59b..e6f0a263 100644
--- a/src/core/formulations.jl
+++ b/src/core/formulations.jl
@@ -5,15 +5,175 @@ abstract type AbstractHybridFormulationWithReserves <: AbstractHybridFormulation
"""
HybridDispatchWithReserves
-Device formulation for a hybrid system (single PCC with renewable, thermal, and storage)
-that participates in both energy and ancillary service (AS) markets. Implements the
-centralized PCM model where the hybrid plant's net power at the PCC is constrained by
-``P_{\\max,\\text{pcc}}`` and AS allocations (``sb^{\\text{out}}_{p,t}``, ``sb^{\\text{in}}_{p,t}``) are assigned to internal assets
-(thermal, renewable, charge, discharge) per the four-quadrant AS model.
+Device formulation for a hybrid system (single point of common coupling (PCC) with renewable,
+thermal, and storage) that participates in both energy and ancillary services markets.
+Implements the centralized production cost modeling (PCM) model where the hybrid plant's net
+power at the PCC is constrained by ``P_{\\max,\\text{pcc}}`` and ancillary service allocations
+(``sb^{\\text{out}}_{p,t}``, ``sb^{\\text{in}}_{p,t}``) are assigned to internal assets (thermal,
+renewable, charge, discharge) per the four-quadrant ancillary service model.
Use with a hybrid system in a
[`PowerSimulations.DeviceModel`](@extref PowerSimulations.DeviceModel) for unit commitment
or economic dispatch.
+
+**Variables:**
+
+ - [`PowerSimulations.ActivePowerOutVariable`](@extref PowerSimulations.ActivePowerOutVariable):
+
+ + Bounds: [0.0, ``P_{\\max,\\text{pcc}}``]
+ + Symbol: ``p^{\\text{out}}_t``
+
+ - [`PowerSimulations.ActivePowerInVariable`](@extref PowerSimulations.ActivePowerInVariable):
+
+ + Bounds: [0.0, ``P_{\\max,\\text{pcc}}``]
+ + Symbol: ``p^{\\text{in}}_t``
+
+ - [`PowerSimulations.ReservationVariable`](@extref PowerSimulations.ReservationVariable):
+
+ + Bounds: {0, 1}
+ + Symbol: ``u^{\\text{st}}_t``
+
+ - `ThermalPower`:
+
+ + Bounds: [0.0, ``P_{\\max,\\text{th}}``] when on
+ + Symbol: ``p^{\\text{th}}_t``
+
+ - [`PowerSimulations.OnVariable`](@extref PowerSimulations.OnVariable):
+
+ + Bounds: {0, 1}
+ + Symbol: ``u^{\\text{th}}_t``
+
+ - `RenewablePower`:
+
+ + Bounds: [0.0, ``P^{*,\\text{re}}_t``]
+ + Symbol: ``p^{\\text{re}}_t``
+
+ - `BatteryCharge`:
+
+ + Bounds: [0.0, ``P_{\\max,\\text{ch}}``] when charging
+ + Symbol: ``p^{\\text{ch}}_t``
+
+ - `BatteryDischarge`:
+
+ + Bounds: [0.0, ``P_{\\max,\\text{ds}}``] when discharging
+ + Symbol: ``p^{\\text{ds}}_t``
+
+ - [`PowerSimulations.EnergyVariable`](@extref PowerSimulations.EnergyVariable):
+
+ + Bounds: [0.0, ``E_{\\max,\\text{st}}``]
+ + Symbol: ``e^{\\text{st}}_t``
+
+ - `BatteryStatus`:
+
+ + Bounds: {0, 1}
+ + Symbol: ``ss^{\\text{st}}_t`` (0 = charge, 1 = discharge)
+
+ - [`ReserveVariableOut`](@ref):
+
+ + Bounds: [0.0, ]
+ + Symbol: ``sb^{\\text{out}}_t``
+
+ - [`ReserveVariableIn`](@ref):
+
+ + Bounds: [0.0, ]
+ + Symbol: ``sb^{\\text{in}}_t``
+
+**Time Series Parameters:**
+
+ - `RenewablePowerTimeSeries`: ``P^{*,\\text{re}}_t`` = renewable forecast at time ``t``
+ - `ElectricLoadTimeSeries`: ``P^{\\text{ld}}_t`` = load consumption at time ``t``
+
+**Static Parameters:**
+
+ - ``P_{\\max,\\text{pcc}}`` = `PowerSystems.get_output_active_power_limits(device).max`
+ - ``P_{\\max,\\text{th}}`` = `PowerSystems.get_active_power_limits(thermal_unit).max`
+ - ``P_{\\min,\\text{th}}`` = `PowerSystems.get_active_power_limits(thermal_unit).min`
+ - ``P_{\\max,\\text{ch}}`` = `PowerSystems.get_input_active_power_limits(storage).max`
+ - ``P_{\\max,\\text{ds}}`` = `PowerSystems.get_output_active_power_limits(storage).max`
+ - ``\\eta_{\\text{ch}}`` = `PowerSystems.get_efficiency(storage).in`
+ - ``\\eta_{\\text{ds}}`` = `PowerSystems.get_efficiency(storage).out`
+ - ``E_{\\max,\\text{st}}`` = `PowerSystems.get_storage_level_limits(storage).max × capacity`
+ - ``E^{\\text{st}}_0`` = initial storage energy
+ - ``R^{*}_{p,t}`` = ancillary service deployment forecast for service ``p`` at time ``t``
+ - ``F_p`` = fraction of ``P_{\\max,\\text{pcc}}`` allowed for service ``p``
+ - ``N_p`` = number of periods of compliance for service ``p``
+
+**Expressions:**
+
+Adds ``p^{\\text{out}}_t`` and ``p^{\\text{in}}_t`` to PowerSimulations' `ActivePowerBalance` expression
+for use in network balance constraints. When services are present, adds reserve expressions
+(`TotalReserveOutUpExpression`, `TotalReserveOutDownExpression`, `TotalReserveInUpExpression`,
+`TotalReserveInDownExpression`) and served reserve expressions for tracking deployed reserves.
+
+**Constraints:**
+
+Let ``\\mathcal{T} = \\{1, \\dots, T\\}`` denote the set of time steps.
+
+PCC and status ([`PowerSimulations.InputActivePowerVariableLimitsConstraint`](@extref PowerSimulations.InputActivePowerVariableLimitsConstraint), [`PowerSimulations.OutputActivePowerVariableLimitsConstraint`](@extref PowerSimulations.OutputActivePowerVariableLimitsConstraint), [`StatusOutOn`](@ref), [`StatusInOn`](@ref)):
+
+```math
+\\begin{align*}
+& 0 \\leq p^{\\text{in}}_t \\leq P_{\\max,\\text{pcc}}, \\quad 0 \\leq p^{\\text{out}}_t \\leq P_{\\max,\\text{pcc}}, \\quad \\forall t \\in \\mathcal{T} \\\\
+& u^{\\text{st}}_t \\in \\{0,1\\} \\quad \\text{(output/input status at PCC)}
+\\end{align*}
+```
+
+Energy asset balance ([`EnergyAssetBalance`](@ref)):
+
+```math
+p^{\\text{th}}_t + p^{\\text{re}}_t + p^{\\text{ds}}_t - p^{\\text{ch}}_t - P^{\\text{ld}}_t = p^{\\text{out}}_t - p^{\\text{in}}_t, \\quad \\forall t \\in \\mathcal{T}
+```
+
+Thermal limits ([`ThermalOnVariableUb`](@ref), [`ThermalOnVariableLb`](@ref)):
+
+```math
+u^{\\text{th}}_t P_{\\min,\\text{th}} \\leq p^{\\text{th}}_t \\leq u^{\\text{th}}_t P_{\\max,\\text{th}}, \\quad u^{\\text{th}}_t \\in \\{0,1\\}, \\quad \\forall t \\in \\mathcal{T}
+```
+
+Renewable limit ([`RenewableActivePowerLimitConstraint`](@ref)):
+
+```math
+0 \\leq p^{\\text{re}}_t \\leq P^{*,\\text{re}}_t, \\quad \\forall t \\in \\mathcal{T}
+```
+
+Storage charge/discharge status ([`BatteryStatusChargeOn`](@ref), [`BatteryStatusDischargeOn`](@ref)):
+
+```math
+\\begin{align*}
+& p^{\\text{ch}}_t \\leq (1 - ss^{\\text{st}}_t) P_{\\max,\\text{ch}}, \\quad p^{\\text{ds}}_t \\leq ss^{\\text{st}}_t P_{\\max,\\text{ds}}, \\quad \\forall t \\in \\mathcal{T} \\\\
+& ss^{\\text{st}}_t \\in \\{0,1\\} \\quad \\text{(0 = charge, 1 = discharge)}
+\\end{align*}
+```
+
+Storage energy balance ([`BatteryBalance`](@ref)):
+
+```math
+e^{\\text{st}}_t = e^{\\text{st}}_{t-1} + \\Delta t \\left( \\eta_{\\text{ch}} p^{\\text{ch}}_t - \\frac{p^{\\text{ds}}_t}{\\eta_{\\text{ds}}} \\right), \\quad \\forall t \\in \\mathcal{T}, \\quad e^{\\text{st}}_0 = E^{\\text{st}}_0
+```
+
+When ancillary services are present: [`ThermalReserveLimit`](@ref), [`RenewableReserveLimit`](@ref), [`ChargingReservePowerLimit`](@ref), [`DischargingReservePowerLimit`](@ref), [`ReserveCoverageConstraint`](@ref), [`ReserveCoverageConstraintEndOfPeriod`](@ref), [`HybridReserveAssignmentConstraint`](@ref), [`ReserveBalance`](@ref).
+
+Cycling limits (if `"cycling" => true`), ([`CyclingCharge`](@ref), [`CyclingDischarge`](@ref)):
+
+```math
+\\begin{align*}
+& \\eta_{\\text{ch}} \\Delta t \\sum_{t \\in \\mathcal{T}} p^{\\text{ch}}_t \\leq C_{\\text{st}} E_{\\max,\\text{st}} \\\\
+& \\frac{\\Delta t}{\\eta_{\\text{ds}}} \\sum_{t \\in \\mathcal{T}} p^{\\text{ds}}_t \\leq C_{\\text{st}} E_{\\max,\\text{st}}
+\\end{align*}
+```
+
+End-of-horizon energy target (if `"energy_target" => true`), ([`StateofChargeTargetConstraint`](@ref)):
+
+```math
+e^{\\text{st}}_T = E^{\\text{st}}_T
+```
+
+Regularization (if `"regularization" => true`): [`ChargeRegularizationConstraint`](@ref), [`DischargeRegularizationConstraint`](@ref).
+
+**Objective:**
+
+Adds cost terms for thermal generation (variable and fixed costs), storage variable O&M,
+and penalties for energy target deviations and cycling violations (if enabled).
"""
struct HybridDispatchWithReserves <: AbstractHybridFormulationWithReserves end
@@ -21,8 +181,151 @@ struct HybridDispatchWithReserves <: AbstractHybridFormulationWithReserves end
HybridEnergyOnlyDispatch
Device formulation for a hybrid system that participates in energy only (no ancillary
-services). Net power at the PCC is ``p^{\\text{out}}_t - p^{\\text{in}}_t`` from thermal, renewable, discharge,
-minus charge and load; subject to ``P_{\\max,\\text{pcc}}`` and asset limits.
+services). Net power at the point of common coupling (PCC) is ``p^{\\text{out}}_t - p^{\\text{in}}_t``
+from thermal, renewable, discharge, minus charge and load; subject to ``P_{\\max,\\text{pcc}}``
+and asset limits.
+
+**Variables:**
+
+ - [`PowerSimulations.ActivePowerOutVariable`](@extref PowerSimulations.ActivePowerOutVariable):
+
+ + Bounds: [0.0, ``P_{\\max,\\text{pcc}}``]
+ + Symbol: ``p^{\\text{out}}_t``
+
+ - [`PowerSimulations.ActivePowerInVariable`](@extref PowerSimulations.ActivePowerInVariable):
+
+ + Bounds: [0.0, ``P_{\\max,\\text{pcc}}``]
+ + Symbol: ``p^{\\text{in}}_t``
+
+ - [`PowerSimulations.ReservationVariable`](@extref PowerSimulations.ReservationVariable):
+
+ + Bounds: {0, 1}
+ + Symbol: ``u^{\\text{st}}_t``
+
+ - `ThermalPower`:
+
+ + Bounds: [0.0, ``P_{\\max,\\text{th}}``] when on
+ + Symbol: ``p^{\\text{th}}_t``
+
+ - [`PowerSimulations.OnVariable`](@extref PowerSimulations.OnVariable):
+
+ + Bounds: {0, 1}
+ + Symbol: ``u^{\\text{th}}_t``
+
+ - `RenewablePower`:
+
+ + Bounds: [0.0, ``P^{*,\\text{re}}_t``]
+ + Symbol: ``p^{\\text{re}}_t``
+
+ - `BatteryCharge`:
+
+ + Bounds: [0.0, ``P_{\\max,\\text{ch}}``] when charging
+ + Symbol: ``p^{\\text{ch}}_t``
+
+ - `BatteryDischarge`:
+
+ + Bounds: [0.0, ``P_{\\max,\\text{ds}}``] when discharging
+ + Symbol: ``p^{\\text{ds}}_t``
+
+ - [`PowerSimulations.EnergyVariable`](@extref PowerSimulations.EnergyVariable):
+
+ + Bounds: [0.0, ``E_{\\max,\\text{st}}``]
+ + Symbol: ``e^{\\text{st}}_t``
+
+ - `BatteryStatus`:
+
+ + Bounds: {0, 1}
+ + Symbol: ``ss^{\\text{st}}_t`` (0 = charge, 1 = discharge)
+
+**Time Series Parameters:**
+
+ - `RenewablePowerTimeSeries`: ``P^{*,\\text{re}}_t`` = renewable forecast at time ``t``
+ - `ElectricLoadTimeSeries`: ``P^{\\text{ld}}_t`` = load consumption at time ``t``
+
+**Static Parameters:**
+
+ - ``P_{\\max,\\text{pcc}}`` = `PowerSystems.get_output_active_power_limits(device).max`
+ - ``P_{\\max,\\text{th}}`` = `PowerSystems.get_active_power_limits(thermal_unit).max`
+ - ``P_{\\min,\\text{th}}`` = `PowerSystems.get_active_power_limits(thermal_unit).min`
+ - ``P_{\\max,\\text{ch}}`` = `PowerSystems.get_input_active_power_limits(storage).max`
+ - ``P_{\\max,\\text{ds}}`` = `PowerSystems.get_output_active_power_limits(storage).max`
+ - ``\\eta_{\\text{ch}}`` = `PowerSystems.get_efficiency(storage).in`
+ - ``\\eta_{\\text{ds}}`` = `PowerSystems.get_efficiency(storage).out`
+ - ``E_{\\max,\\text{st}}`` = `PowerSystems.get_storage_level_limits(storage).max × capacity`
+ - ``E^{\\text{st}}_0`` = initial storage energy
+
+**Expressions:**
+
+Adds ``p^{\\text{out}}_t`` and ``p^{\\text{in}}_t`` to PowerSimulations' `ActivePowerBalance` expression
+for use in network balance constraints.
+
+**Constraints:**
+
+Let ``\\mathcal{T} = \\{1, \\dots, T\\}`` denote the set of time steps.
+
+PCC and status ([`PowerSimulations.InputActivePowerVariableLimitsConstraint`](@extref PowerSimulations.InputActivePowerVariableLimitsConstraint), [`PowerSimulations.OutputActivePowerVariableLimitsConstraint`](@extref PowerSimulations.OutputActivePowerVariableLimitsConstraint), [`StatusOutOn`](@ref), [`StatusInOn`](@ref)):
+
+```math
+\\begin{align*}
+& 0 \\leq p^{\\text{in}}_t \\leq P_{\\max,\\text{pcc}}, \\quad 0 \\leq p^{\\text{out}}_t \\leq P_{\\max,\\text{pcc}}, \\quad \\forall t \\in \\mathcal{T} \\\\
+& u^{\\text{st}}_t \\in \\{0,1\\} \\quad \\text{(output/input status at PCC)}
+\\end{align*}
+```
+
+Energy asset balance ([`EnergyAssetBalance`](@ref)):
+
+```math
+p^{\\text{th}}_t + p^{\\text{re}}_t + p^{\\text{ds}}_t - p^{\\text{ch}}_t - P^{\\text{ld}}_t = p^{\\text{out}}_t - p^{\\text{in}}_t, \\quad \\forall t \\in \\mathcal{T}
+```
+
+Thermal limits ([`ThermalOnVariableUb`](@ref), [`ThermalOnVariableLb`](@ref)):
+
+```math
+u^{\\text{th}}_t P_{\\min,\\text{th}} \\leq p^{\\text{th}}_t \\leq u^{\\text{th}}_t P_{\\max,\\text{th}}, \\quad u^{\\text{th}}_t \\in \\{0,1\\}, \\quad \\forall t \\in \\mathcal{T}
+```
+
+Renewable limit ([`RenewableActivePowerLimitConstraint`](@ref)):
+
+```math
+0 \\leq p^{\\text{re}}_t \\leq P^{*,\\text{re}}_t, \\quad \\forall t \\in \\mathcal{T}
+```
+
+Storage charge/discharge status ([`BatteryStatusChargeOn`](@ref), [`BatteryStatusDischargeOn`](@ref)):
+
+```math
+\\begin{align*}
+& p^{\\text{ch}}_t \\leq (1 - ss^{\\text{st}}_t) P_{\\max,\\text{ch}}, \\quad p^{\\text{ds}}_t \\leq ss^{\\text{st}}_t P_{\\max,\\text{ds}}, \\quad \\forall t \\in \\mathcal{T} \\\\
+& ss^{\\text{st}}_t \\in \\{0,1\\} \\quad \\text{(0 = charge, 1 = discharge)}
+\\end{align*}
+```
+
+Storage energy balance ([`BatteryBalance`](@ref)):
+
+```math
+e^{\\text{st}}_t = e^{\\text{st}}_{t-1} + \\Delta t \\left( \\eta_{\\text{ch}} p^{\\text{ch}}_t - \\frac{p^{\\text{ds}}_t}{\\eta_{\\text{ds}}} \\right), \\quad \\forall t \\in \\mathcal{T}, \\quad e^{\\text{st}}_0 = E^{\\text{st}}_0
+```
+
+Cycling limits (if `"cycling" => true`), ([`CyclingCharge`](@ref), [`CyclingDischarge`](@ref)):
+
+```math
+\\begin{align*}
+& \\eta_{\\text{ch}} \\Delta t \\sum_{t \\in \\mathcal{T}} p^{\\text{ch}}_t \\leq C_{\\text{st}} E_{\\max,\\text{st}} \\\\
+& \\frac{\\Delta t}{\\eta_{\\text{ds}}} \\sum_{t \\in \\mathcal{T}} p^{\\text{ds}}_t \\leq C_{\\text{st}} E_{\\max,\\text{st}}
+\\end{align*}
+```
+
+End-of-horizon energy target (if `"energy_target" => true`), ([`StateofChargeTargetConstraint`](@ref)):
+
+```math
+e^{\\text{st}}_T = E^{\\text{st}}_T
+```
+
+Regularization (if `"regularization" => true`): [`ChargeRegularizationConstraint`](@ref), [`DischargeRegularizationConstraint`](@ref).
+
+**Objective:**
+
+Adds cost terms for thermal generation (variable and fixed costs), storage variable O&M,
+and penalties for energy target deviations and cycling violations (if enabled).
"""
struct HybridEnergyOnlyDispatch <: AbstractHybridFormulation end
@@ -32,6 +335,38 @@ struct HybridEnergyOnlyDispatch <: AbstractHybridFormulation end
Device formulation for a hybrid system with day-ahead (DA) energy bids/offers fixed;
used in multi-step simulations when the real-time (RT) subproblem is solved with
locked DA positions (e.g. merchant co-optimization with "then vs. now" RT adjustment).
+
+**Variables:**
+
+ - [`PowerSimulations.ActivePowerOutVariable`](@extref PowerSimulations.ActivePowerOutVariable):
+
+ + Bounds: [0.0, ``P_{\\max,\\text{pcc}}``]
+ + Symbol: ``p^{\\text{out}}_t``
+
+ - [`PowerSimulations.ActivePowerInVariable`](@extref PowerSimulations.ActivePowerInVariable):
+
+ + Bounds: [0.0, ``P_{\\max,\\text{pcc}}``]
+ + Symbol: ``p^{\\text{in}}_t``
+
+ - `TotalReserve` (if services present):
+
+ + Bounds: [0.0, ]
+ + Symbol: total reserve at PCC
+
+**Expressions:**
+
+Adds ``p^{\\text{out}}_t`` and ``p^{\\text{in}}_t`` to PowerSimulations' `ActivePowerBalance` expression
+for use in network balance constraints.
+
+**Constraints:**
+
+PCC power limits ([`PowerSimulations.InputActivePowerVariableLimitsConstraint`](@extref PowerSimulations.InputActivePowerVariableLimitsConstraint), [`PowerSimulations.OutputActivePowerVariableLimitsConstraint`](@extref PowerSimulations.OutputActivePowerVariableLimitsConstraint)):
+
+```math
+0 \\leq p^{\\text{in}}_t \\leq P_{\\max,\\text{pcc}}, \\quad 0 \\leq p^{\\text{out}}_t \\leq P_{\\max,\\text{pcc}}, \\quad \\forall t \\in \\mathcal{T}
+```
+
+When ancillary services are present: [`HybridReserveAssignmentConstraint`](@ref) links component reserves to total reserve at the PCC.
"""
struct HybridFixedDA <: AbstractHybridFormulation end
diff --git a/src/core/parameters.jl b/src/core/parameters.jl
index 71754243..4972a734 100644
--- a/src/core/parameters.jl
+++ b/src/core/parameters.jl
@@ -31,7 +31,7 @@ struct RealTimeEnergyPrice <: PSI.ObjectiveFunctionParameter end
Objective function parameter for ancillary service price.
Docs abbreviation: ``\\Pi^*_{p,t}`` (USD/MWh) for service ``p \\in P``. Used in the DA
-profit term for AS (``sb^{\\text{out}}`` + ``sb^{\\text{in}}``).
+profit term for ancillary services (``sb^{\\text{out}}`` + ``sb^{\\text{in}}``).
"""
struct AncillaryServicePrice <: PSI.ObjectiveFunctionParameter end
diff --git a/src/core/variables.jl b/src/core/variables.jl
index 8907f15c..fc63732b 100644
--- a/src/core/variables.jl
+++ b/src/core/variables.jl
@@ -3,7 +3,7 @@
"""
EnergyDABidOut
-Variable type for day-ahead energy offer (generating power) at the PCC.
+Variable type for day-ahead energy offer (generating power) at the point of common coupling (PCC).
Docs abbreviation: ``e^{\\text{out}}_{\\text{DA},t} \\in [0, P_{\\max,\\text{pcc}}]`` [MW].
"""
@@ -12,7 +12,7 @@ struct EnergyDABidOut <: PSI.VariableType end
"""
EnergyDABidIn
-Variable type for day-ahead energy bid (consuming power) at the PCC.
+Variable type for day-ahead energy bid (consuming power) at the point of common coupling (PCC).
Docs abbreviation: ``e^{\\text{in}}_{\\text{DA},t} \\in [0, P_{\\max,\\text{pcc}}]`` [MW].
"""
@@ -21,7 +21,7 @@ struct EnergyDABidIn <: PSI.VariableType end
"""
EnergyRTBidOut
-Variable type for real-time energy offer at the PCC.
+Variable type for real-time energy offer at the point of common coupling (PCC).
Docs abbreviation: ``e^{\\text{out}}_{\\text{RT},t}``. Net RT position with DA locked
is used in the merchant profit expression (e.g. DART spread).
@@ -31,7 +31,7 @@ struct EnergyRTBidOut <: PSI.VariableType end
"""
EnergyRTBidIn
-Variable type for real-time energy bid at the PCC.
+Variable type for real-time energy bid at the point of common coupling (PCC).
Docs abbreviation: ``e^{\\text{in}}_{\\text{RT},t}``.
"""
@@ -43,12 +43,12 @@ struct EnergyRenewableBid <: PSI.VariableType end
struct EnergyBatteryChargeBid <: PSI.VariableType end
struct EnergyBatteryDischargeBid <: PSI.VariableType end
-# AS Total DA Bids
+# Ancillary Service Total DA Bids
"""
BidReserveVariableOut
Variable type for day-ahead ancillary service offer (generation direction) for the
-hybrid at the PCC.
+hybrid at the point of common coupling (PCC).
Docs abbreviation: ``sb^{\\text{out}}_{p,t} \\in [0, F_p P_{\\max,\\text{pcc}}]`` for product ``p``.
"""
@@ -58,7 +58,7 @@ struct BidReserveVariableOut <: PSI.VariableType end
BidReserveVariableIn
Variable type for day-ahead ancillary service bid (consumption direction) for the
-hybrid at the PCC.
+hybrid at the point of common coupling (PCC).
Docs abbreviation: ``sb^{\\text{in}}_{p,t} \\in [0, F_p P_{\\max,\\text{pcc}}]`` for product ``p``.
"""
@@ -79,7 +79,7 @@ abstract type BatteryRegularizationVariable <: PSI.VariableType end
struct ChargeRegularizationVariable <: BatteryRegularizationVariable end
struct DischargeRegularizationVariable <: BatteryRegularizationVariable end
-# AS Variable for Hybrid
+# Ancillary Service Variable for Hybrid
abstract type ReserveVariableType <: PSI.VariableType end
abstract type AssetReserveVariableType <: ReserveVariableType end
@@ -103,7 +103,7 @@ struct ReserveVariableIn <: AssetReserveVariableType end
TotalReserve
Auxiliary variable type for the total reserve quantity (sum of component reserves)
-at the PCC. Used in reserve balance constraints; not written to results by default.
+at the point of common coupling (PCC). Used in reserve balance constraints; not written to results by default.
"""
struct TotalReserve <: AssetReserveVariableType end
struct SlackReserveUp <: PSI.VariableType end
diff --git a/src/feedforwards.jl b/src/feedforwards.jl
index a7e72789..634434a6 100644
--- a/src/feedforwards.jl
+++ b/src/feedforwards.jl
@@ -2,9 +2,9 @@
CyclingChargeLimitFeedforward
Feedforward that enforces a cumulative charging cycle limit on the hybrid's storage
-over the simulation. The constraint is ``\\eta_{\\text{ch}} \\Delta t \\sum (p_{\\text{ch}} + \\text{served\\_reg\\_down} - \\text{served\\_reg\\_up}) \\leq \\text{limit}``,
-where the limit is from [`CyclingChargeLimitParameter`](@ref) in recurrent solves or
-``\\text{cycles\\_in\\_horizon} \\times E_{\\max}`` otherwise. Use with PowerSimulations' `add_feedforward!` in a
+over the simulation. The constraint is ``\\eta_{\\text{ch}} \\Delta t \\sum_t (p_{\\text{ch},t} + s^{\\text{down}}_{\\text{reg},t} - s^{\\text{up}}_{\\text{reg},t}) \\leq \\text{limit}``,
+where ``s^{\\text{up}}_{\\text{reg},t}`` and ``s^{\\text{down}}_{\\text{reg},t}`` denote served reserve (up/down). The limit is from [`CyclingChargeLimitParameter`](@ref) in recurrent solves or
+``C_{\\text{horizon}} \\times E_{\\max,\\text{st}}`` otherwise. Use with PowerSimulations' `add_feedforward!` in a
[`PowerSimulations.DeviceModel`](@extref PowerSimulations.DeviceModel) for
[`HybridDispatchWithReserves`](@ref) or [`HybridEnergyOnlyDispatch`](@ref).
"""
@@ -47,7 +47,8 @@ PSI.get_optimization_container_key(ff::CyclingChargeLimitFeedforward) =
CyclingDischargeLimitFeedforward
Feedforward that enforces a cumulative discharging cycle limit on the hybrid's storage:
-``(1/\\eta_{\\text{ds}}) \\Delta t \\sum (p_{\\text{ds}} + \\text{served\\_reg\\_up} - \\text{served\\_reg\\_down}) \\leq \\text{limit}``. The limit comes from
+``(1/\\eta_{\\text{ds}}) \\Delta t \\sum_t (p_{\\text{ds},t} + s^{\\text{up}}_{\\text{reg},t} - s^{\\text{down}}_{\\text{reg},t}) \\leq \\text{limit}``,
+where ``s^{\\text{up}}_{\\text{reg},t}`` and ``s^{\\text{down}}_{\\text{reg},t}`` denote served reserve (up/down). The limit comes from
[`CyclingDischargeLimitParameter`](@ref) in recurrent runs. See
[`CyclingChargeLimitFeedforward`](@ref) for usage pattern.
"""
From 3d2c013f789faa3cc184ff2c1efe5711f2a32ced Mon Sep 17 00:00:00 2001
From: kdayday
Date: Thu, 19 Feb 2026 13:28:08 -0700
Subject: [PATCH 04/46] Fix Complentary typo
---
docs/src/api/public.md | 4 ++--
src/HybridSystemsSimulations.jl | 4 ++--
src/add_constraints.jl | 4 ++--
src/core/constraints.jl | 11 +++++------
src/decision_models/bilevel_decision_model.jl | 4 ++--
5 files changed, 13 insertions(+), 14 deletions(-)
diff --git a/docs/src/api/public.md b/docs/src/api/public.md
index 633a895a..115ea280 100644
--- a/docs/src/api/public.md
+++ b/docs/src/api/public.md
@@ -145,8 +145,8 @@ ComplementarySlacknessBatteryStatusChargeOnUb
ComplementarySlacknessBatteryStatusChargeOnLb
ComplementarySlacknessBatteryBalanceUb
ComplementarySlacknessBatteryBalanceLb
-ComplentarySlacknessCyclingCharge
-ComplentarySlacknessCyclingDischarge
+ComplementarySlacknessCyclingCharge
+ComplementarySlacknessCyclingDischarge
ComplementarySlacknessEnergyLimitUb
ComplementarySlacknessEnergyLimitLb
```
diff --git a/src/HybridSystemsSimulations.jl b/src/HybridSystemsSimulations.jl
index acbac90c..c054f049 100644
--- a/src/HybridSystemsSimulations.jl
+++ b/src/HybridSystemsSimulations.jl
@@ -43,8 +43,8 @@ export ComplementarySlacknessBatteryStatusChargeOnUb
export ComplementarySlacknessBatteryStatusChargeOnLb
export ComplementarySlacknessBatteryBalanceUb
export ComplementarySlacknessBatteryBalanceLb
-export ComplentarySlacknessCyclingCharge
-export ComplentarySlacknessCyclingDischarge
+export ComplementarySlacknessCyclingCharge
+export ComplementarySlacknessCyclingDischarge
export ComplementarySlacknessEnergyLimitUb
export ComplementarySlacknessEnergyLimitLb
#export ComplementarySlacknessThermalOnVariableOn
diff --git a/src/add_constraints.jl b/src/add_constraints.jl
index aba7eb1d..4b9f2298 100644
--- a/src/add_constraints.jl
+++ b/src/add_constraints.jl
@@ -3344,7 +3344,7 @@ end
function add_constraints!(
container::PSI.OptimizationContainer,
- T::Type{<:ComplentarySlacknessCyclingCharge},
+ T::Type{<:ComplementarySlacknessCyclingCharge},
devices::U,
::W,
) where {
@@ -3381,7 +3381,7 @@ end
function add_constraints!(
container::PSI.OptimizationContainer,
- T::Type{<:ComplentarySlacknessCyclingDischarge},
+ T::Type{<:ComplementarySlacknessCyclingDischarge},
devices::U,
::W,
) where {
diff --git a/src/core/constraints.jl b/src/core/constraints.jl
index 208166a4..c22bf530 100644
--- a/src/core/constraints.jl
+++ b/src/core/constraints.jl
@@ -218,19 +218,18 @@ Complementary slackness (lower bound) for storage energy balance.
struct ComplementarySlacknessBatteryBalanceLb <: PSI.ConstraintType end
"""
- ComplentarySlacknessCyclingCharge
+ ComplementarySlacknessCyclingCharge
-Complementary slackness for the charging cycle limit (``c_{\\text{ch}}^-``); note spelling
-"Complentary" is kept for API compatibility.
+Complementary slackness for the charging cycle limit (``c_{\\text{ch}}^-``).
"""
-struct ComplentarySlacknessCyclingCharge <: PSI.ConstraintType end
+struct ComplementarySlacknessCyclingCharge <: PSI.ConstraintType end
"""
- ComplentarySlacknessCyclingDischarge
+ ComplementarySlacknessCyclingDischarge
Complementary slackness for the discharging cycle limit (``c_{\\text{ds}}^-``).
"""
-struct ComplentarySlacknessCyclingDischarge <: PSI.ConstraintType end
+struct ComplementarySlacknessCyclingDischarge <: PSI.ConstraintType end
"""
ComplementarySlacknessEnergyLimitUb
diff --git a/src/decision_models/bilevel_decision_model.jl b/src/decision_models/bilevel_decision_model.jl
index ddf1da64..26dc1ee2 100644
--- a/src/decision_models/bilevel_decision_model.jl
+++ b/src/decision_models/bilevel_decision_model.jl
@@ -949,8 +949,8 @@ function PSI.build_impl!(decision_model::PSI.DecisionModel{MerchantHybridBilevel
ComplementarySlacknessBatteryBalanceLb,
ComplementarySlacknessEnergyLimitUb,
ComplementarySlacknessEnergyLimitLb,
- ComplentarySlacknessCyclingCharge,
- ComplentarySlacknessCyclingDischarge,
+ ComplementarySlacknessCyclingCharge,
+ ComplementarySlacknessCyclingDischarge,
]
add_constraints!(container, c, hybrids, MerchantModelWithReserves())
end
From 0f4bc237c692d6f2ee9ab6ee56cd9defc8e1015f Mon Sep 17 00:00:00 2001
From: kdayday
Date: Thu, 19 Feb 2026 13:43:15 -0700
Subject: [PATCH 05/46] Diataxis and add tutorial infrastructure
---
docs/Project.toml | 3 +
docs/make.jl | 15 +-
docs/make_tutorials.jl | 364 +++++++++++++++++++++++++++++++
docs/src/index.md | 55 ++++-
docs/src/quick_start_guide.md | 3 -
docs/src/tutorials/intro_page.md | 4 -
6 files changed, 429 insertions(+), 15 deletions(-)
create mode 100644 docs/make_tutorials.jl
delete mode 100644 docs/src/quick_start_guide.md
delete mode 100644 docs/src/tutorials/intro_page.md
diff --git a/docs/Project.toml b/docs/Project.toml
index 775cd548..670345e3 100644
--- a/docs/Project.toml
+++ b/docs/Project.toml
@@ -1,9 +1,12 @@
[deps]
+DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0"
DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8"
Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4"
DocumenterInterLinks = "d12716ef-a0f6-4df4-a9f1-a5a34e75c656"
DocumenterTools = "35a29f4d-8980-5a13-9543-d66fff28ecb8"
HybridSystemsSimulations = "bed98974-b02a-5e2f-9ee0-a103f5c450dd"
+Literate = "98b081ad-f1c9-55d3-8b20-4c87d4299306"
+PrettyTables = "08abe8d2-0d0c-5749-adfa-8a2ac140af0d"
[compat]
Documenter = "1.0"
diff --git a/docs/make.jl b/docs/make.jl
index 29c12801..dbe5d4dd 100644
--- a/docs/make.jl
+++ b/docs/make.jl
@@ -2,6 +2,9 @@ using Documenter
using HybridSystemsSimulations
using DataStructures
using DocumenterInterLinks
+using Literate
+
+const _DOCS_BASE_URL = "https://nrel-sienna.github.io/HybridSystemsSimulations.jl/stable"
links = InterLinks(
"Julia" => "https://docs.julialang.org/en/v1/",
@@ -10,12 +13,16 @@ links = InterLinks(
"PowerSimulations" => "https://nrel-sienna.github.io/PowerSimulations.jl/stable/",
)
+include(joinpath(@__DIR__, "make_tutorials.jl"))
+make_tutorials()
+
pages = OrderedDict(
"Welcome Page" => "index.md",
- "Quick Start Guide" => "quick_start_guide.md",
- "Tutorials" => "tutorials/intro_page.md",
- "Public API Reference" => "api/public.md",
- "Internal API Reference" => "api/internal.md",
+ "Tutorials" => Any[],
+ "Reference" => Any[
+ "Public API" => "api/public.md",
+ "Internals" => "api/internal.md",
+ ],
)
makedocs(;
diff --git a/docs/make_tutorials.jl b/docs/make_tutorials.jl
new file mode 100644
index 00000000..54a86f61
--- /dev/null
+++ b/docs/make_tutorials.jl
@@ -0,0 +1,364 @@
+using Pkg
+using Literate
+using DataFrames
+using PrettyTables
+
+# Override show for DataFrames to limit output size during doc builds
+# This ensures large DataFrames are truncated when displayed as expression results in @example blocks
+# Explicit show() calls in tutorials with their own arguments are NOT affected (they use their own kwargs)
+# We override both text/plain and text/html since Documenter may use either
+#
+# Strategy: Call PrettyTables.pretty_table directly with explicit row/column limits.
+# This bypasses DataFrames' default display logic and gives us full control.
+
+function Base.show(io::IO, mime::MIME"text/plain", df::DataFrame)
+ # Call PrettyTables directly with row/column limits
+ # This ensures only 10 rows are shown regardless of DataFrame size
+ PrettyTables.pretty_table(io, df;
+ backend = :text,
+ maximum_number_of_rows = 10,
+ maximum_number_of_columns = 80,
+ show_omitted_cell_summary = true,
+ compact_printing = false,
+ limit_printing = true)
+end
+
+function Base.show(io::IO, mime::MIME"text/html", df::DataFrame)
+ # For HTML output (which Documenter prefers for large outputs)
+ # Use PrettyTables HTML backend with explicit row/column limits
+ PrettyTables.pretty_table(io, df;
+ backend = :html,
+ maximum_number_of_rows = 10,
+ maximum_number_of_columns = 80,
+ show_omitted_cell_summary = true,
+ compact_printing = false,
+ limit_printing = true)
+end
+
+# Function to clean up old generated files
+function clean_old_generated_files(dir::String)
+ if !isdir(dir)
+ @warn "Directory does not exist: $dir"
+ return
+ end
+ generated_files = filter(
+ f ->
+ startswith(f, "generated_") &&
+ (endswith(f, ".md") || endswith(f, ".ipynb")),
+ readdir(dir),
+ )
+ for file in generated_files
+ rm(joinpath(dir, file); force = true)
+ @info "Removed old generated file: $file"
+ end
+end
+
+#########################################################
+# Literate post-processing functions for tutorial generation
+#########################################################
+
+# postprocess function to insert md
+function insert_md(content)
+ m = match(r"APPEND_MARKDOWN\(\"(.*)\"\)", content)
+ if !isnothing(m)
+ md_content = read(m.captures[1], String)
+ content = replace(content, r"APPEND_MARKDOWN\(\"(.*)\"\)" => md_content)
+ end
+ return content
+end
+
+# Default display titles for Documenter admonition types when no custom title is given.
+# See https://documenter.juliadocs.org/stable/showcase/#Admonitions
+const _ADMONITION_DISPLAY_NAMES = Dict{String, String}(
+ "note" => "Note",
+ "info" => "Info",
+ "tip" => "Tip",
+ "warning" => "Warning",
+ "danger" => "Danger",
+ "compat" => "Compat",
+ "todo" => "TODO",
+ "details" => "Details",
+)
+
+# Preprocess Literate source to convert Documenter-style admonitions into Jupyter-friendly
+# blockquotes. Used only for notebook output; markdown keeps `!!! type` and is rendered by
+# Documenter. Admonitions are not recognized by common mark or Jupyter; see
+# https://fredrikekre.github.io/Literate.jl/v2/tips/#admonitions-compatibility
+function preprocess_admonitions_for_notebook(str::AbstractString)
+ lines = split(str, '\n'; keepempty = true)
+ out = String[]
+ i = 1
+ n = length(lines)
+ admonition_start =
+ r"^# !!! (note|info|tip|warning|danger|compat|todo|details)(?:\s+\"([^\"]*)\")?\s*$"
+ content_line = r"^# (.*)$" # Documenter admonition body: # then 4 spaces
+ blank_comment = r"^#\s*$" # # or # with only spaces
+
+ while i <= n
+ line = lines[i]
+ m = match(admonition_start, line)
+ if m !== nothing
+ typ = lowercase(m.captures[1])
+ custom_title = m.captures[2]
+ title = if custom_title !== nothing && !isempty(custom_title)
+ custom_title
+ else
+ get(_ADMONITION_DISPLAY_NAMES, typ, titlecase(typ))
+ end
+ push!(out, "# > *$(title)*")
+ push!(out, "# >")
+ i += 1
+ # Consume blank comment lines and content lines
+ while i <= n
+ l = lines[i]
+ if match(blank_comment, l) !== nothing
+ push!(out, "# >")
+ i += 1
+ elseif (cm = match(content_line, l)) !== nothing
+ push!(out, "# > " * cm.captures[1])
+ i += 1
+ else
+ break
+ end
+ end
+ continue
+ end
+ push!(out, line)
+ i += 1
+ end
+ return join(out, '\n')
+end
+
+# Function to add download links to generated markdown
+function add_download_links(content, jl_file, ipynb_file)
+ # Add download links at the top of the file after the first heading
+ download_section = """
+
+*To follow along, you can download this tutorial as a [Julia script (.jl)]($(jl_file)) or [Jupyter notebook (.ipynb)]($(ipynb_file)).*
+
+"""
+ # Insert after the first heading (which should be the title)
+ # Match the first heading line and replace it with heading + download section
+ m = match(r"^(#+ .+)$"m, content)
+ if m !== nothing
+ heading = m.match
+ content = replace(content, r"^(#+ .+)$"m => heading * download_section; count = 1)
+ end
+ return content
+end
+
+# Function to add Pkg.status() to notebook within the first markdown cell
+function add_pkg_status_to_notebook(nb::Dict)
+ cells = get(nb, "cells", [])
+ if isempty(cells)
+ return nb
+ end
+
+ # Find the first markdown cell
+ first_markdown_idx = nothing
+ for (i, cell) in enumerate(cells)
+ if get(cell, "cell_type", "") == "markdown"
+ first_markdown_idx = i
+ break
+ end
+ end
+
+ if first_markdown_idx === nothing
+ return nb # No markdown cell found, return unchanged
+ end
+
+ first_cell = cells[first_markdown_idx]
+ cell_source = get(first_cell, "source", [])
+
+ # Convert source array to string to find the first heading
+ source_text = join(cell_source)
+
+ # Find the first heading (lines starting with #)
+ heading_pattern = r"^(#+\s+.+?)$"m
+ heading_match = match(heading_pattern, source_text)
+
+ if heading_match === nothing
+ return nb # No heading found, return unchanged
+ end
+
+ # Capture Pkg.status() output at build time
+ io = IOBuffer()
+ Pkg.status(; io = io)
+ pkg_status_output = String(take!(io))
+
+ # Create the content to insert: blockquote "Set up" with setup instructions and pkg.status()
+ # Blockquote title and body; hyperlinks for IJulia and create an environment
+ preface_lines = [
+ "\n",
+ "> **Set up**\n",
+ ">\n",
+ "> To run this notebook, first install the Julia kernel for Jupyter Notebooks using [IJulia](https://julialang.github.io/IJulia.jl/stable/manual/installation/), then [create an environment](https://pkgdocs.julialang.org/v1/environments/) for this tutorial with the packages listed with `using ` further down.\n",
+ ">\n",
+ "> This tutorial has demonstrated compatibility with these package versions. If you run into any errors, first check your package versions for consistency using `Pkg.status()`.\n",
+ ">\n",
+ ]
+
+ # Format Pkg.status() output as a code block inside the blockquote
+ pkg_status_lines = split(pkg_status_output, '\n'; keepempty = true)
+ pkg_status_block = [" > ```\n"]
+ for line in pkg_status_lines
+ push!(pkg_status_block, " > " * line * "\n")
+ end
+ push!(pkg_status_block, " > ```\n", "\n")
+
+ # Find the first heading line in the source array
+ heading_line_idx = nothing
+ for (i, line) in enumerate(cell_source)
+ if match(heading_pattern, line) !== nothing
+ heading_line_idx = i
+ break
+ end
+ end
+
+ if heading_line_idx === nothing
+ return nb # Couldn't find heading line
+ end
+
+ # Build new source array
+ new_source = String[]
+ # Add all lines up to and including the heading line
+ for i in 1:heading_line_idx
+ push!(new_source, cell_source[i])
+ end
+
+ # Add the preface and pkg.status content right after the heading
+ append!(new_source, preface_lines)
+ append!(new_source, pkg_status_block)
+
+ # Add all remaining lines after the heading
+ for i in (heading_line_idx + 1):length(cell_source)
+ push!(new_source, cell_source[i])
+ end
+
+ # Update the cell source
+ first_cell["source"] = new_source
+ cells[first_markdown_idx] = first_cell
+
+ nb["cells"] = cells
+ return nb
+end
+
+# Add italicized "view online" comment after each image from ```@raw html ... ``` (or
+# the raw HTML / markdown form Literate writes). Used as a postprocess in Literate.notebook.
+# Expects _DOCS_BASE_URL to be defined by the includer (e.g. in make.jl).
+# Literate strips the backtick wrapper and outputs raw HTML; we match that multi-line block.
+function add_image_links(nb::Dict, outputfile_base::AbstractString)
+ tutorial_url = "$_DOCS_BASE_URL/tutorials/$(outputfile_base)/"
+ msg = "_If image is not available when viewing in a Jupyter notebook, view the tutorial online [here]($tutorial_url)._"
+ cells = get(nb, "cells", [])
+ for (idx, cell) in enumerate(cells)
+ get(cell, "cell_type", "") != "markdown" && continue
+ source = get(cell, "source", [])
+ isempty(source) && continue
+ text = join(source)
+ # Check if this cell already has the "view online" message to avoid duplicates
+ contains(text, "If image is not available when viewing in a Jupyter notebook") &&
+ continue
+ suffix = "\n\n" * msg * "\n"
+ append_after = m -> string(m) * suffix
+ # Use a single non-overlapping regex to match image-containing fragments:
+ # - ......
(Literate raw HTML paragraphs)
+ # - ```@raw html ... ``` blocks
+ # - Markdown images 
+ # - standalone
tags (only if not already matched by wrapper)
+ p_with_img_pattern = r"
]*>[\s\S]*?
"
+ raw_html_block_pattern = r"```@raw html[\s\S]*?```"
+ markdown_image_pattern = r"!\[[^\]]*\]\([^\)]*\)"
+ standalone_img_pattern = r"
]*?/?>"
+ image_fragment_pattern = Regex(
+ "(?:" *
+ p_with_img_pattern.pattern * "|" *
+ raw_html_block_pattern.pattern * "|" *
+ markdown_image_pattern.pattern * "|" *
+ standalone_img_pattern.pattern * ")",
+ )
+ text = replace(
+ text,
+ image_fragment_pattern =>
+ append_after,
+ )
+ # Convert back to notebook source array (lines, last without trailing \n if non-empty)
+ lines = split(text, "\n"; keepempty = true)
+ new_source = String[]
+ for i in 1:length(lines)
+ if i < length(lines)
+ push!(new_source, lines[i] * "\n")
+ else
+ isempty(lines[i]) || push!(new_source, lines[i])
+ end
+ end
+ cell["source"] = new_source
+ cells[idx] = cell
+ end
+ nb["cells"] = cells
+ return nb
+end
+
+#########################################################
+# Process tutorials with Literate
+#########################################################
+
+# Markdown files are postprocessed to add download links for the Julia script and Jupyter notebook
+# Jupyter notebooks are postprocessed to add image links and pkg.status()
+function make_tutorials()
+ # Exclude helper scripts that start with "_"
+ if isdir("docs/src/tutorials")
+ tutorial_files =
+ filter(
+ x -> occursin(".jl", x) && !startswith(x, "_"),
+ readdir("docs/src/tutorials"),
+ )
+ if !isempty(tutorial_files)
+ # Clean up old generated tutorial files
+ tutorial_outputdir = joinpath(pwd(), "docs", "src", "tutorials")
+ clean_old_generated_files(tutorial_outputdir)
+
+ for file in tutorial_files
+ @show file
+ infile_path = joinpath(pwd(), "docs", "src", "tutorials", file)
+ execute =
+ if occursin("EXECUTE = TRUE", uppercase(readline(infile_path)))
+ true
+ else
+ false
+ end
+ execute && include(infile_path)
+
+ outputfile = string("generated_", replace("$file", ".jl" => ""))
+
+ # Generate markdown
+ Literate.markdown(infile_path,
+ tutorial_outputdir;
+ name = outputfile,
+ credit = false,
+ flavor = Literate.DocumenterFlavor(),
+ documenter = true,
+ postprocess = (
+ content -> add_download_links(
+ insert_md(content),
+ file,
+ string(outputfile, ".ipynb"),
+ )
+ ),
+ execute = execute)
+
+ # Generate notebook (chain add_image_links after add_pkg_status_to_notebook).
+ # preprocess_admonitions_for_notebook converts Documenter admonitions to blockquotes
+ # so they render in Jupyter; markdown output keeps !!! style for Documenter.
+ Literate.notebook(infile_path,
+ tutorial_outputdir;
+ name = outputfile,
+ credit = false,
+ execute = false,
+ preprocess = preprocess_admonitions_for_notebook,
+ postprocess = nb ->
+ add_image_links(add_pkg_status_to_notebook(nb), outputfile))
+ end
+ end
+ end
+end
diff --git a/docs/src/index.md b/docs/src/index.md
index cf843a1b..805ee51b 100644
--- a/docs/src/index.md
+++ b/docs/src/index.md
@@ -1,4 +1,4 @@
-# PowerSystems.jl
+# HybridSystemsSimulations.jl
```@meta
CurrentModule = HybridSystemsSimulations
@@ -6,8 +6,55 @@ CurrentModule = HybridSystemsSimulations
## Overview
-`HybridSystemsSimulations.jl` is a [`Julia`](http://www.julialang.org) package that provides blah blah
+`HybridSystemsSimulations.jl` is a power system operations simulation package that extends
+[`PowerSimulations.jl`](https://nrel-sienna.github.io/PowerSimulations.jl/stable/) to model
+hybrid systems (co-located renewable, thermal, and storage behind a single point of common
+coupling). It provides device formulations, decision models, and constraints for
+production-cost and merchant-style studies, including ancillary services and bilevel
+formulations.
-* * *
+`HybridSystemsSimulations.jl` is an active project under development, and we welcome your
+feedback, suggestions, and bug reports.
-HybridSystemsSimulations has been developed as part of the FlexPower Project at the U.S. Department of Energy's National Renewable Energy Laboratory ([NREL](https://www.nrel.gov/))
+## About Sienna
+
+`HybridSystemsSimulations.jl` is part of the National Laboratory of the Rockies's (NLR, formerly NREL)
+[Sienna ecosystem](https://nrel-sienna.github.io/Sienna/), an open source framework for
+power system modeling, simulation, and optimization. The Sienna ecosystem can be
+[found on Github](https://github.com/NREL-Sienna/Sienna). It contains three applications:
+
+ - [Sienna\Data](https://nrel-sienna.github.io/Sienna/pages/applications/sienna_data.html) enables
+ efficient data input, analysis, and transformation
+ - [Sienna\Ops](https://nrel-sienna.github.io/Sienna/pages/applications/sienna_ops.html)
+ enables system scheduling simulations by formulating and solving optimization problems
+ - [Sienna\Dyn](https://nrel-sienna.github.io/Sienna/pages/applications/sienna_dyn.html) enables
+ system transient analysis including small signal stability and full system dynamic
+ simulations
+
+Each application uses multiple packages in the [`Julia`](http://www.julialang.org)
+programming language.
+
+## FlexPower Project
+
+`HybridSystemsSimulations.jl` has been developed as part of the FlexPower Project at the
+U.S. Department of Energy's National Laboratory of the Rockies
+([NLR](https://www.nlr.gov/)), formerly NREL.
+
+## Installation and Quick Links
+
+ - [Sienna installation page](https://nrel-sienna.github.io/Sienna/SiennaDocs/docs/build/how-to/install/):
+ Instructions to install `HybridSystemsSimulations.jl` and other Sienna\Ops packages
+ - [`JuMP.jl` solver's page](https://jump.dev/JuMP.jl/stable/installation/#Install-a-solver): An appropriate optimization solver is required for running models. Refer to this page to select and install a solver for your application.
+ - [Sienna Documentation Hub](https://nrel-sienna.github.io/Sienna/SiennaDocs/docs/build/index.html):
+ Links to other Sienna packages' documentation
+
+## How To Use This Documentation
+
+This documentation is organized following the [Diataxis](https://diataxis.fr/) framework:
+
+ - **Tutorials** - Detailed walk-throughs to help you *learn* how to use
+ `HybridSystemsSimulations.jl`
+ - **How to...** - Directions to help *guide* your work for a particular task
+ - **Explanation** - Additional details and background information to help you *understand*
+ `HybridSystemsSimulations.jl`, its structure, and how it works behind the scenes
+ - **Reference** - API and technical reference for a quick *look-up* during your work
diff --git a/docs/src/quick_start_guide.md b/docs/src/quick_start_guide.md
deleted file mode 100644
index d5adf773..00000000
--- a/docs/src/quick_start_guide.md
+++ /dev/null
@@ -1,3 +0,0 @@
-# Quick Start Guide
-
-HybridSystemsSimulations.jl is structured to enable stuff
diff --git a/docs/src/tutorials/intro_page.md b/docs/src/tutorials/intro_page.md
deleted file mode 100644
index 9ceae520..00000000
--- a/docs/src/tutorials/intro_page.md
+++ /dev/null
@@ -1,4 +0,0 @@
-# SIIP-Examples
-
-All the tutorials for the SIIP project are part of a separate repository
-[SIIP-Examples](https://github.com//SIIPExamples.jl).
From 4da239aefd33e5756a0fc9ec9419815ba6150c68 Mon Sep 17 00:00:00 2001
From: kdayday
Date: Thu, 19 Feb 2026 20:15:53 -0700
Subject: [PATCH 06/46] Add docs cleanup github action
---
.github/workflows/doc-preview-cleanup.yml | 34 +++++++++++++++++++++++
1 file changed, 34 insertions(+)
create mode 100644 .github/workflows/doc-preview-cleanup.yml
diff --git a/.github/workflows/doc-preview-cleanup.yml b/.github/workflows/doc-preview-cleanup.yml
new file mode 100644
index 00000000..73f291a2
--- /dev/null
+++ b/.github/workflows/doc-preview-cleanup.yml
@@ -0,0 +1,34 @@
+name: Doc Preview Cleanup
+
+on:
+ pull_request:
+ types: [closed]
+
+# Ensure that only one "Doc Preview Cleanup" workflow is force pushing at a time
+concurrency:
+ group: doc-preview-cleanup
+ cancel-in-progress: false
+
+jobs:
+ doc-preview-cleanup:
+ runs-on: ubuntu-latest
+ # This workflow pushes to gh-pages; permissions are per-job and independent of docs.yml
+ permissions:
+ contents: write
+ steps:
+ - name: Checkout gh-pages branch
+ uses: actions/checkout@v4
+ with:
+ ref: gh-pages
+ - name: Delete preview and history + push changes
+ run: |
+ if [ -d "${preview_dir}" ]; then
+ git config user.name "Documenter.jl"
+ git config user.email "documenter@juliadocs.github.io"
+ git rm -rf "${preview_dir}"
+ git commit -m "delete preview"
+ git branch gh-pages-new "$(echo "delete history" | git commit-tree "HEAD^{tree}")"
+ git push --force origin gh-pages-new:gh-pages
+ fi
+ env:
+ preview_dir: previews/PR${{ github.event.number }}
From 07becfb9f018c7e54d68643006c5ca33e9d15733 Mon Sep 17 00:00:00 2001
From: kdayday
Date: Tue, 24 Feb 2026 13:07:22 -0700
Subject: [PATCH 07/46] Draft psy5 updates
---
Project.toml | 12 ++--
src/add_parameters.jl | 9 ++-
src/feedforwards.jl | 65 +++++++++++++++++++
src/hybrid_system_decision_models.jl | 4 +-
src/utils.jl | 2 +-
test/runtests.jl | 4 ++
...t_device_hybrid_generation_constructors.jl | 4 +-
test/test_utils/additional_templates.jl | 37 +++++++++--
test/test_utils/function_utils.jl | 16 +++--
test/test_utils/price_generation_utils.jl | 16 +++--
10 files changed, 135 insertions(+), 34 deletions(-)
diff --git a/Project.toml b/Project.toml
index ebb78635..a163432b 100644
--- a/Project.toml
+++ b/Project.toml
@@ -13,10 +13,10 @@ PowerSimulations = "e690365d-45e2-57bb-ac84-44ba829e73c4"
PowerSystems = "bcd98974-b02a-5e2f-9ee0-a103f5c450dd"
[compat]
-DataStructures = "~0.18"
-DocStringExtensions = "~0.8, ~0.9"
-JuMP = "1"
+DataStructures = "~0.18, ^0.19"
+DocStringExtensions = "0.8, 0.9.2"
+JuMP = "^1.28"
MathOptInterface = "1"
-PowerSimulations = "^0.29"
-PowerSystems = "4"
-julia = "^1.6"
+PowerSimulations = "^0.33"
+PowerSystems = "5.5"
+julia = "^1.10"
diff --git a/src/add_parameters.jl b/src/add_parameters.jl
index 4ded27c6..0af5d829 100644
--- a/src/add_parameters.jl
+++ b/src/add_parameters.jl
@@ -376,7 +376,7 @@ function PSI._update_parameter_values!(
Consider reviewing your models' horizon and interval definitions",
)
end
- _set_param_value!(parameter_array, state_value, name, t)
+ _set_param_value_hss!(parameter_array, state_value, name, t)
end
end
return
@@ -430,14 +430,14 @@ function PSI._update_parameter_values!(
end
state_value += state_value_
end
- PSI._set_param_value!(parameter_array, state_value, name, final_time)
+ _set_param_value_hss!(parameter_array, state_value, name, final_time)
end
return
end
# Container for Total Reserve #
-function PSI._set_param_value!(
+function _set_param_value_hss!(
param::AbstractArray,
value::Float64,
name::String,
@@ -445,11 +445,10 @@ function PSI._set_param_value!(
t::Int,
)
param[name, service_name, t] = value
- #PSI.fix_parameter_value(param[name, service_name, t], value)
return
end
-function PSI._set_param_value!(param::AbstractArray, value::Float64, name::String, t::Int)
+function _set_param_value_hss!(param::AbstractArray, value::Float64, name::String, t::Int)
PSI.fix_parameter_value(param[name, t], value)
return
end
diff --git a/src/feedforwards.jl b/src/feedforwards.jl
index 634434a6..37af328b 100644
--- a/src/feedforwards.jl
+++ b/src/feedforwards.jl
@@ -130,6 +130,71 @@ function PSI._add_feedforward_arguments!(
return
end
+function PSI._add_feedforward_arguments!(
+ container::PSI.OptimizationContainer,
+ model::PSI.DeviceModel{D, U},
+ devices::IS.FlattenIteratorWrapper{D},
+ ff::PSI.SemiContinuousFeedforward,
+) where {D <: PSY.HybridSystem, U <: PSI.AbstractDeviceFormulation}
+ parameter_type = PSI.get_default_parameter_type(ff, D)
+ PSI.add_parameters!(container, parameter_type, ff, model, devices)
+ PSI.add_to_expression!(
+ container,
+ PSI.ActivePowerRangeExpressionUB,
+ parameter_type(),
+ devices,
+ model,
+ )
+ PSI.add_to_expression!(
+ container,
+ PSI.ActivePowerRangeExpressionLB,
+ parameter_type(),
+ devices,
+ model,
+ )
+ return
+end
+
+function PSI._add_feedforward_arguments!(
+ container::PSI.OptimizationContainer,
+ model::PSI.DeviceModel{D, U},
+ devices::IS.FlattenIteratorWrapper{D},
+ ff::PSI.UpperBoundFeedforward,
+) where {D <: PSY.HybridSystem, U <: PSI.AbstractDeviceFormulation}
+ parameter_type = PSI.get_default_parameter_type(ff, D)
+ PSI.add_parameters!(container, parameter_type, ff, model, devices)
+ if PSI.get_slacks(ff)
+ PSI._add_feedforward_slack_variables!(
+ container,
+ PSI.UpperBoundFeedForwardSlack(),
+ ff,
+ model,
+ devices,
+ )
+ end
+ return
+end
+
+function PSI._add_feedforward_arguments!(
+ container::PSI.OptimizationContainer,
+ model::PSI.DeviceModel{D, U},
+ devices::IS.FlattenIteratorWrapper{D},
+ ff::PSI.LowerBoundFeedforward,
+) where {D <: PSY.HybridSystem, U <: PSI.AbstractDeviceFormulation}
+ parameter_type = PSI.get_default_parameter_type(ff, D)
+ PSI.add_parameters!(container, parameter_type, ff, model, devices)
+ if PSI.get_slacks(ff)
+ PSI._add_feedforward_slack_variables!(
+ container,
+ PSI.LowerBoundFeedForwardSlack(),
+ ff,
+ model,
+ devices,
+ )
+ end
+ return
+end
+
function PSI.add_feedforward_constraints!(
container::PSI.OptimizationContainer,
model::PSI.DeviceModel,
diff --git a/src/hybrid_system_decision_models.jl b/src/hybrid_system_decision_models.jl
index 45941305..dbcc1b69 100644
--- a/src/hybrid_system_decision_models.jl
+++ b/src/hybrid_system_decision_models.jl
@@ -262,7 +262,7 @@ function PSI._update_parameter_values!(
Consider reviewing your models' horizon and interval definitions",
)
end
- PSI._set_param_value!(parameter_array, state_value, name, t)
+ _set_param_value_hss!(parameter_array, state_value, name, t)
end
end
return
@@ -393,7 +393,7 @@ function PSI._update_parameter_values!(
Consider reviewing your models' horizon and interval definitions",
)
end
- PSI._set_param_value!(parameter_array, state_value, name, service_name, t)
+ _set_param_value_hss!(parameter_array, state_value, name, service_name, t)
end
end
return
diff --git a/src/utils.jl b/src/utils.jl
index 958d4a74..4bd8f08b 100644
--- a/src/utils.jl
+++ b/src/utils.jl
@@ -59,7 +59,7 @@ function _update_parameter_values!(
horizon,
)
for (t, value) in enumerate(ts_vector)
- _set_param_value!(param_array, value, ts_uuid, string(subcomp_type), t)
+ _set_param_value_hss!(param_array, value, ts_uuid, string(subcomp_type), t)
end
push!(ts_uuids, ts_uuid)
end
diff --git a/test/runtests.jl b/test/runtests.jl
index bf1fe9cc..2fdc9bed 100644
--- a/test/runtests.jl
+++ b/test/runtests.jl
@@ -10,6 +10,7 @@ using CSV
using InfrastructureSystems
using Test
using Logging
+using Dates
import Aqua
Aqua.test_unbound_args(HybridSystemsSimulations)
@@ -46,6 +47,9 @@ HiGHS_optimizer = JuMP.optimizer_with_attributes(
"mip_rel_gap" => 3e-1,
)
+fast_ipopt_optimizer() = HiGHS_optimizer
+scs_solver() = HiGHS_optimizer
+
# Load
PSI_DIR = string(dirname(dirname(pathof(PowerSimulations))))
include(joinpath(PSI_DIR, "test/test_utils/mock_operation_models.jl"))
diff --git a/test/test_device_hybrid_generation_constructors.jl b/test/test_device_hybrid_generation_constructors.jl
index 7aaf4352..2787728d 100644
--- a/test/test_device_hybrid_generation_constructors.jl
+++ b/test/test_device_hybrid_generation_constructors.jl
@@ -10,7 +10,7 @@
model =
DecisionModel(MockOperationProblem, DCPPowerModel, sys; store_variable_names = true)
mock_construct_device!(model, device_model)
- moi_tests(model, 816, 0, 720, 192, 192, true)
+ moi_tests(model, 1008, 0, 904, 376, 208, true)
psi_checkobjfun_test(model, GAEVF)
end
@@ -25,6 +25,6 @@ end
# No Parameters Testing
model = DecisionModel(MockOperationProblem, PTDFPowerModel, sys)
mock_construct_device!(model, device_model)
- moi_tests(model, 816, 0, 720, 192, 192, true)
+ moi_tests(model, 1008, 0, 904, 376, 208, true)
psi_checkobjfun_test(model, GAEVF)
end
diff --git a/test/test_utils/additional_templates.jl b/test/test_utils/additional_templates.jl
index f9e956e8..b6d4e088 100644
--- a/test/test_utils/additional_templates.jl
+++ b/test/test_utils/additional_templates.jl
@@ -8,11 +8,12 @@ function set_uc_models!(template_uc)
#set_device_model!(template_uc, ThermalMultiStart, ThermalStandardUnitCommitment)
set_device_model!(template_uc, ThermalStandard, ThermalStandardUnitCommitment)
set_device_model!(template_uc, RenewableDispatch, RenewableFullDispatch)
- set_device_model!(template_uc, RenewableFix, FixedOutput)
+ set_device_model!(template_uc, RenewableNonDispatch, FixedOutput)
set_device_model!(template_uc, PowerLoad, StaticPowerLoad)
#set_device_model!(template_uc, Transformer2W, StaticBranchUnbounded)
set_device_model!(template_uc, TapTransformer, StaticBranchUnbounded)
- set_device_model!(template_uc, HydroDispatch, FixedOutput)
+ # Hydros are not needed for hybrid-focused tests under PSY5/PSI0.33
+ # set_device_model!(template_uc, HydroDispatch, FixedOutput)
set_device_model!(
template_uc,
DeviceModel(
@@ -21,7 +22,7 @@ function set_uc_models!(template_uc)
attributes = Dict{String, Any}("cycling" => false),
),
)
- set_device_model!(template_uc, GenericBattery, BookKeeping)
+ set_device_model!(template_uc, PSY.EnergyReservoirStorage, BookKeeping)
set_service_model!(template_uc, ServiceModel(VariableReserve{ReserveUp}, RangeReserve))
set_service_model!(
template_uc,
@@ -33,12 +34,40 @@ end
function update_ed_models!(template_ed)
#set_device_model!(template_ed, ThermalMultiStart, ThermalStandardDispatch)
set_device_model!(template_ed, ThermalStandard, ThermalBasicDispatch)
- set_device_model!(template_ed, HydroDispatch, FixedOutput)
+ # Hydros are not needed for hybrid-focused tests under PSY5/PSI0.33
+ # set_device_model!(template_ed, HydroDispatch, FixedOutput)
#set_device_model!(template_ed, HydroEnergyReservoir, HydroDispatchRunOfRiver)
empty!(template_ed.services)
return
end
+function get_template_basic_uc_simulation()
+ template = ProblemTemplate(CopperPlatePowerModel)
+ set_device_model!(template, ThermalStandard, ThermalBasicDispatch)
+ set_device_model!(template, RenewableDispatch, RenewableFullDispatch)
+ set_device_model!(template, PowerLoad, StaticPowerLoad)
+ set_device_model!(template, InterruptiblePowerLoad, StaticPowerLoad)
+ return template
+end
+
+function get_template_standard_uc_simulation()
+ template = get_template_basic_uc_simulation()
+ set_device_model!(template, ThermalStandard, ThermalStandardUnitCommitment)
+ return template
+end
+
+function get_thermal_dispatch_template_network(network = CopperPlatePowerModel)
+ template = ProblemTemplate(network)
+ set_device_model!(template, ThermalStandard, ThermalBasicDispatch)
+ set_device_model!(template, PowerLoad, StaticPowerLoad)
+ set_device_model!(template, MonitoredLine, StaticBranchBounds)
+ set_device_model!(template, Line, StaticBranch)
+ set_device_model!(template, Transformer2W, StaticBranch)
+ set_device_model!(template, TapTransformer, StaticBranch)
+ set_device_model!(template, TwoTerminalGenericHVDCLine, HVDCTwoTerminalLossless)
+ return template
+end
+
###############################
###### Line Templates #########
###############################
diff --git a/test/test_utils/function_utils.jl b/test/test_utils/function_utils.jl
index 50816664..cc949a55 100644
--- a/test/test_utils/function_utils.jl
+++ b/test/test_utils/function_utils.jl
@@ -22,7 +22,7 @@ function get_rt_max_active_power_series(r_gen, starttime, steps::Int)
return DataFrame(; DateTime = timestamp(ta), MaxPower = values(ta))
end
-function get_battery_params(b_gen::GenericBattery)
+function get_battery_params(b_gen::PSY.EnergyReservoirStorage)
battery_params_names = [
"initial_energy",
"SoC_min",
@@ -74,7 +74,7 @@ function modify_ren_curtailment_cost!(sys)
rdispatch = get_components(RenewableDispatch, sys)
for ren in rdispatch
# We consider 15 $/MWh as a reasonable cost for renewable curtailment
- cost = TwoPartCost(15.0, 0.0)
+ cost = PSY.RenewableGenerationCost(nothing)
set_operation_cost!(ren, cost)
end
return
@@ -88,13 +88,15 @@ function _build_battery(
efficiency_out,
)
name = string(bus.number) * "_BATTERY"
- device = GenericBattery(;
+ device = PSY.EnergyReservoirStorage(;
name = name,
available = true,
bus = bus,
prime_mover_type = PSY.PrimeMovers.BA,
- initial_energy = energy_capacity / 2,
- state_of_charge_limits = (min = energy_capacity * 0.05, max = energy_capacity),
+ storage_technology_type = PSY.StorageTech.OTHER_CHEM,
+ storage_capacity = energy_capacity,
+ storage_level_limits = (min = 0.05, max = 1.0),
+ initial_storage_capacity_level = 0.5,
rating = rating,
active_power = rating,
input_active_power_limits = (min = 0.0, max = rating),
@@ -103,7 +105,7 @@ function _build_battery(
reactive_power = 0.0,
reactive_power_limits = nothing,
base_power = 100.0,
- operation_cost = PSY.TwoPartCost(0.0, 0.0),
+ operation_cost = PSY.StorageCost(nothing),
)
return device
end
@@ -136,7 +138,7 @@ function add_hybrid_to_chuhsi_bus!(sys::System)
active_power = 1.0,
reactive_power = 0.0,
base_power = 100.0,
- operation_cost = TwoPartCost(nothing),
+ operation_cost = PSY.MarketBidCost(nothing),
thermal_unit = thermal, #new_th,
electric_load = load, #new_load,
storage = bat,
diff --git a/test/test_utils/price_generation_utils.jl b/test/test_utils/price_generation_utils.jl
index 50816664..cc949a55 100644
--- a/test/test_utils/price_generation_utils.jl
+++ b/test/test_utils/price_generation_utils.jl
@@ -22,7 +22,7 @@ function get_rt_max_active_power_series(r_gen, starttime, steps::Int)
return DataFrame(; DateTime = timestamp(ta), MaxPower = values(ta))
end
-function get_battery_params(b_gen::GenericBattery)
+function get_battery_params(b_gen::PSY.EnergyReservoirStorage)
battery_params_names = [
"initial_energy",
"SoC_min",
@@ -74,7 +74,7 @@ function modify_ren_curtailment_cost!(sys)
rdispatch = get_components(RenewableDispatch, sys)
for ren in rdispatch
# We consider 15 $/MWh as a reasonable cost for renewable curtailment
- cost = TwoPartCost(15.0, 0.0)
+ cost = PSY.RenewableGenerationCost(nothing)
set_operation_cost!(ren, cost)
end
return
@@ -88,13 +88,15 @@ function _build_battery(
efficiency_out,
)
name = string(bus.number) * "_BATTERY"
- device = GenericBattery(;
+ device = PSY.EnergyReservoirStorage(;
name = name,
available = true,
bus = bus,
prime_mover_type = PSY.PrimeMovers.BA,
- initial_energy = energy_capacity / 2,
- state_of_charge_limits = (min = energy_capacity * 0.05, max = energy_capacity),
+ storage_technology_type = PSY.StorageTech.OTHER_CHEM,
+ storage_capacity = energy_capacity,
+ storage_level_limits = (min = 0.05, max = 1.0),
+ initial_storage_capacity_level = 0.5,
rating = rating,
active_power = rating,
input_active_power_limits = (min = 0.0, max = rating),
@@ -103,7 +105,7 @@ function _build_battery(
reactive_power = 0.0,
reactive_power_limits = nothing,
base_power = 100.0,
- operation_cost = PSY.TwoPartCost(0.0, 0.0),
+ operation_cost = PSY.StorageCost(nothing),
)
return device
end
@@ -136,7 +138,7 @@ function add_hybrid_to_chuhsi_bus!(sys::System)
active_power = 1.0,
reactive_power = 0.0,
base_power = 100.0,
- operation_cost = TwoPartCost(nothing),
+ operation_cost = PSY.MarketBidCost(nothing),
thermal_unit = thermal, #new_th,
electric_load = load, #new_load,
storage = bat,
From aa84db45d301d250185a8eea18ef2eb0a47c5813 Mon Sep 17 00:00:00 2001
From: kdayday
Date: Mon, 2 Mar 2026 15:27:17 -0700
Subject: [PATCH 08/46] Specify initial conditions model to avoid errors for
models with reserves
---
src/hybrid_system_device_models.jl | 23 +++++++++++++++++++++--
1 file changed, 21 insertions(+), 2 deletions(-)
diff --git a/src/hybrid_system_device_models.jl b/src/hybrid_system_device_models.jl
index 1b4ff3c5..df9213cb 100644
--- a/src/hybrid_system_device_models.jl
+++ b/src/hybrid_system_device_models.jl
@@ -22,10 +22,29 @@ function PSI.get_default_attributes(
)
end
+# Preserve formulation for initial conditions: formulations that support services
+# must be preserved so IC build can handle hybrids with services.
PSI.get_initial_conditions_device_model(
::PSI.OperationModel,
- ::PSI.DeviceModel{T, <:AbstractHybridFormulation},
-) where {T <: PSY.HybridSystem} = PSI.DeviceModel(T, HybridEnergyOnlyDispatch)
+ model::PSI.DeviceModel{T, HybridDispatchWithReserves},
+) where {T <: PSY.HybridSystem} = model
+
+PSI.get_initial_conditions_device_model(
+ ::PSI.OperationModel,
+ model::PSI.DeviceModel{T, HybridEnergyOnlyDispatch},
+) where {T <: PSY.HybridSystem} = model
+
+PSI.get_initial_conditions_device_model(
+ ::PSI.OperationModel,
+ model::PSI.DeviceModel{T, HybridFixedDA},
+) where {T <: PSY.HybridSystem} = model
+
+# Fallback for other AbstractHybridFormulation
+PSI.get_initial_conditions_device_model(
+ ::PSI.OperationModel,
+ ::PSI.DeviceModel{T, D},
+) where {T <: PSY.HybridSystem, D <: AbstractHybridFormulation} =
+ PSI.DeviceModel(T, HybridEnergyOnlyDispatch)
PSI.get_multiplier_value(
::RenewablePowerTimeSeries,
From b0f253f52d18ce3f5cc41fbfb93483ff6f4fd621 Mon Sep 17 00:00:00 2001
From: kdayday
Date: Mon, 2 Mar 2026 15:28:37 -0700
Subject: [PATCH 09/46] Extend validate_time_series
---
src/core/decision_models.jl | 47 +++++++++++++++++++++++++++++++++++++
1 file changed, 47 insertions(+)
diff --git a/src/core/decision_models.jl b/src/core/decision_models.jl
index aa27d764..7686d5ce 100644
--- a/src/core/decision_models.jl
+++ b/src/core/decision_models.jl
@@ -35,3 +35,50 @@ Decision problem implementing a bilevel formulation for the merchant hybrid
equilibrium or regulatory analysis. #TODO DOCS
"""
struct MerchantHybridBilevelCase <: HybridDecisionProblem end
+
+###############################################################################
+# validate_time_series! for HybridDecisionProblem
+###############################################################################
+# Merchant models (HybridDecisionProblem) use custom builds and get horizon/resolution
+# from sys.ext, but the PowerSimulations DecisionModel constructor always calls
+# validate_time_series!. We extend it here with checks appropriate for merchant:
+# resolution/horizon initialization when UNSET, and forecast_count >= 1 (merchant
+# models require PowerSystems forecasts for renewables/loads).
+
+function PSI.validate_time_series!(model::PSI.DecisionModel{<:HybridDecisionProblem})
+ sys = PSI.get_system(model)
+ settings = PSI.get_settings(model)
+ available_resolutions = PSY.get_time_series_resolutions(sys)
+
+ if PSI.get_resolution(settings) == PSI.UNSET_RESOLUTION &&
+ length(available_resolutions) != 1
+ throw(
+ IS.ConflictingInputsError(
+ "Data contains multiple resolutions, the resolution keyword argument must be added to the Model. Time Series Resolutions: $(available_resolutions)",
+ ),
+ )
+ elseif PSI.get_resolution(settings) != PSI.UNSET_RESOLUTION &&
+ length(available_resolutions) > 1
+ if PSI.get_resolution(settings) ∉ available_resolutions
+ throw(
+ IS.ConflictingInputsError(
+ "Resolution $(PSI.get_resolution(settings)) is not available in the system data. Time Series Resolutions: $(available_resolutions)",
+ ),
+ )
+ end
+ else
+ PSI.set_resolution!(settings, first(available_resolutions))
+ end
+
+ if PSI.get_horizon(settings) == PSI.UNSET_HORIZON
+ PSI.set_horizon!(settings, PSY.get_forecast_horizon(sys))
+ end
+
+ counts = PSY.get_time_series_counts(sys)
+ if counts.forecast_count < 1
+ error(
+ "The system does not contain forecast data. A DecisionModel can't be built.",
+ )
+ end
+ return
+end
From 4f24490d3026e88c076d0877e4c8fd925cb9d5e5 Mon Sep 17 00:00:00 2001
From: kdayday
Date: Mon, 2 Mar 2026 15:31:04 -0700
Subject: [PATCH 10/46] Add StorageDispatchWithReserves tests and update tests
for PSI version updates
---
test/test_hybrid_device.jl | 24 ++++++++++----------
test/test_hybrid_simulations.jl | 40 +++++++++++++++++++++++++++++----
2 files changed, 48 insertions(+), 16 deletions(-)
diff --git a/test/test_hybrid_device.jl b/test/test_hybrid_device.jl
index 70d5b9cb..e1eb811f 100644
--- a/test/test_hybrid_device.jl
+++ b/test/test_hybrid_device.jl
@@ -30,15 +30,15 @@
)
build_out = PSI.build!(m; output_dir = mktempdir(; cleanup = true))
- @test build_out == PSI.BuildStatus.BUILT
+ @test build_out == PSI.ModelBuildStatus.BUILT
solve_out = PSI.solve!(m)
- @test solve_out == PSI.RunStatus.SUCCESSFUL
+ @test solve_out == PSI.RunStatus.SUCCESSFULLY_FINALIZED
- res = ProblemResults(m)
- dic_res = get_variable_values(res)
+ res = PSI.OptimizationProblemResults(m)
+ dic_res = PSI.get_variable_values(res)
- p_out = read_variable(res, "ActivePowerOutVariable__HybridSystem")[!, 2]
- p_in = read_variable(res, "ActivePowerInVariable__HybridSystem")[!, 2]
+ p_out = PSI.read_variable(res, "ActivePowerOutVariable__HybridSystem")[!, 2]
+ p_in = PSI.read_variable(res, "ActivePowerInVariable__HybridSystem")[!, 2]
@test length(p_out) == 48
@test length(p_in) == 48
@@ -86,15 +86,15 @@ end
)
build_out = PSI.build!(m; output_dir = mktempdir(; cleanup = true))
- @test build_out == PSI.BuildStatus.BUILT
+ @test build_out == PSI.ModelBuildStatus.BUILT
solve_out = PSI.solve!(m)
- @test solve_out == PSI.RunStatus.SUCCESSFUL
+ @test solve_out == PSI.RunStatus.SUCCESSFULLY_FINALIZED
- res = ProblemResults(m)
- dic_res = get_variable_values(res)
+ res = PSI.OptimizationProblemResults(m)
+ dic_res = PSI.get_variable_values(res)
- p_out = read_variable(res, "ActivePowerOutVariable__HybridSystem")[!, 2]
- p_in = read_variable(res, "ActivePowerInVariable__HybridSystem")[!, 2]
+ p_out = PSI.read_variable(res, "ActivePowerOutVariable__HybridSystem")[!, 2]
+ p_in = PSI.read_variable(res, "ActivePowerInVariable__HybridSystem")[!, 2]
@test length(p_out) == 48
@test length(p_in) == 48
diff --git a/test/test_hybrid_simulations.jl b/test/test_hybrid_simulations.jl
index db8f7498..472e3aa1 100644
--- a/test/test_hybrid_simulations.jl
+++ b/test/test_hybrid_simulations.jl
@@ -38,8 +38,8 @@
simulation_folder = mktempdir(; cleanup = true),
)
build_out = build!(sim)
- @test build_out == PSI.BuildStatus.BUILT
- @test execute!(sim) == PSI.RunStatus.SUCCESSFUL
+ @test build_out == PSI.SimulationBuildStatus.BUILT
+ @test execute!(sim) == PSI.RunStatus.SUCCESSFULLY_FINALIZED
end
@testset "Test HybridSystem Simulation UC + ED" begin
sys_uc = PSB.build_system(PSITestSystems, "c_sys5_hybrid_uc")
@@ -100,7 +100,39 @@ end
simulation_folder = mktempdir(; cleanup = true),
)
build_out = build!(sim)
- @test build_out == PSI.BuildStatus.BUILT
+ @test build_out == PSI.SimulationBuildStatus.BUILT
execute_out = execute!(sim)
- @test execute_out == PSI.RunStatus.SUCCESSFUL
+ @test execute_out == PSI.RunStatus.SUCCESSFULLY_FINALIZED
+end
+
+@testset "Test HybridSystem with StorageDispatchWithReserves (energy_target)" begin
+ # Test StorageDispatchWithReserves with energy_target attribute
+ template = get_template_standard_uc_simulation()
+ set_device_model!(
+ template,
+ DeviceModel(
+ PSY.HybridSystem,
+ HybridEnergyOnlyDispatch;
+ attributes = Dict{String, Any}("cycling" => false),
+ ),
+ )
+ set_device_model!(
+ template,
+ DeviceModel(
+ PSY.EnergyReservoirStorage,
+ StorageDispatchWithReserves;
+ attributes = Dict{String, Any}(
+ "reservation" => true,
+ "cycling_limits" => false,
+ "energy_target" => true,
+ "complete_coverage" => false,
+ "regularization" => false,
+ ),
+ ),
+ )
+ set_network_model!(template, NetworkModel(CopperPlatePowerModel; use_slacks = true))
+ sys = PSB.build_system(PSITestSystems, "c_sys5_hybrid_uc")
+ model = DecisionModel(template, sys; optimizer = HiGHS_optimizer, initialize_model = false)
+ @test build!(model; output_dir = mktempdir(; cleanup = true)) == PSI.ModelBuildStatus.BUILT
+ @test solve!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED
end
From 46e7513a5cbb67dbec82cf2cc2b9f5a4e6d73f3a Mon Sep 17 00:00:00 2001
From: kdayday
Date: Mon, 2 Mar 2026 15:33:26 -0700
Subject: [PATCH 11/46] Bookkeeping -> StorageDispatchWithReserves
---
test/test_utils/additional_templates.jl | 15 ++++++++++++++-
1 file changed, 14 insertions(+), 1 deletion(-)
diff --git a/test/test_utils/additional_templates.jl b/test/test_utils/additional_templates.jl
index b6d4e088..e0610894 100644
--- a/test/test_utils/additional_templates.jl
+++ b/test/test_utils/additional_templates.jl
@@ -22,7 +22,20 @@ function set_uc_models!(template_uc)
attributes = Dict{String, Any}("cycling" => false),
),
)
- set_device_model!(template_uc, PSY.EnergyReservoirStorage, BookKeeping)
+ set_device_model!(
+ template_uc,
+ DeviceModel(
+ PSY.EnergyReservoirStorage,
+ StorageDispatchWithReserves;
+ attributes = Dict{String, Any}(
+ "reservation" => true,
+ "cycling_limits" => false,
+ "energy_target" => false,
+ "complete_coverage" => false,
+ "regularization" => true,
+ ),
+ ),
+ )
set_service_model!(template_uc, ServiceModel(VariableReserve{ReserveUp}, RangeReserve))
set_service_model!(
template_uc,
From 4bfb4caeb9cce9f82e48ee5f734d2b8ba90c58c0 Mon Sep 17 00:00:00 2001
From: kdayday
Date: Mon, 2 Mar 2026 17:29:36 -0700
Subject: [PATCH 12/46] IS, PSY, PSI, comment version updates
---
src/add_parameters.jl | 4 ++--
src/decision_models/bilevel_decision_model.jl | 8 ++++++--
src/decision_models/cooptimizer_decision_model.jl | 11 ++++++++---
src/decision_models/only_energy_decision_model.jl | 8 ++++++--
test/test_utils/function_utils.jl | 2 +-
test/test_utils/price_generation_utils.jl | 2 +-
6 files changed, 24 insertions(+), 11 deletions(-)
diff --git a/src/add_parameters.jl b/src/add_parameters.jl
index 0af5d829..b49e78c3 100644
--- a/src/add_parameters.jl
+++ b/src/add_parameters.jl
@@ -16,7 +16,7 @@ function _add_time_series_parameters(
initial_values = Dict{String, AbstractArray}()
for device in devices
push!(device_names, PSY.get_name(device))
- ts_uuid = PSI.get_time_series_uuid(ts_type, device, ts_name)
+ ts_uuid = IS.get_time_series_uuid(ts_type, device, ts_name)
if !(ts_uuid in keys(initial_values))
initial_values[ts_uuid] =
PSI.get_time_series_initial_values!(container, ts_type, device, ts_name)
@@ -50,7 +50,7 @@ function _add_time_series_parameters(
PSI.add_component_name!(
PSI.get_attributes(param_container),
name,
- PSI.get_time_series_uuid(ts_type, device, ts_name),
+ IS.get_time_series_uuid(ts_type, device, ts_name),
)
end
return
diff --git a/src/decision_models/bilevel_decision_model.jl b/src/decision_models/bilevel_decision_model.jl
index 26dc1ee2..624b923e 100644
--- a/src/decision_models/bilevel_decision_model.jl
+++ b/src/decision_models/bilevel_decision_model.jl
@@ -8,11 +8,15 @@ function PSI.build_impl!(decision_model::PSI.DecisionModel{MerchantHybridBilevel
sys = PSI.get_system(decision_model)
T = PSY.HybridSystem
# Resolution
- RT_resolution = PSY.get_time_series_resolution(sys)
+ RT_resolution = first(PSY.get_time_series_resolutions(sys))
Δt_DA = 1.0
Δt_RT = Dates.value(Dates.Minute(RT_resolution)) / PSI.MINUTES_IN_HOUR
# Initialize Container
- PSI.init_optimization_container!(container, PSI.CopperPlatePowerModel, sys)
+ PSI.init_optimization_container!(
+ container,
+ PSI.get_network_model(PSI.get_template(decision_model)),
+ sys,
+ )
PSI.init_model_store_params!(decision_model)
# Create Multiple Time Horizons based on ext horizons
diff --git a/src/decision_models/cooptimizer_decision_model.jl b/src/decision_models/cooptimizer_decision_model.jl
index 7e317ad8..e5f3969d 100644
--- a/src/decision_models/cooptimizer_decision_model.jl
+++ b/src/decision_models/cooptimizer_decision_model.jl
@@ -9,11 +9,15 @@ function PSI.build_impl!(decision_model::PSI.DecisionModel{MerchantHybridCooptim
sys = PSI.get_system(decision_model)
T = PSY.HybridSystem
# Resolution
- RT_resolution = PSY.get_time_series_resolution(sys)
+ RT_resolution = first(PSY.get_time_series_resolutions(sys))
Δt_DA = 1.0
Δt_RT = Dates.value(Dates.Minute(RT_resolution)) / PSI.MINUTES_IN_HOUR
# Initialize Container
- PSI.init_optimization_container!(container, PSI.CopperPlatePowerModel, sys)
+ PSI.init_optimization_container!(
+ container,
+ PSI.get_network_model(PSI.get_template(decision_model)),
+ sys,
+ )
PSI.init_model_store_params!(decision_model)
# Create Multiple Time Horizons based on ext horizons
@@ -65,7 +69,8 @@ function PSI.build_impl!(decision_model::PSI.DecisionModel{MerchantHybridCooptim
end
device_model = PSI.get_model(PSI.get_template(decision_model), PSY.HybridSystem)
- device_formulation = PSI.get_formulation(device_model)
+ device_formulation =
+ device_model === nothing ? MerchantModelWithReserves : PSI.get_formulation(device_model)
network_model = PSI.get_network_model(PSI.get_template(decision_model))
###############################
diff --git a/src/decision_models/only_energy_decision_model.jl b/src/decision_models/only_energy_decision_model.jl
index e4f165d7..211921fa 100644
--- a/src/decision_models/only_energy_decision_model.jl
+++ b/src/decision_models/only_energy_decision_model.jl
@@ -7,11 +7,15 @@ function PSI.build_impl!(decision_model::PSI.DecisionModel{MerchantHybridEnergyC
sys = PSI.get_system(decision_model)
# Resolution
Δt_DA = 1.0
- RT_resolution = PSY.get_time_series_resolution(sys)
+ RT_resolution = first(PSY.get_time_series_resolutions(sys))
sys = PSI.get_system(decision_model)
Δt_RT = Dates.value(Dates.Minute(RT_resolution)) / PSI.MINUTES_IN_HOUR
# Initialize Container
- PSI.init_optimization_container!(container, PSI.CopperPlatePowerModel, sys)
+ PSI.init_optimization_container!(
+ container,
+ PSI.get_network_model(PSI.get_template(decision_model)),
+ sys,
+ )
PSI.init_model_store_params!(decision_model)
# Create Multiple Time Horizons based on ext horizons
diff --git a/test/test_utils/function_utils.jl b/test/test_utils/function_utils.jl
index cc949a55..cbeb4588 100644
--- a/test/test_utils/function_utils.jl
+++ b/test/test_utils/function_utils.jl
@@ -149,7 +149,7 @@ function add_hybrid_to_chuhsi_bus!(sys::System)
output_active_power_limits = (min = 0.0, max = 10.0),
reactive_power_limits = nothing,
)
- # Add Hybrid
+ # Add Hybrid (add_component! internally copies subcomponent time series to hybrid)
add_component!(sys, hybrid)
return
end
diff --git a/test/test_utils/price_generation_utils.jl b/test/test_utils/price_generation_utils.jl
index cc949a55..cbeb4588 100644
--- a/test/test_utils/price_generation_utils.jl
+++ b/test/test_utils/price_generation_utils.jl
@@ -149,7 +149,7 @@ function add_hybrid_to_chuhsi_bus!(sys::System)
output_active_power_limits = (min = 0.0, max = 10.0),
reactive_power_limits = nothing,
)
- # Add Hybrid
+ # Add Hybrid (add_component! internally copies subcomponent time series to hybrid)
add_component!(sys, hybrid)
return
end
From 46fc2da5d25716032b341934ef72e1943127ca3a Mon Sep 17 00:00:00 2001
From: kdayday
Date: Tue, 3 Mar 2026 13:39:05 -0700
Subject: [PATCH 13/46] Update input data in docstrings
---
src/core/decision_models.jl | 60 ++++++++++++++++++++++++++-
src/core/formulations.jl | 81 +++++++++++++++++++++++++++++--------
src/core/parameters.jl | 46 +++++++++++++++++++++
src/feedforwards.jl | 17 ++++++++
4 files changed, 187 insertions(+), 17 deletions(-)
diff --git a/src/core/decision_models.jl b/src/core/decision_models.jl
index 7686d5ce..aa54362d 100644
--- a/src/core/decision_models.jl
+++ b/src/core/decision_models.jl
@@ -6,6 +6,29 @@ abstract type HybridDecisionProblem <: PSI.DecisionProblem end
Decision problem for a merchant hybrid resource that co-optimizes energy bids/offers
in day-ahead and real-time markets only (no ancillary services). The hybrid optimizer
maximizes profit from energy (e.g. DA/RT spread) subject to internal asset limits.
+
+**Data requirements:**
+
+ - **System:** A [`PowerSystems.System`](@extref PowerSystems.System) containing at least one
+ [`PowerSystems.HybridSystem`](@extref PowerSystems.HybridSystem) with the subcomponents
+ required by the chosen device formulation (e.g. [`HybridEnergyOnlyDispatch`](@ref)).
+ - **Time series:** For each hybrid, forecasts with default names
+ `"RenewableDispatch__max_active_power"` (or `"RenewableDispatch__max_active_power_da"` for
+ day-ahead-only builds) for renewable capacity and `"PowerLoad__max_active_power"` for load,
+ or custom names configured when adding parameters. The canonical mapping from parameters to
+ time-series names is given by
+ [`PowerSimulations.get_default_time_series_names`](@extref PowerSimulations.get_default_time_series_names).
+ - **System ext data:** Use the
+ [`ext` supplemental data dictionary](@extref additional_fields) on
+ [`PowerSystems.System`](@extref PowerSystems.System) with keys
+ `\"λ_da_df\"` and `\"λ_rt_df\"`, each a `DataFrame` with column `"DateTime"` and one column
+ per bus name (matching `PowerSystems.get_name(PowerSystems.get_bus(hybrid))`). Optional
+ integer keys `\"horizon_DA\"` and `\"horizon_RT\"` override the number of DA/RT steps
+ (defaults: the length of the corresponding `"DateTime"` column).
+ - **Hybrid ext data:** Each [`PowerSystems.HybridSystem`](@extref PowerSystems.HybridSystem)
+ should have its own [`ext` dictionary](@extref additional_fields) containing the same price
+ tables and horizon keys, typically copied from the system-level `ext` before constructing the
+ [`PowerSimulations.DecisionModel`](@extref PowerSimulations.DecisionModel).
"""
struct MerchantHybridEnergyCase <: HybridDecisionProblem end
@@ -14,6 +37,15 @@ struct MerchantHybridEnergyCase <: HybridDecisionProblem end
Decision problem for a merchant hybrid with fixed day-ahead energy positions; used
when solving the real-time subproblem with locked DA bids/offers.
+
+**Data requirements:**
+
+ - Same [`PowerSystems.System`](@extref PowerSystems.System),
+ [`PowerSystems.HybridSystem`](@extref PowerSystems.HybridSystem), and time-series
+ requirements as [`MerchantHybridEnergyCase`](@ref).
+ - Same use of the [`ext` supplemental data dictionary](@extref additional_fields) on the
+ system and hybrids: keys `\"λ_da_df\"`, `\"λ_rt_df\"`, and optional `\"horizon_DA\"`,
+ `\"horizon_RT\"` as described for [`MerchantHybridEnergyCase`](@ref).
"""
struct MerchantHybridEnergyFixedDA <: HybridDecisionProblem end
@@ -24,6 +56,23 @@ Decision problem for a merchant hybrid that co-optimizes energy and ancillary se
in day-ahead and real-time markets. Maximizes ``d'y - c_h' x`` (revenue from bids/offers minus operating cost) subject to
market and asset constraints; ancillary services are committed in DA and fulfilled by internal asset
allocation in RT.
+
+**Data requirements:**
+
+ - **System and time series:** As for [`MerchantHybridEnergyCase`](@ref). The problem template
+ must include a
+ [`PowerSimulations.DeviceModel`](@extref PowerSimulations.DeviceModel) constructed as
+ `DeviceModel(PSY.HybridSystem, HybridDispatchWithReserves)` (or another appropriate hybrid
+ formulation with reserves).
+ - **ext data:** Same use of the [`ext` supplemental data dictionary](@extref additional_fields)
+ on the [`PowerSystems.System`](@extref PowerSystems.System) and each
+ [`PowerSystems.HybridSystem`](@extref PowerSystems.HybridSystem) as in
+ [`MerchantHybridEnergyCase`](@ref), plus per-service price tables for ancillary services
+ (see [`AncillaryServicePrice`](@ref)).
+ - The canonical mapping from parameters to default time-series names can be obtained via
+ [`PowerSimulations.get_default_time_series_names`](@extref PowerSimulations.get_default_time_series_names)
+ and from the \"Time Series Names\" table printed by `show(model)` for an instantiated device
+ model.
"""
struct MerchantHybridCooptimizerCase <: HybridDecisionProblem end
@@ -32,7 +81,16 @@ struct MerchantHybridCooptimizerCase <: HybridDecisionProblem end
Decision problem implementing a bilevel formulation for the merchant hybrid
(e.g. upper level: bids/offers, lower level: internal dispatch); used for
-equilibrium or regulatory analysis. #TODO DOCS
+equilibrium or regulatory analysis.
+
+**Data requirements:**
+
+ - **System and time series:** Same as [`MerchantHybridEnergyCase`](@ref) (at least one
+ [`PowerSystems.HybridSystem`](@extref PowerSystems.HybridSystem) with required forecasts and
+ time-series names).
+ - **ext data:** Same use of the [`ext` supplemental data dictionary](@extref additional_fields)
+ and keys `\"λ_da_df\"`, `\"λ_rt_df\"`, optional `\"horizon_DA\"`, `\"horizon_RT\"` on the
+ system and hybrids as in [`MerchantHybridEnergyCase`](@ref).
"""
struct MerchantHybridBilevelCase <: HybridDecisionProblem end
diff --git a/src/core/formulations.jl b/src/core/formulations.jl
index e6f0a263..55539553 100644
--- a/src/core/formulations.jl
+++ b/src/core/formulations.jl
@@ -80,19 +80,38 @@ or economic dispatch.
**Time Series Parameters:**
- - `RenewablePowerTimeSeries`: ``P^{*,\\text{re}}_t`` = renewable forecast at time ``t``
- - `ElectricLoadTimeSeries`: ``P^{\\text{ld}}_t`` = load consumption at time ``t``
+ - `RenewablePowerTimeSeries`: ``P^{*,\\text{re}}_t`` = renewable forecast at time ``t`` (default time series name: `"RenewableDispatch__max_active_power"`)
+ - `ElectricLoadTimeSeries`: ``P^{\\text{ld}}_t`` = load consumption at time ``t`` (default time series name: `"PowerLoad__max_active_power"`)
+
+ The canonical mapping is given by
+ [`PowerSimulations.get_default_time_series_names`](@extref PowerSimulations.get_default_time_series_names)
+ for `PSY.HybridSystem` and `HybridDispatchWithReserves`.
+
+**Data requirements:**
+
+ - **Device:** A [`PowerSystems.HybridSystem`](@extref PowerSystems.HybridSystem) with at least
+ one of: thermal unit (`PowerSystems.get_thermal_unit`), renewable unit
+ (`PowerSystems.get_renewable_unit`), storage (`PowerSystems.get_storage`), and optionally
+ electric load (`PowerSystems.get_electric_load`). Static limits are read from these
+ subcomponents via the `PowerSystems.get_*` accessors listed below.
+ - **Time series:** Each hybrid must have forecast time series attached with the default names
+ above (or custom names passed when adding parameters).
**Static Parameters:**
- - ``P_{\\max,\\text{pcc}}`` = `PowerSystems.get_output_active_power_limits(device).max`
- - ``P_{\\max,\\text{th}}`` = `PowerSystems.get_active_power_limits(thermal_unit).max`
+ - ``P_{\\max,\\text{pcc}}`` =
+ [`PowerSystems.get_output_active_power_limits`](@extref PowerSystems.get_output_active_power_limits)(device).max
+ - ``P_{\\max,\\text{th}}`` =
+ [`PowerSystems.get_active_power_limits`](@extref PowerSystems.get_active_power_limits)(thermal_unit).max
- ``P_{\\min,\\text{th}}`` = `PowerSystems.get_active_power_limits(thermal_unit).min`
- - ``P_{\\max,\\text{ch}}`` = `PowerSystems.get_input_active_power_limits(storage).max`
- - ``P_{\\max,\\text{ds}}`` = `PowerSystems.get_output_active_power_limits(storage).max`
- - ``\\eta_{\\text{ch}}`` = `PowerSystems.get_efficiency(storage).in`
+ - ``P_{\\max,\\text{ch}}`` =
+ [`PowerSystems.get_input_active_power_limits`](@extref PowerSystems.get_input_active_power_limits)(storage).max
+ - ``P_{\\max,\\text{ds}}`` =
+ [`PowerSystems.get_output_active_power_limits`](@extref PowerSystems.get_output_active_power_limits)(storage).max
+ - ``\\eta_{\\text{ch}}`` = [`PowerSystems.get_efficiency`](@extref PowerSystems.get_efficiency)(storage).in
- ``\\eta_{\\text{ds}}`` = `PowerSystems.get_efficiency(storage).out`
- - ``E_{\\max,\\text{st}}`` = `PowerSystems.get_storage_level_limits(storage).max × capacity`
+ - ``E_{\\max,\\text{st}}`` =
+ [`PowerSystems.get_storage_level_limits`](@extref PowerSystems.get_storage_level_limits)(storage).max × capacity
- ``E^{\\text{st}}_0`` = initial storage energy
- ``R^{*}_{p,t}`` = ancillary service deployment forecast for service ``p`` at time ``t``
- ``F_p`` = fraction of ``P_{\\max,\\text{pcc}}`` allowed for service ``p``
@@ -239,19 +258,38 @@ and asset limits.
**Time Series Parameters:**
- - `RenewablePowerTimeSeries`: ``P^{*,\\text{re}}_t`` = renewable forecast at time ``t``
- - `ElectricLoadTimeSeries`: ``P^{\\text{ld}}_t`` = load consumption at time ``t``
+ - `RenewablePowerTimeSeries`: ``P^{*,\\text{re}}_t`` = renewable forecast at time ``t`` (default time series name: `"RenewableDispatch__max_active_power"`)
+ - `ElectricLoadTimeSeries`: ``P^{\\text{ld}}_t`` = load consumption at time ``t`` (default time series name: `"PowerLoad__max_active_power"`)
+
+ The canonical mapping is given by
+ [`PowerSimulations.get_default_time_series_names`](@extref PowerSimulations.get_default_time_series_names)
+ for `PSY.HybridSystem` and `HybridEnergyOnlyDispatch`.
+
+**Data requirements:**
+
+ - **Device:** A [`PowerSystems.HybridSystem`](@extref PowerSystems.HybridSystem) with at least
+ one of: thermal unit (`PowerSystems.get_thermal_unit`), renewable unit
+ (`PowerSystems.get_renewable_unit`), storage (`PowerSystems.get_storage`), and optionally
+ electric load (`PowerSystems.get_electric_load`). Static limits are read from these
+ subcomponents via the `PowerSystems.get_*` accessors listed below.
+ - **Time series:** Each hybrid must have forecast time series attached with the default names
+ above (or custom names passed when adding parameters).
**Static Parameters:**
- - ``P_{\\max,\\text{pcc}}`` = `PowerSystems.get_output_active_power_limits(device).max`
- - ``P_{\\max,\\text{th}}`` = `PowerSystems.get_active_power_limits(thermal_unit).max`
+ - ``P_{\\max,\\text{pcc}}`` =
+ [`PowerSystems.get_output_active_power_limits`](@extref PowerSystems.get_output_active_power_limits)(device).max
+ - ``P_{\\max,\\text{th}}`` =
+ [`PowerSystems.get_active_power_limits`](@extref PowerSystems.get_active_power_limits)(thermal_unit).max
- ``P_{\\min,\\text{th}}`` = `PowerSystems.get_active_power_limits(thermal_unit).min`
- - ``P_{\\max,\\text{ch}}`` = `PowerSystems.get_input_active_power_limits(storage).max`
- - ``P_{\\max,\\text{ds}}`` = `PowerSystems.get_output_active_power_limits(storage).max`
- - ``\\eta_{\\text{ch}}`` = `PowerSystems.get_efficiency(storage).in`
+ - ``P_{\\max,\\text{ch}}`` =
+ [`PowerSystems.get_input_active_power_limits`](@extref PowerSystems.get_input_active_power_limits)(storage).max
+ - ``P_{\\max,\\text{ds}}`` =
+ [`PowerSystems.get_output_active_power_limits`](@extref PowerSystems.get_output_active_power_limits)(storage).max
+ - ``\\eta_{\\text{ch}}`` = [`PowerSystems.get_efficiency`](@extref PowerSystems.get_efficiency)(storage).in
- ``\\eta_{\\text{ds}}`` = `PowerSystems.get_efficiency(storage).out`
- - ``E_{\\max,\\text{st}}`` = `PowerSystems.get_storage_level_limits(storage).max × capacity`
+ - ``E_{\\max,\\text{st}}`` =
+ [`PowerSystems.get_storage_level_limits`](@extref PowerSystems.get_storage_level_limits)(storage).max × capacity
- ``E^{\\text{st}}_0`` = initial storage energy
**Expressions:**
@@ -353,6 +391,17 @@ locked DA positions (e.g. merchant co-optimization with "then vs. now" RT adjust
+ Bounds: [0.0, ]
+ Symbol: total reserve at PCC
+**Data requirements:**
+
+ - **Device:** A [`PowerSystems.HybridSystem`](@extref PowerSystems.HybridSystem) with PCC
+ limits. Internal asset composition is not modeled in this formulation; only net power at the
+ PCC and optional total reserve are used.
+ - **Price and horizon data:** Horizon and price data are provided through the merchant
+ decision models (e.g. [`MerchantHybridEnergyCase`](@ref),
+ [`MerchantHybridCooptimizerCase`](@ref)) using the [`ext` supplemental data
+ dictionary](@extref additional_fields) on the system and hybrids as described in their
+ docstrings.
+
**Expressions:**
Adds ``p^{\\text{out}}_t`` and ``p^{\\text{in}}_t`` to PowerSimulations' `ActivePowerBalance` expression
diff --git a/src/core/parameters.jl b/src/core/parameters.jl
index 4972a734..05e84650 100644
--- a/src/core/parameters.jl
+++ b/src/core/parameters.jl
@@ -12,6 +12,16 @@ Objective function parameter for day-ahead energy price.
Docs abbreviation: ``\\Pi^*_{\\text{DA},t}`` (USD/MWh). Used in the merchant objective
(e.g. ``f_{\\text{DA},t}`` term) when building the decision model.
+
+**Input data:**
+
+ - **System ext:** The [`ext` supplemental data dictionary](@extref additional_fields) on
+ [`PowerSystems.System`](@extref PowerSystems.System) must contain `\"λ_da_df\"`, a
+ `DataFrame` with column `"DateTime"` and one column per bus name, and optionally
+ `\"horizon_DA\"::Int` giving the number of day-ahead steps.
+ - **Hybrid ext:** Each [`PowerSystems.HybridSystem`](@extref PowerSystems.HybridSystem)
+ reads the same keys from its own [`ext` dictionary](@extref additional_fields); values are
+ sliced starting at the current forecast time and used over the model horizon.
"""
struct DayAheadEnergyPrice <: PSI.ObjectiveFunctionParameter end
@@ -22,6 +32,17 @@ Objective function parameter for real-time energy price.
Docs abbreviation: ``\\Pi^*_{\\text{RT},t}`` (USD/MWh). Used in the merchant profit
expression for RT energy and DART spread.
+
+**Input data:**
+
+ - **System ext:** The [`ext` supplemental data dictionary](@extref additional_fields) on
+ [`PowerSystems.System`](@extref PowerSystems.System) must contain `\"λ_rt_df\"`, a
+ `DataFrame` with column `"DateTime"` and one column per bus name, and optionally
+ `\"horizon_RT\"::Int` giving the number of real-time steps.
+ - **Hybrid ext:** Each [`PowerSystems.HybridSystem`](@extref PowerSystems.HybridSystem)
+ reads `\"λ_rt_df\"`, `\"horizon_RT\"`, and a mapping `\"tmap\"` from its own
+ [`ext` dictionary](@extref additional_fields), used to align real-time steps to day-ahead
+ steps where needed.
"""
struct RealTimeEnergyPrice <: PSI.ObjectiveFunctionParameter end
@@ -32,6 +53,14 @@ Objective function parameter for ancillary service price.
Docs abbreviation: ``\\Pi^*_{p,t}`` (USD/MWh) for service ``p \\in P``. Used in the DA
profit term for ancillary services (``sb^{\\text{out}}`` + ``sb^{\\text{in}}``).
+
+**Input data:**
+
+ - **Hybrid ext:** For each service, the hybrid's [`ext` dictionary](@extref additional_fields)
+ contains a key `\"λ_\"` (e.g. `\"λ_Regulation_Up\"`) with a `DataFrame` that
+ has column `"DateTime"` and one column per bus name, plus `\"horizon_DA\"` giving the number
+ of day-ahead steps. Used by [`MerchantHybridCooptimizerCase`](@ref) when ancillary services
+ are attached to the hybrid.
"""
struct AncillaryServicePrice <: PSI.ObjectiveFunctionParameter end
@@ -44,6 +73,15 @@ Variable-value parameter that provides the right-hand side for the storage charg
cycle limit: ``\\eta_{\\text{ch}} \\Delta t \\sum_t p_{\\text{ch},t} - c_{\\text{ch}}^- \\leq C_{\\text{st}} E_{\\max,\\text{st}}``. Used with
[`CyclingChargeLimitFeedforward`](@ref) in recurrent simulations to pass cumulative
cycling from previous horizons.
+
+**Input data:**
+
+ - **Storage limits:** Initial values (when not updated from state) are computed from the
+ hybrid's storage using
+ [`PowerSystems.get_cycle_limits`](@extref PowerSystems.get_cycle_limits) and
+ [`PowerSystems.get_storage_level_limits`](@extref PowerSystems.get_storage_level_limits).
+ - **State updates:** In recurrent runs, values are updated from the simulation state
+ (cumulative charge usage).
"""
struct CyclingChargeLimitParameter <: PSI.VariableValueParameter end
@@ -53,6 +91,14 @@ struct CyclingChargeLimitParameter <: PSI.VariableValueParameter end
Variable-value parameter for the storage discharging cycle limit:
``(\\Delta t/\\eta_{\\text{ds}}) \\sum_t p_{\\text{ds},t} - c_{\\text{ds}}^- \\leq C_{\\text{st}} E_{\\max,\\text{st}}``. Used with
[`CyclingDischargeLimitFeedforward`](@ref).
+
+**Input data:**
+
+ - Same as [`CyclingChargeLimitParameter`](@ref): initial values based on
+ [`PowerSystems.get_cycle_limits`](@extref PowerSystems.get_cycle_limits) and
+ [`PowerSystems.get_storage_level_limits`](@extref PowerSystems.get_storage_level_limits)
+ for the hybrid's storage; in recurrent runs, updated from state (cumulative discharge
+ usage).
"""
struct CyclingDischargeLimitParameter <: PSI.VariableValueParameter end
diff --git a/src/feedforwards.jl b/src/feedforwards.jl
index 37af328b..c33c8c01 100644
--- a/src/feedforwards.jl
+++ b/src/feedforwards.jl
@@ -7,6 +7,15 @@ where ``s^{\\text{up}}_{\\text{reg},t}`` and ``s^{\\text{down}}_{\\text{reg},t}`
``C_{\\text{horizon}} \\times E_{\\max,\\text{st}}`` otherwise. Use with PowerSimulations' `add_feedforward!` in a
[`PowerSimulations.DeviceModel`](@extref PowerSimulations.DeviceModel) for
[`HybridDispatchWithReserves`](@ref) or [`HybridEnergyOnlyDispatch`](@ref).
+
+**Input data:**
+
+ - **Storage limits:** Limit supplied by [`CyclingChargeLimitParameter`](@ref), which is derived
+ from the hybrid's storage using
+ [`PowerSystems.get_cycle_limits`](@extref PowerSystems.get_cycle_limits) and
+ [`PowerSystems.get_storage_level_limits`](@extref PowerSystems.get_storage_level_limits).
+ - Not compatible with the device attribute `"cycling" => true` (cycling limits are then
+ enforced in the formulation).
"""
struct CyclingChargeLimitFeedforward <: PSI.AbstractAffectFeedforward
optimization_container_key::PSI.OptimizationContainerKey
@@ -51,6 +60,14 @@ Feedforward that enforces a cumulative discharging cycle limit on the hybrid's s
where ``s^{\\text{up}}_{\\text{reg},t}`` and ``s^{\\text{down}}_{\\text{reg},t}`` denote served reserve (up/down). The limit comes from
[`CyclingDischargeLimitParameter`](@ref) in recurrent runs. See
[`CyclingChargeLimitFeedforward`](@ref) for usage pattern.
+
+**Input data:**
+
+ - Same as [`CyclingChargeLimitFeedforward`](@ref): limit from
+ [`CyclingDischargeLimitParameter`](@ref), derived from the hybrid's storage using
+ [`PowerSystems.get_cycle_limits`](@extref PowerSystems.get_cycle_limits) and
+ [`PowerSystems.get_storage_level_limits`](@extref PowerSystems.get_storage_level_limits).
+ - Not compatible with device attribute `"cycling" => true`.
"""
struct CyclingDischargeLimitFeedforward <: PSI.AbstractAffectFeedforward
optimization_container_key::PSI.OptimizationContainerKey
From f2cc7bff8dabf4cd6eab77d6e03f9da06508b4dd Mon Sep 17 00:00:00 2001
From: kdayday
Date: Tue, 3 Mar 2026 13:53:21 -0700
Subject: [PATCH 14/46] Clean up extrefs and simplify
---
src/core/decision_models.jl | 13 +++--------
src/core/formulations.jl | 46 +++++++++++--------------------------
src/core/parameters.jl | 11 ++++-----
src/feedforwards.jl | 8 +++----
4 files changed, 24 insertions(+), 54 deletions(-)
diff --git a/src/core/decision_models.jl b/src/core/decision_models.jl
index aa54362d..f9ffe9ad 100644
--- a/src/core/decision_models.jl
+++ b/src/core/decision_models.jl
@@ -14,10 +14,7 @@ maximizes profit from energy (e.g. DA/RT spread) subject to internal asset limit
required by the chosen device formulation (e.g. [`HybridEnergyOnlyDispatch`](@ref)).
- **Time series:** For each hybrid, forecasts with default names
`"RenewableDispatch__max_active_power"` (or `"RenewableDispatch__max_active_power_da"` for
- day-ahead-only builds) for renewable capacity and `"PowerLoad__max_active_power"` for load,
- or custom names configured when adding parameters. The canonical mapping from parameters to
- time-series names is given by
- [`PowerSimulations.get_default_time_series_names`](@extref PowerSimulations.get_default_time_series_names).
+ day-ahead-only builds) for renewable capacity and `"PowerLoad__max_active_power"` for load.
- **System ext data:** Use the
[`ext` supplemental data dictionary](@extref additional_fields) on
[`PowerSystems.System`](@extref PowerSystems.System) with keys
@@ -27,8 +24,8 @@ maximizes profit from energy (e.g. DA/RT spread) subject to internal asset limit
(defaults: the length of the corresponding `"DateTime"` column).
- **Hybrid ext data:** Each [`PowerSystems.HybridSystem`](@extref PowerSystems.HybridSystem)
should have its own [`ext` dictionary](@extref additional_fields) containing the same price
- tables and horizon keys, typically copied from the system-level `ext` before constructing the
- [`PowerSimulations.DecisionModel`](@extref PowerSimulations.DecisionModel).
+ tables and horizon keys, typically copied from the system-level `ext` before constructing a
+ `PowerSimulations.DecisionModel`.
"""
struct MerchantHybridEnergyCase <: HybridDecisionProblem end
@@ -69,10 +66,6 @@ allocation in RT.
[`PowerSystems.HybridSystem`](@extref PowerSystems.HybridSystem) as in
[`MerchantHybridEnergyCase`](@ref), plus per-service price tables for ancillary services
(see [`AncillaryServicePrice`](@ref)).
- - The canonical mapping from parameters to default time-series names can be obtained via
- [`PowerSimulations.get_default_time_series_names`](@extref PowerSimulations.get_default_time_series_names)
- and from the \"Time Series Names\" table printed by `show(model)` for an instantiated device
- model.
"""
struct MerchantHybridCooptimizerCase <: HybridDecisionProblem end
diff --git a/src/core/formulations.jl b/src/core/formulations.jl
index 55539553..c54828e6 100644
--- a/src/core/formulations.jl
+++ b/src/core/formulations.jl
@@ -83,35 +83,26 @@ or economic dispatch.
- `RenewablePowerTimeSeries`: ``P^{*,\\text{re}}_t`` = renewable forecast at time ``t`` (default time series name: `"RenewableDispatch__max_active_power"`)
- `ElectricLoadTimeSeries`: ``P^{\\text{ld}}_t`` = load consumption at time ``t`` (default time series name: `"PowerLoad__max_active_power"`)
- The canonical mapping is given by
- [`PowerSimulations.get_default_time_series_names`](@extref PowerSimulations.get_default_time_series_names)
- for `PSY.HybridSystem` and `HybridDispatchWithReserves`.
-
**Data requirements:**
- **Device:** A [`PowerSystems.HybridSystem`](@extref PowerSystems.HybridSystem) with at least
one of: thermal unit (`PowerSystems.get_thermal_unit`), renewable unit
(`PowerSystems.get_renewable_unit`), storage (`PowerSystems.get_storage`), and optionally
- electric load (`PowerSystems.get_electric_load`). Static limits are read from these
- subcomponents via the `PowerSystems.get_*` accessors listed below.
+ electric load (`PowerSystems.get_electric_load`).
- **Time series:** Each hybrid must have forecast time series attached with the default names
above (or custom names passed when adding parameters).
**Static Parameters:**
- - ``P_{\\max,\\text{pcc}}`` =
- [`PowerSystems.get_output_active_power_limits`](@extref PowerSystems.get_output_active_power_limits)(device).max
- - ``P_{\\max,\\text{th}}`` =
- [`PowerSystems.get_active_power_limits`](@extref PowerSystems.get_active_power_limits)(thermal_unit).max
+ - ``P_{\\max,\\text{pcc}}`` = `PowerSystems.get_output_active_power_limits(device).max`
+ - ``P_{\\max,\\text{th}}`` = `PowerSystems.get_active_power_limits(thermal_unit).max`
- ``P_{\\min,\\text{th}}`` = `PowerSystems.get_active_power_limits(thermal_unit).min`
- - ``P_{\\max,\\text{ch}}`` =
- [`PowerSystems.get_input_active_power_limits`](@extref PowerSystems.get_input_active_power_limits)(storage).max
- - ``P_{\\max,\\text{ds}}`` =
- [`PowerSystems.get_output_active_power_limits`](@extref PowerSystems.get_output_active_power_limits)(storage).max
- - ``\\eta_{\\text{ch}}`` = [`PowerSystems.get_efficiency`](@extref PowerSystems.get_efficiency)(storage).in
+ - ``P_{\\max,\\text{ch}}`` = `PowerSystems.get_input_active_power_limits(storage).max`
+ - ``P_{\\max,\\text{ds}}`` = `PowerSystems.get_output_active_power_limits(storage).max`
+ - ``\\eta_{\\text{ch}}`` = `PowerSystems.get_efficiency(storage).in`
- ``\\eta_{\\text{ds}}`` = `PowerSystems.get_efficiency(storage).out`
- ``E_{\\max,\\text{st}}`` =
- [`PowerSystems.get_storage_level_limits`](@extref PowerSystems.get_storage_level_limits)(storage).max × capacity
+ `PowerSystems.get_storage_level_limits(storage).max × capacity`
- ``E^{\\text{st}}_0`` = initial storage energy
- ``R^{*}_{p,t}`` = ancillary service deployment forecast for service ``p`` at time ``t``
- ``F_p`` = fraction of ``P_{\\max,\\text{pcc}}`` allowed for service ``p``
@@ -261,35 +252,26 @@ and asset limits.
- `RenewablePowerTimeSeries`: ``P^{*,\\text{re}}_t`` = renewable forecast at time ``t`` (default time series name: `"RenewableDispatch__max_active_power"`)
- `ElectricLoadTimeSeries`: ``P^{\\text{ld}}_t`` = load consumption at time ``t`` (default time series name: `"PowerLoad__max_active_power"`)
- The canonical mapping is given by
- [`PowerSimulations.get_default_time_series_names`](@extref PowerSimulations.get_default_time_series_names)
- for `PSY.HybridSystem` and `HybridEnergyOnlyDispatch`.
-
**Data requirements:**
- **Device:** A [`PowerSystems.HybridSystem`](@extref PowerSystems.HybridSystem) with at least
one of: thermal unit (`PowerSystems.get_thermal_unit`), renewable unit
(`PowerSystems.get_renewable_unit`), storage (`PowerSystems.get_storage`), and optionally
- electric load (`PowerSystems.get_electric_load`). Static limits are read from these
- subcomponents via the `PowerSystems.get_*` accessors listed below.
+ electric load (`PowerSystems.get_electric_load`).
- **Time series:** Each hybrid must have forecast time series attached with the default names
above (or custom names passed when adding parameters).
**Static Parameters:**
- - ``P_{\\max,\\text{pcc}}`` =
- [`PowerSystems.get_output_active_power_limits`](@extref PowerSystems.get_output_active_power_limits)(device).max
- - ``P_{\\max,\\text{th}}`` =
- [`PowerSystems.get_active_power_limits`](@extref PowerSystems.get_active_power_limits)(thermal_unit).max
+ - ``P_{\\max,\\text{pcc}}`` = `PowerSystems.get_output_active_power_limits(device).max`
+ - ``P_{\\max,\\text{th}}`` = `PowerSystems.get_active_power_limits(thermal_unit).max`
- ``P_{\\min,\\text{th}}`` = `PowerSystems.get_active_power_limits(thermal_unit).min`
- - ``P_{\\max,\\text{ch}}`` =
- [`PowerSystems.get_input_active_power_limits`](@extref PowerSystems.get_input_active_power_limits)(storage).max
- - ``P_{\\max,\\text{ds}}`` =
- [`PowerSystems.get_output_active_power_limits`](@extref PowerSystems.get_output_active_power_limits)(storage).max
- - ``\\eta_{\\text{ch}}`` = [`PowerSystems.get_efficiency`](@extref PowerSystems.get_efficiency)(storage).in
+ - ``P_{\\max,\\text{ch}}`` = `PowerSystems.get_input_active_power_limits(storage).max`
+ - ``P_{\\max,\\text{ds}}`` = `PowerSystems.get_output_active_power_limits(storage).max`
+ - ``\\eta_{\\text{ch}}`` = `PowerSystems.get_efficiency(storage).in`
- ``\\eta_{\\text{ds}}`` = `PowerSystems.get_efficiency(storage).out`
- ``E_{\\max,\\text{st}}`` =
- [`PowerSystems.get_storage_level_limits`](@extref PowerSystems.get_storage_level_limits)(storage).max × capacity
+ `PowerSystems.get_storage_level_limits(storage).max × capacity`
- ``E^{\\text{st}}_0`` = initial storage energy
**Expressions:**
diff --git a/src/core/parameters.jl b/src/core/parameters.jl
index 05e84650..8e361f10 100644
--- a/src/core/parameters.jl
+++ b/src/core/parameters.jl
@@ -77,9 +77,8 @@ cycling from previous horizons.
**Input data:**
- **Storage limits:** Initial values (when not updated from state) are computed from the
- hybrid's storage using
- [`PowerSystems.get_cycle_limits`](@extref PowerSystems.get_cycle_limits) and
- [`PowerSystems.get_storage_level_limits`](@extref PowerSystems.get_storage_level_limits).
+ hybrid's storage using `PowerSystems.get_cycle_limits` and
+ `PowerSystems.get_storage_level_limits`.
- **State updates:** In recurrent runs, values are updated from the simulation state
(cumulative charge usage).
"""
@@ -95,10 +94,8 @@ Variable-value parameter for the storage discharging cycle limit:
**Input data:**
- Same as [`CyclingChargeLimitParameter`](@ref): initial values based on
- [`PowerSystems.get_cycle_limits`](@extref PowerSystems.get_cycle_limits) and
- [`PowerSystems.get_storage_level_limits`](@extref PowerSystems.get_storage_level_limits)
- for the hybrid's storage; in recurrent runs, updated from state (cumulative discharge
- usage).
+ `PowerSystems.get_cycle_limits` and `PowerSystems.get_storage_level_limits` for the
+ hybrid's storage; in recurrent runs, updated from state (cumulative discharge usage).
"""
struct CyclingDischargeLimitParameter <: PSI.VariableValueParameter end
diff --git a/src/feedforwards.jl b/src/feedforwards.jl
index c33c8c01..54809759 100644
--- a/src/feedforwards.jl
+++ b/src/feedforwards.jl
@@ -11,9 +11,8 @@ where ``s^{\\text{up}}_{\\text{reg},t}`` and ``s^{\\text{down}}_{\\text{reg},t}`
**Input data:**
- **Storage limits:** Limit supplied by [`CyclingChargeLimitParameter`](@ref), which is derived
- from the hybrid's storage using
- [`PowerSystems.get_cycle_limits`](@extref PowerSystems.get_cycle_limits) and
- [`PowerSystems.get_storage_level_limits`](@extref PowerSystems.get_storage_level_limits).
+ from the hybrid's storage using `PowerSystems.get_cycle_limits` and
+ `PowerSystems.get_storage_level_limits`.
- Not compatible with the device attribute `"cycling" => true` (cycling limits are then
enforced in the formulation).
"""
@@ -65,8 +64,7 @@ where ``s^{\\text{up}}_{\\text{reg},t}`` and ``s^{\\text{down}}_{\\text{reg},t}`
- Same as [`CyclingChargeLimitFeedforward`](@ref): limit from
[`CyclingDischargeLimitParameter`](@ref), derived from the hybrid's storage using
- [`PowerSystems.get_cycle_limits`](@extref PowerSystems.get_cycle_limits) and
- [`PowerSystems.get_storage_level_limits`](@extref PowerSystems.get_storage_level_limits).
+ `PowerSystems.get_cycle_limits` and `PowerSystems.get_storage_level_limits`.
- Not compatible with device attribute `"cycling" => true`.
"""
struct CyclingDischargeLimitFeedforward <: PSI.AbstractAffectFeedforward
From dd8d78d265ae87eb438317933af434079dd177be Mon Sep 17 00:00:00 2001
From: kdayday
Date: Thu, 5 Mar 2026 09:28:04 -0700
Subject: [PATCH 15/46] Error if merchant model called without HybridSystem
---
src/decision_models/cooptimizer_decision_model.jl | 11 +++++++++--
1 file changed, 9 insertions(+), 2 deletions(-)
diff --git a/src/decision_models/cooptimizer_decision_model.jl b/src/decision_models/cooptimizer_decision_model.jl
index e5f3969d..49e6052d 100644
--- a/src/decision_models/cooptimizer_decision_model.jl
+++ b/src/decision_models/cooptimizer_decision_model.jl
@@ -69,8 +69,15 @@ function PSI.build_impl!(decision_model::PSI.DecisionModel{MerchantHybridCooptim
end
device_model = PSI.get_model(PSI.get_template(decision_model), PSY.HybridSystem)
- device_formulation =
- device_model === nothing ? MerchantModelWithReserves : PSI.get_formulation(device_model)
+ if device_model === nothing
+ error(
+ "MerchantHybridCooptimizerCase requires a DeviceModel for HybridSystem in the " *
+ "ProblemTemplate. Call set_device_model!(template, DeviceModel(PSY.HybridSystem, " *
+ "HybridDispatchWithReserves)) or another appropriate hybrid formulation before " *
+ "constructing the DecisionModel.",
+ )
+ end
+ device_formulation = PSI.get_formulation(device_model)
network_model = PSI.get_network_model(PSI.get_template(decision_model))
###############################
From 34346020865c7e9036a5bf2253043f7e96453017 Mon Sep 17 00:00:00 2001
From: kdayday
Date: Thu, 5 Mar 2026 09:32:40 -0700
Subject: [PATCH 16/46] Remove redundant price utils, bug fixes, and
in-progress day ahead ts adder
---
src/add_parameters.jl | 9 +-
test/runtests.jl | 1 -
test/test_merchant_cooptimizer.jl | 4 +-
test/test_merchant_only_energy.jl | 4 +-
test/test_utils/function_utils.jl | 34 +++++
test/test_utils/price_generation_utils.jl | 155 ----------------------
6 files changed, 45 insertions(+), 162 deletions(-)
delete mode 100644 test/test_utils/price_generation_utils.jl
diff --git a/src/add_parameters.jl b/src/add_parameters.jl
index b49e78c3..f9acd797 100644
--- a/src/add_parameters.jl
+++ b/src/add_parameters.jl
@@ -16,7 +16,7 @@ function _add_time_series_parameters(
initial_values = Dict{String, AbstractArray}()
for device in devices
push!(device_names, PSY.get_name(device))
- ts_uuid = IS.get_time_series_uuid(ts_type, device, ts_name)
+ ts_uuid = string(IS.get_time_series_uuid(ts_type, device, ts_name))
if !(ts_uuid in keys(initial_values))
initial_values[ts_uuid] =
PSI.get_time_series_initial_values!(container, ts_type, device, ts_name)
@@ -31,6 +31,7 @@ function _add_time_series_parameters(
ts_name,
collect(keys(initial_values)),
device_names,
+ (), # additional_axes: no extra axes for RenewablePowerTimeSeries
time_steps,
)
jump_model = PSI.get_jump_model(container)
@@ -50,7 +51,7 @@ function _add_time_series_parameters(
PSI.add_component_name!(
PSI.get_attributes(param_container),
name,
- IS.get_time_series_uuid(ts_type, device, ts_name),
+ string(IS.get_time_series_uuid(ts_type, device, ts_name)),
)
end
return
@@ -85,7 +86,7 @@ function _add_price_time_series_parameters(
container,
param,
PSY.HybridSystem,
- var,
+ (var,),
PSI.SOSStatusVariable.NO_VARIABLE,
false,
Float64,
@@ -143,7 +144,7 @@ function _add_price_time_series_parameters(
container,
param,
PSY.HybridSystem,
- var,
+ (var,),
PSI.SOSStatusVariable.NO_VARIABLE,
false,
Float64,
diff --git a/test/runtests.jl b/test/runtests.jl
index 2fdc9bed..7c58e559 100644
--- a/test/runtests.jl
+++ b/test/runtests.jl
@@ -59,7 +59,6 @@ include(joinpath(PSI_DIR, "test/test_utils/model_checks.jl"))
TEST_DIR = isempty(dirname(@__FILE__)) ? "test" : dirname(@__FILE__)
include(joinpath(TEST_DIR, "test_utils/function_utils.jl"))
include(joinpath(TEST_DIR, "test_utils/additional_templates.jl"))
-include(joinpath(TEST_DIR, "test_utils/price_generation_utils.jl"))
"""
Copied @includetests from https://github.com/ssfrr/TestSetExtensions.jl.
diff --git a/test/test_merchant_cooptimizer.jl b/test/test_merchant_cooptimizer.jl
index 04e02d11..74d46629 100644
--- a/test/test_merchant_cooptimizer.jl
+++ b/test/test_merchant_cooptimizer.jl
@@ -52,9 +52,11 @@
PSY.set_ext!(hy_sys, deepcopy(dic))
# Set decision model for Optimizer
+ template = ProblemTemplate(CopperPlatePowerModel)
+ set_device_model!(template, DeviceModel(PSY.HybridSystem, HybridDispatchWithReserves))
decision_optimizer_DA = DecisionModel(
MerchantHybridCooptimizerCase,
- ProblemTemplate(CopperPlatePowerModel),
+ template,
sys;
optimizer = HiGHS_optimizer,
calculate_conflict = true,
diff --git a/test/test_merchant_only_energy.jl b/test/test_merchant_only_energy.jl
index c4985519..7e2d15c6 100644
--- a/test/test_merchant_only_energy.jl
+++ b/test/test_merchant_only_energy.jl
@@ -34,9 +34,11 @@
PSY.set_ext!(hy_sys, deepcopy(dic))
# Set decision model for Optimizer
+ template = ProblemTemplate(CopperPlatePowerModel)
+ set_device_model!(template, DeviceModel(PSY.HybridSystem, HybridEnergyOnlyDispatch))
decision_optimizer_DA = DecisionModel(
MerchantHybridEnergyCase,
- ProblemTemplate(CopperPlatePowerModel),
+ template,
sys;
optimizer = HiGHS_optimizer,
calculate_conflict = true,
diff --git a/test/test_utils/function_utils.jl b/test/test_utils/function_utils.jl
index cbeb4588..473435d4 100644
--- a/test/test_utils/function_utils.jl
+++ b/test/test_utils/function_utils.jl
@@ -151,5 +151,39 @@ function add_hybrid_to_chuhsi_bus!(sys::System)
)
# Add Hybrid (add_component! internally copies subcomponent time series to hybrid)
add_component!(sys, hybrid)
+ # Ensure DA-named time series exists so merchant decision models that request
+ # "RenewableDispatch__max_active_power_da" (DA path) find metadata on the hybrid.
+ _add_hybrid_renewable_da_time_series!(sys, hybrid)
+ return
+end
+
+function _add_hybrid_renewable_da_time_series!(sys::PSY.System, hybrid::PSY.HybridSystem)
+ try
+ ts = PSY.get_time_series(IS.SingleTimeSeries, hybrid, "RenewableDispatch__max_active_power")
+ single_da = IS.SingleTimeSeries(ts, "RenewableDispatch__max_active_power_da")
+ PSY.add_time_series!(sys, hybrid, single_da)
+ catch
+ nothing
+ end
+
+ # Use a horizon long enough to cover the
+ # decision model window (e.g. 48 steps at 5-min = 4 hours); otherwise get_window
+ # fails in smoke testswith "timestamp not within" when the model requests 4 hours of data.
+ try
+ ts_det = PSY.get_time_series(
+ IS.DeterministicSingleTimeSeries,
+ hybrid,
+ "RenewableDispatch__max_active_power",
+ )
+ horizon = IS.get_horizon(ts_det)
+ interval = IS.get_interval(ts_det)
+ resolution = IS.get_resolution(ts_det)
+ if resolution == Dates.Minute(5) && horizon < Dates.Hour(4)
+ horizon = Dates.Hour(4)
+ end
+ PSY.transform_single_time_series!(sys, horizon, interval; resolution = resolution)
+ catch
+ nothing
+ end
return
end
diff --git a/test/test_utils/price_generation_utils.jl b/test/test_utils/price_generation_utils.jl
deleted file mode 100644
index cbeb4588..00000000
--- a/test/test_utils/price_generation_utils.jl
+++ /dev/null
@@ -1,155 +0,0 @@
-using TimeSeries
-
-function get_da_max_active_power_series(r_gen, starttime, steps::Int)
- ta = get_time_series_array(
- SingleTimeSeries,
- r_gen,
- "max_active_power";
- start_time = starttime,
- len = 24 * steps,
- )
- return DataFrame(; DateTime = timestamp(ta), MaxPower = values(ta))
-end
-
-function get_rt_max_active_power_series(r_gen, starttime, steps::Int)
- ta = get_time_series_array(
- SingleTimeSeries,
- r_gen,
- "max_active_power";
- start_time = starttime,
- len = 24 * 12 * steps,
- )
- return DataFrame(; DateTime = timestamp(ta), MaxPower = values(ta))
-end
-
-function get_battery_params(b_gen::PSY.EnergyReservoirStorage)
- battery_params_names = [
- "initial_energy",
- "SoC_min",
- "SoC_max",
- "P_ch_min",
- "P_ch_max",
- "P_ds_min",
- "P_ds_max",
- "η_in",
- "η_out",
- ]
- SoC_min, SoC_max = get_state_of_charge_limits(b_gen)
- P_ch_min, P_ch_max = get_input_active_power_limits(b_gen)
- P_ds_min, P_ds_max = get_output_active_power_limits(b_gen)
- η_in, η_out = get_efficiency(b_gen)
- battery_params_vals = [
- get_initial_energy(b_gen),
- SoC_min,
- SoC_max,
- P_ch_min,
- P_ch_max,
- P_ds_min,
- P_ds_max,
- η_in,
- η_out,
- ]
- return DataFrame(; ParamName = battery_params_names, Value = battery_params_vals)
-end
-
-function get_thermal_params(t_gen)
- P_min, P_max = get_active_power_limits(t_gen)
- # TODO Implement the proper three part cost
- three_cost = get_operation_cost(t_gen)
- first_part = three_cost.variable[1]
- second_part = three_cost.variable[2]
- slope = (second_part[1] - first_part[1]) / (second_part[2] - first_part[2]) # $/MWh
- fix_cost = three_cost.fixed # $/h
- return DataFrame(;
- ParamName = ["P_min", "P_max", "C_var", "C_fix"],
- Value = [P_min, P_max, slope, fix_cost],
- )
-end
-
-function get_row_val(df, row_name)
- return df[only(findall(==(row_name), df.ParamName)), :]["Value"]
-end
-
-function modify_ren_curtailment_cost!(sys)
- rdispatch = get_components(RenewableDispatch, sys)
- for ren in rdispatch
- # We consider 15 $/MWh as a reasonable cost for renewable curtailment
- cost = PSY.RenewableGenerationCost(nothing)
- set_operation_cost!(ren, cost)
- end
- return
-end
-
-function _build_battery(
- bus::PSY.Bus,
- energy_capacity,
- rating,
- efficiency_in,
- efficiency_out,
-)
- name = string(bus.number) * "_BATTERY"
- device = PSY.EnergyReservoirStorage(;
- name = name,
- available = true,
- bus = bus,
- prime_mover_type = PSY.PrimeMovers.BA,
- storage_technology_type = PSY.StorageTech.OTHER_CHEM,
- storage_capacity = energy_capacity,
- storage_level_limits = (min = 0.05, max = 1.0),
- initial_storage_capacity_level = 0.5,
- rating = rating,
- active_power = rating,
- input_active_power_limits = (min = 0.0, max = rating),
- output_active_power_limits = (min = 0.0, max = rating),
- efficiency = (in = efficiency_in, out = efficiency_out),
- reactive_power = 0.0,
- reactive_power_limits = nothing,
- base_power = 100.0,
- operation_cost = PSY.StorageCost(nothing),
- )
- return device
-end
-
-function add_battery_to_bus!(sys::System, bus_name::String)
- bus = get_component(Bus, sys, bus_name)
- bat = _build_battery(bus, 4.0, 2.0, 0.93, 0.93)
- add_component!(sys, bat)
- return
-end
-
-function add_hybrid_to_chuhsi_bus!(sys::System)
- bus = get_component(Bus, sys, "Chuhsi")
- bat = _build_battery(bus, 4.0, 2.0, 0.93, 0.93)
- # Wind is taken from Bus 317: Chuhsi
- # Thermal and Load is taken from adjacent bus 318: Clark
- ren_name = "317_WIND_1"
- thermal_name = "318_CC_1"
- load_name = "Clark"
- renewable = get_component(StaticInjection, sys, ren_name)
- thermal = get_component(StaticInjection, sys, thermal_name)
- load = get_component(PowerLoad, sys, load_name)
- # Create the Hybrid
- hybrid_name = string(bus.number) * "_Hybrid"
- hybrid = PSY.HybridSystem(;
- name = hybrid_name,
- available = true,
- status = true,
- bus = bus,
- active_power = 1.0,
- reactive_power = 0.0,
- base_power = 100.0,
- operation_cost = PSY.MarketBidCost(nothing),
- thermal_unit = thermal, #new_th,
- electric_load = load, #new_load,
- storage = bat,
- renewable_unit = renewable, #new_ren,
- interconnection_impedance = 0.0 + 0.0im,
- interconnection_rating = nothing,
- input_active_power_limits = (min = 0.0, max = 10.0),
- output_active_power_limits = (min = 0.0, max = 10.0),
- reactive_power_limits = nothing,
- )
- # Add Hybrid (add_component! internally copies subcomponent time series to hybrid)
- add_component!(sys, hybrid)
- return
-end
From 6ee4d2fb8d3cafbb2e42ebe3f7d037bf2ffae7e7 Mon Sep 17 00:00:00 2001
From: kdayday
Date: Thu, 5 Mar 2026 16:59:36 -0700
Subject: [PATCH 17/46] Remove dead code from test utils
---
test/test_utils/function_utils.jl | 70 -------------------------------
1 file changed, 70 deletions(-)
diff --git a/test/test_utils/function_utils.jl b/test/test_utils/function_utils.jl
index 473435d4..e813c176 100644
--- a/test/test_utils/function_utils.jl
+++ b/test/test_utils/function_utils.jl
@@ -1,75 +1,5 @@
using TimeSeries
-function get_da_max_active_power_series(r_gen, starttime, steps::Int)
- ta = get_time_series_array(
- SingleTimeSeries,
- r_gen,
- "max_active_power";
- start_time = starttime,
- len = 24 * steps,
- )
- return DataFrame(; DateTime = timestamp(ta), MaxPower = values(ta))
-end
-
-function get_rt_max_active_power_series(r_gen, starttime, steps::Int)
- ta = get_time_series_array(
- SingleTimeSeries,
- r_gen,
- "max_active_power";
- start_time = starttime,
- len = 24 * 12 * steps,
- )
- return DataFrame(; DateTime = timestamp(ta), MaxPower = values(ta))
-end
-
-function get_battery_params(b_gen::PSY.EnergyReservoirStorage)
- battery_params_names = [
- "initial_energy",
- "SoC_min",
- "SoC_max",
- "P_ch_min",
- "P_ch_max",
- "P_ds_min",
- "P_ds_max",
- "η_in",
- "η_out",
- ]
- SoC_min, SoC_max = get_state_of_charge_limits(b_gen)
- P_ch_min, P_ch_max = get_input_active_power_limits(b_gen)
- P_ds_min, P_ds_max = get_output_active_power_limits(b_gen)
- η_in, η_out = get_efficiency(b_gen)
- battery_params_vals = [
- get_initial_energy(b_gen),
- SoC_min,
- SoC_max,
- P_ch_min,
- P_ch_max,
- P_ds_min,
- P_ds_max,
- η_in,
- η_out,
- ]
- return DataFrame(; ParamName = battery_params_names, Value = battery_params_vals)
-end
-
-function get_thermal_params(t_gen)
- P_min, P_max = get_active_power_limits(t_gen)
- # TODO Implement the proper three part cost
- three_cost = get_operation_cost(t_gen)
- first_part = three_cost.variable[1]
- second_part = three_cost.variable[2]
- slope = (second_part[1] - first_part[1]) / (second_part[2] - first_part[2]) # $/MWh
- fix_cost = three_cost.fixed # $/h
- return DataFrame(;
- ParamName = ["P_min", "P_max", "C_var", "C_fix"],
- Value = [P_min, P_max, slope, fix_cost],
- )
-end
-
-function get_row_val(df, row_name)
- return df[only(findall(==(row_name), df.ParamName)), :]["Value"]
-end
-
function modify_ren_curtailment_cost!(sys)
rdispatch = get_components(RenewableDispatch, sys)
for ren in rdispatch
From 143fe9f6a09b0de3b566738b2c83b2caa8f38320 Mon Sep 17 00:00:00 2001
From: kdayday
Date: Tue, 14 Apr 2026 14:39:45 -0600
Subject: [PATCH 18/46] Remove system_to_file for PSI compat
---
test/test_utils/additional_templates.jl | 3 ---
1 file changed, 3 deletions(-)
diff --git a/test/test_utils/additional_templates.jl b/test/test_utils/additional_templates.jl
index e0610894..c8619459 100644
--- a/test/test_utils/additional_templates.jl
+++ b/test/test_utils/additional_templates.jl
@@ -208,7 +208,6 @@ function build_simulation_case(
sys_da;
name = "UC",
optimizer = HiGHS_optimizer,
- system_to_file = false,
initialize_model = true,
optimizer_solve_log_print = true,
direct_mode_optimizer = true,
@@ -221,7 +220,6 @@ function build_simulation_case(
sys_rt;
name = "ED",
optimizer = optimizer_with_attributes(Xpress.Optimizer),
- system_to_file = false,
initialize_model = true,
optimizer_solve_log_print = false,
check_numerical_bounds = false,
@@ -278,7 +276,6 @@ function build_simulation_case_optimizer(
sys_da;
name = "UC",
optimizer = HiGHS_optimizer,
- system_to_file = false,
initialize_model = true,
optimizer_solve_log_print = false,
direct_mode_optimizer = true,
From 43925ec473ebb01726d2757545a6d82c78259f3b Mon Sep 17 00:00:00 2001
From: kdayday
Date: Thu, 16 Apr 2026 10:23:29 -0600
Subject: [PATCH 19/46] Update merchant thermal costs for new fuel and cost
formats and add PSI._update_parameter_values! override to fix merchant sims
---
src/add_constraints.jl | 7 +-
src/add_parameters.jl | 76 ++++++++++-
src/decision_models/bilevel_decision_model.jl | 5 +-
.../cooptimizer_decision_model.jl | 32 ++---
.../only_energy_decision_model.jl | 25 ++--
src/objective_function.jl | 121 +++++++++++++++++-
test/test_merchant_cooptimizer.jl | 66 +++++-----
test/test_merchant_only_energy.jl | 42 +++---
test/test_merchant_sequence.jl | 51 ++++++++
test/test_utils/function_utils.jl | 42 ++++--
test/x_test_cooptimizer_with_build.jl | 50 --------
test/x_test_optimizer_sequence.jl | 89 -------------
test/x_test_optimizer_with_build.jl | 50 --------
13 files changed, 356 insertions(+), 300 deletions(-)
create mode 100644 test/test_merchant_sequence.jl
delete mode 100644 test/x_test_cooptimizer_with_build.jl
delete mode 100644 test/x_test_optimizer_sequence.jl
delete mode 100644 test/x_test_optimizer_with_build.jl
diff --git a/src/add_constraints.jl b/src/add_constraints.jl
index 4b9f2298..77e84b75 100644
--- a/src/add_constraints.jl
+++ b/src/add_constraints.jl
@@ -2666,13 +2666,8 @@ function add_constraints!(
jm = PSI.get_jump_model(container)
for dev in devices
n = PSY.get_name(dev)
- t_gen = dev.thermal_unit
- three_cost = PSY.get_operation_cost(t_gen)
- first_part = three_cost.variable[1]
- second_part = three_cost.variable[2]
- slope = (second_part[1] - first_part[1]) / (second_part[2] - first_part[2]) # $/MWh
- C_th_var = slope * 100.0 # Multiply by 100 to transform to $/pu
for t in time_steps
+ C_th_var = get_thermal_marginal_cost_per_system_unit(container, dev, t)
# Written to match latex model
con[n, t] = JuMP.@constraint(
jm,
diff --git a/src/add_parameters.jl b/src/add_parameters.jl
index f9acd797..d0491c0f 100644
--- a/src/add_parameters.jl
+++ b/src/add_parameters.jl
@@ -255,6 +255,67 @@ function PSI.update_parameter_values!(
return
end
+"""
+During `Simulation` execution, PSI calls `_update_parameter_values!(..., ::ObjectiveFunctionParameter, ...)`
+from `update_cost_parameters.jl`, which uses `handle_variable_cost_parameter` with
+`PSY.get_operation_cost(component)`. Merchant hybrids use `MarketBidCost(nothing)` while prices
+live in `ext`; that generic path has no `MarketBidCost` method. Route simulation updates to the
+same ext-based logic as `update_parameter_values!(..., ::InMemoryDataset)`.
+"""
+function _merchant_hybrid_price_parameter_key(
+ container::PSI.OptimizationContainer,
+ parameter_array,
+ ::Type{P},
+) where {P <: Union{DayAheadEnergyPrice, RealTimeEnergyPrice}}
+ for (k, v) in PSI.get_parameters(container)
+ (k isa ISOPT.ParameterKey{P, PSY.HybridSystem}) || continue
+ if PSI.get_parameter_array(v) === parameter_array
+ return k
+ end
+ end
+ return nothing
+end
+
+function PSI._update_parameter_values!(
+ parameter_array::JuMP.Containers.DenseAxisArray{Float64, 2},
+ ::DayAheadEnergyPrice,
+ parameter_multiplier::JuMP.Containers.DenseAxisArray{Float64, 2},
+ attributes::PSI.CostFunctionAttributes,
+ ::Type{PSY.HybridSystem},
+ model::PSI.DecisionModel{T},
+ input::PSI.DatasetContainer{PSI.InMemoryDataset},
+) where {T <: HybridDecisionProblem}
+ container = PSI.get_optimization_container(model)
+ key = _merchant_hybrid_price_parameter_key(container, parameter_array, DayAheadEnergyPrice)
+ if key === nothing
+ error(
+ "Could not match DayAheadEnergyPrice parameter array to a registered HybridSystem parameter key",
+ )
+ end
+ _update_parameter_values!(model, key)
+ return
+end
+
+function PSI._update_parameter_values!(
+ parameter_array::JuMP.Containers.DenseAxisArray{Float64, 2},
+ ::RealTimeEnergyPrice,
+ parameter_multiplier::JuMP.Containers.DenseAxisArray{Float64, 2},
+ attributes::PSI.CostFunctionAttributes,
+ ::Type{PSY.HybridSystem},
+ model::PSI.DecisionModel{T},
+ input::PSI.DatasetContainer{PSI.InMemoryDataset},
+) where {T <: HybridDecisionProblem}
+ container = PSI.get_optimization_container(model)
+ key = _merchant_hybrid_price_parameter_key(container, parameter_array, RealTimeEnergyPrice)
+ if key === nothing
+ error(
+ "Could not match RealTimeEnergyPrice parameter array to a registered HybridSystem parameter key",
+ )
+ end
+ _update_parameter_values!(model, key)
+ return
+end
+
function _update_parameter_values!(
model::PSI.DecisionModel{T},
key::PSI.ParameterKey{DayAheadEnergyPrice, PSY.HybridSystem},
@@ -278,6 +339,7 @@ function _update_parameter_values!(
# Since the DA variables are hourly, this will revert the dt multiplication
PSI._set_param_value!(parameter_array, value * 1.0 * 100.0, name, t)
PSI.update_variable_cost!(
+ DayAheadEnergyPrice(),
container,
parameter_array,
parameter_multiplier,
@@ -293,6 +355,14 @@ end
# The definition of these two methods is required because of the two resolutions used
# in the model. Updating the real-time price requires using the mapping. Normally we don't
# want to expose this level of detail to users wanting to make extensions
+function _merchant_real_time_price_variable_type(meta::String)
+ meta == string(nameof(EnergyDABidOut)) && return EnergyDABidOut
+ meta == string(nameof(EnergyDABidIn)) && return EnergyDABidIn
+ meta == string(nameof(EnergyRTBidOut)) && return EnergyRTBidOut
+ meta == string(nameof(EnergyRTBidIn)) && return EnergyRTBidIn
+ error("Unknown RealTimeEnergyPrice parameter meta: $(repr(meta))")
+end
+
function _update_parameter_values!(
model::PSI.DecisionModel{T},
key::PSI.ParameterKey{RealTimeEnergyPrice, PSY.HybridSystem},
@@ -304,8 +374,8 @@ function _update_parameter_values!(
parameter_array = PSI.get_parameter_array(container, key)
attributes = PSI.get_parameter_attributes(container, key)
components = PSI.get_available_components(PSY.HybridSystem, PSI.get_system(model))
- variable =
- PSI.get_variable(container, PSI.get_variable_type(attributes)(), PSY.HybridSystem)
+ Vtype = _merchant_real_time_price_variable_type(key.meta)
+ variable = PSI.get_variable(container, Vtype(), PSY.HybridSystem)
parameter_multiplier = PSI.get_parameter_multiplier_array(container, key)
for component in components
ext = PSY.get_ext(component)
@@ -319,7 +389,7 @@ function _update_parameter_values!(
mul_ = parameter_multiplier[name, t] * 100.0
_val = value * dt * mul_
PSI._set_param_value!(parameter_array, _val, name, t)
- if PSI.get_variable_type(attributes) ∈ (EnergyDABidOut, EnergyDABidIn)
+ if Vtype ∈ (EnergyDABidOut, EnergyDABidIn)
hy_cost = -variable[name, tmap[t]] * _val
else
hy_cost = variable[name, t] * _val
diff --git a/src/decision_models/bilevel_decision_model.jl b/src/decision_models/bilevel_decision_model.jl
index 624b923e..67c7a7ff 100644
--- a/src/decision_models/bilevel_decision_model.jl
+++ b/src/decision_models/bilevel_decision_model.jl
@@ -571,10 +571,7 @@ function PSI.build_impl!(decision_model::PSI.DecisionModel{MerchantHybridBilevel
PSI.add_to_objective_variant_expression!(container, service_in_cost)
end
if !isnothing(dev.thermal_unit)
- # Workaround to add ThermalCost with a Linear Cost Since the model doesn't include PWL cost
- t_gen = dev.thermal_unit
- three_cost = PSY.get_operation_cost(t_gen)
- C_th_fix = three_cost.fixed # $/h
+ C_th_fix = get_thermal_fixed_cost_per_hour(dev)
lin_cost_on_th = Δt_DA * C_th_fix * on_th[name, t]
PSI.add_to_objective_invariant_expression!(container, lin_cost_on_th)
end
diff --git a/src/decision_models/cooptimizer_decision_model.jl b/src/decision_models/cooptimizer_decision_model.jl
index 49e6052d..c47090d6 100644
--- a/src/decision_models/cooptimizer_decision_model.jl
+++ b/src/decision_models/cooptimizer_decision_model.jl
@@ -778,10 +778,7 @@ function PSI.build_impl!(decision_model::PSI.DecisionModel{MerchantHybridCooptim
PSI.add_to_objective_variant_expression!(container, service_in_cost)
end
if !isnothing(dev.thermal_unit)
- # Workaround
- t_gen = dev.thermal_unit
- three_cost = PSY.get_operation_cost(t_gen)
- C_th_fix = three_cost.fixed # $/h
+ C_th_fix = get_thermal_fixed_cost_per_hour(dev)
lin_cost_on_th = Δt_DA * C_th_fix * on_th[name, t]
PSI.add_to_objective_invariant_expression!(container, lin_cost_on_th)
end
@@ -831,7 +828,7 @@ function PSI.build_impl!(decision_model::PSI.DecisionModel{MerchantHybridCooptim
end
end
- if len_DA == 24
+ if len_DA == 24 && !isempty(services)
res_slack_up = PSI.get_variable(container, SlackReserveUp(), PSY.HybridSystem)
res_slack_dn = PSI.get_variable(container, SlackReserveDown(), PSY.HybridSystem)
end
@@ -848,25 +845,24 @@ function PSI.build_impl!(decision_model::PSI.DecisionModel{MerchantHybridCooptim
PSI.add_to_objective_variant_expression!(container, lin_cost_dart_out)
PSI.add_to_objective_variant_expression!(container, lin_cost_dart_in)
if !isnothing(dev.thermal_unit)
- # Workaround to add ThermalCost with a Linear Cost Since the model doesn't include PWL cost
- t_gen = dev.thermal_unit
- three_cost = PSY.get_operation_cost(t_gen)
- first_part = three_cost.variable[1]
- second_part = three_cost.variable[2]
- slope = (second_part[1] - first_part[1]) / (second_part[2] - first_part[2]) # $/MWh
- fix_cost = three_cost.fixed # $/h
- C_th_var = slope * 100.0 # Multiply by 100 to transform to $/pu
+ C_th_var = get_thermal_marginal_cost_per_system_unit(container, dev, t)
lin_cost_p_th = Δt_RT * C_th_var * p_th[name, t]
PSI.add_to_objective_invariant_expression!(container, lin_cost_p_th)
end
if !isnothing(dev.storage)
- VOM = dev.storage.operation_cost.variable.cost
- lin_cost_p_ch = 100.0 * Δt_RT * VOM * p_ch[name, t]
- lin_cost_p_ds = 100.0 * Δt_RT * VOM * p_ds[name, t]
+ storage_cost = PSY.get_operation_cost(dev.storage)
+ charge_vom = PSY.get_proportional_term(
+ PSY.get_vom_cost(PSY.get_charge_variable_cost(storage_cost)),
+ )
+ discharge_vom = PSY.get_proportional_term(
+ PSY.get_vom_cost(PSY.get_discharge_variable_cost(storage_cost)),
+ )
+ lin_cost_p_ch = 100.0 * Δt_RT * charge_vom * p_ch[name, t]
+ lin_cost_p_ds = 100.0 * Δt_RT * discharge_vom * p_ds[name, t]
PSI.add_to_objective_invariant_expression!(container, lin_cost_p_ch)
PSI.add_to_objective_invariant_expression!(container, lin_cost_p_ds)
end
- if len_DA == 24
+ if len_DA == 24 && !isempty(services)
dev_services = PSY.get_services(dev)
for service in dev_services
service_name = PSY.get_name(service)
@@ -1005,7 +1001,7 @@ function PSI.build_impl!(decision_model::PSI.DecisionModel{MerchantHybridCooptim
MerchantModelWithReserves(),
)
- if PSI.get_attribute(device_model, "cycling")
+ if something(PSI.get_attribute(device_model, "cycling"), false)
PSI.add_constraints!(
container,
CyclingCharge,
diff --git a/src/decision_models/only_energy_decision_model.jl b/src/decision_models/only_energy_decision_model.jl
index 211921fa..f19be73d 100644
--- a/src/decision_models/only_energy_decision_model.jl
+++ b/src/decision_models/only_energy_decision_model.jl
@@ -240,9 +240,7 @@ function PSI.build_impl!(decision_model::PSI.DecisionModel{MerchantHybridEnergyC
PSI.add_to_objective_variant_expression!(container, lin_cost_da_out)
PSI.add_to_objective_variant_expression!(container, lin_cost_da_in)
if !isnothing(dev.thermal_unit)
- t_gen = dev.thermal_unit
- three_cost = PSY.get_operation_cost(t_gen)
- C_th_fix = three_cost.fixed # $/h
+ C_th_fix = get_thermal_fixed_cost_per_hour(dev)
lin_cost_on_th = Δt_DA * C_th_fix * on_th[name, t]
PSI.add_to_objective_invariant_expression!(container, lin_cost_on_th)
end
@@ -295,19 +293,20 @@ function PSI.build_impl!(decision_model::PSI.DecisionModel{MerchantHybridEnergyC
PSI.add_to_objective_variant_expression!(container, lin_cost_dart_out)
PSI.add_to_objective_variant_expression!(container, lin_cost_dart_in)
if !isnothing(dev.thermal_unit)
- t_gen = dev.thermal_unit
- three_cost = PSY.get_operation_cost(t_gen)
- first_part = three_cost.variable[1]
- second_part = three_cost.variable[2]
- slope = (second_part[1] - first_part[1]) / (second_part[2] - first_part[2]) # $/MWh
- C_th_var = slope * 100.0 # Multiply by 100 to transform to $/pu
+ C_th_var = get_thermal_marginal_cost_per_system_unit(container, dev, t)
lin_cost_p_th = Δt_RT * C_th_var * p_th[name, t]
PSI.add_to_objective_invariant_expression!(container, lin_cost_p_th)
end
if !isnothing(dev.storage)
- VOM = dev.storage.operation_cost.variable.cost
- lin_cost_p_ch = Δt_RT * VOM * p_ch[name, t]
- lin_cost_p_ds = Δt_RT * VOM * p_ds[name, t]
+ storage_cost = PSY.get_operation_cost(dev.storage)
+ charge_vom = PSY.get_proportional_term(
+ PSY.get_vom_cost(PSY.get_charge_variable_cost(storage_cost)),
+ )
+ discharge_vom = PSY.get_proportional_term(
+ PSY.get_vom_cost(PSY.get_discharge_variable_cost(storage_cost)),
+ )
+ lin_cost_p_ch = Δt_RT * charge_vom * p_ch[name, t]
+ lin_cost_p_ds = Δt_RT * discharge_vom * p_ds[name, t]
PSI.add_to_objective_invariant_expression!(container, lin_cost_p_ch)
PSI.add_to_objective_invariant_expression!(container, lin_cost_p_ds)
end
@@ -530,7 +529,7 @@ function PSI.build_impl!(decision_model::PSI.DecisionModel{MerchantHybridEnergyC
η_ch = storage.efficiency.in
η_ds = storage.efficiency.out
inv_η_ds = 1.0 / η_ds
- E_min, E_max = PSY.get_state_of_charge_limits(storage)
+ E_max = PSY.get_storage_level_limits(storage).max
constraint_cycling_charge[name] = JuMP.@constraint(
model,
inv_η_ds * Δt_RT * sum(p_ds[name, t] for t in T_rt) <= Cycles * E_max
diff --git a/src/objective_function.jl b/src/objective_function.jl
index 8a0aaf46..5de43fa0 100644
--- a/src/objective_function.jl
+++ b/src/objective_function.jl
@@ -206,7 +206,9 @@ function PSI.add_proportional_cost!(
} where {D <: PSY.HybridSystem}
multiplier = PSI.objective_function_multiplier(T(), W())
for d in devices
- op_cost_data = PSY.get_operation_cost(PSY.get_storage(d))
+ thermal_component = PSY.get_thermal_unit(d)
+ isnothing(thermal_component) && continue
+ op_cost_data = PSY.get_operation_cost(thermal_component)
isnothing(op_cost_data) && continue
cost_term = PSI.proportional_cost(op_cost_data, T(), d, W())
iszero(cost_term) && continue
@@ -246,6 +248,123 @@ function PSI.get_fuel_cost_value(
return PSI.get_fuel_cost_value(container, thermal_component, t, is_time_variant)
end
+_extract_first_numeric_value(value::Number) = Float64(value)
+_extract_first_numeric_value(value::AbstractArray) = Float64(first(value))
+_extract_first_numeric_value(value) =
+ hasproperty(value, :values) ? Float64(first(getproperty(value, :values))) :
+ throw(
+ ArgumentError(
+ "Unable to extract scalar fuel cost from $(typeof(value)); expected Number or array-like values.",
+ ),
+ )
+
+_time_step_datetime(container::PSI.OptimizationContainer, t::Int) =
+ PSI.get_initial_time(container) + (t - 1) * PSI.get_resolution(container)
+
+function _first_piecewise_slope(variable_cost)
+ value_curve = PSY.get_value_curve(variable_cost)
+ cost_component = PSY.get_function_data(value_curve)
+ x_coords = PSY.get_x_coords(cost_component)
+ y_coords = PSY.get_y_coords(cost_component)
+ if length(x_coords) == length(y_coords) + 1
+ # Piecewise-incremental format stores segment slopes directly in y-coordinates.
+ return first(y_coords)
+ elseif length(x_coords) == length(y_coords) && length(x_coords) >= 2
+ return (y_coords[2] - y_coords[1]) / (x_coords[2] - x_coords[1])
+ end
+ throw(
+ ArgumentError(
+ "Unsupported piecewise curve data shape for $(typeof(cost_component)): " *
+ "length(x_coords)=$(length(x_coords)), length(y_coords)=$(length(y_coords)).",
+ ),
+ )
+end
+
+function get_thermal_fixed_cost_per_hour(component::PSY.HybridSystem)
+ thermal_component = PSY.get_thermal_unit(component)
+ isnothing(thermal_component) && return 0.0
+ return PSY.get_fixed(PSY.get_operation_cost(thermal_component))
+end
+
+function get_thermal_marginal_cost_per_system_unit(
+ container::PSI.OptimizationContainer,
+ component::PSY.HybridSystem,
+ t::Int,
+)
+ thermal_component = PSY.get_thermal_unit(component)
+ isnothing(thermal_component) && return 0.0
+ op_cost = PSY.get_operation_cost(thermal_component)
+ variable_cost = PSY.get_variable(op_cost)
+ base_power = PSI.get_base_power(container)
+ device_base_power = PSY.get_base_power(thermal_component)
+
+ if variable_cost isa PSY.CostCurve{PSY.LinearCurve}
+ value_curve = PSY.get_value_curve(variable_cost)
+ cost_component = PSY.get_function_data(value_curve)
+ proportional_term = PSY.get_proportional_term(cost_component)
+ return PSI.get_proportional_cost_per_system_unit(
+ proportional_term,
+ PSY.get_power_units(variable_cost),
+ base_power,
+ device_base_power,
+ )
+ elseif variable_cost isa Union{
+ PSY.CostCurve{PSY.PiecewiseIncrementalCurve},
+ PSY.CostCurve{PSY.PiecewisePointCurve},
+ }
+ slope = _first_piecewise_slope(variable_cost)
+ return PSI.get_proportional_cost_per_system_unit(
+ slope,
+ PSY.get_power_units(variable_cost),
+ base_power,
+ device_base_power,
+ )
+ elseif variable_cost isa PSY.FuelCurve{PSY.LinearCurve}
+ value_curve = PSY.get_value_curve(variable_cost)
+ cost_component = PSY.get_function_data(value_curve)
+ proportional_term = PSY.get_proportional_term(cost_component)
+ fuel_curve_per_unit = PSI.get_proportional_cost_per_system_unit(
+ proportional_term,
+ PSY.get_power_units(variable_cost),
+ base_power,
+ device_base_power,
+ )
+ fuel_cost_data = PSY.get_fuel_cost(variable_cost)
+ fuel_cost_value = if PSI.is_time_variant(fuel_cost_data)
+ timestamp = _time_step_datetime(container, t)
+ PSY.get_fuel_cost(thermal_component; start_time = timestamp, len = 1)
+ else
+ PSY.get_fuel_cost(thermal_component)
+ end
+ return fuel_curve_per_unit * _extract_first_numeric_value(fuel_cost_value)
+ elseif variable_cost isa Union{
+ PSY.FuelCurve{PSY.PiecewiseIncrementalCurve},
+ PSY.FuelCurve{PSY.PiecewisePointCurve},
+ }
+ slope = _first_piecewise_slope(variable_cost)
+ fuel_curve_per_unit = PSI.get_proportional_cost_per_system_unit(
+ slope,
+ PSY.get_power_units(variable_cost),
+ base_power,
+ device_base_power,
+ )
+ fuel_cost_data = PSY.get_fuel_cost(variable_cost)
+ fuel_cost_value = if PSI.is_time_variant(fuel_cost_data)
+ timestamp = _time_step_datetime(container, t)
+ PSY.get_fuel_cost(thermal_component; start_time = timestamp, len = 1)
+ else
+ PSY.get_fuel_cost(thermal_component)
+ end
+ return fuel_curve_per_unit * _extract_first_numeric_value(fuel_cost_value)
+ end
+
+ throw(
+ ArgumentError(
+ "Unsupported thermal variable cost type $(typeof(variable_cost)) for merchant HybridSystem models. Use linear CostCurve or linear FuelCurve.",
+ ),
+ )
+end
+
############### Renewable costs, HybridSystem #######################
PSI.objective_function_multiplier(::RenewablePower, ::AbstractHybridFormulation) =
diff --git a/test/test_merchant_cooptimizer.jl b/test/test_merchant_cooptimizer.jl
index 74d46629..f6263a72 100644
--- a/test/test_merchant_cooptimizer.jl
+++ b/test/test_merchant_cooptimizer.jl
@@ -1,28 +1,17 @@
-@testset "Test HybridSystem Merchant Decision Model Cooptimizer" begin
- #### Create Systems ####
+function _run_cooptimizer_case(with_services::Bool)
horizon_merchant_rt = 288
horizon_merchant_da = 24
- sys_rts_merchant = PSB.build_RTS_GMLC_RT_sys(;
+ sys = PSB.build_RTS_GMLC_RT_sys(;
raw_data = PSB.RTS_DIR,
horizon = horizon_merchant_rt,
interval = Hour(24),
)
- sys_rts_da = PSB.build_RTS_GMLC_DA_sys(; raw_data = PSB.RTS_DIR, horizon = 24)
- # There is no Wind + Thermal in a Single Bus.
- # We will try to pick the Wind in 317 bus Chuhsi
- # It does not have thermal and load, so we will pick the adjacent bus 318: Clark
- for s in [sys_rts_da, sys_rts_merchant]
- bus_to_add = "Chuhsi" # "Barton"
- modify_ren_curtailment_cost!(s)
- add_hybrid_to_chuhsi_bus!(s)
- end
+ modify_ren_curtailment_cost!(sys)
+ add_hybrid_to_chuhsi_bus!(sys; horizon_rt_steps = horizon_merchant_rt)
- sys = sys_rts_merchant
sys.internal.ext = Dict{String, DataFrame}()
dic = PSY.get_ext(sys)
-
- # Add prices to ext. Only three days.
bus_name = "chuhsi"
dic["λ_da_df"] =
CSV.read(joinpath(TEST_DIR, "inputs/$(bus_name)_DA_prices.csv"), DataFrame)
@@ -38,20 +27,25 @@
dic["horizon_DA"] = horizon_merchant_da
hy_sys = first(get_components(HybridSystem, sys))
- services = get_components(VariableReserve, sys)
- for service in services
- serv_name = get_name(service)
- if contains(serv_name, "Spin_Up_R1") |
- contains(serv_name, "Spin_Up_R2") |
- contains(serv_name, "Flex")
- continue
- else
- add_service!(hy_sys, service, sys)
+ ts_rt =
+ PSY.get_time_series(IS.DeterministicSingleTimeSeries, hy_sys, "RenewableDispatch__max_active_power")
+ @test IS.get_horizon(ts_rt) >= horizon_merchant_rt * IS.get_resolution(ts_rt)
+
+ if with_services
+ services = get_components(VariableReserve, sys)
+ for service in services
+ serv_name = get_name(service)
+ if contains(serv_name, "Spin_Up_R1") ||
+ contains(serv_name, "Spin_Up_R2") ||
+ contains(serv_name, "Flex")
+ continue
+ else
+ add_service!(hy_sys, service, sys)
+ end
end
end
PSY.set_ext!(hy_sys, deepcopy(dic))
- # Set decision model for Optimizer
template = ProblemTemplate(CopperPlatePowerModel)
set_device_model!(template, DeviceModel(PSY.HybridSystem, HybridDispatchWithReserves))
decision_optimizer_DA = DecisionModel(
@@ -69,15 +63,25 @@
build!(decision_optimizer_DA; output_dir = mktempdir())
solve!(decision_optimizer_DA)
- results = ProblemResults(decision_optimizer_DA)
+ results = PSI.OptimizationProblemResults(decision_optimizer_DA)
var_results = results.variable_values
rt_bid_out = read_variable(results, "EnergyRTBidOut__HybridSystem")
da_bid_out = var_results[PSI.VariableKey{HSS.EnergyDABidOut, HybridSystem}("")]
- regup_bid_out =
- var_results[PSI.VariableKey{HSS.BidReserveVariableOut, VariableReserve{ReserveUp}}(
- "Reg_Up",
- )]
@test length(da_bid_out[!, 1]) == 24
@test length(rt_bid_out[!, 1]) == 288
- @test length(regup_bid_out[!, 1]) == 24
+ if with_services
+ regup_bid_out =
+ var_results[PSI.VariableKey{HSS.BidReserveVariableOut, VariableReserve{ReserveUp}}(
+ "Reg_Up",
+ )]
+ @test length(regup_bid_out[!, 1]) == 24
+ end
+end
+
+@testset "Test HybridSystem Merchant Decision Model Cooptimizer" begin
+ _run_cooptimizer_case(true)
+end
+
+@testset "Test HybridSystem Merchant Decision Model Cooptimizer Minimal Services" begin
+ _run_cooptimizer_case(false)
end
diff --git a/test/test_merchant_only_energy.jl b/test/test_merchant_only_energy.jl
index 7e2d15c6..647ee8ff 100644
--- a/test/test_merchant_only_energy.jl
+++ b/test/test_merchant_only_energy.jl
@@ -1,27 +1,14 @@
-@testset "Test HybridSystem Merchant Decision Model Only Energy" begin
- horizon_merchant_rt = 288
- horizon_merchant_da = 24
- sys_rts_merchant = PSB.build_RTS_GMLC_RT_sys(;
+function _run_only_energy_case(horizon_merchant_rt::Int, horizon_merchant_da::Int)
+ sys = PSB.build_RTS_GMLC_RT_sys(;
raw_data = PSB.RTS_DIR,
horizon = horizon_merchant_rt,
interval = Hour(24),
)
- sys_rts_da = PSB.build_RTS_GMLC_DA_sys(; raw_data = PSB.RTS_DIR, horizon = 24)
-
- # There is no Wind + Thermal in a Single Bus.
- # We will try to pick the Wind in 317 bus Chuhsi
- # It does not have thermal and load, so we will pick the adjacent bus 318: Clark
- for s in [sys_rts_da, sys_rts_merchant]
- bus_to_add = "Chuhsi" # "Barton"
- modify_ren_curtailment_cost!(s)
- add_hybrid_to_chuhsi_bus!(s)
- end
+ modify_ren_curtailment_cost!(sys)
+ add_hybrid_to_chuhsi_bus!(sys; horizon_rt_steps = horizon_merchant_rt)
- sys = sys_rts_merchant
sys.internal.ext = Dict{String, DataFrame}()
dic = PSY.get_ext(sys)
-
- # Add prices to ext. Only three days.
bus_name = "chuhsi"
dic["λ_da_df"] =
CSV.read(joinpath(TEST_DIR, "inputs/$(bus_name)_DA_prices.csv"), DataFrame)
@@ -32,8 +19,12 @@
hy_sys = first(get_components(HybridSystem, sys))
PSY.set_ext!(hy_sys, deepcopy(dic))
+ ts_da = PSY.get_time_series(IS.SingleTimeSeries, hy_sys, "RenewableDispatch__max_active_power_da")
+ ts_rt =
+ PSY.get_time_series(IS.DeterministicSingleTimeSeries, hy_sys, "RenewableDispatch__max_active_power")
+ @test !isnothing(ts_da)
+ @test IS.get_horizon(ts_rt) >= horizon_merchant_rt * IS.get_resolution(ts_rt)
- # Set decision model for Optimizer
template = ProblemTemplate(CopperPlatePowerModel)
set_device_model!(template, DeviceModel(PSY.HybridSystem, HybridEnergyOnlyDispatch))
decision_optimizer_DA = DecisionModel(
@@ -43,16 +34,25 @@
optimizer = HiGHS_optimizer,
calculate_conflict = true,
store_variable_names = true,
+ initial_time = DateTime("2020-10-03T00:00:00"),
name = "MerchantHybridEnergyCase_DA",
)
build!(decision_optimizer_DA; output_dir = mktempdir())
solve!(decision_optimizer_DA)
- results = ProblemResults(decision_optimizer_DA)
+ results = PSI.OptimizationProblemResults(decision_optimizer_DA)
var_results = results.variable_values
rt_bid_out = read_variable(results, "EnergyRTBidOut__HybridSystem")
da_bid_out = var_results[PSI.VariableKey{HSS.EnergyDABidOut, HybridSystem}("")]
- @test length(da_bid_out[!, 1]) == 24
- @test length(rt_bid_out[!, 1]) == 288
+ @test length(da_bid_out[!, 1]) == horizon_merchant_da
+ @test length(rt_bid_out[!, 1]) == horizon_merchant_rt
+end
+
+@testset "Test HybridSystem Merchant Decision Model Only Energy" begin
+ _run_only_energy_case(288, 24)
+end
+
+@testset "Test HybridSystem Merchant Decision Model Only Energy Extended Horizon" begin
+ _run_only_energy_case(864, 72)
end
diff --git a/test/test_merchant_sequence.jl b/test/test_merchant_sequence.jl
new file mode 100644
index 00000000..3456c1cb
--- /dev/null
+++ b/test/test_merchant_sequence.jl
@@ -0,0 +1,51 @@
+@testset "Test HybridSystem Merchant Optimizer Sequence Build" begin
+ sys_rts_da = PSB.build_RTS_GMLC_DA_sys(; raw_data = PSB.RTS_DIR, horizon = 24)
+ sys_rts_rt = PSB.build_RTS_GMLC_RT_sys(;
+ raw_data = PSB.RTS_DIR,
+ horizon = 288,
+ interval = Hour(24),
+ )
+
+ modify_ren_curtailment_cost!(sys_rts_rt)
+ add_hybrid_to_chuhsi_bus!(sys_rts_rt; horizon_rt_steps = 288)
+
+ bus_name = "chuhsi"
+ sys_rts_rt.internal.ext = Dict{String, DataFrame}()
+ dic = get_ext(sys_rts_rt)
+ dic["λ_da_df"] = CSV.read(joinpath(TEST_DIR, "inputs/$(bus_name)_DA_prices.csv"), DataFrame)
+ dic["λ_rt_df"] = CSV.read(joinpath(TEST_DIR, "inputs/$(bus_name)_RT_prices.csv"), DataFrame)
+ dic["horizon_RT"] = 288
+ dic["horizon_DA"] = 24
+
+ hy_sys = first(get_components(HybridSystem, sys_rts_rt))
+ PSY.set_ext!(hy_sys, deepcopy(dic))
+
+ template = ProblemTemplate(CopperPlatePowerModel)
+ set_device_model!(template, DeviceModel(PSY.HybridSystem, HybridEnergyOnlyDispatch))
+ decision_optimizer = DecisionModel(
+ MerchantHybridEnergyCase,
+ template,
+ sys_rts_rt;
+ optimizer = HiGHS_optimizer,
+ calculate_conflict = true,
+ store_variable_names = true,
+ initial_time = DateTime("2020-10-03T00:00:00"),
+ horizon = Hour(24),
+ resolution = Minute(5),
+ name = "MerchantHybridEnergyCase_Sequence",
+ )
+
+ sim_optimizer = build_simulation_case_optimizer(
+ get_uc_dcp_template(),
+ decision_optimizer,
+ sys_rts_da,
+ sys_rts_rt,
+ 2,
+ 0.01,
+ DateTime("2020-10-03T00:00:00"),
+ )
+
+ @test build!(sim_optimizer) == PSI.SimulationBuildStatus.BUILT
+ @test execute!(sim_optimizer; enable_progress_bar = false) ==
+ PSI.RunStatus.SUCCESSFULLY_FINALIZED
+end
diff --git a/test/test_utils/function_utils.jl b/test/test_utils/function_utils.jl
index e813c176..e27c43da 100644
--- a/test/test_utils/function_utils.jl
+++ b/test/test_utils/function_utils.jl
@@ -47,7 +47,7 @@ function add_battery_to_bus!(sys::System, bus_name::String)
return
end
-function add_hybrid_to_chuhsi_bus!(sys::System)
+function add_hybrid_to_chuhsi_bus!(sys::System; horizon_rt_steps::Union{Nothing, Int} = nothing)
bus = get_component(Bus, sys, "Chuhsi")
bat = _build_battery(bus, 4.0, 2.0, 0.93, 0.93)
# Wind is taken from Bus 317: Chuhsi
@@ -83,37 +83,51 @@ function add_hybrid_to_chuhsi_bus!(sys::System)
add_component!(sys, hybrid)
# Ensure DA-named time series exists so merchant decision models that request
# "RenewableDispatch__max_active_power_da" (DA path) find metadata on the hybrid.
- _add_hybrid_renewable_da_time_series!(sys, hybrid)
+ _add_hybrid_renewable_da_time_series!(sys, hybrid; horizon_rt_steps = horizon_rt_steps)
return
end
-function _add_hybrid_renewable_da_time_series!(sys::PSY.System, hybrid::PSY.HybridSystem)
+function _add_hybrid_renewable_da_time_series!(
+ sys::PSY.System,
+ hybrid::PSY.HybridSystem;
+ horizon_rt_steps::Union{Nothing, Int} = nothing,
+)
try
- ts = PSY.get_time_series(IS.SingleTimeSeries, hybrid, "RenewableDispatch__max_active_power")
+ ts = PSY.get_time_series(
+ IS.SingleTimeSeries,
+ hybrid,
+ "RenewableDispatch__max_active_power",
+ )
single_da = IS.SingleTimeSeries(ts, "RenewableDispatch__max_active_power_da")
PSY.add_time_series!(sys, hybrid, single_da)
catch
nothing
end
- # Use a horizon long enough to cover the
- # decision model window (e.g. 48 steps at 5-min = 4 hours); otherwise get_window
- # fails in smoke testswith "timestamp not within" when the model requests 4 hours of data.
+ # Force deterministic windows to exactly match the merchant RT horizon request
+ # when provided (instead of only "at least as long"), so simulation updates
+ # don't request out-of-window ranges.
try
ts_det = PSY.get_time_series(
IS.DeterministicSingleTimeSeries,
hybrid,
"RenewableDispatch__max_active_power",
)
- horizon = IS.get_horizon(ts_det)
- interval = IS.get_interval(ts_det)
resolution = IS.get_resolution(ts_det)
- if resolution == Dates.Minute(5) && horizon < Dates.Hour(4)
- horizon = Dates.Hour(4)
- end
- PSY.transform_single_time_series!(sys, horizon, interval; resolution = resolution)
+ interval = IS.get_interval(ts_det)
+ current_horizon = IS.get_horizon(ts_det)
+
+ target_horizon =
+ isnothing(horizon_rt_steps) ? current_horizon : (horizon_rt_steps * resolution)
+
+ PSY.transform_single_time_series!(
+ sys,
+ target_horizon,
+ interval;
+ resolution = resolution,
+ )
catch
nothing
end
return
-end
+end
\ No newline at end of file
diff --git a/test/x_test_cooptimizer_with_build.jl b/test/x_test_cooptimizer_with_build.jl
deleted file mode 100644
index 84c15822..00000000
--- a/test/x_test_cooptimizer_with_build.jl
+++ /dev/null
@@ -1,50 +0,0 @@
-@testset "Test HybridSystem CoOptimizer DecisionModel" begin
- sys = PSB.build_RTS_GMLC_RT_sys(; raw_data = PSB.RTS_DIR, horizon = 864)
-
- # Attach Data to System Ext
- bus_name = "chuhsi"
-
- sys.internal.ext = Dict{String, DataFrame}()
- dic = get_ext(sys)
- dic["b_df"] =
- CSV.read(joinpath(TEST_DIR, "inputs/$(bus_name)_battery_data.csv"), DataFrame)
- dic["th_df"] =
- CSV.read(joinpath(TEST_DIR, "inputs/$(bus_name)_thermal_data.csv"), DataFrame)
- dic["P_da"] = CSV.read(
- joinpath(TEST_DIR, "inputs/$(bus_name)_renewable_forecast_DA.csv"),
- DataFrame,
- )
- dic["P_rt"] = CSV.read(
- joinpath(TEST_DIR, "inputs/$(bus_name)_renewable_forecast_RT.csv"),
- DataFrame,
- )
- dic["λ_da_df"] =
- CSV.read(joinpath(TEST_DIR, "inputs/$(bus_name)_DA_AS_prices.csv"), DataFrame)
- dic["λ_rt_df"] =
- CSV.read(joinpath(TEST_DIR, "inputs/$(bus_name)_RT_prices.csv"), DataFrame)
- dic["Pload_da"] =
- CSV.read(joinpath(TEST_DIR, "inputs/$(bus_name)_load_forecast_DA.csv"), DataFrame)
- dic["Pload_rt"] =
- CSV.read(joinpath(TEST_DIR, "inputs/$(bus_name)_load_forecast_RT.csv"), DataFrame)
-
- ### Create Decision Problem
- m = DecisionModel(
- MerchantHybridCooptimized,
- ProblemTemplate(CopperPlatePowerModel),
- sys;
- optimizer = HiGHS_optimizer,
- store_variable_names = true,
- )
- build_out = PSI.build!(m; output_dir = mktempdir(; cleanup = true))
- @test build_out == PSI.BuildStatus.BUILT
- solve_out = PSI.solve!(m)
- @test solve_out == PSI.RunStatus.SUCCESSFUL
- res = ProblemResults(m)
- dic_res = get_variable_values(res)
-
- energy_rt_out = read_variable(res, "EnergyRTBidOut__HybridSystem")[!, 2]
- da_bid_out = dic_res[PSI.VariableKey{EnergyDABidOut, HybridSystem}("")][!, 1]
-
- @test length(energy_rt_out) == 864
- @test length(da_bid_out) == 72
-end
diff --git a/test/x_test_optimizer_sequence.jl b/test/x_test_optimizer_sequence.jl
deleted file mode 100644
index f851cc47..00000000
--- a/test/x_test_optimizer_sequence.jl
+++ /dev/null
@@ -1,89 +0,0 @@
-@testset "Test HybridSystem Optimizer DecisionModel Sequence" begin
- ###############################
- ######## Load Systems #########
- ###############################
-
- sys_rts_da = PSB.build_RTS_GMLC_DA_sys(; raw_data = PSB.RTS_DIR, horizon = 48)
- sys_rts_rt =
- PSB.build_RTS_GMLC_RT_sys(;
- raw_data = PSB.RTS_DIR,
- horizon = 864,
- interval = Minute(1440),
- )
-
- # There is no Wind + Thermal in a Single Bus.
- # We will try to pick the Wind in 317 bus Chuhsi
- # It does not have thermal and load, so we will pick the adjacent bus 318: Clark
-
- systems = [sys_rts_da, sys_rts_rt]
- for sys in systems
- bus_to_add = "Chuhsi" # "Barton"
- modify_ren_curtailment_cost!(sys)
- add_battery_to_bus!(sys, bus_to_add)
- end
-
- ###############################
- ###### Create Templates #######
- ###############################
-
- template_uc_dcp = get_uc_dcp_template()
-
- ###############################
- ###### Simulation Params ######
- ###############################
-
- mipgap = 0.01
- num_steps = 3
- starttime = DateTime("2020-10-03T00:00:00")
-
- # Attach Data to System Ext
- bus_name = "chuhsi"
-
- sys_rts_rt.internal.ext = Dict{String, DataFrame}()
- dic = get_ext(sys_rts_rt)
- dic["b_df"] =
- CSV.read(joinpath(TEST_DIR, "inputs/$(bus_name)_battery_data.csv"), DataFrame)
- dic["th_df"] =
- CSV.read(joinpath(TEST_DIR, "inputs/$(bus_name)_thermal_data.csv"), DataFrame)
- dic["P_da"] = CSV.read(
- joinpath(TEST_DIR, "inputs/$(bus_name)_renewable_forecast_DA.csv"),
- DataFrame,
- )
- dic["P_rt"] = CSV.read(
- joinpath(TEST_DIR, "inputs/$(bus_name)_renewable_forecast_RT.csv"),
- DataFrame,
- )
- dic["λ_da_df"] =
- CSV.read(joinpath(TEST_DIR, "inputs/$(bus_name)_DA_AS_prices.csv"), DataFrame)
- dic["λ_rt_df"] =
- CSV.read(joinpath(TEST_DIR, "inputs/$(bus_name)_RT_prices.csv"), DataFrame)
- dic["Pload_da"] =
- CSV.read(joinpath(TEST_DIR, "inputs/$(bus_name)_load_forecast_DA.csv"), DataFrame)
- dic["Pload_rt"] =
- CSV.read(joinpath(TEST_DIR, "inputs/$(bus_name)_load_forecast_RT.csv"), DataFrame)
-
- ### Create Decision Problem
- m = DecisionModel(
- MerchantHybridEnergyOnly,
- ProblemTemplate(CopperPlatePowerModel),
- sys_rts_rt;
- optimizer = HiGHS_optimizer,
- horizon = 864,
- )
-
- sim_optimizer = build_simulation_case_optimizer(
- template_uc_dcp,
- m,
- sys_rts_da,
- sys_rts_rt,
- num_steps,
- 0.01,
- starttime,
- )
-
- build_out = build!(sim_optimizer)
- @test build_out == PSI.BuildStatus.BUILT
-
- # Fix Issue src and dest arrays
- #@test execute!(sim_optimizer; enable_progress_bar=true) == PSI.RunStatus.SUCCESSFUL
-end
diff --git a/test/x_test_optimizer_with_build.jl b/test/x_test_optimizer_with_build.jl
deleted file mode 100644
index f0f0ded2..00000000
--- a/test/x_test_optimizer_with_build.jl
+++ /dev/null
@@ -1,50 +0,0 @@
-@testset "Test HybridSystem CoOptimizer DecisionModel" begin
- sys = PSB.build_RTS_GMLC_RT_sys(; raw_data = PSB.RTS_DIR, horizon = 864)
-
- # Attach Data to System Ext
- bus_name = "chuhsi"
-
- sys.internal.ext = Dict{String, DataFrame}()
- dic = get_ext(sys)
- dic["b_df"] =
- CSV.read(joinpath(TEST_DIR, "inputs/$(bus_name)_battery_data.csv"), DataFrame)
- dic["th_df"] =
- CSV.read(joinpath(TEST_DIR, "inputs/$(bus_name)_thermal_data.csv"), DataFrame)
- dic["P_da"] = CSV.read(
- joinpath(TEST_DIR, "inputs/$(bus_name)_renewable_forecast_DA.csv"),
- DataFrame,
- )
- dic["P_rt"] = CSV.read(
- joinpath(TEST_DIR, "inputs/$(bus_name)_renewable_forecast_RT.csv"),
- DataFrame,
- )
- dic["λ_da_df"] =
- CSV.read(joinpath(TEST_DIR, "inputs/$(bus_name)_DA_AS_prices.csv"), DataFrame)
- dic["λ_rt_df"] =
- CSV.read(joinpath(TEST_DIR, "inputs/$(bus_name)_RT_prices.csv"), DataFrame)
- dic["Pload_da"] =
- CSV.read(joinpath(TEST_DIR, "inputs/$(bus_name)_load_forecast_DA.csv"), DataFrame)
- dic["Pload_rt"] =
- CSV.read(joinpath(TEST_DIR, "inputs/$(bus_name)_load_forecast_RT.csv"), DataFrame)
-
- m = DecisionModel(
- MerchantHybridEnergyOnly,
- ProblemTemplate(CopperPlatePowerModel),
- sys;
- optimizer = HiGHS_optimizer,
- calculate_conflict = true,
- store_variable_names = true,
- )
- build_out = PSI.build!(m; output_dir = mktempdir(; cleanup = true))
- @test build_out == PSI.BuildStatus.BUILT
- solve_out = PSI.solve!(m)
- @test solve_out == PSI.RunStatus.SUCCESSFUL
- res = ProblemResults(m)
- dic_res = get_variable_values(res)
-
- energy_rt_out = dic_res[PSI.VariableKey{EnergyRTBidOut, HybridSystem}("")][!, 1]
- da_bid_out = dic_res[PSI.VariableKey{EnergyDABidOut, HybridSystem}("")][!, 1]
-
- @test length(energy_rt_out) == 864
- @test length(da_bid_out) == 72
-end
From 323b743b75e1c355dd2bb8fb3385fc53dd1b8e36 Mon Sep 17 00:00:00 2001
From: kdayday
Date: Thu, 16 Apr 2026 10:27:09 -0600
Subject: [PATCH 20/46] Add Sienna.md from IS
---
.claude/Sienna.md | 165 ++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 165 insertions(+)
create mode 100644 .claude/Sienna.md
diff --git a/.claude/Sienna.md b/.claude/Sienna.md
new file mode 100644
index 00000000..782f81ef
--- /dev/null
+++ b/.claude/Sienna.md
@@ -0,0 +1,165 @@
+# Sienna Programming Practices
+
+This document describes general programming practices and conventions that apply across all Sienna packages (PowerSystems.jl, PowerSimulations.jl, PowerFlows.jl, PowerNetworkMatrices.jl, InfrastructureSystems.jl, etc.).
+
+## Performance Requirements
+
+**Priority:** Critical. See the [Julia Performance Tips](https://docs.julialang.org/en/v1/manual/performance-tips/).
+
+### Anti-Patterns to Avoid
+
+#### Type instability
+
+Functions must return consistent concrete types. Check with `@code_warntype`.
+
+- Bad: `f(x) = x > 0 ? 1 : 1.0`
+- Good: `f(x) = x > 0 ? 1.0 : 1.0`
+
+#### Abstract field types
+
+Struct fields must have concrete types or be parameterized.
+
+- Bad: `struct Foo; data::AbstractVector; end`
+- Good: `struct Foo{T<:AbstractVector}; data::T; end`
+
+#### Untyped containers
+
+- Bad: `Vector{Any}()`, `Vector{Real}()`
+- Good: `Vector{Float64}()`, `Vector{Int}()`
+
+#### Non-const globals
+
+- Bad: `THRESHOLD = 0.5`
+- Good: `const THRESHOLD = 0.5`
+
+#### Unnecessary allocations
+
+- Use views instead of copies (`@view`, `@views`)
+- Pre-allocate arrays instead of `push!` in loops
+- Use in-place operations (functions ending with `!`)
+
+#### Captured variables
+
+Avoid closures that capture variables causing boxing. Pass variables as function arguments instead.
+
+#### Splatting penalty
+
+Avoid splatting (`...`) in performance-critical code.
+
+#### Abstract return types
+
+Avoid returning `Union` types or abstract types.
+
+### Best Practices
+
+- Use `@inbounds` when bounds are verified
+- Use broadcasting (dot syntax) for element-wise operations
+- Avoid `try-catch` in hot paths
+- Use function barriers to isolate type instability
+
+> Apply these guidelines with judgment. Not every function is performance-critical. Focus optimization efforts on hot paths and frequently called code.
+
+## Code Conventions
+
+Style guide:
+
+Formatter (JuliaFormatter): Use the formatter script provided in each package.
+
+Key rules:
+
+- Constructors: use `function Foo()` not `Foo() = ...`
+- Asserts: prefer `InfrastructureSystems.@assert_op` over `@assert`
+- Globals: `UPPER_CASE` for constants
+- Exports: all exports in main module file
+- Comments: complete sentences, describe why not how
+
+## Documentation Practices and Requirements
+
+Framework: [Diataxis](https://diataxis.fr/)
+
+Sienna guide:
+
+Sienna guide for Diataxis-style tutorials:
+Format for tutorial scripts:
+Sienna guide for Diataxis-style how-to's:
+Sienna guide for APIs:
+
+Docstring requirements:
+
+- Scope: all elements of public interface (IS is selective about exports)
+- Include: function signatures and arguments list
+- Automation: `DocStringExtensions.TYPEDSIGNATURES` (`TYPEDFIELDS` used sparingly in IS)
+- See also: add links for functions with same name (multiple dispatch)
+
+API docs:
+
+- Public: typically in `docs/src/api/public.md` using `@autodocs` with `Public=true, Private=false`
+- Internals: typically in `docs/src/api/internals.md`
+
+## Design Principles
+
+- Elegance and concision in both interface and implementation
+- Fail fast with actionable error messages rather than hiding problems
+- Validate invariants explicitly in subtle cases
+- Avoid over-adherence to backwards compatibility for internal helpers
+
+## Contribution Workflow
+
+**Note:** The default branch for all Sienna packages is `main`, not `master`.
+
+Branch naming: `feature/description` or `fix/description`
+
+1. Create feature branch
+2. Follow style guide and run formatter
+3. Ensure tests pass
+4. Submit pull request
+
+## AI Agent Guidance
+
+**Key priorities:** Read existing patterns first, maintain consistency, use concrete types in hot paths, run formatter, add docstrings to public API, ensure tests pass.
+
+**Critical rules:**
+- Always use `julia --project=` (never bare `julia`)
+- Never edit auto-generated files directly
+- Verify type stability with `@code_warntype` for performance-critical code
+- Consider downstream package impact
+
+## Julia Environment Best Practices
+
+**CRITICAL:** Always use `julia --project=` when running Julia code in Sienna repositories. **NEVER** use bare `julia` or `julia --project` without specifying the environment. Each package typically defines dependencies in `test/Project.toml` for testing.
+
+Common patterns:
+
+```sh
+# Run tests (using test environment)
+julia --project=test test/runtests.jl
+
+# Run specific test
+julia --project=test test/runtests.jl test_file_name
+
+# Run expression
+julia --project=test -e 'using PackageName; ...'
+
+# Instantiate environment
+julia --project=test -e 'using Pkg; Pkg.instantiate()'
+
+# Build docs (using docs environment)
+julia --project=docs docs/make.jl
+```
+
+**Why this matters:** Running without `--project=` will fail because required packages won't be available in the default environment. The test/docs environments contain all necessary dependencies for their respective tasks.
+
+## Troubleshooting
+
+**Type instability**
+- Symptom: Poor performance, many allocations
+- Diagnosis: `@code_warntype` on suspect function
+- Solution: See performance anti-patterns above
+
+**Formatter fails**
+- Symptom: Formatter command returns error
+- Solution: Run the formatter script provided in the package (e.g., `julia -e 'include("scripts/formatter/formatter_code.jl")'`)
+
+**Test failures**
+- Symptom: Tests fail unexpectedly
+- Solution: `julia --project=test -e 'using Pkg; Pkg.instantiate()'`
From 9561ffd9cc4e7058d7c7269add644f57e88fa9ea Mon Sep 17 00:00:00 2001
From: kdayday
Date: Thu, 16 Apr 2026 11:20:42 -0600
Subject: [PATCH 21/46] Remove docs todo
---
src/core/constraints.jl | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/core/constraints.jl b/src/core/constraints.jl
index c22bf530..3869ee5f 100644
--- a/src/core/constraints.jl
+++ b/src/core/constraints.jl
@@ -138,7 +138,7 @@ struct OptConditionBatteryDischarge <: PSI.ConstraintType end
OptConditionEnergyVariable
Constraint enforcing Karush-Kuhn-Tucker (KKT) stationarity for the energy variable at the point of common coupling (PCC) in the
-merchant model. #TODO DOCS
+merchant model.
"""
struct OptConditionEnergyVariable <: PSI.ConstraintType end
From 10fae251cacde55d6ccc9cdc6987e40770fc2eef Mon Sep 17 00:00:00 2001
From: kdayday
Date: Thu, 16 Apr 2026 16:10:00 -0600
Subject: [PATCH 22/46] Remove HPS and SSS from test dependencies and remove
dead test code
---
test/Project.toml | 2 -
test/runtests.jl | 1 -
test/test_hybrid_device.jl | 22 ---
test/test_hybrid_simulations.jl | 14 +-
test/test_utils/additional_templates.jl | 189 +-----------------------
test/test_utils/function_utils.jl | 7 -
6 files changed, 4 insertions(+), 231 deletions(-)
diff --git a/test/Project.toml b/test/Project.toml
index aacc1a54..79ff5b1a 100644
--- a/test/Project.toml
+++ b/test/Project.toml
@@ -5,7 +5,6 @@ DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0"
Dates = "ade2ca70-3891-5945-98fb-dc099432e06a"
HiGHS = "87dc4568-4c63-4d18-b0c0-bb2238e4078b"
HybridSystemsSimulations = "bed98974-b02a-5e2f-9ee0-a103f5c450dd"
-HydroPowerSimulations = "fc1677e0-6ad7-4515-bf3a-bd6bf20a0b1b"
InfrastructureSystems = "2cd47ed4-ca9b-11e9-27f2-ab636a7671f1"
JuMP = "4076af6c-e467-56ae-b986-b466b2749572"
Logging = "56ddb016-857b-54e1-b83d-db4d58db5568"
@@ -16,7 +15,6 @@ PowerSimulations = "e690365d-45e2-57bb-ac84-44ba829e73c4"
PowerSystemCaseBuilder = "f00506e0-b84f-492a-93c2-c0a9afc4364e"
PowerSystems = "bcd98974-b02a-5e2f-9ee0-a103f5c450dd"
Revise = "295af30f-e4ad-537b-8983-00126c2a3abe"
-StorageSystemsSimulations = "e2f1a126-19d0-4674-9252-42b2384f8e3c"
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
TimeSeries = "9e3dc215-6440-5c97-bce1-76c03772f85e"
diff --git a/test/runtests.jl b/test/runtests.jl
index 7c58e559..bed4f0de 100644
--- a/test/runtests.jl
+++ b/test/runtests.jl
@@ -4,7 +4,6 @@ using PowerSimulations
using PowerSystemCaseBuilder
using PowerNetworkMatrices
using HybridSystemsSimulations
-using StorageSystemsSimulations
using DataFrames
using CSV
using InfrastructureSystems
diff --git a/test/test_hybrid_device.jl b/test/test_hybrid_device.jl
index e1eb811f..8d5e685a 100644
--- a/test/test_hybrid_device.jl
+++ b/test/test_hybrid_device.jl
@@ -1,14 +1,5 @@
@testset "Test HybridSystem OnlyEnergy DeviceModel" begin
- ###############################
- ######## Load Systems #########
- ###############################
-
sys_rts_da = build_system(PSISystems, "modified_RTS_GMLC_DA_sys")
-
- # There is no Wind + Thermal in a Single Bus.
- # We will try to pick the Wind in 317 bus Chuhsi
- # It does not have thermal and load, so we will pick the adjacent bus 318: Clark
- bus_to_add = "Chuhsi" # "Barton"
modify_ren_curtailment_cost!(sys_rts_da)
add_hybrid_to_chuhsi_bus!(sys_rts_da)
@@ -35,8 +26,6 @@
@test solve_out == PSI.RunStatus.SUCCESSFULLY_FINALIZED
res = PSI.OptimizationProblemResults(m)
- dic_res = PSI.get_variable_values(res)
-
p_out = PSI.read_variable(res, "ActivePowerOutVariable__HybridSystem")[!, 2]
p_in = PSI.read_variable(res, "ActivePowerInVariable__HybridSystem")[!, 2]
@@ -45,16 +34,7 @@
end
@testset "Test HybridSystem DispatchWithReserves DeviceModel" begin
- ###############################
- ######## Load Systems #########
- ###############################
-
sys_rts_da = build_system(PSISystems, "modified_RTS_GMLC_DA_sys")
-
- # There is no Wind + Thermal in a Single Bus.
- # We will try to pick the Wind in 317 bus Chuhsi
- # It does not have thermal and load, so we will pick the adjacent bus 318: Clark
- bus_to_add = "Chuhsi" # "Barton"
modify_ren_curtailment_cost!(sys_rts_da)
add_hybrid_to_chuhsi_bus!(sys_rts_da)
hybrid = first(get_components(HybridSystem, sys_rts_da))
@@ -91,8 +71,6 @@ end
@test solve_out == PSI.RunStatus.SUCCESSFULLY_FINALIZED
res = PSI.OptimizationProblemResults(m)
- dic_res = PSI.get_variable_values(res)
-
p_out = PSI.read_variable(res, "ActivePowerOutVariable__HybridSystem")[!, 2]
p_in = PSI.read_variable(res, "ActivePowerInVariable__HybridSystem")[!, 2]
diff --git a/test/test_hybrid_simulations.jl b/test/test_hybrid_simulations.jl
index 472e3aa1..1fead581 100644
--- a/test/test_hybrid_simulations.jl
+++ b/test/test_hybrid_simulations.jl
@@ -105,27 +105,17 @@ end
@test execute_out == PSI.RunStatus.SUCCESSFULLY_FINALIZED
end
-@testset "Test HybridSystem with StorageDispatchWithReserves (energy_target)" begin
- # Test StorageDispatchWithReserves with energy_target attribute
+@testset "Test HybridSystem embedded storage (energy_target)" begin
template = get_template_standard_uc_simulation()
set_device_model!(
template,
DeviceModel(
PSY.HybridSystem,
HybridEnergyOnlyDispatch;
- attributes = Dict{String, Any}("cycling" => false),
- ),
- )
- set_device_model!(
- template,
- DeviceModel(
- PSY.EnergyReservoirStorage,
- StorageDispatchWithReserves;
attributes = Dict{String, Any}(
+ "cycling" => false,
"reservation" => true,
- "cycling_limits" => false,
"energy_target" => true,
- "complete_coverage" => false,
"regularization" => false,
),
),
diff --git a/test/test_utils/additional_templates.jl b/test/test_utils/additional_templates.jl
index c8619459..1b8ecbb6 100644
--- a/test/test_utils/additional_templates.jl
+++ b/test/test_utils/additional_templates.jl
@@ -2,18 +2,12 @@
###### Model Templates ########
###############################
-# Some models are commented for RTS model
-
function set_uc_models!(template_uc)
- #set_device_model!(template_uc, ThermalMultiStart, ThermalStandardUnitCommitment)
set_device_model!(template_uc, ThermalStandard, ThermalStandardUnitCommitment)
set_device_model!(template_uc, RenewableDispatch, RenewableFullDispatch)
set_device_model!(template_uc, RenewableNonDispatch, FixedOutput)
set_device_model!(template_uc, PowerLoad, StaticPowerLoad)
- #set_device_model!(template_uc, Transformer2W, StaticBranchUnbounded)
set_device_model!(template_uc, TapTransformer, StaticBranchUnbounded)
- # Hydros are not needed for hybrid-focused tests under PSY5/PSI0.33
- # set_device_model!(template_uc, HydroDispatch, FixedOutput)
set_device_model!(
template_uc,
DeviceModel(
@@ -22,20 +16,6 @@ function set_uc_models!(template_uc)
attributes = Dict{String, Any}("cycling" => false),
),
)
- set_device_model!(
- template_uc,
- DeviceModel(
- PSY.EnergyReservoirStorage,
- StorageDispatchWithReserves;
- attributes = Dict{String, Any}(
- "reservation" => true,
- "cycling_limits" => false,
- "energy_target" => false,
- "complete_coverage" => false,
- "regularization" => true,
- ),
- ),
- )
set_service_model!(template_uc, ServiceModel(VariableReserve{ReserveUp}, RangeReserve))
set_service_model!(
template_uc,
@@ -44,16 +24,6 @@ function set_uc_models!(template_uc)
return
end
-function update_ed_models!(template_ed)
- #set_device_model!(template_ed, ThermalMultiStart, ThermalStandardDispatch)
- set_device_model!(template_ed, ThermalStandard, ThermalBasicDispatch)
- # Hydros are not needed for hybrid-focused tests under PSY5/PSI0.33
- # set_device_model!(template_ed, HydroDispatch, FixedOutput)
- #set_device_model!(template_ed, HydroEnergyReservoir, HydroDispatchRunOfRiver)
- empty!(template_ed.services)
- return
-end
-
function get_template_basic_uc_simulation()
template = ProblemTemplate(CopperPlatePowerModel)
set_device_model!(template, ThermalStandard, ThermalBasicDispatch)
@@ -81,28 +51,6 @@ function get_thermal_dispatch_template_network(network = CopperPlatePowerModel)
return template
end
-###############################
-###### Line Templates #########
-###############################
-
-function set_ptdf_line_unbounded_template!(template_uc)
- set_device_model!(template_uc, DeviceModel(Line, StaticBranchUnbounded))
- return
-end
-
-function set_ptdf_line_template!(template_uc)
- set_device_model!(
- template_uc,
- DeviceModel(Line, StaticBranch; duals = [NetworkFlowConstraint]),
- )
- return
-end
-
-function set_dcp_line_unbounded_template!(template_uc)
- set_device_model!(template_uc, DeviceModel(Line, StaticBranchUnbounded))
- return
-end
-
function set_dcp_line_template!(template_uc)
set_device_model!(template_uc, DeviceModel(Line, StaticBranch))
return
@@ -112,64 +60,6 @@ end
###### Get Templates ##########
###############################
-### PTDF Bounded ####
-
-function get_uc_ptdf_template(sys_rts_da)
- template_uc = ProblemTemplate(
- NetworkModel(
- PTDFPowerModel;
- use_slacks = true,
- PTDF_matrix = PTDF(sys_rts_da),
- duals = [CopperPlateBalanceConstraint],
- ),
- )
- set_uc_models!(template_uc)
- set_ptdf_line_template!(template_uc)
- return template_uc
-end
-
-function get_ed_ptdf_template(sys_rts_da)
- template_ed = get_uc_ptdf_template(sys_rts_da)
- update_ed_models!(template_ed)
- return template_ed
-end
-
-#### PTDF Unbounded ####
-
-function get_uc_ptdf_unbounded_template(sys_rts_da)
- template_uc = get_uc_ptdf_template(sys_rts_da)
- set_ptdf_line_unbounded_template!(template_uc)
- return template_uc
-end
-
-function get_ed_ptdf_unbounded_template(sys_rts_rt)
- template_ed = get_ed_ptdf_template(sys_rts_rt)
- set_ptdf_line_unbounded_template!(template_ed)
- return template_ed
-end
-
-#### CopperPlate ####
-
-function get_uc_copperplate_template(sys_rts_da)
- template_uc = ProblemTemplate(
- NetworkModel(
- CopperPlatePowerModel;
- use_slacks = true,
- PTDF_matrix = PTDF(sys_rts_da),
- duals = [CopperPlateBalanceConstraint],
- ),
- )
- set_uc_models!(template_uc)
- set_ptdf_line_unbounded_template!(template_uc)
- return template_uc
-end
-
-function get_ed_copperplate_template(sys_rts_da)
- template_ed = get_uc_copperplate_template(sys_rts_da)
- update_ed_models!(template_ed)
- return template_ed
-end
-
#### DCP ####
function get_uc_dcp_template()
@@ -185,87 +75,13 @@ function get_uc_dcp_template()
return template_uc
end
-function get_ed_dcp_template()
- template_ed = get_uc_dcp_template()
- update_ed_models!(template_ed)
- return template_ed
-end
-
-# No emulation
-function build_simulation_case(
- template_uc,
- template_ed,
- sys_da::System,
- sys_rt::System,
- num_steps::Int,
- mipgap::Float64,
- start_time,
-)
- models = SimulationModels(;
- decision_models = [
- DecisionModel(
- template_uc,
- sys_da;
- name = "UC",
- optimizer = HiGHS_optimizer,
- initialize_model = true,
- optimizer_solve_log_print = true,
- direct_mode_optimizer = true,
- rebuild_model = false,
- store_variable_names = true,
- #check_numerical_bounds=false,
- ),
- DecisionModel(
- template_ed,
- sys_rt;
- name = "ED",
- optimizer = optimizer_with_attributes(Xpress.Optimizer),
- initialize_model = true,
- optimizer_solve_log_print = false,
- check_numerical_bounds = false,
- rebuild_model = false,
- calculate_conflict = true,
- store_variable_names = true,
- #export_pwl_vars = true,
- ),
- ],
- )
-
- # Set-up the sequence UC-ED
- sequence = SimulationSequence(;
- models = models,
- feedforwards = Dict(
- "ED" => [
- SemiContinuousFeedforward(;
- component_type = ThermalStandard,
- source = OnVariable,
- affected_values = [ActivePowerVariable],
- ),
- ],
- ),
- ini_cond_chronology = InterProblemChronology(),
- )
-
- sim = Simulation(;
- name = "compact_sim",
- steps = num_steps,
- models = models,
- sequence = sequence,
- initial_time = start_time,
- simulation_folder = mktempdir(; cleanup = true),
- )
-
- return sim
-end
-
-# No emulation
function build_simulation_case_optimizer(
template_uc,
decision_optimizer,
sys_da::System,
- sys_rt::System,
+ _sys_rt::System,
num_steps::Int,
- mipgap::Float64,
+ _mipgap::Float64,
start_time,
)
models = SimulationModels(;
@@ -281,7 +97,6 @@ function build_simulation_case_optimizer(
direct_mode_optimizer = true,
rebuild_model = false,
store_variable_names = true,
- #check_numerical_bounds=false,
),
],
)
diff --git a/test/test_utils/function_utils.jl b/test/test_utils/function_utils.jl
index e27c43da..1813d4e7 100644
--- a/test/test_utils/function_utils.jl
+++ b/test/test_utils/function_utils.jl
@@ -40,13 +40,6 @@ function _build_battery(
return device
end
-function add_battery_to_bus!(sys::System, bus_name::String)
- bus = get_component(Bus, sys, bus_name)
- bat = _build_battery(bus, 4.0, 2.0, 0.93, 0.93)
- add_component!(sys, bat)
- return
-end
-
function add_hybrid_to_chuhsi_bus!(sys::System; horizon_rt_steps::Union{Nothing, Int} = nothing)
bus = get_component(Bus, sys, "Chuhsi")
bat = _build_battery(bus, 4.0, 2.0, 0.93, 0.93)
From bcf63c7e576f1ca6b4e2e83e57ba65759266ac11 Mon Sep 17 00:00:00 2001
From: kdayday
Date: Fri, 17 Apr 2026 09:02:54 -0600
Subject: [PATCH 23/46] Formatter
---
.claude/Sienna.md | 106 ++++++++++++++++--------------
src/add_parameters.jl | 12 +++-
src/objective_function.jl | 7 +-
test/test_hybrid_simulations.jl | 6 +-
test/test_merchant_cooptimizer.jl | 11 +++-
test/test_merchant_only_energy.jl | 12 +++-
test/test_merchant_sequence.jl | 8 ++-
test/test_utils/function_utils.jl | 7 +-
8 files changed, 103 insertions(+), 66 deletions(-)
diff --git a/.claude/Sienna.md b/.claude/Sienna.md
index 782f81ef..8b4c637a 100644
--- a/.claude/Sienna.md
+++ b/.claude/Sienna.md
@@ -12,31 +12,31 @@ This document describes general programming practices and conventions that apply
Functions must return consistent concrete types. Check with `@code_warntype`.
-- Bad: `f(x) = x > 0 ? 1 : 1.0`
-- Good: `f(x) = x > 0 ? 1.0 : 1.0`
+ - Bad: `f(x) = x > 0 ? 1 : 1.0`
+ - Good: `f(x) = x > 0 ? 1.0 : 1.0`
#### Abstract field types
Struct fields must have concrete types or be parameterized.
-- Bad: `struct Foo; data::AbstractVector; end`
-- Good: `struct Foo{T<:AbstractVector}; data::T; end`
+ - Bad: `struct Foo; data::AbstractVector; end`
+ - Good: `struct Foo{T<:AbstractVector}; data::T; end`
#### Untyped containers
-- Bad: `Vector{Any}()`, `Vector{Real}()`
-- Good: `Vector{Float64}()`, `Vector{Int}()`
+ - Bad: `Vector{Any}()`, `Vector{Real}()`
+ - Good: `Vector{Float64}()`, `Vector{Int}()`
#### Non-const globals
-- Bad: `THRESHOLD = 0.5`
-- Good: `const THRESHOLD = 0.5`
+ - Bad: `THRESHOLD = 0.5`
+ - Good: `const THRESHOLD = 0.5`
#### Unnecessary allocations
-- Use views instead of copies (`@view`, `@views`)
-- Pre-allocate arrays instead of `push!` in loops
-- Use in-place operations (functions ending with `!`)
+ - Use views instead of copies (`@view`, `@views`)
+ - Pre-allocate arrays instead of `push!` in loops
+ - Use in-place operations (functions ending with `!`)
#### Captured variables
@@ -52,56 +52,56 @@ Avoid returning `Union` types or abstract types.
### Best Practices
-- Use `@inbounds` when bounds are verified
-- Use broadcasting (dot syntax) for element-wise operations
-- Avoid `try-catch` in hot paths
-- Use function barriers to isolate type instability
+ - Use `@inbounds` when bounds are verified
+ - Use broadcasting (dot syntax) for element-wise operations
+ - Avoid `try-catch` in hot paths
+ - Use function barriers to isolate type instability
> Apply these guidelines with judgment. Not every function is performance-critical. Focus optimization efforts on hot paths and frequently called code.
## Code Conventions
-Style guide:
+Style guide: [https://nrel-sienna.github.io/InfrastructureSystems.jl/stable/style/](https://nrel-sienna.github.io/InfrastructureSystems.jl/stable/style/)
Formatter (JuliaFormatter): Use the formatter script provided in each package.
Key rules:
-- Constructors: use `function Foo()` not `Foo() = ...`
-- Asserts: prefer `InfrastructureSystems.@assert_op` over `@assert`
-- Globals: `UPPER_CASE` for constants
-- Exports: all exports in main module file
-- Comments: complete sentences, describe why not how
+ - Constructors: use `function Foo()` not `Foo() = ...`
+ - Asserts: prefer `InfrastructureSystems.@assert_op` over `@assert`
+ - Globals: `UPPER_CASE` for constants
+ - Exports: all exports in main module file
+ - Comments: complete sentences, describe why not how
## Documentation Practices and Requirements
Framework: [Diataxis](https://diataxis.fr/)
-Sienna guide:
+Sienna guide: [https://nrel-sienna.github.io/InfrastructureSystems.jl/stable/docs_best_practices/explanation/](https://nrel-sienna.github.io/InfrastructureSystems.jl/stable/docs_best_practices/explanation/)
-Sienna guide for Diataxis-style tutorials:
-Format for tutorial scripts:
-Sienna guide for Diataxis-style how-to's:
-Sienna guide for APIs:
+Sienna guide for Diataxis-style tutorials: [https://nrel-sienna.github.io/InfrastructureSystems.jl/stable/docs_best_practices/how-to/write_a_tutorial/](https://nrel-sienna.github.io/InfrastructureSystems.jl/stable/docs_best_practices/how-to/write_a_tutorial/)
+Format for tutorial scripts: [https://fredrikekre.github.io/Literate.jl/v2/](https://fredrikekre.github.io/Literate.jl/v2/)
+Sienna guide for Diataxis-style how-to's: [https://nrel-sienna.github.io/InfrastructureSystems.jl/stable/docs_best_practices/how-to/write_a_how-to/](https://nrel-sienna.github.io/InfrastructureSystems.jl/stable/docs_best_practices/how-to/write_a_how-to/)
+Sienna guide for APIs: [https://nrel-sienna.github.io/InfrastructureSystems.jl/stable/docs_best_practices/how-to/write_docstrings_org_api/](https://nrel-sienna.github.io/InfrastructureSystems.jl/stable/docs_best_practices/how-to/write_docstrings_org_api/)
Docstring requirements:
-- Scope: all elements of public interface (IS is selective about exports)
-- Include: function signatures and arguments list
-- Automation: `DocStringExtensions.TYPEDSIGNATURES` (`TYPEDFIELDS` used sparingly in IS)
-- See also: add links for functions with same name (multiple dispatch)
+ - Scope: all elements of public interface (IS is selective about exports)
+ - Include: function signatures and arguments list
+ - Automation: `DocStringExtensions.TYPEDSIGNATURES` (`TYPEDFIELDS` used sparingly in IS)
+ - See also: add links for functions with same name (multiple dispatch)
API docs:
-- Public: typically in `docs/src/api/public.md` using `@autodocs` with `Public=true, Private=false`
-- Internals: typically in `docs/src/api/internals.md`
+ - Public: typically in `docs/src/api/public.md` using `@autodocs` with `Public=true, Private=false`
+ - Internals: typically in `docs/src/api/internals.md`
## Design Principles
-- Elegance and concision in both interface and implementation
-- Fail fast with actionable error messages rather than hiding problems
-- Validate invariants explicitly in subtle cases
-- Avoid over-adherence to backwards compatibility for internal helpers
+ - Elegance and concision in both interface and implementation
+ - Fail fast with actionable error messages rather than hiding problems
+ - Validate invariants explicitly in subtle cases
+ - Avoid over-adherence to backwards compatibility for internal helpers
## Contribution Workflow
@@ -109,20 +109,21 @@ API docs:
Branch naming: `feature/description` or `fix/description`
-1. Create feature branch
-2. Follow style guide and run formatter
-3. Ensure tests pass
-4. Submit pull request
+ 1. Create feature branch
+ 2. Follow style guide and run formatter
+ 3. Ensure tests pass
+ 4. Submit pull request
## AI Agent Guidance
**Key priorities:** Read existing patterns first, maintain consistency, use concrete types in hot paths, run formatter, add docstrings to public API, ensure tests pass.
**Critical rules:**
-- Always use `julia --project=` (never bare `julia`)
-- Never edit auto-generated files directly
-- Verify type stability with `@code_warntype` for performance-critical code
-- Consider downstream package impact
+
+ - Always use `julia --project=` (never bare `julia`)
+ - Never edit auto-generated files directly
+ - Verify type stability with `@code_warntype` for performance-critical code
+ - Consider downstream package impact
## Julia Environment Best Practices
@@ -152,14 +153,17 @@ julia --project=docs docs/make.jl
## Troubleshooting
**Type instability**
-- Symptom: Poor performance, many allocations
-- Diagnosis: `@code_warntype` on suspect function
-- Solution: See performance anti-patterns above
+
+ - Symptom: Poor performance, many allocations
+ - Diagnosis: `@code_warntype` on suspect function
+ - Solution: See performance anti-patterns above
**Formatter fails**
-- Symptom: Formatter command returns error
-- Solution: Run the formatter script provided in the package (e.g., `julia -e 'include("scripts/formatter/formatter_code.jl")'`)
+
+ - Symptom: Formatter command returns error
+ - Solution: Run the formatter script provided in the package (e.g., `julia -e 'include("scripts/formatter/formatter_code.jl")'`)
**Test failures**
-- Symptom: Tests fail unexpectedly
-- Solution: `julia --project=test -e 'using Pkg; Pkg.instantiate()'`
+
+ - Symptom: Tests fail unexpectedly
+ - Solution: `julia --project=test -e 'using Pkg; Pkg.instantiate()'`
diff --git a/src/add_parameters.jl b/src/add_parameters.jl
index d0491c0f..7d90c6bb 100644
--- a/src/add_parameters.jl
+++ b/src/add_parameters.jl
@@ -286,7 +286,11 @@ function PSI._update_parameter_values!(
input::PSI.DatasetContainer{PSI.InMemoryDataset},
) where {T <: HybridDecisionProblem}
container = PSI.get_optimization_container(model)
- key = _merchant_hybrid_price_parameter_key(container, parameter_array, DayAheadEnergyPrice)
+ key = _merchant_hybrid_price_parameter_key(
+ container,
+ parameter_array,
+ DayAheadEnergyPrice,
+ )
if key === nothing
error(
"Could not match DayAheadEnergyPrice parameter array to a registered HybridSystem parameter key",
@@ -306,7 +310,11 @@ function PSI._update_parameter_values!(
input::PSI.DatasetContainer{PSI.InMemoryDataset},
) where {T <: HybridDecisionProblem}
container = PSI.get_optimization_container(model)
- key = _merchant_hybrid_price_parameter_key(container, parameter_array, RealTimeEnergyPrice)
+ key = _merchant_hybrid_price_parameter_key(
+ container,
+ parameter_array,
+ RealTimeEnergyPrice,
+ )
if key === nothing
error(
"Could not match RealTimeEnergyPrice parameter array to a registered HybridSystem parameter key",
diff --git a/src/objective_function.jl b/src/objective_function.jl
index 5de43fa0..bbbbde02 100644
--- a/src/objective_function.jl
+++ b/src/objective_function.jl
@@ -251,12 +251,15 @@ end
_extract_first_numeric_value(value::Number) = Float64(value)
_extract_first_numeric_value(value::AbstractArray) = Float64(first(value))
_extract_first_numeric_value(value) =
- hasproperty(value, :values) ? Float64(first(getproperty(value, :values))) :
- throw(
+ if hasproperty(value, :values)
+ Float64(first(getproperty(value, :values)))
+ else
+ throw(
ArgumentError(
"Unable to extract scalar fuel cost from $(typeof(value)); expected Number or array-like values.",
),
)
+ end
_time_step_datetime(container::PSI.OptimizationContainer, t::Int) =
PSI.get_initial_time(container) + (t - 1) * PSI.get_resolution(container)
diff --git a/test/test_hybrid_simulations.jl b/test/test_hybrid_simulations.jl
index 1fead581..cfa90073 100644
--- a/test/test_hybrid_simulations.jl
+++ b/test/test_hybrid_simulations.jl
@@ -122,7 +122,9 @@ end
)
set_network_model!(template, NetworkModel(CopperPlatePowerModel; use_slacks = true))
sys = PSB.build_system(PSITestSystems, "c_sys5_hybrid_uc")
- model = DecisionModel(template, sys; optimizer = HiGHS_optimizer, initialize_model = false)
- @test build!(model; output_dir = mktempdir(; cleanup = true)) == PSI.ModelBuildStatus.BUILT
+ model =
+ DecisionModel(template, sys; optimizer = HiGHS_optimizer, initialize_model = false)
+ @test build!(model; output_dir = mktempdir(; cleanup = true)) ==
+ PSI.ModelBuildStatus.BUILT
@test solve!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED
end
diff --git a/test/test_merchant_cooptimizer.jl b/test/test_merchant_cooptimizer.jl
index f6263a72..49d8284c 100644
--- a/test/test_merchant_cooptimizer.jl
+++ b/test/test_merchant_cooptimizer.jl
@@ -28,7 +28,11 @@ function _run_cooptimizer_case(with_services::Bool)
hy_sys = first(get_components(HybridSystem, sys))
ts_rt =
- PSY.get_time_series(IS.DeterministicSingleTimeSeries, hy_sys, "RenewableDispatch__max_active_power")
+ PSY.get_time_series(
+ IS.DeterministicSingleTimeSeries,
+ hy_sys,
+ "RenewableDispatch__max_active_power",
+ )
@test IS.get_horizon(ts_rt) >= horizon_merchant_rt * IS.get_resolution(ts_rt)
if with_services
@@ -71,7 +75,10 @@ function _run_cooptimizer_case(with_services::Bool)
@test length(rt_bid_out[!, 1]) == 288
if with_services
regup_bid_out =
- var_results[PSI.VariableKey{HSS.BidReserveVariableOut, VariableReserve{ReserveUp}}(
+ var_results[PSI.VariableKey{
+ HSS.BidReserveVariableOut,
+ VariableReserve{ReserveUp},
+ }(
"Reg_Up",
)]
@test length(regup_bid_out[!, 1]) == 24
diff --git a/test/test_merchant_only_energy.jl b/test/test_merchant_only_energy.jl
index 647ee8ff..16342932 100644
--- a/test/test_merchant_only_energy.jl
+++ b/test/test_merchant_only_energy.jl
@@ -19,9 +19,17 @@ function _run_only_energy_case(horizon_merchant_rt::Int, horizon_merchant_da::In
hy_sys = first(get_components(HybridSystem, sys))
PSY.set_ext!(hy_sys, deepcopy(dic))
- ts_da = PSY.get_time_series(IS.SingleTimeSeries, hy_sys, "RenewableDispatch__max_active_power_da")
+ ts_da = PSY.get_time_series(
+ IS.SingleTimeSeries,
+ hy_sys,
+ "RenewableDispatch__max_active_power_da",
+ )
ts_rt =
- PSY.get_time_series(IS.DeterministicSingleTimeSeries, hy_sys, "RenewableDispatch__max_active_power")
+ PSY.get_time_series(
+ IS.DeterministicSingleTimeSeries,
+ hy_sys,
+ "RenewableDispatch__max_active_power",
+ )
@test !isnothing(ts_da)
@test IS.get_horizon(ts_rt) >= horizon_merchant_rt * IS.get_resolution(ts_rt)
diff --git a/test/test_merchant_sequence.jl b/test/test_merchant_sequence.jl
index 3456c1cb..2b4526d4 100644
--- a/test/test_merchant_sequence.jl
+++ b/test/test_merchant_sequence.jl
@@ -12,8 +12,10 @@
bus_name = "chuhsi"
sys_rts_rt.internal.ext = Dict{String, DataFrame}()
dic = get_ext(sys_rts_rt)
- dic["λ_da_df"] = CSV.read(joinpath(TEST_DIR, "inputs/$(bus_name)_DA_prices.csv"), DataFrame)
- dic["λ_rt_df"] = CSV.read(joinpath(TEST_DIR, "inputs/$(bus_name)_RT_prices.csv"), DataFrame)
+ dic["λ_da_df"] =
+ CSV.read(joinpath(TEST_DIR, "inputs/$(bus_name)_DA_prices.csv"), DataFrame)
+ dic["λ_rt_df"] =
+ CSV.read(joinpath(TEST_DIR, "inputs/$(bus_name)_RT_prices.csv"), DataFrame)
dic["horizon_RT"] = 288
dic["horizon_DA"] = 24
@@ -47,5 +49,5 @@
@test build!(sim_optimizer) == PSI.SimulationBuildStatus.BUILT
@test execute!(sim_optimizer; enable_progress_bar = false) ==
- PSI.RunStatus.SUCCESSFULLY_FINALIZED
+ PSI.RunStatus.SUCCESSFULLY_FINALIZED
end
diff --git a/test/test_utils/function_utils.jl b/test/test_utils/function_utils.jl
index 1813d4e7..7508ac67 100644
--- a/test/test_utils/function_utils.jl
+++ b/test/test_utils/function_utils.jl
@@ -40,7 +40,10 @@ function _build_battery(
return device
end
-function add_hybrid_to_chuhsi_bus!(sys::System; horizon_rt_steps::Union{Nothing, Int} = nothing)
+function add_hybrid_to_chuhsi_bus!(
+ sys::System;
+ horizon_rt_steps::Union{Nothing, Int} = nothing,
+)
bus = get_component(Bus, sys, "Chuhsi")
bat = _build_battery(bus, 4.0, 2.0, 0.93, 0.93)
# Wind is taken from Bus 317: Chuhsi
@@ -123,4 +126,4 @@ function _add_hybrid_renewable_da_time_series!(
nothing
end
return
-end
\ No newline at end of file
+end
From 82235836897a2e29966bc478803a365d9826518a Mon Sep 17 00:00:00 2001
From: kdayday
Date: Fri, 17 Apr 2026 09:03:05 -0600
Subject: [PATCH 24/46] Add pre-commit yaml
---
.pre-commit-config.yaml | 9 +++++++++
1 file changed, 9 insertions(+)
create mode 100644 .pre-commit-config.yaml
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 00000000..2cdcf383
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,9 @@
+repos:
+ - repo: local
+ hooks:
+ - id: julia-formatter
+ name: Run Julia formatter
+ entry: julia scripts/formatter/formatter_code.jl
+ language: system
+ types: [file]
+ pass_filenames: false
From fee3a7959be507f02a8597c1ba52cb39a53c702b Mon Sep 17 00:00:00 2001
From: kdayday
Date: Fri, 17 Apr 2026 10:40:31 -0600
Subject: [PATCH 25/46] Docstring parameter clarifications and clean-up
---
docs/make.jl | 2 +-
src/core/decision_models.jl | 124 ++++++++++++++++++++++++++++--------
src/core/formulations.jl | 12 ++--
src/core/parameters.jl | 19 +++---
src/objective_function.jl | 8 +--
5 files changed, 120 insertions(+), 45 deletions(-)
diff --git a/docs/make.jl b/docs/make.jl
index dbe5d4dd..957c1ed4 100644
--- a/docs/make.jl
+++ b/docs/make.jl
@@ -18,7 +18,7 @@ make_tutorials()
pages = OrderedDict(
"Welcome Page" => "index.md",
- "Tutorials" => Any[],
+ # "Tutorials" => Any[],
"Reference" => Any[
"Public API" => "api/public.md",
"Internals" => "api/internal.md",
diff --git a/src/core/decision_models.jl b/src/core/decision_models.jl
index f9ffe9ad..dee92d6d 100644
--- a/src/core/decision_models.jl
+++ b/src/core/decision_models.jl
@@ -12,20 +12,33 @@ maximizes profit from energy (e.g. DA/RT spread) subject to internal asset limit
- **System:** A [`PowerSystems.System`](@extref PowerSystems.System) containing at least one
[`PowerSystems.HybridSystem`](@extref PowerSystems.HybridSystem) with the subcomponents
required by the chosen device formulation (e.g. [`HybridEnergyOnlyDispatch`](@ref)).
- - **Time series:** For each hybrid, forecasts with default names
- `"RenewableDispatch__max_active_power"` (or `"RenewableDispatch__max_active_power_da"` for
- day-ahead-only builds) for renewable capacity and `"PowerLoad__max_active_power"` for load.
- - **System ext data:** Use the
+ - **Time series:** Default names:
+
+ | Parameter | Default Time Series Name |
+ | :--- | :--- |
+ | `RenewablePowerTimeSeries` | `"RenewableDispatch__max_active_power"` |
+ | `RenewablePowerTimeSeries` (day-ahead-only merchant builds) | `"RenewableDispatch__max_active_power_da"` |
+ | `ElectricLoadTimeSeries` | `"PowerLoad__max_active_power"` |
+ - **System ext data:** Keys in the
[`ext` supplemental data dictionary](@extref additional_fields) on
- [`PowerSystems.System`](@extref PowerSystems.System) with keys
- `\"λ_da_df\"` and `\"λ_rt_df\"`, each a `DataFrame` with column `"DateTime"` and one column
- per bus name (matching `PowerSystems.get_name(PowerSystems.get_bus(hybrid))`). Optional
- integer keys `\"horizon_DA\"` and `\"horizon_RT\"` override the number of DA/RT steps
- (defaults: the length of the corresponding `"DateTime"` column).
+ [`PowerSystems.System`](@extref PowerSystems.System):
+
+ | Key | Required | Description |
+ | :--- | :--- | :--- |
+ | `"λ_da_df"` | Yes | System-level DA table used primarily for its `"DateTime"` axis when deriving horizon windows; bus-price columns are not used for objective pricing. |
+ | `"λ_rt_df"` | Yes | System-level RT table used primarily for its `"DateTime"` axis when deriving horizon windows; bus-price columns are not used for objective pricing. |
+ | `"horizon_DA"` | Optional | DA index length used during model build; defaults to `length(ext["λ_da_df"][!, "DateTime"])` when omitted. |
+ | `"horizon_RT"` | Optional | RT index length used during model build; defaults to `length(ext["λ_rt_df"][!, "DateTime"])` when omitted. |
+
- **Hybrid ext data:** Each [`PowerSystems.HybridSystem`](@extref PowerSystems.HybridSystem)
- should have its own [`ext` dictionary](@extref additional_fields) containing the same price
- tables and horizon keys, typically copied from the system-level `ext` before constructing a
- `PowerSimulations.DecisionModel`.
+ has its own [`ext` dictionary](@extref additional_fields) with the same keys:
+
+ | Key | Required | Description |
+ | :--- | :--- | :--- |
+ | `"λ_da_df"` | Yes | Hybrid-level DA price table used for bus-level objective prices and rolling parameter updates. |
+ | `"λ_rt_df"` | Yes | Hybrid-level RT price table used for bus-level objective prices and rolling parameter updates. |
+ | `"horizon_DA"` | Yes (current implementation) | DA parameter time-step dimension used in parameter construction and updates; also referenced in reserve-assignment constraint logic (e.g., `horizon_DA == 24`). |
+ | `"horizon_RT"` | Yes (current implementation) | RT parameter time-step dimension used in parameter construction and updates. |
"""
struct MerchantHybridEnergyCase <: HybridDecisionProblem end
@@ -40,9 +53,23 @@ when solving the real-time subproblem with locked DA bids/offers.
- Same [`PowerSystems.System`](@extref PowerSystems.System),
[`PowerSystems.HybridSystem`](@extref PowerSystems.HybridSystem), and time-series
requirements as [`MerchantHybridEnergyCase`](@ref).
- - Same use of the [`ext` supplemental data dictionary](@extref additional_fields) on the
- system and hybrids: keys `\"λ_da_df\"`, `\"λ_rt_df\"`, and optional `\"horizon_DA\"`,
- `\"horizon_RT\"` as described for [`MerchantHybridEnergyCase`](@ref).
+ - **System ext data:** Same key requirements as [`MerchantHybridEnergyCase`](@ref):
+
+ | Key | Required | Description |
+ | :--- | :--- | :--- |
+ | `"λ_da_df"` | Yes | System-level DA table used primarily for its `"DateTime"` axis when deriving horizon windows. |
+ | `"λ_rt_df"` | Yes | System-level RT table used primarily for its `"DateTime"` axis when deriving horizon windows. |
+ | `"horizon_DA"` | Optional | DA index length used during model build; defaults to table length when omitted. |
+ | `"horizon_RT"` | Optional | RT index length used during model build; defaults to table length when omitted. |
+
+ - **Hybrid ext data:** Same key requirements as [`MerchantHybridEnergyCase`](@ref):
+
+ | Key | Required | Description |
+ | :--- | :--- | :--- |
+ | `"λ_da_df"` | Yes | Hybrid-level DA price table used for bus-level objective prices and rolling parameter updates. |
+ | `"λ_rt_df"` | Yes | Hybrid-level RT price table used for bus-level objective prices and rolling parameter updates. |
+ | `"horizon_DA"` | Yes (current implementation) | DA parameter time-step dimension used in parameter construction and updates; also referenced in reserve-assignment constraint logic (e.g., `horizon_DA == 24`). |
+ | `"horizon_RT"` | Yes (current implementation) | RT parameter time-step dimension used in parameter construction and updates. |
"""
struct MerchantHybridEnergyFixedDA <: HybridDecisionProblem end
@@ -56,16 +83,36 @@ allocation in RT.
**Data requirements:**
- - **System and time series:** As for [`MerchantHybridEnergyCase`](@ref). The problem template
- must include a
+ - **System:** As for [`MerchantHybridEnergyCase`](@ref). The problem template must include a
[`PowerSimulations.DeviceModel`](@extref PowerSimulations.DeviceModel) constructed as
`DeviceModel(PSY.HybridSystem, HybridDispatchWithReserves)` (or another appropriate hybrid
formulation with reserves).
- - **ext data:** Same use of the [`ext` supplemental data dictionary](@extref additional_fields)
- on the [`PowerSystems.System`](@extref PowerSystems.System) and each
- [`PowerSystems.HybridSystem`](@extref PowerSystems.HybridSystem) as in
- [`MerchantHybridEnergyCase`](@ref), plus per-service price tables for ancillary services
- (see [`AncillaryServicePrice`](@ref)).
+ - **Time series:** Default names:
+
+ | Parameter | Default Time Series Name |
+ | :--- | :--- |
+ | `RenewablePowerTimeSeries` | `"RenewableDispatch__max_active_power"` |
+ | `RenewablePowerTimeSeries` (day-ahead-only merchant builds) | `"RenewableDispatch__max_active_power_da"` |
+ | `ElectricLoadTimeSeries` | `"PowerLoad__max_active_power"` |
+ - **System ext data:** Same key requirements as [`MerchantHybridEnergyCase`](@ref):
+
+ | Key | Required | Description |
+ | :--- | :--- | :--- |
+ | `"λ_da_df"` | Yes | System-level DA table used primarily for its `"DateTime"` axis when deriving horizon windows. |
+ | `"λ_rt_df"` | Yes | System-level RT table used primarily for its `"DateTime"` axis when deriving horizon windows. |
+ | `"horizon_DA"` | Optional | DA index length used during model build; defaults to table length when omitted. |
+ | `"horizon_RT"` | Optional | RT index length used during model build; defaults to table length when omitted. |
+
+ - **Hybrid ext data:** Keys in each hybrid's
+ [`ext` dictionary](@extref additional_fields):
+
+ | Key | Required | Description |
+ | :--- | :--- | :--- |
+ | `"λ_da_df"` | Yes | Hybrid-level DA energy price table used for bus-level objective prices and rolling parameter updates. |
+ | `"λ_rt_df"` | Yes | Hybrid-level RT energy price table used for bus-level objective prices and rolling parameter updates. |
+ | `"horizon_DA"` | Yes (current implementation) | DA parameter time-step dimension used in parameter construction and updates; also referenced in reserve-assignment constraint logic (e.g., `horizon_DA == 24`). |
+ | `"horizon_RT"` | Yes (current implementation) | RT parameter time-step dimension used in parameter construction and updates. |
+ | `"λ_"` | Yes (per attached service) | Ancillary-service DA price table for each attached service (e.g., `"λ_Regulation_Up"`), used in objective pricing with `"DateTime"` and bus columns. |
"""
struct MerchantHybridCooptimizerCase <: HybridDecisionProblem end
@@ -78,12 +125,33 @@ equilibrium or regulatory analysis.
**Data requirements:**
- - **System and time series:** Same as [`MerchantHybridEnergyCase`](@ref) (at least one
- [`PowerSystems.HybridSystem`](@extref PowerSystems.HybridSystem) with required forecasts and
- time-series names).
- - **ext data:** Same use of the [`ext` supplemental data dictionary](@extref additional_fields)
- and keys `\"λ_da_df\"`, `\"λ_rt_df\"`, optional `\"horizon_DA\"`, `\"horizon_RT\"` on the
- system and hybrids as in [`MerchantHybridEnergyCase`](@ref).
+ - **System:** Same as [`MerchantHybridEnergyCase`](@ref) (at least one
+ [`PowerSystems.HybridSystem`](@extref PowerSystems.HybridSystem) with required forecasts).
+ - **Time series:** Default names:
+
+ | Parameter | Default Time Series Name |
+ | :--- | :--- |
+ | `RenewablePowerTimeSeries` | `"RenewableDispatch__max_active_power"` |
+ | `RenewablePowerTimeSeries` (day-ahead-only merchant builds) | `"RenewableDispatch__max_active_power_da"` |
+ | `ElectricLoadTimeSeries` | `"PowerLoad__max_active_power"` |
+ - **System ext data:** Same key requirements as [`MerchantHybridEnergyCase`](@ref):
+
+ | Key | Required | Description |
+ | :--- | :--- | :--- |
+ | `"λ_da_df"` | Yes | System-level DA table used primarily for its `"DateTime"` axis when deriving horizon windows. |
+ | `"λ_rt_df"` | Yes | System-level RT table used primarily for its `"DateTime"` axis when deriving horizon windows. |
+ | `"horizon_DA"` | Optional | DA index length used during model build; defaults to table length when omitted. |
+ | `"horizon_RT"` | Optional | RT index length used during model build; defaults to table length when omitted. |
+
+ - **Hybrid ext data:** Keys in each hybrid's
+ [`ext` dictionary](@extref additional_fields):
+
+ | Key | Required | Description |
+ | :--- | :--- | :--- |
+ | `"λ_da_df"` | Yes | Hybrid-level DA energy price table used for bus-level objective prices and rolling parameter updates. |
+ | `"λ_rt_df"` | Yes | Hybrid-level RT energy price table used for bus-level objective prices and rolling parameter updates. |
+ | `"horizon_DA"` | Yes (current implementation) | DA parameter time-step dimension used in parameter construction and updates; also referenced in reserve-assignment constraint logic (e.g., `horizon_DA == 24`). |
+ | `"horizon_RT"` | Yes (current implementation) | RT parameter time-step dimension used in parameter construction and updates. |
"""
struct MerchantHybridBilevelCase <: HybridDecisionProblem end
diff --git a/src/core/formulations.jl b/src/core/formulations.jl
index c54828e6..fd3e9a98 100644
--- a/src/core/formulations.jl
+++ b/src/core/formulations.jl
@@ -80,8 +80,10 @@ or economic dispatch.
**Time Series Parameters:**
- - `RenewablePowerTimeSeries`: ``P^{*,\\text{re}}_t`` = renewable forecast at time ``t`` (default time series name: `"RenewableDispatch__max_active_power"`)
- - `ElectricLoadTimeSeries`: ``P^{\\text{ld}}_t`` = load consumption at time ``t`` (default time series name: `"PowerLoad__max_active_power"`)
+| Parameter | Default Time Series Name |
+| :--- | :--- |
+| `RenewablePowerTimeSeries` | `"RenewableDispatch__max_active_power"` |
+| `ElectricLoadTimeSeries` | `"PowerLoad__max_active_power"` |
**Data requirements:**
@@ -249,8 +251,10 @@ and asset limits.
**Time Series Parameters:**
- - `RenewablePowerTimeSeries`: ``P^{*,\\text{re}}_t`` = renewable forecast at time ``t`` (default time series name: `"RenewableDispatch__max_active_power"`)
- - `ElectricLoadTimeSeries`: ``P^{\\text{ld}}_t`` = load consumption at time ``t`` (default time series name: `"PowerLoad__max_active_power"`)
+| Parameter | Default Time Series Name |
+| :--- | :--- |
+| `RenewablePowerTimeSeries` | `"RenewableDispatch__max_active_power"` |
+| `ElectricLoadTimeSeries` | `"PowerLoad__max_active_power"` |
**Data requirements:**
diff --git a/src/core/parameters.jl b/src/core/parameters.jl
index 8e361f10..3bb54b56 100644
--- a/src/core/parameters.jl
+++ b/src/core/parameters.jl
@@ -17,11 +17,13 @@ Docs abbreviation: ``\\Pi^*_{\\text{DA},t}`` (USD/MWh). Used in the merchant obj
- **System ext:** The [`ext` supplemental data dictionary](@extref additional_fields) on
[`PowerSystems.System`](@extref PowerSystems.System) must contain `\"λ_da_df\"`, a
- `DataFrame` with column `"DateTime"` and one column per bus name, and optionally
- `\"horizon_DA\"::Int` giving the number of day-ahead steps.
+ `DataFrame` with column `"DateTime"` and one column per bus name. `\"horizon_DA\"::Int`
+ is optional and, when absent, defaults to the `"DateTime"` length.
- **Hybrid ext:** Each [`PowerSystems.HybridSystem`](@extref PowerSystems.HybridSystem)
- reads the same keys from its own [`ext` dictionary](@extref additional_fields); values are
- sliced starting at the current forecast time and used over the model horizon.
+ reads the same keys from its own [`ext` dictionary](@extref additional_fields). In current
+ implementation, `\"horizon_DA\"` is expected in hybrid `ext` for parameter construction and
+ updates; values are sliced from `\"λ_da_df\"` starting at the current forecast time and used
+ over the model horizon.
"""
struct DayAheadEnergyPrice <: PSI.ObjectiveFunctionParameter end
@@ -37,12 +39,13 @@ expression for RT energy and DART spread.
- **System ext:** The [`ext` supplemental data dictionary](@extref additional_fields) on
[`PowerSystems.System`](@extref PowerSystems.System) must contain `\"λ_rt_df\"`, a
- `DataFrame` with column `"DateTime"` and one column per bus name, and optionally
- `\"horizon_RT\"::Int` giving the number of real-time steps.
+ `DataFrame` with column `"DateTime"` and one column per bus name. `\"horizon_RT\"::Int`
+ is optional and, when absent, defaults to the `"DateTime"` length.
- **Hybrid ext:** Each [`PowerSystems.HybridSystem`](@extref PowerSystems.HybridSystem)
reads `\"λ_rt_df\"`, `\"horizon_RT\"`, and a mapping `\"tmap\"` from its own
- [`ext` dictionary](@extref additional_fields), used to align real-time steps to day-ahead
- steps where needed.
+ [`ext` dictionary](@extref additional_fields). In current implementation, `\"horizon_RT\"`
+ is expected in hybrid `ext` for parameter construction and updates; `\"tmap\"` aligns
+ real-time steps to day-ahead steps where needed.
"""
struct RealTimeEnergyPrice <: PSI.ObjectiveFunctionParameter end
diff --git a/src/objective_function.jl b/src/objective_function.jl
index bbbbde02..2e4fe668 100644
--- a/src/objective_function.jl
+++ b/src/objective_function.jl
@@ -255,10 +255,10 @@ _extract_first_numeric_value(value) =
Float64(first(getproperty(value, :values)))
else
throw(
- ArgumentError(
- "Unable to extract scalar fuel cost from $(typeof(value)); expected Number or array-like values.",
- ),
- )
+ ArgumentError(
+ "Unable to extract scalar fuel cost from $(typeof(value)); expected Number or array-like values.",
+ ),
+ )
end
_time_step_datetime(container::PSI.OptimizationContainer, t::Int) =
From 35138d7ba367ed7cc97ff4a70cf9d11cb88ba7d8 Mon Sep 17 00:00:00 2001
From: kdayday
Date: Fri, 17 Apr 2026 17:41:42 -0600
Subject: [PATCH 26/46] Apply suggestions from code review
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: kdayday
---
CONTRIBUTING.md | 2 +-
docs/Project.toml | 2 +-
docs/src/index.md | 2 +-
test/test_utils/function_utils.jl | 1 -
4 files changed, 3 insertions(+), 4 deletions(-)
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 65a8ddce..a945a596 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,6 +1,6 @@
# Contributing
-Community driven development of this package is encouraged. To maintain code quality standards, please adhere to the following guidlines when contributing:
+Community driven development of this package is encouraged. To maintain code quality standards, please adhere to the following guidelines when contributing:
- To get started, sign the Contributor License Agreement.
- Please do your best to adhere to our [coding style guide](docs/src/developer/style.md).
diff --git a/docs/Project.toml b/docs/Project.toml
index 670345e3..c1b1faf6 100644
--- a/docs/Project.toml
+++ b/docs/Project.toml
@@ -10,4 +10,4 @@ PrettyTables = "08abe8d2-0d0c-5749-adfa-8a2ac140af0d"
[compat]
Documenter = "1.0"
-julia = "^1.6"
+julia = "^1.10"
diff --git a/docs/src/index.md b/docs/src/index.md
index 805ee51b..1b8012a2 100644
--- a/docs/src/index.md
+++ b/docs/src/index.md
@@ -21,7 +21,7 @@ feedback, suggestions, and bug reports.
`HybridSystemsSimulations.jl` is part of the National Laboratory of the Rockies's (NLR, formerly NREL)
[Sienna ecosystem](https://nrel-sienna.github.io/Sienna/), an open source framework for
power system modeling, simulation, and optimization. The Sienna ecosystem can be
-[found on Github](https://github.com/NREL-Sienna/Sienna). It contains three applications:
+[found on GitHub](https://github.com/NREL-Sienna/Sienna). It contains three applications:
- [Sienna\Data](https://nrel-sienna.github.io/Sienna/pages/applications/sienna_data.html) enables
efficient data input, analysis, and transformation
diff --git a/test/test_utils/function_utils.jl b/test/test_utils/function_utils.jl
index 7508ac67..1f77eb50 100644
--- a/test/test_utils/function_utils.jl
+++ b/test/test_utils/function_utils.jl
@@ -3,7 +3,6 @@ using TimeSeries
function modify_ren_curtailment_cost!(sys)
rdispatch = get_components(RenewableDispatch, sys)
for ren in rdispatch
- # We consider 15 $/MWh as a reasonable cost for renewable curtailment
cost = PSY.RenewableGenerationCost(nothing)
set_operation_cost!(ren, cost)
end
From 94c15701a38648265d2399c4aa7854c71536662d Mon Sep 17 00:00:00 2001
From: kdayday
Date: Thu, 30 Apr 2026 13:36:32 -0600
Subject: [PATCH 27/46] Apply literate changes from PF PR
---
.github/workflows/docs.yml | 10 ++
docs/make.jl | 2 -
docs/make_tutorials.jl | 208 ++++++++++++++++++++++++++++---------
3 files changed, 169 insertions(+), 51 deletions(-)
diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
index e53ea5da..2f1dc782 100644
--- a/.github/workflows/docs.yml
+++ b/.github/workflows/docs.yml
@@ -18,6 +18,16 @@ jobs:
version: '1'
- name: Install dependencies
run: julia --project=docs/ -e 'using Pkg; Pkg.develop(PackageSpec(path=pwd())); Pkg.instantiate()'
+
+ - name: Set DOCUMENTER_CURRENT_VERSION for tutorial download links
+ run: |
+ if [[ "${{ github.event_name }}" == "pull_request" ]]; then
+ echo "DOCUMENTER_CURRENT_VERSION=previews/PR${{ github.event.pull_request.number }}" >> "$GITHUB_ENV"
+ elif [[ "${{ github.ref }}" == refs/tags/* ]]; then
+ echo "DOCUMENTER_CURRENT_VERSION=${GITHUB_REF_NAME}" >> "$GITHUB_ENV"
+ elif [[ "${{ github.ref }}" == "refs/heads/main" ]] || [[ "${{ github.ref }}" =~ ^refs/heads/release- ]]; then
+ echo "DOCUMENTER_CURRENT_VERSION=dev" >> "$GITHUB_ENV"
+ fi
- name: Build and deploy
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/docs/make.jl b/docs/make.jl
index 957c1ed4..6bef9065 100644
--- a/docs/make.jl
+++ b/docs/make.jl
@@ -4,8 +4,6 @@ using DataStructures
using DocumenterInterLinks
using Literate
-const _DOCS_BASE_URL = "https://nrel-sienna.github.io/HybridSystemsSimulations.jl/stable"
-
links = InterLinks(
"Julia" => "https://docs.julialang.org/en/v1/",
"InfrastructureSystems" => "https://nrel-sienna.github.io/InfrastructureSystems.jl/stable/",
diff --git a/docs/make_tutorials.jl b/docs/make_tutorials.jl
index 54a86f61..3659a4e6 100644
--- a/docs/make_tutorials.jl
+++ b/docs/make_tutorials.jl
@@ -3,39 +3,50 @@ using Literate
using DataFrames
using PrettyTables
-# Override show for DataFrames to limit output size during doc builds
-# This ensures large DataFrames are truncated when displayed as expression results in @example blocks
-# Explicit show() calls in tutorials with their own arguments are NOT affected (they use their own kwargs)
-# We override both text/plain and text/html since Documenter may use either
-#
-# Strategy: Call PrettyTables.pretty_table directly with explicit row/column limits.
-# This bypasses DataFrames' default display logic and gives us full control.
+# Limit DataFrame rendering during docs generation to avoid huge literal outputs.
+# Notes:
+# - Environment-variable approaches tested (`DATAFRAMES_ROWS`, `DATAFRAMES_COLUMNS`,
+# `LINES`, `COLUMNS`) did not constrain DataFrames output in this pipeline.
+# - We keep a docs-local Base.show override as a fallback and accept `kwargs...`
+# so explicit show(...; kwargs) calls do not error on unsupported keywords.
+function _env_int(name::String, default::Int)
+ parsed = tryparse(Int, get(ENV, name, string(default)))
+ return something(parsed, default)
+end
-function Base.show(io::IO, mime::MIME"text/plain", df::DataFrame)
- # Call PrettyTables directly with row/column limits
- # This ensures only 10 rows are shown regardless of DataFrame size
+const _DF_MAX_ROWS = _env_int("SIENNA_DOCS_DF_MAX_ROWS", 10)
+const _DF_MAX_COLS = _env_int("SIENNA_DOCS_DF_MAX_COLS", 80)
+
+function Base.show(io::IO, mime::MIME"text/plain", df::DataFrame; kwargs...)
+ # Keep docs output bounded while allowing explicit caller kwargs.
PrettyTables.pretty_table(io, df;
backend = :text,
- maximum_number_of_rows = 10,
- maximum_number_of_columns = 80,
+ maximum_number_of_rows = _DF_MAX_ROWS,
+ maximum_number_of_columns = _DF_MAX_COLS,
show_omitted_cell_summary = true,
compact_printing = false,
- limit_printing = true)
+ limit_printing = true,
+ kwargs...)
end
-function Base.show(io::IO, mime::MIME"text/html", df::DataFrame)
- # For HTML output (which Documenter prefers for large outputs)
- # Use PrettyTables HTML backend with explicit row/column limits
+function Base.show(io::IO, mime::MIME"text/html", df::DataFrame; kwargs...)
PrettyTables.pretty_table(io, df;
backend = :html,
- maximum_number_of_rows = 10,
- maximum_number_of_columns = 80,
+ maximum_number_of_rows = _DF_MAX_ROWS,
+ maximum_number_of_columns = _DF_MAX_COLS,
show_omitted_cell_summary = true,
compact_printing = false,
- limit_printing = true)
+ limit_printing = true,
+ kwargs...)
end
-# Function to clean up old generated files
+# Remove previously generated tutorial artifacts so a docs build only reflects
+# current source tutorials.
+#
+# Input:
+# - dir: tutorial output directory that can contain generated_*.md/ipynb.
+# Output:
+# - Deletes matching files in-place and logs each deletion.
function clean_old_generated_files(dir::String)
if !isdir(dir)
@warn "Directory does not exist: $dir"
@@ -57,12 +68,77 @@ end
# Literate post-processing functions for tutorial generation
#########################################################
-# postprocess function to insert md
+# Compute docs base URL from Documenter deploy context.
+#
+# Behavior:
+# - previews/PR123 -> .../previews/PR123
+# - dev (or custom DOCUMENTER_DEVURL) -> .../dev
+# - tagged versions like v0.9 -> .../v0.9
+# - fallback -> .../stable
+#
+# This keeps generated download/view-online links correct across preview, dev,
+# tagged, and stable deployments.
+function _compute_docs_base_url()
+ base = "https://nrel-sienna.github.io/HybridSystemsSimulations.jl"
+
+ current_version = get(ENV, "DOCUMENTER_CURRENT_VERSION", "")
+
+ # Preview builds (e.g. "previews/PR123")
+ if startswith(current_version, "previews/PR")
+ return "$base/$current_version"
+ end
+
+ # Dev builds
+ if current_version == "dev"
+ dev_suffix = get(ENV, "DOCUMENTER_DEVURL", "dev")
+ return "$base/$dev_suffix"
+ end
+
+ # Tagged/versioned builds (e.g. "v0.9", "v1.2.3")
+ if !isempty(current_version) && current_version != "stable"
+ return "$base/$current_version"
+ end
+
+ # Default to stable
+ return "$base/stable"
+end
+
+const _DOCS_BASE_URL = _compute_docs_base_url()
+
+"""
+Choose how tutorial download links are written in generated markdown.
+
+- **Absolute** (under `_DOCS_BASE_URL/tutorials/`): CI / Documenter context (`GITHUB_ACTIONS` or
+ non-empty `DOCUMENTER_CURRENT_VERSION`) so previews, `dev`, and versioned URLs match
+ `_compute_docs_base_url()`.
+- **Relative** (bare filenames): local/offline builds; files sit next to `generated_*.md`
+ under `docs/src/tutorials/`.
+
+Override: `SIENNA_DOCS_DOWNLOAD_LINKS`=`absolute` or `relative`.
+"""
+function _downloads_use_absolute_urls()
+ o = get(ENV, "SIENNA_DOCS_DOWNLOAD_LINKS", "")
+ o == "absolute" && return true
+ o == "relative" && return false
+ haskey(ENV, "GITHUB_ACTIONS") && return true
+ !isempty(get(ENV, "DOCUMENTER_CURRENT_VERSION", "")) && return true
+ return false
+end
+
+# Replace APPEND_MARKDOWN("path/to/file.md") placeholders with file contents.
+#
+# Sample input:
+# "Before\nAPPEND_MARKDOWN(\"docs/src/tutorials/_snippet.md\")\nAfter"
+# Sample output:
+# "Before\n\nAfter"
+#
+# Notes:
+# - Uses a non-greedy-safe capture (`[^\"]*`) so multiple placeholders can be
+# replaced independently.
function insert_md(content)
- m = match(r"APPEND_MARKDOWN\(\"(.*)\"\)", content)
- if !isnothing(m)
- md_content = read(m.captures[1], String)
- content = replace(content, r"APPEND_MARKDOWN\(\"(.*)\"\)" => md_content)
+ pattern = r"APPEND_MARKDOWN\(\"([^\"]*)\"\)"
+ if occursin(pattern, content)
+ content = replace(content, pattern => m -> read(m.captures[1], String))
end
return content
end
@@ -129,12 +205,26 @@ function preprocess_admonitions_for_notebook(str::AbstractString)
return join(out, '\n')
end
-# Function to add download links to generated markdown
+# Inject a short "download tutorial files" sentence after the first markdown
+# heading in generated tutorial pages.
+#
+# Sample input:
+# "# Title\nBody..."
+# Sample output (conceptual):
+# "# Title\n\n*To follow along... [Julia script](.../tutorial.jl)...*\n\nBody..."
+#
+# Download links:
+# - **Deployed / CI**: absolute URLs under `_DOCS_BASE_URL` when `_downloads_use_absolute_urls()` is true.
+# - **Local**: bare filenames (siblings of `generated_*.md` in `docs/src/tutorials/`).
function add_download_links(content, jl_file, ipynb_file)
- # Add download links at the top of the file after the first heading
+ script_link, notebook_link = if _downloads_use_absolute_urls()
+ ("$_DOCS_BASE_URL/tutorials/$(jl_file)", "$_DOCS_BASE_URL/tutorials/$(ipynb_file)")
+ else
+ (jl_file, ipynb_file)
+ end
download_section = """
-*To follow along, you can download this tutorial as a [Julia script (.jl)]($(jl_file)) or [Jupyter notebook (.ipynb)]($(ipynb_file)).*
+*To follow along, you can download this tutorial as a [Julia script (.jl)]($(script_link)) or [Jupyter notebook (.ipynb)]($(notebook_link)).*
"""
# Insert after the first heading (which should be the title)
@@ -147,7 +237,12 @@ function add_download_links(content, jl_file, ipynb_file)
return content
end
-# Function to add Pkg.status() to notebook within the first markdown cell
+# Insert a setup preface and captured `Pkg.status()` into the first markdown
+# cell of a generated notebook, immediately after the first heading.
+#
+# Sample effect:
+# - First markdown cell gains a "Set up" blockquote and an embedded code block
+# containing package versions from the docs build environment.
function add_pkg_status_to_notebook(nb::Dict)
cells = get(nb, "cells", [])
if isempty(cells)
@@ -245,8 +340,11 @@ end
# Add italicized "view online" comment after each image from ```@raw html ... ``` (or
# the raw HTML / markdown form Literate writes). Used as a postprocess in Literate.notebook.
-# Expects _DOCS_BASE_URL to be defined by the includer (e.g. in make.jl).
# Literate strips the backtick wrapper and outputs raw HTML; we match that multi-line block.
+# Sample effect:
+# - If a markdown cell contains one or more image fragments, append exactly one
+# "view online" fallback note at the end of that cell.
+# - If the note already exists in the cell, no change is applied.
function add_image_links(nb::Dict, outputfile_base::AbstractString)
tutorial_url = "$_DOCS_BASE_URL/tutorials/$(outputfile_base)/"
msg = "_If image is not available when viewing in a Jupyter notebook, view the tutorial online [here]($tutorial_url)._"
@@ -260,16 +358,26 @@ function add_image_links(nb::Dict, outputfile_base::AbstractString)
contains(text, "If image is not available when viewing in a Jupyter notebook") &&
continue
suffix = "\n\n" * msg * "\n"
- append_after = m -> string(m) * suffix
- # Use a single non-overlapping regex to match image-containing fragments:
- # - ......
(Literate raw HTML paragraphs)
- # - ```@raw html ... ``` blocks
- # - Markdown images 
- # - standalone
tags (only if not already matched by wrapper)
+ # If the cell has any of the image shapes below, we append one "view online" note.
+ # We build one alternation pattern from sub-patterns (each line is one case).
+ #
+ # HTML paragraph wrapping an
(Literate often emits
…
…
).
+ # ]*> — opening
and attributes
+ # [\s\S]*? — any chars, non-greedy, up to the first
— from
p_with_img_pattern = r"
]*>[\s\S]*?
"
+ # Documenter @raw html chunk that Literate inlines in the notebook (backticks removed in output).
+ # ```@raw html — start marker
+ # [\s\S]*? — block body, non-greedy
+ # ``` — end fence
raw_html_block_pattern = r"```@raw html[\s\S]*?```"
+ # Standard markdown image: 
+ # !\[…\] — alt in brackets; \(…\) — path in parens
markdown_image_pattern = r"!\[[^\]]*\]\([^\)]*\)"
+ # A bare
not already covered by the
…
…
case above.
+ #
]*? — attributes; /?> — self-closing or >
standalone_img_pattern = r"
]*?/?>"
+ # Union of the four cases: (?: A | B | C | D )
image_fragment_pattern = Regex(
"(?:" *
p_with_img_pattern.pattern * "|" *
@@ -277,11 +385,9 @@ function add_image_links(nb::Dict, outputfile_base::AbstractString)
markdown_image_pattern.pattern * "|" *
standalone_img_pattern.pattern * ")",
)
- text = replace(
- text,
- image_fragment_pattern =>
- append_after,
- )
+ if occursin(image_fragment_pattern, text)
+ text *= suffix
+ end
# Convert back to notebook source array (lines, last without trailing \n if non-empty)
lines = split(text, "\n"; keepempty = true)
new_source = String[]
@@ -303,31 +409,35 @@ end
# Process tutorials with Literate
#########################################################
-# Markdown files are postprocessed to add download links for the Julia script and Jupyter notebook
-# Jupyter notebooks are postprocessed to add image links and pkg.status()
+# Generate tutorial markdown + notebook artifacts from literate .jl sources.
+#
+# Pipeline:
+# 1) discover tutorial .jl files (excluding helper files starting with "_")
+# 2) generate Documenter-flavored markdown with injected download links
+# 3) generate notebook with admonition conversion, setup preface, and image note
function make_tutorials()
+ tutorials_dir = abspath(joinpath(@__DIR__, "src", "tutorials"))
# Exclude helper scripts that start with "_"
- if isdir("docs/src/tutorials")
+ if isdir(tutorials_dir)
tutorial_files =
filter(
- x -> occursin(".jl", x) && !startswith(x, "_"),
- readdir("docs/src/tutorials"),
+ x -> endswith(x, ".jl") && !startswith(x, "_"),
+ readdir(tutorials_dir),
)
if !isempty(tutorial_files)
# Clean up old generated tutorial files
- tutorial_outputdir = joinpath(pwd(), "docs", "src", "tutorials")
+ tutorial_outputdir = tutorials_dir
clean_old_generated_files(tutorial_outputdir)
for file in tutorial_files
@show file
- infile_path = joinpath(pwd(), "docs", "src", "tutorials", file)
+ infile_path = joinpath(tutorials_dir, file)
execute =
if occursin("EXECUTE = TRUE", uppercase(readline(infile_path)))
true
else
false
end
- execute && include(infile_path)
outputfile = string("generated_", replace("$file", ".jl" => ""))
From 6aa810ece0cb72f9b78b61ea3c6ca2c5d06a9985 Mon Sep 17 00:00:00 2001
From: kdayday
Date: Thu, 30 Apr 2026 14:15:10 -0600
Subject: [PATCH 28/46] Skip cleanup on forks
---
.github/workflows/doc-preview-cleanup.yml | 1 +
1 file changed, 1 insertion(+)
diff --git a/.github/workflows/doc-preview-cleanup.yml b/.github/workflows/doc-preview-cleanup.yml
index 73f291a2..7fb59141 100644
--- a/.github/workflows/doc-preview-cleanup.yml
+++ b/.github/workflows/doc-preview-cleanup.yml
@@ -12,6 +12,7 @@ concurrency:
jobs:
doc-preview-cleanup:
runs-on: ubuntu-latest
+ if: github.event.pull_request.head.repo.fork == false
# This workflow pushes to gh-pages; permissions are per-job and independent of docs.yml
permissions:
contents: write
From 720598f68e5dacae0f52103fdc94764df6ad1ebc Mon Sep 17 00:00:00 2001
From: kdayday
Date: Thu, 30 Apr 2026 15:17:23 -0600
Subject: [PATCH 29/46] Replace remaining nrel-sienna links
---
.claude/Sienna.md | 10 +++++-----
docs/make.jl | 6 +++---
docs/make_tutorials.jl | 2 +-
docs/src/index.md | 16 ++++++++--------
4 files changed, 17 insertions(+), 17 deletions(-)
diff --git a/.claude/Sienna.md b/.claude/Sienna.md
index 8b4c637a..1cf6f893 100644
--- a/.claude/Sienna.md
+++ b/.claude/Sienna.md
@@ -61,7 +61,7 @@ Avoid returning `Union` types or abstract types.
## Code Conventions
-Style guide: [https://nrel-sienna.github.io/InfrastructureSystems.jl/stable/style/](https://nrel-sienna.github.io/InfrastructureSystems.jl/stable/style/)
+Style guide: [https://sienna-platform.github.io/InfrastructureSystems.jl/stable/style/](https://sienna-platform.github.io/InfrastructureSystems.jl/stable/style/)
Formatter (JuliaFormatter): Use the formatter script provided in each package.
@@ -77,12 +77,12 @@ Key rules:
Framework: [Diataxis](https://diataxis.fr/)
-Sienna guide: [https://nrel-sienna.github.io/InfrastructureSystems.jl/stable/docs_best_practices/explanation/](https://nrel-sienna.github.io/InfrastructureSystems.jl/stable/docs_best_practices/explanation/)
+Sienna guide: [https://sienna-platform.github.io/InfrastructureSystems.jl/stable/docs_best_practices/explanation/](https://sienna-platform.github.io/InfrastructureSystems.jl/stable/docs_best_practices/explanation/)
-Sienna guide for Diataxis-style tutorials: [https://nrel-sienna.github.io/InfrastructureSystems.jl/stable/docs_best_practices/how-to/write_a_tutorial/](https://nrel-sienna.github.io/InfrastructureSystems.jl/stable/docs_best_practices/how-to/write_a_tutorial/)
+Sienna guide for Diataxis-style tutorials: [https://sienna-platform.github.io/InfrastructureSystems.jl/stable/docs_best_practices/how-to/write_a_tutorial/](https://sienna-platform.github.io/InfrastructureSystems.jl/stable/docs_best_practices/how-to/write_a_tutorial/)
Format for tutorial scripts: [https://fredrikekre.github.io/Literate.jl/v2/](https://fredrikekre.github.io/Literate.jl/v2/)
-Sienna guide for Diataxis-style how-to's: [https://nrel-sienna.github.io/InfrastructureSystems.jl/stable/docs_best_practices/how-to/write_a_how-to/](https://nrel-sienna.github.io/InfrastructureSystems.jl/stable/docs_best_practices/how-to/write_a_how-to/)
-Sienna guide for APIs: [https://nrel-sienna.github.io/InfrastructureSystems.jl/stable/docs_best_practices/how-to/write_docstrings_org_api/](https://nrel-sienna.github.io/InfrastructureSystems.jl/stable/docs_best_practices/how-to/write_docstrings_org_api/)
+Sienna guide for Diataxis-style how-to's: [https://sienna-platform.github.io/InfrastructureSystems.jl/stable/docs_best_practices/how-to/write_a_how-to/](https://sienna-platform.github.io/InfrastructureSystems.jl/stable/docs_best_practices/how-to/write_a_how-to/)
+Sienna guide for APIs: [https://sienna-platform.github.io/InfrastructureSystems.jl/stable/docs_best_practices/how-to/write_docstrings_org_api/](https://sienna-platform.github.io/InfrastructureSystems.jl/stable/docs_best_practices/how-to/write_docstrings_org_api/)
Docstring requirements:
diff --git a/docs/make.jl b/docs/make.jl
index 8989cefb..3daa1fe7 100644
--- a/docs/make.jl
+++ b/docs/make.jl
@@ -6,9 +6,9 @@ using Literate
links = InterLinks(
"Julia" => "https://docs.julialang.org/en/v1/",
- "InfrastructureSystems" => "https://nrel-sienna.github.io/InfrastructureSystems.jl/stable/",
- "PowerSystems" => "https://nrel-sienna.github.io/PowerSystems.jl/stable/",
- "PowerSimulations" => "https://nrel-sienna.github.io/PowerSimulations.jl/stable/",
+ "InfrastructureSystems" => "https://sienna-platform.github.io/InfrastructureSystems.jl/stable/",
+ "PowerSystems" => "https://sienna-platform.github.io/PowerSystems.jl/stable/",
+ "PowerSimulations" => "https://sienna-platform.github.io/PowerSimulations.jl/stable/",
)
include(joinpath(@__DIR__, "make_tutorials.jl"))
diff --git a/docs/make_tutorials.jl b/docs/make_tutorials.jl
index 3659a4e6..d7df22be 100644
--- a/docs/make_tutorials.jl
+++ b/docs/make_tutorials.jl
@@ -79,7 +79,7 @@ end
# This keeps generated download/view-online links correct across preview, dev,
# tagged, and stable deployments.
function _compute_docs_base_url()
- base = "https://nrel-sienna.github.io/HybridSystemsSimulations.jl"
+ base = "https://sienna-platform.github.io/HybridSystemsSimulations.jl"
current_version = get(ENV, "DOCUMENTER_CURRENT_VERSION", "")
diff --git a/docs/src/index.md b/docs/src/index.md
index 1b8012a2..c315a636 100644
--- a/docs/src/index.md
+++ b/docs/src/index.md
@@ -7,7 +7,7 @@ CurrentModule = HybridSystemsSimulations
## Overview
`HybridSystemsSimulations.jl` is a power system operations simulation package that extends
-[`PowerSimulations.jl`](https://nrel-sienna.github.io/PowerSimulations.jl/stable/) to model
+[`PowerSimulations.jl`](https://sienna-platform.github.io/PowerSimulations.jl/stable/) to model
hybrid systems (co-located renewable, thermal, and storage behind a single point of common
coupling). It provides device formulations, decision models, and constraints for
production-cost and merchant-style studies, including ancillary services and bilevel
@@ -19,15 +19,15 @@ feedback, suggestions, and bug reports.
## About Sienna
`HybridSystemsSimulations.jl` is part of the National Laboratory of the Rockies's (NLR, formerly NREL)
-[Sienna ecosystem](https://nrel-sienna.github.io/Sienna/), an open source framework for
+[Sienna ecosystem](https://sienna-platform.github.io/Sienna/), an open source framework for
power system modeling, simulation, and optimization. The Sienna ecosystem can be
-[found on GitHub](https://github.com/NREL-Sienna/Sienna). It contains three applications:
+[found on GitHub](https://github.com/sienna-platform/Sienna). It contains three applications:
- - [Sienna\Data](https://nrel-sienna.github.io/Sienna/pages/applications/sienna_data.html) enables
+ - [Sienna\Data](https://sienna-platform.github.io/Sienna/pages/applications/sienna_data.html) enables
efficient data input, analysis, and transformation
- - [Sienna\Ops](https://nrel-sienna.github.io/Sienna/pages/applications/sienna_ops.html)
+ - [Sienna\Ops](https://sienna-platform.github.io/Sienna/pages/applications/sienna_ops.html)
enables system scheduling simulations by formulating and solving optimization problems
- - [Sienna\Dyn](https://nrel-sienna.github.io/Sienna/pages/applications/sienna_dyn.html) enables
+ - [Sienna\Dyn](https://sienna-platform.github.io/Sienna/pages/applications/sienna_dyn.html) enables
system transient analysis including small signal stability and full system dynamic
simulations
@@ -42,10 +42,10 @@ U.S. Department of Energy's National Laboratory of the Rockies
## Installation and Quick Links
- - [Sienna installation page](https://nrel-sienna.github.io/Sienna/SiennaDocs/docs/build/how-to/install/):
+ - [Sienna installation page](https://sienna-platform.github.io/Sienna/SiennaDocs/docs/build/how-to/install/):
Instructions to install `HybridSystemsSimulations.jl` and other Sienna\Ops packages
- [`JuMP.jl` solver's page](https://jump.dev/JuMP.jl/stable/installation/#Install-a-solver): An appropriate optimization solver is required for running models. Refer to this page to select and install a solver for your application.
- - [Sienna Documentation Hub](https://nrel-sienna.github.io/Sienna/SiennaDocs/docs/build/index.html):
+ - [Sienna Documentation Hub](https://sienna-platform.github.io/Sienna/SiennaDocs/docs/build/index.html):
Links to other Sienna packages' documentation
## How To Use This Documentation
From d8fd3d20848b33e400af1ad9946e90cd35fd80fb Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 30 Apr 2026 21:21:58 +0000
Subject: [PATCH 30/46] Fix bare catch blocks in
_add_hybrid_renewable_da_time_series!
Agent-Logs-Url: https://github.com/Sienna-Platform/HybridSystemsSimulations.jl/sessions/ed090d9d-2ba8-422d-be3d-23f23463db3f
Co-authored-by: kdayday <12451220+kdayday@users.noreply.github.com>
---
test/test_utils/function_utils.jl | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/test/test_utils/function_utils.jl b/test/test_utils/function_utils.jl
index 1f77eb50..a90f005a 100644
--- a/test/test_utils/function_utils.jl
+++ b/test/test_utils/function_utils.jl
@@ -95,8 +95,8 @@ function _add_hybrid_renewable_da_time_series!(
)
single_da = IS.SingleTimeSeries(ts, "RenewableDispatch__max_active_power_da")
PSY.add_time_series!(sys, hybrid, single_da)
- catch
- nothing
+ catch e
+ e isa ArgumentError || rethrow()
end
# Force deterministic windows to exactly match the merchant RT horizon request
@@ -121,8 +121,8 @@ function _add_hybrid_renewable_da_time_series!(
interval;
resolution = resolution,
)
- catch
- nothing
+ catch e
+ e isa ArgumentError || rethrow()
end
return
end
From fb759f11100eb415d4d820ee2dbe4d4bc1fe774f Mon Sep 17 00:00:00 2001
From: kdayday
Date: Fri, 1 May 2026 13:14:02 -0600
Subject: [PATCH 31/46] Fix interval horizon issues for PSI 0.34
---
Project.toml | 4 +--
src/add_parameters.jl | 33 ++++++++++++++++++++++---
src/core/decision_models.jl | 30 +++++++++++++++++++++-
src/hybrid_system_decision_models.jl | 11 ++++++---
test/test_hybrid_simulations.jl | 8 +++---
test/test_merchant_sequence.jl | 3 ++-
test/test_utils/additional_templates.jl | 11 ++++++---
7 files changed, 80 insertions(+), 20 deletions(-)
diff --git a/Project.toml b/Project.toml
index a163432b..183fdc10 100644
--- a/Project.toml
+++ b/Project.toml
@@ -17,6 +17,6 @@ DataStructures = "~0.18, ^0.19"
DocStringExtensions = "0.8, 0.9.2"
JuMP = "^1.28"
MathOptInterface = "1"
-PowerSimulations = "^0.33"
-PowerSystems = "5.5"
+PowerSimulations = "^0.34"
+PowerSystems = "^5.8"
julia = "^1.10"
diff --git a/src/add_parameters.jl b/src/add_parameters.jl
index 7d90c6bb..605198c5 100644
--- a/src/add_parameters.jl
+++ b/src/add_parameters.jl
@@ -11,15 +11,32 @@ function _add_time_series_parameters(
ts_name
ts_type = PSI.get_default_time_series_type(container)
time_steps = PSI.get_time_steps(container)
+ settings = PSI.get_settings(container)
+ model_resolution = PSI.get_resolution(settings)
+ model_interval = PSI.get_interval(settings)
device_names = String[]
initial_values = Dict{String, AbstractArray}()
for device in devices
push!(device_names, PSY.get_name(device))
- ts_uuid = string(IS.get_time_series_uuid(ts_type, device, ts_name))
+ ts_uuid = string(
+ IS.get_time_series_uuid(
+ ts_type,
+ device,
+ ts_name;
+ resolution = PSI._to_is_resolution(model_resolution),
+ interval = PSI._to_is_interval(model_interval),
+ ),
+ )
if !(ts_uuid in keys(initial_values))
- initial_values[ts_uuid] =
- PSI.get_time_series_initial_values!(container, ts_type, device, ts_name)
+ initial_values[ts_uuid] = PSI.get_time_series_initial_values!(
+ container,
+ ts_type,
+ device,
+ ts_name;
+ resolution = model_resolution,
+ interval = model_interval,
+ )
end
end
@@ -51,7 +68,15 @@ function _add_time_series_parameters(
PSI.add_component_name!(
PSI.get_attributes(param_container),
name,
- string(IS.get_time_series_uuid(ts_type, device, ts_name)),
+ string(
+ IS.get_time_series_uuid(
+ ts_type,
+ device,
+ ts_name;
+ resolution = PSI._to_is_resolution(model_resolution),
+ interval = PSI._to_is_interval(model_interval),
+ ),
+ ),
)
end
return
diff --git a/src/core/decision_models.jl b/src/core/decision_models.jl
index dee92d6d..7fbf3cb0 100644
--- a/src/core/decision_models.jl
+++ b/src/core/decision_models.jl
@@ -189,8 +189,36 @@ function PSI.validate_time_series!(model::PSI.DecisionModel{<:HybridDecisionProb
PSI.set_resolution!(settings, first(available_resolutions))
end
+ model_interval = PSI.get_interval(settings)
+ available_intervals = Set(
+ row.interval for
+ row in eachrow(PSY.get_forecast_summary_table(sys)) if row.interval !== nothing
+ )
+ if model_interval == PSI.UNSET_INTERVAL && length(available_intervals) > 1
+ throw(
+ IS.ConflictingInputsError(
+ "The system contains multiple forecast intervals $(available_intervals). " *
+ "The `interval` keyword argument must be provided to the DecisionModel constructor " *
+ "to select which interval to use.",
+ ),
+ )
+ elseif model_interval != PSI.UNSET_INTERVAL && !isempty(available_intervals)
+ if model_interval ∉ available_intervals
+ throw(
+ IS.ConflictingInputsError(
+ "Interval $(Dates.canonicalize(model_interval)) is not available in the system data. " *
+ "Available forecast intervals: $(available_intervals)",
+ ),
+ )
+ end
+ end
+ interval_kwarg =
+ model_interval == PSI.UNSET_INTERVAL ? (;) : (; interval = model_interval)
if PSI.get_horizon(settings) == PSI.UNSET_HORIZON
- PSI.set_horizon!(settings, PSY.get_forecast_horizon(sys))
+ PSI.set_horizon!(
+ settings,
+ PSY.get_forecast_horizon(sys; interval_kwarg...),
+ )
end
counts = PSY.get_time_series_counts(sys)
diff --git a/src/hybrid_system_decision_models.jl b/src/hybrid_system_decision_models.jl
index dbcc1b69..5de7d202 100644
--- a/src/hybrid_system_decision_models.jl
+++ b/src/hybrid_system_decision_models.jl
@@ -193,9 +193,8 @@ function PSI.update_decision_state!(
) where {T <: Union{EnergyDABidOut, EnergyDABidIn}}
@debug "updating decision state $simulation_time"
state_data = PSI.get_decision_state_data(state, key)
- model_resolution = PSI.get_resolution(model_params) # var res: 1 hour
- model_resolution = Dates.Hour(1) #TODO: Find a ext hack
- state_resolution = PSI.get_data_resolution(state_data) # 5 min
+ model_resolution = PSI.get_resolution(model_params)
+ state_resolution = PSI.get_data_resolution(state_data)
resolution_ratio = model_resolution ÷ state_resolution
state_timestamps = state_data.timestamps
PSI.IS.@assert_op resolution_ratio >= 1
@@ -240,7 +239,11 @@ function PSI._update_parameter_values!(
component_names, time = axes(parameter_array)
model_resolution = PSI.get_resolution(model)
state_data = PSI.get_dataset(state, PSI.get_attribute_key(attributes))
- t_step = model_resolution ÷ state_data.resolution
+ if model_resolution < state_data.resolution
+ t_step = 1
+ else
+ t_step = model_resolution ÷ state_data.resolution
+ end
@assert t_step > 0
state_timestamps = state_data.timestamps
max_state_index = PSI.get_num_rows(state_data)
diff --git a/test/test_hybrid_simulations.jl b/test/test_hybrid_simulations.jl
index cfa90073..7ddf8663 100644
--- a/test/test_hybrid_simulations.jl
+++ b/test/test_hybrid_simulations.jl
@@ -1,7 +1,7 @@
@testset "Test HybridSystem Simulation Only UC" begin
sys_uc = PSB.build_system(PSITestSystems, "c_sys5_hybrid_uc")
- template_uc = get_template_standard_uc_simulation()
+ template_uc = get_hss_template_standard_uc_simulation()
set_device_model!(
template_uc,
DeviceModel(
@@ -45,7 +45,7 @@ end
sys_uc = PSB.build_system(PSITestSystems, "c_sys5_hybrid_uc")
sys_ed = PSB.build_system(PSITestSystems, "c_sys5_hybrid_ed")
- template_uc = get_template_standard_uc_simulation()
+ template_uc = get_hss_template_standard_uc_simulation()
set_device_model!(
template_uc,
DeviceModel(
@@ -55,7 +55,7 @@ end
),
)
set_network_model!(template_uc, NetworkModel(CopperPlatePowerModel; use_slacks = true))
- template_ed = get_thermal_dispatch_template_network(
+ template_ed = get_hss_thermal_dispatch_template_network(
NetworkModel(CopperPlatePowerModel; use_slacks = true),
)
set_device_model!(
@@ -106,7 +106,7 @@ end
end
@testset "Test HybridSystem embedded storage (energy_target)" begin
- template = get_template_standard_uc_simulation()
+ template = get_hss_template_standard_uc_simulation()
set_device_model!(
template,
DeviceModel(
diff --git a/test/test_merchant_sequence.jl b/test/test_merchant_sequence.jl
index 2b4526d4..bbdd81b7 100644
--- a/test/test_merchant_sequence.jl
+++ b/test/test_merchant_sequence.jl
@@ -3,7 +3,7 @@
sys_rts_rt = PSB.build_RTS_GMLC_RT_sys(;
raw_data = PSB.RTS_DIR,
horizon = 288,
- interval = Hour(24),
+ interval = Hour(1),
)
modify_ren_curtailment_cost!(sys_rts_rt)
@@ -34,6 +34,7 @@
initial_time = DateTime("2020-10-03T00:00:00"),
horizon = Hour(24),
resolution = Minute(5),
+ interval = Hour(1),
name = "MerchantHybridEnergyCase_Sequence",
)
diff --git a/test/test_utils/additional_templates.jl b/test/test_utils/additional_templates.jl
index 1b8ecbb6..792d12e6 100644
--- a/test/test_utils/additional_templates.jl
+++ b/test/test_utils/additional_templates.jl
@@ -24,7 +24,7 @@ function set_uc_models!(template_uc)
return
end
-function get_template_basic_uc_simulation()
+function get_hss_template_basic_uc_simulation()
template = ProblemTemplate(CopperPlatePowerModel)
set_device_model!(template, ThermalStandard, ThermalBasicDispatch)
set_device_model!(template, RenewableDispatch, RenewableFullDispatch)
@@ -33,13 +33,13 @@ function get_template_basic_uc_simulation()
return template
end
-function get_template_standard_uc_simulation()
- template = get_template_basic_uc_simulation()
+function get_hss_template_standard_uc_simulation()
+ template = get_hss_template_basic_uc_simulation()
set_device_model!(template, ThermalStandard, ThermalStandardUnitCommitment)
return template
end
-function get_thermal_dispatch_template_network(network = CopperPlatePowerModel)
+function get_hss_thermal_dispatch_template_network(network = CopperPlatePowerModel)
template = ProblemTemplate(network)
set_device_model!(template, ThermalStandard, ThermalBasicDispatch)
set_device_model!(template, PowerLoad, StaticPowerLoad)
@@ -92,6 +92,9 @@ function build_simulation_case_optimizer(
sys_da;
name = "UC",
optimizer = HiGHS_optimizer,
+ # PSI 0.34: later stage horizon must not exceed prior.
+ horizon = Hour(24),
+ interval = Hour(1),
initialize_model = true,
optimizer_solve_log_print = false,
direct_mode_optimizer = true,
From adf7b825607497f4f49708f762ad8ed38ed7c39d Mon Sep 17 00:00:00 2001
From: kdayday
Date: Sat, 2 May 2026 12:28:31 -0600
Subject: [PATCH 32/46] Harden hybrid time series parameter naming for PSI 0.34
---
src/add_parameters.jl | 30 ++++++++++++++++++++++++++----
1 file changed, 26 insertions(+), 4 deletions(-)
diff --git a/src/add_parameters.jl b/src/add_parameters.jl
index 605198c5..5fcf8206 100644
--- a/src/add_parameters.jl
+++ b/src/add_parameters.jl
@@ -6,7 +6,7 @@ function _add_time_series_parameters(
container::PSI.OptimizationContainer,
ts_name::String,
param,
- devices::Vector{PSY.HybridSystem},
+ devices::AbstractVector{<:PSY.HybridSystem},
)
ts_name
ts_type = PSI.get_default_time_series_type(container)
@@ -61,7 +61,7 @@ function _add_time_series_parameters(
for device in devices
name = PSY.get_name(device)
- multiplier = PSY.get_max_active_power(device.renewable_unit)
+ multiplier = _get_hybrid_ts_multiplier(param, device)
for step in time_steps
PSI.set_multiplier!(param_container, multiplier, name, step)
end
@@ -82,6 +82,14 @@ function _add_time_series_parameters(
return
end
+function _get_hybrid_ts_multiplier(::RenewablePowerTimeSeries, device::PSY.HybridSystem)
+ return PSY.get_max_active_power(PSY.get_renewable_unit(device))
+end
+
+function _get_hybrid_ts_multiplier(::ElectricLoadTimeSeries, device::PSY.HybridSystem)
+ return PSY.get_max_active_power(PSY.get_electric_load(device))
+end
+
# Multipliers consider that the objective function is a Maximization problem
# But the default direction in PSI is Min.
_get_multiplier(::Type{EnergyDABidOut}, ::DayAheadEnergyPrice) = -1.0
@@ -208,7 +216,7 @@ end
function add_time_series_parameters!(
container::PSI.OptimizationContainer,
param::RenewablePowerTimeSeries,
- devices::Vector{PSY.HybridSystem},
+ devices::AbstractVector{<:PSY.HybridSystem},
ts_name = "RenewableDispatch__max_active_power",
)
_add_time_series_parameters(container, ts_name, param, devices)
@@ -217,13 +225,27 @@ end
function add_time_series_parameters!(
container::PSI.OptimizationContainer,
param::ElectricLoadTimeSeries,
- devices::Vector{PSY.HybridSystem},
+ devices::AbstractVector{<:PSY.HybridSystem},
ts_name = "PowerLoad__max_active_power",
)
_add_time_series_parameters(container, ts_name, param, devices)
return
end
+function PSI._add_parameters!(
+ container::PSI.OptimizationContainer,
+ param::T,
+ devices::U,
+ model::PSI.DeviceModel{D, W},
+) where {
+ T <: Union{RenewablePowerTimeSeries, ElectricLoadTimeSeries},
+ U <: Union{Vector{D}, IS.FlattenIteratorWrapper{D}},
+ W <: AbstractHybridFormulation,
+} where {D <: PSY.HybridSystem}
+ add_time_series_parameters!(container, param, collect(devices))
+ return
+end
+
function PSI.add_parameters!(
container::PSI.OptimizationContainer,
param::T,
From 24f8ab2ab2dd6657aeffbe5bbd4b32c3980170fc Mon Sep 17 00:00:00 2001
From: kdayday
Date: Tue, 5 May 2026 18:23:42 -0600
Subject: [PATCH 33/46] Update for directly attached multi-resolution
time-series instead of in ext
---
src/add_constraints.jl | 109 ++-
src/add_parameters.jl | 459 ++++++++--
src/add_variables.jl | 18 +-
src/core/decision_models.jl | 231 +++--
src/core/parameters.jl | 35 +-
src/decision_models/bilevel_decision_model.jl | 45 +-
.../cooptimizer_decision_model.jl | 70 +-
.../only_energy_decision_model.jl | 60 +-
src/hybrid_system_decision_models.jl | 4 +-
test/inputs/chuhsi_DA_prices.csv | 146 +--
test/inputs/chuhsi_DA_prices_24.csv | 25 +
test/inputs/chuhsi_DA_prices_5min.csv | 865 ++++++++++++++++++
test/inputs/chuhsi_DA_prices_5min_300.csv | 301 ++++++
test/inputs/chuhsi_RT_prices_300.csv | 301 ++++++
test/inputs/chuhsi_RegDown_prices.csv | 146 +--
test/inputs/chuhsi_RegDown_prices_24.csv | 25 +
test/inputs/chuhsi_RegDown_prices_5min.csv | 865 ++++++++++++++++++
.../inputs/chuhsi_RegDown_prices_5min_300.csv | 301 ++++++
test/inputs/chuhsi_RegUp_prices.csv | 146 +--
test/inputs/chuhsi_RegUp_prices_24.csv | 25 +
test/inputs/chuhsi_RegUp_prices_5min.csv | 865 ++++++++++++++++++
test/inputs/chuhsi_RegUp_prices_5min_300.csv | 301 ++++++
test/inputs/chuhsi_Spin_prices.csv | 146 +--
test/inputs/chuhsi_Spin_prices_24.csv | 25 +
test/inputs/chuhsi_Spin_prices_5min.csv | 865 ++++++++++++++++++
test/inputs/chuhsi_Spin_prices_5min_300.csv | 301 ++++++
test/test_merchant_cooptimizer.jl | 56 +-
test/test_merchant_only_energy.jl | 46 +-
test/test_merchant_sequence.jl | 42 +-
test/test_utils/function_utils.jl | 411 ++++++++-
30 files changed, 6535 insertions(+), 700 deletions(-)
create mode 100644 test/inputs/chuhsi_DA_prices_24.csv
create mode 100644 test/inputs/chuhsi_DA_prices_5min.csv
create mode 100644 test/inputs/chuhsi_DA_prices_5min_300.csv
create mode 100644 test/inputs/chuhsi_RT_prices_300.csv
create mode 100644 test/inputs/chuhsi_RegDown_prices_24.csv
create mode 100644 test/inputs/chuhsi_RegDown_prices_5min.csv
create mode 100644 test/inputs/chuhsi_RegDown_prices_5min_300.csv
create mode 100644 test/inputs/chuhsi_RegUp_prices_24.csv
create mode 100644 test/inputs/chuhsi_RegUp_prices_5min.csv
create mode 100644 test/inputs/chuhsi_RegUp_prices_5min_300.csv
create mode 100644 test/inputs/chuhsi_Spin_prices_24.csv
create mode 100644 test/inputs/chuhsi_Spin_prices_5min.csv
create mode 100644 test/inputs/chuhsi_Spin_prices_5min_300.csv
diff --git a/src/add_constraints.jl b/src/add_constraints.jl
index 77e84b75..fe39769f 100644
--- a/src/add_constraints.jl
+++ b/src/add_constraints.jl
@@ -2,6 +2,25 @@
#################### Device Model Constraints #####################
###################################################################
+"""Map RT step `rt_t` to a DA index when RT and DA horizon lengths need not divide evenly."""
+function _map_rt_to_da_index(rt_t::Int, rt_count::Int, da_count::Int)
+ @assert rt_count >= 1 && da_count >= 1
+ return min(da_count, div((rt_t - 1) * da_count, rt_count) + 1)
+end
+
+function _has_reserve_slack_variables(
+ container::PSI.OptimizationContainer,
+ ::Type{D},
+) where {D <: PSY.HybridSystem}
+ try
+ PSI.get_variable(container, SlackReserveUp(), D)
+ PSI.get_variable(container, SlackReserveDown(), D)
+ return true
+ catch
+ return false
+ end
+end
+
############ Total Power Constraints, HybridSystem ################
function PSI.add_constraints!(
container::PSI.OptimizationContainer,
@@ -2027,7 +2046,17 @@ function _add_constraints_reserve_assignment!(
time_steps,
)
- tmap = PSY.get_ext(first(devices))["tmap"]
+ da_steps = axes(
+ PSI.get_variable(
+ container,
+ out_var,
+ typeof(first(services)),
+ PSY.get_name(first(services)),
+ ),
+ )[2]
+ rt_count = length(time_steps)
+ da_count = length(da_steps)
+ has_reserve_slack = _has_reserve_slack_variables(container, D)
for service in services
service_name = PSY.get_name(service)
@@ -2035,14 +2064,14 @@ function _add_constraints_reserve_assignment!(
res_in = PSI.get_variable(container, in_var, typeof(service), service_name)
res_var = PSI.get_variable(container, assignment_var, D)
for device in devices, t in time_steps
- horizon_DA = PSY.get_ext(device)["horizon_DA"]
ci_name = PSY.get_name(device)
- if horizon_DA == 24
+ da_t = _map_rt_to_da_index(t, rt_count, da_count)
+ if has_reserve_slack
slack_up = PSI.get_variable(container, SlackReserveUp(), D)
slack_dn = PSI.get_variable(container, SlackReserveDown(), D)
con[ci_name, service_name, t] = JuMP.@constraint(
PSI.get_jump_model(container),
- res_out[ci_name, tmap[t]] + res_in[ci_name, tmap[t]] -
+ res_out[ci_name, da_t] + res_in[ci_name, da_t] -
res_var[ci_name, service_name, t] -
slack_up[ci_name, service_name, t] +
slack_dn[ci_name, service_name, t] == 0.0
@@ -2050,7 +2079,7 @@ function _add_constraints_reserve_assignment!(
else
con[ci_name, service_name, t] = JuMP.@constraint(
PSI.get_jump_model(container),
- res_out[ci_name, tmap[t]] + res_in[ci_name, tmap[t]] -
+ res_out[ci_name, da_t] + res_in[ci_name, da_t] -
res_var[ci_name, service_name, t] == 0.0
)
end
@@ -2280,19 +2309,21 @@ function add_constraints_realtimelimit_out_withreserves!(
PSI.add_constraints_container!(container, T(), D, names, time_steps; meta = "ub")
con_lb =
PSI.add_constraints_container!(container, T(), D, names, time_steps; meta = "lb")
+ da_count = size(res_out_up, 2)
+ rt_count = length(time_steps)
for device in devices, t in time_steps
- tmap = PSY.get_ext(device)["tmap"]
ci_name = PSY.get_name(device)
+ da_t = _map_rt_to_da_index(t, rt_count, da_count)
max_limit = PSI.get_variable_upper_bound(PSI.ActivePowerOutVariable(), device, W())
@assert max_limit !== nothing ci_name
con_ub[ci_name, t] = JuMP.@constraint(
PSI.get_jump_model(container),
- bid_out[ci_name, t] + res_out_up[ci_name, tmap[t]] <= max_limit
+ bid_out[ci_name, t] + res_out_up[ci_name, da_t] <= max_limit
)
con_lb[ci_name, t] = JuMP.@constraint(
PSI.get_jump_model(container),
- bid_out[ci_name, t] - res_out_down[ci_name, tmap[t]] >= 0.0
+ bid_out[ci_name, t] - res_out_down[ci_name, da_t] >= 0.0
)
end
return
@@ -2317,19 +2348,21 @@ function add_constraints_realtimelimit_in_withreserves!(
PSI.add_constraints_container!(container, T(), D, names, time_steps; meta = "ub")
con_lb =
PSI.add_constraints_container!(container, T(), D, names, time_steps; meta = "lb")
+ da_count = size(res_in_up, 2)
+ rt_count = length(time_steps)
for device in devices, t in time_steps
- tmap = PSY.get_ext(device)["tmap"]
ci_name = PSY.get_name(device)
+ da_t = _map_rt_to_da_index(t, rt_count, da_count)
max_limit = PSI.get_variable_upper_bound(PSI.ActivePowerInVariable(), device, W())
@assert max_limit !== nothing ci_name
con_ub[ci_name, t] = JuMP.@constraint(
PSI.get_jump_model(container),
- bid_in[ci_name, t] + res_in_down[ci_name, tmap[t]] <= max_limit
+ bid_in[ci_name, t] + res_in_down[ci_name, da_t] <= max_limit
)
con_lb[ci_name, t] = JuMP.@constraint(
PSI.get_jump_model(container),
- bid_in[ci_name, t] - res_in_up[ci_name, tmap[t]] >= 0.0
+ bid_in[ci_name, t] - res_in_up[ci_name, da_t] >= 0.0
)
end
return
@@ -2355,18 +2388,20 @@ function _add_thermallimit_withreserves!(
PSI.add_constraints_container!(container, T(), D, names, time_steps; meta = "ub")
con_lb =
PSI.add_constraints_container!(container, T(), D, names, time_steps; meta = "lb")
+ da_count = size(varon, 2)
+ rt_count = length(time_steps)
for device in devices, t in time_steps
- tmap = PSY.get_ext(device)["tmap"]
ci_name = PSY.get_name(device)
+ da_t = _map_rt_to_da_index(t, rt_count, da_count)
min_limit, max_limit = PSY.get_active_power_limits(PSY.get_thermal_unit(device))
con_ub[ci_name, t] = JuMP.@constraint(
PSI.get_jump_model(container),
- p_th[ci_name, t] + reg_th_up[ci_name, t] <= max_limit * varon[ci_name, tmap[t]]
+ p_th[ci_name, t] + reg_th_up[ci_name, t] <= max_limit * varon[ci_name, da_t]
)
con_lb[ci_name, t] = JuMP.@constraint(
PSI.get_jump_model(container),
- p_th[ci_name, t] - reg_th_dn[ci_name, t] >= min_limit * varon[ci_name, tmap[t]]
+ p_th[ci_name, t] - reg_th_dn[ci_name, t] >= min_limit * varon[ci_name, da_t]
)
end
end
@@ -2387,14 +2422,16 @@ function _add_constraints_thermalon_variableon!(
p_th = PSI.get_variable(container, ThermalPower(), D)
con_ub =
PSI.add_constraints_container!(container, T(), D, names, time_steps; meta = "ub")
+ da_count = size(varon, 2)
+ rt_count = length(time_steps)
for device in devices, t in time_steps
- tmap = PSY.get_ext(device)["tmap"]
ci_name = PSY.get_name(device)
+ da_t = _map_rt_to_da_index(t, rt_count, da_count)
max_limit = PSY.get_active_power_limits(PSY.get_thermal_unit(device)).max
con_ub[ci_name, t] = JuMP.@constraint(
PSI.get_jump_model(container),
- p_th[ci_name, t] <= max_limit * varon[ci_name, tmap[t]]
+ p_th[ci_name, t] <= max_limit * varon[ci_name, da_t]
)
end
return
@@ -2416,14 +2453,16 @@ function _add_constraints_thermalon_variableoff!(
p_th = PSI.get_variable(container, ThermalPower(), D)
con_lb =
PSI.add_constraints_container!(container, T(), D, names, time_steps; meta = "lb")
+ da_count = size(varon, 2)
+ rt_count = length(time_steps)
for device in devices, t in time_steps
- tmap = PSY.get_ext(device)["tmap"]
ci_name = PSY.get_name(device)
+ da_t = _map_rt_to_da_index(t, rt_count, da_count)
min_limit = PSY.get_active_power_limits(PSY.get_thermal_unit(device)).min
con_lb[ci_name, t] = JuMP.@constraint(
PSI.get_jump_model(container),
- min_limit * varon[ci_name, tmap[t]] <= p_th[ci_name, t]
+ min_limit * varon[ci_name, da_t] <= p_th[ci_name, t]
)
end
return
@@ -2530,8 +2569,9 @@ function _add_constraints_reservebalance!(
time_steps;
meta = service_name,
)
+ da_count = size(res_out, 2)
+ rt_count = length(time_steps)
for device in devices
- tmap = PSY.get_ext(device)["tmap"]
ci_name = PSY.get_name(device)
vars_pos = Set{JUMP_SET_TYPE}()
@@ -2570,7 +2610,8 @@ function _add_constraints_reservebalance!(
push!(vars_pos, res_ch[ci_name, :])
end
for t in time_steps
- total_reserve = -res_out[ci_name, tmap[t]] - res_in[ci_name, tmap[t]]
+ da_t = _map_rt_to_da_index(t, rt_count, da_count)
+ total_reserve = -res_out[ci_name, da_t] - res_in[ci_name, da_t]
for vp in vars_pos
JuMP.add_to_expression!(total_reserve, vp[t])
end
@@ -2599,14 +2640,16 @@ function _add_constraints_out_marketconvergence!(
res_out_up = PSI.get_expression(container, ServedReserveOutUpExpression(), D)
res_out_down = PSI.get_expression(container, ServedReserveOutDownExpression(), D)
con = PSI.add_constraints_container!(container, T(), D, names, time_steps)
+ da_count = size(res_out_up, 2)
+ rt_count = length(time_steps)
for device in devices, t in time_steps
- tmap = PSY.get_ext(device)["tmap"]
ci_name = PSY.get_name(device)
+ da_t = _map_rt_to_da_index(t, rt_count, da_count)
con[ci_name, t] = JuMP.@constraint(
PSI.get_jump_model(container),
- bid_out[ci_name, t] + res_out_up[ci_name, tmap[t]] -
- res_out_down[ci_name, tmap[t]] == p_out[ci_name, t]
+ bid_out[ci_name, t] + res_out_up[ci_name, da_t] -
+ res_out_down[ci_name, da_t] == p_out[ci_name, t]
)
end
return
@@ -2628,14 +2671,16 @@ function _add_constraints_in_marketconvergence!(
res_in_up = PSI.get_expression(container, ServedReserveInUpExpression(), D)
res_in_down = PSI.get_expression(container, ServedReserveInDownExpression(), D)
con = PSI.add_constraints_container!(container, T(), D, names, time_steps)
+ da_count = size(res_in_up, 2)
+ rt_count = length(time_steps)
for device in devices, t in time_steps
- tmap = PSY.get_ext(device)["tmap"]
ci_name = PSY.get_name(device)
+ da_t = _map_rt_to_da_index(t, rt_count, da_count)
con[ci_name, t] = JuMP.@constraint(
PSI.get_jump_model(container),
- bid_in[ci_name, t] + res_in_down[ci_name, tmap[t]] -
- res_in_up[ci_name, tmap[t]] == p_in[ci_name, t]
+ bid_in[ci_name, t] + res_in_down[ci_name, da_t] -
+ res_in_up[ci_name, da_t] == p_in[ci_name, t]
)
end
return
@@ -2961,15 +3006,17 @@ function add_constraints!(
sos_constraint =
PSI.add_constraints_container!(container, T(), D, names, time_steps; meta = "sos")
jm = PSI.get_jump_model(container)
+ da_count = size(varon, 2)
+ rt_count = length(time_steps)
for dev in devices
- tmap = PSY.get_ext(dev)["tmap"]
n = PSY.get_name(dev)
thermal = PSY.get_thermal_unit(dev)
p_max_th = PSY.get_active_power_limits(thermal).max
for t in time_steps
+ da_t = _map_rt_to_da_index(t, rt_count, da_count)
assignment_constraint[n, t] = JuMP.@constraint(
jm,
- k_variable[n, t] == primal_var[n, t] - varon[n, tmap[t]] * p_max_th
+ k_variable[n, t] == primal_var[n, t] - varon[n, da_t] * p_max_th
)
sos_constraint[n, t] =
JuMP.@constraint(jm, [k_variable[n, t], dual_var[n, t]] in JuMP.SOS1())
@@ -2999,15 +3046,17 @@ function add_constraints!(
sos_constraint =
PSI.add_constraints_container!(container, T(), D, names, time_steps; meta = "sos")
jm = PSI.get_jump_model(container)
+ da_count = size(varon, 2)
+ rt_count = length(time_steps)
for dev in devices
- tmap = PSY.get_ext(dev)["tmap"]
n = PSY.get_name(dev)
thermal = PSY.get_thermal_unit(dev)
p_min_th = PSY.get_active_power_limits(thermal).min
for t in time_steps
+ da_t = _map_rt_to_da_index(t, rt_count, da_count)
assignment_constraint[n, t] = JuMP.@constraint(
jm,
- k_variable[n, t] == -primal_var[n, t] + varon[n, tmap[t]] * p_min_th
+ k_variable[n, t] == -primal_var[n, t] + varon[n, da_t] * p_min_th
)
sos_constraint[n, t] =
JuMP.@constraint(jm, [k_variable[n, t], dual_var[n, t]] in JuMP.SOS1())
diff --git a/src/add_parameters.jl b/src/add_parameters.jl
index 5fcf8206..b12a186f 100644
--- a/src/add_parameters.jl
+++ b/src/add_parameters.jl
@@ -2,41 +2,187 @@
################### Decision Model Parameters #####################
###################################################################
+function _hybrid_profile_initial_values(
+ container::PSI.OptimizationContainer,
+ ts_type,
+ device::PSY.HybridSystem,
+ ts_name::String,
+ model_resolution,
+ model_interval,
+ feat_kw::NamedTuple,
+)
+ initial_time = PSI.get_initial_time(container)
+ time_steps = PSI.get_time_steps(container)
+ forecast = PSY.get_time_series(
+ ts_type,
+ device,
+ ts_name;
+ start_time = initial_time,
+ count = 1,
+ interval = PSI._to_is_interval(model_interval),
+ resolution = PSI._to_is_resolution(model_resolution),
+ feat_kw...,
+ )
+ return IS.get_time_series_values(
+ device,
+ forecast;
+ start_time = initial_time,
+ len = length(time_steps),
+ ignore_scaling_factors = true,
+ )
+end
+
+"""
+Read injection profile points (`RenewableDispatch__max_active_power`, `PowerLoad__max_active_power`)
+from the wrapped `SingleTimeSeries` stored on the hybrid, slicing `length(time_steps)` contiguous
+values from `start_time`. This avoids `DeterministicSingleTimeSeries` forecast windows that may
+only span a short sub-interval of the underlying data.
+"""
+function _hybrid_profile_parameter_slice(
+ container::PSI.OptimizationContainer,
+ device::PSY.HybridSystem,
+ ts_name::String,
+ start_time::Dates.DateTime;
+ feat_kw::NamedTuple = (;),
+)
+ n = length(PSI.get_time_steps(container))
+ sts = _unwrap_hybrid_underlying_single_time_series(container, device, ts_name, feat_kw)
+ ta = IS.get_data(sts)
+ timestamps = collect(getfield(ta, :timestamp))
+ valmatrix = getfield(ta, :values)
+ vals = ndims(valmatrix) == 1 ? Vector(valmatrix) : vec(valmatrix[:, 1])
+ start_ix = PSI.find_timestamp_index(timestamps, start_time)
+ start_ix + n - 1 <= length(vals) ||
+ error(
+ "Hybrid profile $(repr(ts_name)) on $(PSY.get_name(device)) ends before step $(n) at $(start_time); " *
+ "ensure the underlying SingleTimeSeries spans the optimization horizon (see test helpers `ensure_hybrid_injection_profiles!`).",
+ )
+ return vals[start_ix:(start_ix + n - 1)]
+end
+
+function _get_hybrid_profile_parameter_values(
+ container::PSI.OptimizationContainer,
+ device::PSY.HybridSystem,
+ ts_name::String;
+ feat_kw::NamedTuple = (;),
+)
+ return _hybrid_profile_parameter_slice(
+ container,
+ device,
+ ts_name,
+ PSI.get_initial_time(container);
+ feat_kw,
+ )
+end
+
+function _unwrap_hybrid_underlying_single_time_series(
+ container::PSI.OptimizationContainer,
+ device::PSY.HybridSystem,
+ ts_name::String,
+ feat_kw::NamedTuple,
+)
+ settings = PSI.get_settings(container)
+ res_kw = PSI._to_is_resolution(PSI.get_resolution(settings))
+ int_kw = PSI._to_is_interval(PSI.get_interval(settings))
+ try
+ if isempty(feat_kw)
+ return PSY.get_time_series(IS.SingleTimeSeries, device, ts_name)
+ end
+ return PSY.get_time_series(IS.SingleTimeSeries, device, ts_name; feat_kw...)
+ catch
+ if isempty(feat_kw)
+ dst = PSY.get_time_series(
+ IS.DeterministicSingleTimeSeries,
+ device,
+ ts_name;
+ resolution = res_kw,
+ interval = int_kw,
+ )
+ else
+ dst = PSY.get_time_series(
+ IS.DeterministicSingleTimeSeries,
+ device,
+ ts_name;
+ resolution = res_kw,
+ interval = int_kw,
+ feat_kw...,
+ )
+ end
+ return IS.get_single_time_series(dst)
+ end
+end
+
function _add_time_series_parameters(
container::PSI.OptimizationContainer,
ts_name::String,
param,
- devices::AbstractVector{<:PSY.HybridSystem},
+ devices::AbstractVector{<:PSY.HybridSystem};
+ timeseries_key::Union{Nothing, String} = nothing,
)
- ts_name
- ts_type = PSI.get_default_time_series_type(container)
+ # Injection profiles live as static `SingleTimeSeries` on the hybrid; registering them as the
+ # system default `DeterministicSingleTimeSeries` breaks simulation updates (HDF5 slice vs full
+ # horizon) when PSI advances `current_time`.
+ ts_type =
+ if timeseries_key === nothing
+ PSY.SingleTimeSeries
+ else
+ PSI.get_default_time_series_type(container)
+ end
time_steps = PSI.get_time_steps(container)
settings = PSI.get_settings(container)
model_resolution = PSI.get_resolution(settings)
model_interval = PSI.get_interval(settings)
+ feat_kw =
+ if timeseries_key === nothing
+ (;)
+ else
+ (; HYBRID_TIME_SERIES_FEATURE_KEY => timeseries_key)
+ end
device_names = String[]
initial_values = Dict{String, AbstractArray}()
for device in devices
push!(device_names, PSY.get_name(device))
- ts_uuid = string(
- IS.get_time_series_uuid(
- ts_type,
- device,
- ts_name;
- resolution = PSI._to_is_resolution(model_resolution),
- interval = PSI._to_is_interval(model_interval),
- ),
- )
+ ts_metadata =
+ if ts_type === PSY.SingleTimeSeries
+ IS.get_time_series_metadata(
+ PSY.SingleTimeSeries,
+ device,
+ ts_name;
+ resolution = PSI._to_is_resolution(model_resolution),
+ feat_kw...,
+ )
+ else
+ IS.get_time_series_metadata(
+ ts_type,
+ device,
+ ts_name;
+ resolution = PSI._to_is_resolution(model_resolution),
+ interval = PSI._to_is_interval(model_interval),
+ feat_kw...,
+ )
+ end
+ ts_uuid = string(IS.get_time_series_uuid(ts_metadata))
if !(ts_uuid in keys(initial_values))
- initial_values[ts_uuid] = PSI.get_time_series_initial_values!(
- container,
- ts_type,
- device,
- ts_name;
- resolution = model_resolution,
- interval = model_interval,
- )
+ initial_values[ts_uuid] =
+ if timeseries_key === nothing
+ _get_hybrid_profile_parameter_values(
+ container,
+ device,
+ ts_name;
+ feat_kw,
+ )
+ else
+ _hybrid_profile_initial_values(
+ container,
+ ts_type,
+ device,
+ ts_name,
+ model_resolution,
+ model_interval,
+ feat_kw,
+ )
+ end
end
end
@@ -65,23 +211,75 @@ function _add_time_series_parameters(
for step in time_steps
PSI.set_multiplier!(param_container, multiplier, name, step)
end
- PSI.add_component_name!(
- PSI.get_attributes(param_container),
- name,
- string(
- IS.get_time_series_uuid(
+ ts_metadata =
+ if ts_type === PSY.SingleTimeSeries
+ IS.get_time_series_metadata(
+ PSY.SingleTimeSeries,
+ device,
+ ts_name;
+ resolution = PSI._to_is_resolution(model_resolution),
+ feat_kw...,
+ )
+ else
+ IS.get_time_series_metadata(
ts_type,
device,
ts_name;
resolution = PSI._to_is_resolution(model_resolution),
interval = PSI._to_is_interval(model_interval),
- ),
- ),
+ feat_kw...,
+ )
+ end
+ PSI.add_component_name!(
+ PSI.get_attributes(param_container),
+ name,
+ string(IS.get_time_series_uuid(ts_metadata)),
)
end
return
end
+function PSI._update_parameter_values!(
+ parameter_array::AbstractArray{T},
+ ::P,
+ attributes::PSI.TimeSeriesAttributes{PSY.SingleTimeSeries},
+ ::Type{PSY.HybridSystem},
+ model::PSI.DecisionModel,
+ ::PSI.DatasetContainer{PSI.InMemoryDataset},
+) where {
+ T <: Union{JuMP.VariableRef, Float64},
+ P <: Union{RenewablePowerTimeSeries, ElectricLoadTimeSeries},
+}
+ container = PSI.get_optimization_container(model)
+ ts_name = PSI.get_time_series_name(attributes)
+ current_time = PSI.get_current_time(model)
+ template = PSI.get_template(model)
+ device_model = PSI.get_model(template, PSY.HybridSystem)
+ components = PSI.get_available_components(device_model, PSI.get_system(model))
+ ts_uuids = Set{String}()
+ for component in components
+ ts_uuid = PSI._get_ts_uuid(attributes, PSY.get_name(component))
+ if !(ts_uuid in ts_uuids)
+ ts_vector = _hybrid_profile_parameter_slice(
+ container,
+ component,
+ ts_name,
+ current_time,
+ )
+ for (t, value) in enumerate(ts_vector)
+ if !isfinite(value)
+ error(
+ "Hybrid profile $(repr(ts_name)) has non-finite value at step $t for $(PSY.get_name(component))",
+ )
+ end
+ PSI._set_param_value!(parameter_array, value, ts_uuid, t)
+ end
+ push!(ts_uuids, ts_uuid)
+ end
+ end
+ return
+end
+
function _get_hybrid_ts_multiplier(::RenewablePowerTimeSeries, device::PSY.HybridSystem)
return PSY.get_max_active_power(PSY.get_renewable_unit(device))
end
@@ -90,6 +288,30 @@ function _get_hybrid_ts_multiplier(::ElectricLoadTimeSeries, device::PSY.HybridS
return PSY.get_max_active_power(PSY.get_electric_load(device))
end
+function _get_hybrid_scalar_forecast_values(
+ container::PSI.OptimizationContainer,
+ hybrid::PSY.HybridSystem,
+ ts_full_name::String;
+ forecast_time::Union{Nothing, Dates.DateTime} = nothing,
+ n_steps::Union{Nothing, Int} = nothing,
+)
+ initial_time = something(forecast_time, PSI.get_initial_time(container))
+ n = something(n_steps, length(PSI.get_time_steps(container)))
+ # Merchant hybrid prices are attached as `SingleTimeSeries` with explicit names; read the
+ # contiguous stored array directly so we are not limited by a shorter deterministic forecast
+ # window from system-wide transforms.
+ ts = PSY.get_time_series(IS.SingleTimeSeries, hybrid, ts_full_name)
+ data = IS.get_data(ts)
+ timestamps = getfield(data, :timestamp)
+ values = getfield(data, :values)
+ start_ix = PSI.find_timestamp_index(timestamps, initial_time)
+ end_ix = min(size(values, 1), start_ix + n - 1)
+ end_ix < start_ix + n - 1 && error(
+ "Scalar series $(repr(ts_full_name)) ends before step $(n) at $(initial_time) on $(PSY.get_name(hybrid))",
+ )
+ return vec(values[start_ix:end_ix, 1])
+end
+
# Multipliers consider that the objective function is a Maximization problem
# But the default direction in PSI is Min.
_get_multiplier(::Type{EnergyDABidOut}, ::DayAheadEnergyPrice) = -1.0
@@ -105,12 +327,23 @@ _get_multiplier(::Type{BidReserveVariableIn}, ::AncillaryServicePrice) = 1.0
function _add_price_time_series_parameters(
container::PSI.OptimizationContainer,
param::Union{RealTimeEnergyPrice, DayAheadEnergyPrice},
- ts_key::String,
+ ts_full_name::String,
devices::Vector{PSY.HybridSystem},
- time_step_string::String,
vars::Vector,
)
- time_steps = 1:PSY.get_ext(first(devices))[time_step_string]
+ time_steps =
+ if param isa DayAheadEnergyPrice
+ merchant_da_time_step_range(container, first(devices))
+ else
+ PSI.get_time_steps(container)
+ end
+ n_price = length(time_steps)
+ first_values = _get_hybrid_scalar_forecast_values(
+ container,
+ first(devices),
+ ts_full_name;
+ n_steps = n_price,
+ )
device_names = PSY.get_name.(devices)
jump_model = PSI.get_jump_model(container)
@@ -129,9 +362,12 @@ function _add_price_time_series_parameters(
)
for device in devices
- λ = PSY.get_ext(device)[ts_key]
- Bus_name = PSY.get_name(PSY.get_bus(device))
- price_value = λ[!, Bus_name]
+ price_value = _get_hybrid_scalar_forecast_values(
+ container,
+ device,
+ ts_full_name;
+ n_steps = n_price,
+ )
name = PSY.get_name(device)
for step in time_steps
PSI.set_parameter!(
@@ -159,17 +395,24 @@ function _add_price_time_series_parameters(
param::AncillaryServicePrice,
ts_key::String,
devices::Vector{PSY.HybridSystem},
- time_step_string::String,
vars::Vector,
)
- time_steps = 1:PSY.get_ext(first(devices))[time_step_string]
- device_names = PSY.get_name.(devices)
- jump_model = PSI.get_jump_model(container)
-
services = Set()
for d in devices
union!(services, PSY.get_services(d))
end
+ isempty(services) && return
+ first_service = PSY.get_name(first(services))
+ time_steps = merchant_da_time_step_range(container, first(devices))
+ n_price = length(time_steps)
+ first_values = _get_hybrid_scalar_forecast_values(
+ container,
+ first(devices),
+ hybrid_ancillary_service_price_time_series_name(first_service, ts_key);
+ n_steps = n_price,
+ )
+ device_names = PSY.get_name.(devices)
+ jump_model = PSI.get_jump_model(container)
for var in vars
for service in services
service_name = PSY.get_name(service)
@@ -187,10 +430,12 @@ function _add_price_time_series_parameters(
)
for device in devices
- ts_key_service = "$(ts_key)_$(service_name)"
- λ = PSY.get_ext(device)[ts_key_service]
- Bus_name = PSY.get_name(PSY.get_bus(device))
- price_value = λ[!, Bus_name]
+ price_value = _get_hybrid_scalar_forecast_values(
+ container,
+ device,
+ hybrid_ancillary_service_price_time_series_name(service_name, ts_key);
+ n_steps = n_price,
+ )
name = PSY.get_name(device)
for step in time_steps
PSI.set_parameter!(
@@ -217,18 +462,20 @@ function add_time_series_parameters!(
container::PSI.OptimizationContainer,
param::RenewablePowerTimeSeries,
devices::AbstractVector{<:PSY.HybridSystem},
- ts_name = "RenewableDispatch__max_active_power",
+ ts_name = "RenewableDispatch__max_active_power";
+ timeseries_key::Union{Nothing, String} = nothing,
)
- _add_time_series_parameters(container, ts_name, param, devices)
+ _add_time_series_parameters(container, ts_name, param, devices; timeseries_key)
end
function add_time_series_parameters!(
container::PSI.OptimizationContainer,
param::ElectricLoadTimeSeries,
devices::AbstractVector{<:PSY.HybridSystem},
- ts_name = "PowerLoad__max_active_power",
+ ts_name = "PowerLoad__max_active_power";
+ timeseries_key::Union{Nothing, String} = nothing,
)
- _add_time_series_parameters(container, ts_name, param, devices)
+ _add_time_series_parameters(container, ts_name, param, devices; timeseries_key)
return
end
@@ -242,7 +489,12 @@ function PSI._add_parameters!(
U <: Union{Vector{D}, IS.FlattenIteratorWrapper{D}},
W <: AbstractHybridFormulation,
} where {D <: PSY.HybridSystem}
- add_time_series_parameters!(container, param, collect(devices))
+ add_time_series_parameters!(
+ container,
+ param,
+ collect(devices);
+ timeseries_key = nothing,
+ )
return
end
@@ -263,9 +515,15 @@ function add_time_series_parameters!(
param::DayAheadEnergyPrice,
devices::Vector{PSY.HybridSystem},
)
- ts_key = "λ_da_df"
+ ts_key = get_day_ahead_time_series_key(container)
vars = [EnergyDABidOut, EnergyDABidIn]
- _add_price_time_series_parameters(container, param, ts_key, devices, "horizon_DA", vars)
+ _add_price_time_series_parameters(
+ container,
+ param,
+ hybrid_energy_price_time_series_name(ts_key),
+ devices,
+ vars,
+ )
return
end
@@ -274,9 +532,15 @@ function add_time_series_parameters!(
param::RealTimeEnergyPrice,
devices::Vector{PSY.HybridSystem},
)
- ts_key = "λ_rt_df"
+ ts_key = get_real_time_time_series_key(container)
vars = [EnergyDABidOut, EnergyDABidIn, EnergyRTBidOut, EnergyRTBidIn]
- _add_price_time_series_parameters(container, param, ts_key, devices, "horizon_RT", vars)
+ _add_price_time_series_parameters(
+ container,
+ param,
+ hybrid_energy_price_time_series_name(ts_key),
+ devices,
+ vars,
+ )
return
end
@@ -285,9 +549,9 @@ function add_time_series_parameters!(
param::AncillaryServicePrice,
devices::Vector{PSY.HybridSystem},
)
- ts_key = "λ"
+ ts_key = get_day_ahead_time_series_key(container)
vars = [BidReserveVariableOut, BidReserveVariableIn]
- _add_price_time_series_parameters(container, param, ts_key, devices, "horizon_DA", vars)
+ _add_price_time_series_parameters(container, param, ts_key, devices, vars)
return
end
@@ -302,12 +566,59 @@ function PSI.update_parameter_values!(
return
end
+"""
+Clamp decision-state writes for merchant hybrid price parameters when store horizon extends
+beyond the state buffer length during rolling simulation updates.
+"""
+function PSI.update_decision_state!(
+ state::PSI.SimulationState,
+ key::PSI.ParameterKey{T, PSY.HybridSystem},
+ store_data::PSI.DenseAxisArray{Float64, 2},
+ simulation_time::Dates.DateTime,
+ model_params::PSI.ModelStoreParams,
+) where {T <: Union{DayAheadEnergyPrice, RealTimeEnergyPrice}}
+ state_data = PSI.get_decision_state_data(state, key)
+ column_names = PSI.get_column_names(key, state_data)[1]
+ model_resolution = PSI.get_resolution(model_params)
+ state_resolution = PSI.get_data_resolution(state_data)
+ resolution_ratio = model_resolution ÷ state_resolution
+ state_timestamps = state_data.timestamps
+ PSI.IS.@assert_op resolution_ratio >= 1
+
+ if simulation_time > PSI.get_end_of_step_timestamp(state_data)
+ state_data_index = 1
+ state_data.timestamps[:] .= range(
+ simulation_time;
+ step = state_resolution,
+ length = PSI.get_num_rows(state_data),
+ )
+ else
+ state_data_index = PSI.find_timestamp_index(state_timestamps, simulation_time)
+ end
+
+ max_state_index = PSI.get_num_rows(state_data)
+ offset = resolution_ratio - 1
+ result_time_index = axes(store_data)[2]
+ PSI.set_update_timestamp!(state_data, simulation_time)
+ for t in result_time_index
+ state_data_index > max_state_index && break
+ state_range = state_data_index:min(max_state_index, state_data_index + offset)
+ for name in column_names, i in state_range
+ state_data.values[name, i] = store_data[name, t]
+ end
+ PSI.set_last_recorded_row!(state_data, state_range[end])
+ state_data_index += resolution_ratio
+ end
+ return
+end
+
"""
During `Simulation` execution, PSI calls `_update_parameter_values!(..., ::ObjectiveFunctionParameter, ...)`
from `update_cost_parameters.jl`, which uses `handle_variable_cost_parameter` with
-`PSY.get_operation_cost(component)`. Merchant hybrids use `MarketBidCost(nothing)` while prices
-live in `ext`; that generic path has no `MarketBidCost` method. Route simulation updates to the
-same ext-based logic as `update_parameter_values!(..., ::InMemoryDataset)`.
+`PSY.get_operation_cost(component)`. Merchant hybrids use `MarketBidCost(nothing)`; energy prices
+are read from hybrid-attached scalar `"HybridSystem__energy_price"` time series (keyed DA/RT) instead.
+This hooks the generic simulation update path into the same hybrid scalar forecast logic as
+`update_parameter_values!(..., ::InMemoryDataset)`.
"""
function _merchant_hybrid_price_parameter_key(
container::PSI.OptimizationContainer,
@@ -381,14 +692,19 @@ function _update_parameter_values!(
parameter_multiplier = PSI.get_parameter_multiplier_array(container, key)
attributes = PSI.get_parameter_attributes(container, key)
components = PSI.get_available_components(PSY.HybridSystem, PSI.get_system(model))
- resolution = PSI.get_resolution(container)
- dt = Dates.value(Dates.Second(resolution)) / PSI.SECONDS_IN_HOUR
+ ts_key = get_day_ahead_time_series_key(container)
+ n_da = min(
+ length(merchant_da_time_step_range(container, first(components))),
+ length(PSI.get_time_steps(container)),
+ )
for component in components
- ext = PSY.get_ext(component)
- horizon = ext["horizon_DA"]
- bus_name = PSY.get_name(PSY.get_bus(component))
- ix = PSI.find_timestamp_index(ext["λ_da_df"][!, "DateTime"], initial_forecast_time)
- λ = ext["λ_da_df"][!, bus_name][ix:(ix + horizon - 1)]
+ λ = _get_hybrid_scalar_forecast_values(
+ container,
+ component,
+ hybrid_energy_price_time_series_name(ts_key);
+ forecast_time = PSI.get_current_time(model),
+ n_steps = n_da,
+ )
name = PSY.get_name(component)
for (t, value) in enumerate(λ)
# Since the DA variables are hourly, this will revert the dt multiplication
@@ -422,23 +738,26 @@ function _update_parameter_values!(
model::PSI.DecisionModel{T},
key::PSI.ParameterKey{RealTimeEnergyPrice, PSY.HybridSystem},
) where {T <: HybridDecisionProblem}
- initial_forecast_time = PSI.get_current_time(model)
container = PSI.get_optimization_container(model)
resolution = PSI.get_resolution(container)
dt = Dates.value(Dates.Second(resolution)) / PSI.SECONDS_IN_HOUR
+ da_len = size(PSI.get_variable(container, EnergyDABidOut(), PSY.HybridSystem), 2)
+ rt_len = size(PSI.get_variable(container, EnergyRTBidOut(), PSY.HybridSystem), 2)
+ tmap = merchant_rt_to_da_tmap(rt_len, da_len)
parameter_array = PSI.get_parameter_array(container, key)
attributes = PSI.get_parameter_attributes(container, key)
components = PSI.get_available_components(PSY.HybridSystem, PSI.get_system(model))
Vtype = _merchant_real_time_price_variable_type(key.meta)
variable = PSI.get_variable(container, Vtype(), PSY.HybridSystem)
parameter_multiplier = PSI.get_parameter_multiplier_array(container, key)
+ ts_key = get_real_time_time_series_key(container)
for component in components
- ext = PSY.get_ext(component)
- tmap = ext["tmap"]
- horizon = ext["horizon_RT"]
- bus_name = PSY.get_name(PSY.get_bus(component))
- ix = PSI.find_timestamp_index(ext["λ_rt_df"][!, "DateTime"], initial_forecast_time)
- λ = ext["λ_rt_df"][!, bus_name][ix:(ix + horizon - 1)]
+ λ = _get_hybrid_scalar_forecast_values(
+ container,
+ component,
+ hybrid_energy_price_time_series_name(ts_key);
+ forecast_time = PSI.get_current_time(model),
+ )
name = PSY.get_name(component)
for (t, value) in enumerate(λ)
mul_ = parameter_multiplier[name, t] * 100.0
diff --git a/src/add_variables.jl b/src/add_variables.jl
index 521d608b..434f35a1 100644
--- a/src/add_variables.jl
+++ b/src/add_variables.jl
@@ -2,6 +2,18 @@
################### Decision Model Variables ######################
###################################################################
+function _get_day_ahead_time_steps(
+ container::PSI.OptimizationContainer,
+ devices::Vector{PSY.HybridSystem},
+)
+ da_key = get_day_ahead_time_series_key(container)
+ metadata = first_matching_hybrid_scalar_metadata(
+ first(devices),
+ hybrid_energy_price_time_series_name(da_key),
+ )
+ return 1:time_series_metadata_horizon_steps(metadata)
+end
+
# Energy Day-Ahead Bids
function PSI.add_variables!(
container::PSI.OptimizationContainer,
@@ -10,7 +22,7 @@ function PSI.add_variables!(
formulation::U,
) where {T <: Union{EnergyDABidOut, EnergyDABidIn}, U <: AbstractHybridFormulation}
@assert !isempty(devices)
- time_steps = PSY.get_ext(first(devices))["T_da"]
+ time_steps = _get_day_ahead_time_steps(container, devices)
variable = PSI.add_variable_container!(
container,
T(),
@@ -45,7 +57,7 @@ function PSI.add_variables!(
U <: Union{MerchantHybridEnergyCase, MerchantModelWithReserves},
}
@assert !isempty(devices)
- time_steps = PSY.get_ext(first(devices))["T_da"]
+ time_steps = _get_day_ahead_time_steps(container, devices)
variable = PSI.add_variable_container!(
container,
T(),
@@ -73,7 +85,7 @@ function PSI.add_variables!(
formulation::MerchantModelWithReserves,
) where {W <: Union{BidReserveVariableOut, BidReserveVariableIn}}
@assert !isempty(devices)
- time_steps = PSY.get_ext(first(devices))["T_da"]
+ time_steps = _get_day_ahead_time_steps(container, devices)
# TODO
# Best way to create this variable? We need to have all services and its type.
services = Set()
diff --git a/src/core/decision_models.jl b/src/core/decision_models.jl
index 7fbf3cb0..9e7434f6 100644
--- a/src/core/decision_models.jl
+++ b/src/core/decision_models.jl
@@ -1,5 +1,110 @@
abstract type HybridDecisionProblem <: PSI.DecisionProblem end
+const DAY_AHEAD_TIME_SERIES_KEY = "DA"
+const REAL_TIME_TIME_SERIES_KEY = "RT"
+const HYBRID_TIME_SERIES_FEATURE_KEY = :timeseries_key
+const ANCILLARY_PRICE_TIME_SERIES_PREFIX = "HybridSystem__ancillary_service_price__"
+
+"""Scalar energy price time series name for a given user key (e.g. [`DAY_AHEAD_TIME_SERIES_KEY`](@ref))."""
+function hybrid_energy_price_time_series_name(key::AbstractString)
+ return "HybridSystem__energy_price__" * string(key)
+end
+
+"""
+Scalar ancillary price time series name; include the key in the name so DA/RT copies stay
+distinct after `transform_single_time_series!` (metadata `features` are not preserved on the
+`Deterministic` record in InfrastructureSystems).
+"""
+function hybrid_ancillary_service_price_time_series_name(
+ service_name::AbstractString,
+ key::AbstractString = DAY_AHEAD_TIME_SERIES_KEY,
+)
+ return ANCILLARY_PRICE_TIME_SERIES_PREFIX * string(service_name) * "__" * string(key)
+end
+
+"""Match metadata whether the series is still `SingleTimeSeries` or already transformed."""
+function first_matching_hybrid_scalar_metadata(
+ hybrid::PSY.HybridSystem,
+ ts_name::AbstractString,
+)
+ # Prefer STS metadata because its length matches the scalar series points used by
+ # merchant price slicing. DST metadata `count` is the number of forecast windows.
+ for T in (IS.SingleTimeSeries, IS.DeterministicSingleTimeSeries)
+ try
+ return IS.get_time_series_metadata(T, hybrid, string(ts_name))
+ catch e
+ e isa ArgumentError || rethrow()
+ end
+ end
+ throw(
+ ArgumentError(
+ "No time series named $(repr(ts_name)) on hybrid $(repr(PSY.get_name(hybrid)))",
+ ),
+ )
+end
+
+time_series_metadata_horizon_steps(metadata::IS.DeterministicMetadata) =
+ IS.get_count(metadata)
+time_series_metadata_horizon_steps(metadata::IS.SingleTimeSeriesMetadata) =
+ IS.get_length(metadata)
+
+"""Integer-safe DA index for each RT step when DA and RT horizons need not divide evenly."""
+function merchant_rt_to_da_tmap(rt_len::Int, da_len::Int)
+ @assert rt_len >= 1 && da_len >= 1
+ return [min(da_len, div((k - 1) * da_len, rt_len) + 1) for k in 1:rt_len]
+end
+
+"""Day-ahead energy price indices `1:n_DA` aligned with hourly DA slots and attached DA metadata."""
+function merchant_da_time_step_range(
+ container::PSI.OptimizationContainer,
+ hybrid::PSY.HybridSystem,
+)
+ da_key = get_day_ahead_time_series_key(container)
+ da_metadata = first_matching_hybrid_scalar_metadata(
+ hybrid,
+ hybrid_energy_price_time_series_name(da_key),
+ )
+ len_DA_meta = time_series_metadata_horizon_steps(da_metadata)
+ settings = PSI.get_settings(container)
+ h_ms = Dates.value(PSI.get_horizon(settings))
+ # Must use the same unit as `h_ms` (milliseconds); `Dates.value(Hour(1))` is 1, not 3600000.
+ da_slot_ms = Dates.value(Dates.Millisecond(Dates.Hour(1)))
+ n_DA = max(1, div(h_ms, da_slot_ms))
+ return 1:min(n_DA, len_DA_meta)
+end
+
+function get_day_ahead_time_series_key(
+ model::PSI.DecisionModel{<:HybridDecisionProblem},
+)
+ return string(get(model.ext, "day_ahead_time_series_key", DAY_AHEAD_TIME_SERIES_KEY))
+end
+
+function get_real_time_time_series_key(
+ model::PSI.DecisionModel{<:HybridDecisionProblem},
+)
+ return string(get(model.ext, "real_time_time_series_key", REAL_TIME_TIME_SERIES_KEY))
+end
+
+function get_day_ahead_time_series_key(container::PSI.OptimizationContainer)
+ ext = PSI.get_ext(PSI.get_settings(container))
+ return string(get(ext, "day_ahead_time_series_key", DAY_AHEAD_TIME_SERIES_KEY))
+end
+
+function get_real_time_time_series_key(container::PSI.OptimizationContainer)
+ ext = PSI.get_ext(PSI.get_settings(container))
+ return string(get(ext, "real_time_time_series_key", REAL_TIME_TIME_SERIES_KEY))
+end
+
+function set_time_series_keys!(
+ container::PSI.OptimizationContainer,
+ model::PSI.DecisionModel{<:HybridDecisionProblem},
+)
+ ext = PSI.get_ext(PSI.get_settings(container))
+ ext["day_ahead_time_series_key"] = get_day_ahead_time_series_key(model)
+ ext["real_time_time_series_key"] = get_real_time_time_series_key(model)
+ return
+end
+
"""
MerchantHybridEnergyCase
@@ -12,33 +117,22 @@ maximizes profit from energy (e.g. DA/RT spread) subject to internal asset limit
- **System:** A [`PowerSystems.System`](@extref PowerSystems.System) containing at least one
[`PowerSystems.HybridSystem`](@extref PowerSystems.HybridSystem) with the subcomponents
required by the chosen device formulation (e.g. [`HybridEnergyOnlyDispatch`](@ref)).
- - **Time series:** Default names:
-
- | Parameter | Default Time Series Name |
+ - **Attached scalar time series (each hybrid):** Market prices are bus-selected
+ `InfrastructureSystems.SingleTimeSeries` objects with **distinct names** for each logical key
+ (defaults `"DA"` / `"RT"`): see [`hybrid_energy_price_time_series_name`](@ref). Profiles use the
+ standard renewable/load names below. Override keys via `model.ext["day_ahead_time_series_key"]`
+ / `"real_time_time_series_key"` on the [`PowerSimulations.DecisionModel`](@extref PowerSimulations.DecisionModel)
+ (propagated with [`set_time_series_keys!`](@ref)).
+
+ | Role | Time series name |
| :--- | :--- |
- | `RenewablePowerTimeSeries` | `"RenewableDispatch__max_active_power"` |
- | `RenewablePowerTimeSeries` (day-ahead-only merchant builds) | `"RenewableDispatch__max_active_power_da"` |
- | `ElectricLoadTimeSeries` | `"PowerLoad__max_active_power"` |
- - **System ext data:** Keys in the
- [`ext` supplemental data dictionary](@extref additional_fields) on
- [`PowerSystems.System`](@extref PowerSystems.System):
-
- | Key | Required | Description |
- | :--- | :--- | :--- |
- | `"λ_da_df"` | Yes | System-level DA table used primarily for its `"DateTime"` axis when deriving horizon windows; bus-price columns are not used for objective pricing. |
- | `"λ_rt_df"` | Yes | System-level RT table used primarily for its `"DateTime"` axis when deriving horizon windows; bus-price columns are not used for objective pricing. |
- | `"horizon_DA"` | Optional | DA index length used during model build; defaults to `length(ext["λ_da_df"][!, "DateTime"])` when omitted. |
- | `"horizon_RT"` | Optional | RT index length used during model build; defaults to `length(ext["λ_rt_df"][!, "DateTime"])` when omitted. |
-
- - **Hybrid ext data:** Each [`PowerSystems.HybridSystem`](@extref PowerSystems.HybridSystem)
- has its own [`ext` dictionary](@extref additional_fields) with the same keys:
-
- | Key | Required | Description |
- | :--- | :--- | :--- |
- | `"λ_da_df"` | Yes | Hybrid-level DA price table used for bus-level objective prices and rolling parameter updates. |
- | `"λ_rt_df"` | Yes | Hybrid-level RT price table used for bus-level objective prices and rolling parameter updates. |
- | `"horizon_DA"` | Yes (current implementation) | DA parameter time-step dimension used in parameter construction and updates; also referenced in reserve-assignment constraint logic (e.g., `horizon_DA == 24`). |
- | `"horizon_RT"` | Yes (current implementation) | RT parameter time-step dimension used in parameter construction and updates. |
+ | Day-ahead energy price | [`hybrid_energy_price_time_series_name`](@ref)(`day_ahead_time_series_key`) |
+ | Real-time energy price | [`hybrid_energy_price_time_series_name`](@ref)(`real_time_time_series_key`) |
+ | Renewable availability | `"RenewableDispatch__max_active_power"` |
+ | Electric load | `"PowerLoad__max_active_power"` |
+
+ Horizons, resolutions, and DA↔RT step alignment come from model settings plus series metadata (not
+ from `System`/`Hybrid` `ext` DataFrames or `\"λ_*\"` keys).
"""
struct MerchantHybridEnergyCase <: HybridDecisionProblem end
@@ -51,25 +145,9 @@ when solving the real-time subproblem with locked DA bids/offers.
**Data requirements:**
- Same [`PowerSystems.System`](@extref PowerSystems.System),
- [`PowerSystems.HybridSystem`](@extref PowerSystems.HybridSystem), and time-series
- requirements as [`MerchantHybridEnergyCase`](@ref).
- - **System ext data:** Same key requirements as [`MerchantHybridEnergyCase`](@ref):
-
- | Key | Required | Description |
- | :--- | :--- | :--- |
- | `"λ_da_df"` | Yes | System-level DA table used primarily for its `"DateTime"` axis when deriving horizon windows. |
- | `"λ_rt_df"` | Yes | System-level RT table used primarily for its `"DateTime"` axis when deriving horizon windows. |
- | `"horizon_DA"` | Optional | DA index length used during model build; defaults to table length when omitted. |
- | `"horizon_RT"` | Optional | RT index length used during model build; defaults to table length when omitted. |
-
- - **Hybrid ext data:** Same key requirements as [`MerchantHybridEnergyCase`](@ref):
-
- | Key | Required | Description |
- | :--- | :--- | :--- |
- | `"λ_da_df"` | Yes | Hybrid-level DA price table used for bus-level objective prices and rolling parameter updates. |
- | `"λ_rt_df"` | Yes | Hybrid-level RT price table used for bus-level objective prices and rolling parameter updates. |
- | `"horizon_DA"` | Yes (current implementation) | DA parameter time-step dimension used in parameter construction and updates; also referenced in reserve-assignment constraint logic (e.g., `horizon_DA == 24`). |
- | `"horizon_RT"` | Yes (current implementation) | RT parameter time-step dimension used in parameter construction and updates. |
+ [`PowerSystems.HybridSystem`](@extref PowerSystems.HybridSystem), and hybrid-attached
+ time-series contract as [`MerchantHybridEnergyCase`](@ref) (keyed scalar DA/RT prices and
+ profiles on each hybrid).
"""
struct MerchantHybridEnergyFixedDA <: HybridDecisionProblem end
@@ -87,32 +165,10 @@ allocation in RT.
[`PowerSimulations.DeviceModel`](@extref PowerSimulations.DeviceModel) constructed as
`DeviceModel(PSY.HybridSystem, HybridDispatchWithReserves)` (or another appropriate hybrid
formulation with reserves).
- - **Time series:** Default names:
-
- | Parameter | Default Time Series Name |
- | :--- | :--- |
- | `RenewablePowerTimeSeries` | `"RenewableDispatch__max_active_power"` |
- | `RenewablePowerTimeSeries` (day-ahead-only merchant builds) | `"RenewableDispatch__max_active_power_da"` |
- | `ElectricLoadTimeSeries` | `"PowerLoad__max_active_power"` |
- - **System ext data:** Same key requirements as [`MerchantHybridEnergyCase`](@ref):
-
- | Key | Required | Description |
- | :--- | :--- | :--- |
- | `"λ_da_df"` | Yes | System-level DA table used primarily for its `"DateTime"` axis when deriving horizon windows. |
- | `"λ_rt_df"` | Yes | System-level RT table used primarily for its `"DateTime"` axis when deriving horizon windows. |
- | `"horizon_DA"` | Optional | DA index length used during model build; defaults to table length when omitted. |
- | `"horizon_RT"` | Optional | RT index length used during model build; defaults to table length when omitted. |
-
- - **Hybrid ext data:** Keys in each hybrid's
- [`ext` dictionary](@extref additional_fields):
-
- | Key | Required | Description |
- | :--- | :--- | :--- |
- | `"λ_da_df"` | Yes | Hybrid-level DA energy price table used for bus-level objective prices and rolling parameter updates. |
- | `"λ_rt_df"` | Yes | Hybrid-level RT energy price table used for bus-level objective prices and rolling parameter updates. |
- | `"horizon_DA"` | Yes (current implementation) | DA parameter time-step dimension used in parameter construction and updates; also referenced in reserve-assignment constraint logic (e.g., `horizon_DA == 24`). |
- | `"horizon_RT"` | Yes (current implementation) | RT parameter time-step dimension used in parameter construction and updates. |
- | `"λ_"` | Yes (per attached service) | Ancillary-service DA price table for each attached service (e.g., `"λ_Regulation_Up"`), used in objective pricing with `"DateTime"` and bus columns. |
+ - **Hybrid-attached time series:** Same DA/RT keyed energy prices and renewable/load series as
+ [`MerchantHybridEnergyCase`](@ref). Additionally, for each ancillary product attached to the
+ hybrid, attach a scalar `SingleTimeSeries` named
+ [`hybrid_ancillary_service_price_time_series_name`](@ref)(``, ``).
"""
struct MerchantHybridCooptimizerCase <: HybridDecisionProblem end
@@ -127,40 +183,17 @@ equilibrium or regulatory analysis.
- **System:** Same as [`MerchantHybridEnergyCase`](@ref) (at least one
[`PowerSystems.HybridSystem`](@extref PowerSystems.HybridSystem) with required forecasts).
- - **Time series:** Default names:
-
- | Parameter | Default Time Series Name |
- | :--- | :--- |
- | `RenewablePowerTimeSeries` | `"RenewableDispatch__max_active_power"` |
- | `RenewablePowerTimeSeries` (day-ahead-only merchant builds) | `"RenewableDispatch__max_active_power_da"` |
- | `ElectricLoadTimeSeries` | `"PowerLoad__max_active_power"` |
- - **System ext data:** Same key requirements as [`MerchantHybridEnergyCase`](@ref):
-
- | Key | Required | Description |
- | :--- | :--- | :--- |
- | `"λ_da_df"` | Yes | System-level DA table used primarily for its `"DateTime"` axis when deriving horizon windows. |
- | `"λ_rt_df"` | Yes | System-level RT table used primarily for its `"DateTime"` axis when deriving horizon windows. |
- | `"horizon_DA"` | Optional | DA index length used during model build; defaults to table length when omitted. |
- | `"horizon_RT"` | Optional | RT index length used during model build; defaults to table length when omitted. |
-
- - **Hybrid ext data:** Keys in each hybrid's
- [`ext` dictionary](@extref additional_fields):
-
- | Key | Required | Description |
- | :--- | :--- | :--- |
- | `"λ_da_df"` | Yes | Hybrid-level DA energy price table used for bus-level objective prices and rolling parameter updates. |
- | `"λ_rt_df"` | Yes | Hybrid-level RT energy price table used for bus-level objective prices and rolling parameter updates. |
- | `"horizon_DA"` | Yes (current implementation) | DA parameter time-step dimension used in parameter construction and updates; also referenced in reserve-assignment constraint logic (e.g., `horizon_DA == 24`). |
- | `"horizon_RT"` | Yes (current implementation) | RT parameter time-step dimension used in parameter construction and updates. |
+ - **Hybrid-attached time series:** Same keyed scalar DA/RT market and profile series as
+ [`MerchantHybridEnergyCase`](@ref).
"""
struct MerchantHybridBilevelCase <: HybridDecisionProblem end
###############################################################################
# validate_time_series! for HybridDecisionProblem
###############################################################################
-# Merchant models (HybridDecisionProblem) use custom builds and get horizon/resolution
-# from sys.ext, but the PowerSimulations DecisionModel constructor always calls
-# validate_time_series!. We extend it here with checks appropriate for merchant:
+# Merchant models (HybridDecisionProblem) use custom builds; horizons/resolutions follow model
+# settings and attached time-series metadata. The PowerSimulations DecisionModel constructor always
+# calls validate_time_series!. We extend it here with checks appropriate for merchant:
# resolution/horizon initialization when UNSET, and forecast_count >= 1 (merchant
# models require PowerSystems forecasts for renewables/loads).
diff --git a/src/core/parameters.jl b/src/core/parameters.jl
index 3bb54b56..ddfc63b5 100644
--- a/src/core/parameters.jl
+++ b/src/core/parameters.jl
@@ -15,15 +15,11 @@ Docs abbreviation: ``\\Pi^*_{\\text{DA},t}`` (USD/MWh). Used in the merchant obj
**Input data:**
- - **System ext:** The [`ext` supplemental data dictionary](@extref additional_fields) on
- [`PowerSystems.System`](@extref PowerSystems.System) must contain `\"λ_da_df\"`, a
- `DataFrame` with column `"DateTime"` and one column per bus name. `\"horizon_DA\"::Int`
- is optional and, when absent, defaults to the `"DateTime"` length.
- - **Hybrid ext:** Each [`PowerSystems.HybridSystem`](@extref PowerSystems.HybridSystem)
- reads the same keys from its own [`ext` dictionary](@extref additional_fields). In current
- implementation, `\"horizon_DA\"` is expected in hybrid `ext` for parameter construction and
- updates; values are sliced from `\"λ_da_df\"` starting at the current forecast time and used
- over the model horizon.
+ - **Hybrid-attached time series:** Each [`PowerSystems.HybridSystem`](@extref PowerSystems.HybridSystem)
+ must have a bus-selected scalar day-ahead energy price series whose name is given by
+ `hybrid_energy_price_time_series_name()` (default key `"DA"`), stored as
+ `InfrastructureSystems.SingleTimeSeries` / deterministic forecast. Values are taken over the
+ model horizon from forecast timestamps starting at the problem initial time.
"""
struct DayAheadEnergyPrice <: PSI.ObjectiveFunctionParameter end
@@ -37,15 +33,10 @@ expression for RT energy and DART spread.
**Input data:**
- - **System ext:** The [`ext` supplemental data dictionary](@extref additional_fields) on
- [`PowerSystems.System`](@extref PowerSystems.System) must contain `\"λ_rt_df\"`, a
- `DataFrame` with column `"DateTime"` and one column per bus name. `\"horizon_RT\"::Int`
- is optional and, when absent, defaults to the `"DateTime"` length.
- - **Hybrid ext:** Each [`PowerSystems.HybridSystem`](@extref PowerSystems.HybridSystem)
- reads `\"λ_rt_df\"`, `\"horizon_RT\"`, and a mapping `\"tmap\"` from its own
- [`ext` dictionary](@extref additional_fields). In current implementation, `\"horizon_RT\"`
- is expected in hybrid `ext` for parameter construction and updates; `\"tmap\"` aligns
- real-time steps to day-ahead steps where needed.
+ - **Hybrid-attached time series:** Real-time energy price uses
+ `hybrid_energy_price_time_series_name()` (default key `"RT"`). Day-ahead ↔
+ real-time alignment for spread terms uses variable axis sizes and an internal index map derived
+ from model horizons, not hybrid `ext`.
"""
struct RealTimeEnergyPrice <: PSI.ObjectiveFunctionParameter end
@@ -59,11 +50,9 @@ profit term for ancillary services (``sb^{\\text{out}}`` + ``sb^{\\text{in}}``).
**Input data:**
- - **Hybrid ext:** For each service, the hybrid's [`ext` dictionary](@extref additional_fields)
- contains a key `\"λ_\"` (e.g. `\"λ_Regulation_Up\"`) with a `DataFrame` that
- has column `"DateTime"` and one column per bus name, plus `\"horizon_DA\"` giving the number
- of day-ahead steps. Used by [`MerchantHybridCooptimizerCase`](@ref) when ancillary services
- are attached to the hybrid.
+ - **Hybrid-attached time series:** For each attached ancillary product, a scalar series named per
+ `hybrid_ancillary_service_price_time_series_name(, )`. Used by
+ [`MerchantHybridCooptimizerCase`](@ref) when services are attached to the hybrid.
"""
struct AncillaryServicePrice <: PSI.ObjectiveFunctionParameter end
diff --git a/src/decision_models/bilevel_decision_model.jl b/src/decision_models/bilevel_decision_model.jl
index 67c7a7ff..cfafa57f 100644
--- a/src/decision_models/bilevel_decision_model.jl
+++ b/src/decision_models/bilevel_decision_model.jl
@@ -18,20 +18,36 @@ function PSI.build_impl!(decision_model::PSI.DecisionModel{MerchantHybridBilevel
sys,
)
PSI.init_model_store_params!(decision_model)
-
- # Create Multiple Time Horizons based on ext horizons
- ext = PSY.get_ext(sys)
- dates_da = ext["λ_da_df"][!, "DateTime"]
- dates_rt = ext["λ_rt_df"][!, "DateTime"]
- len_DA = get(ext, "horizon_DA", length(dates_da))
- len_RT = get(ext, "horizon_RT", length(dates_rt))
- T_da = 1:len_DA
- T_rt = 1:len_RT
- container.time_steps = T_rt
+ set_time_series_keys!(container, decision_model)
+
+ da_key = get_day_ahead_time_series_key(decision_model)
+ rt_key = get_real_time_time_series_key(decision_model)
+ hybrid_ref = first(collect(PSY.get_components(PSY.HybridSystem, sys)))
+ da_metadata = first_matching_hybrid_scalar_metadata(
+ hybrid_ref,
+ hybrid_energy_price_time_series_name(da_key),
+ )
+ rt_metadata = first_matching_hybrid_scalar_metadata(
+ hybrid_ref,
+ hybrid_energy_price_time_series_name(rt_key),
+ )
+ len_DA_meta = time_series_metadata_horizon_steps(da_metadata)
+ len_RT_meta = time_series_metadata_horizon_steps(rt_metadata)
+ settings = PSI.get_settings(container)
+ h_ms = Dates.value(PSI.get_horizon(settings))
+ da_slot_ms = Dates.value(Dates.Millisecond(Dates.Hour(1)))
+ n_DA = max(1, div(h_ms, da_slot_ms))
+ T_da = 1:min(n_DA, len_DA_meta)
+
+ T_rt = PSI.get_time_steps(container)
+ len_RT = length(T_rt)
+ len_RT_meta < len_RT && error(
+ "Hybrid RT energy price series ($(len_RT_meta) pts) is shorter than model horizon ($(len_RT) steps).",
+ )
time_steps = T_rt
# Map for DA to RT
- tmap = [div(k - 1, Int(length(T_rt) / length(T_da))) + 1 for k in T_rt]
+ tmap = merchant_rt_to_da_tmap(len_RT, length(T_da))
###############################
######## Parameters ###########
@@ -39,11 +55,6 @@ function PSI.build_impl!(decision_model::PSI.DecisionModel{MerchantHybridBilevel
hybrids = collect(PSY.get_components(PSY.HybridSystem, sys))
h_names = PSY.get_name.(hybrids)
- for h in hybrids
- PSY.get_ext(h)["T_da"] = T_da
- PSY.get_ext(h)["tmap"] = tmap
- end
-
services = Set()
for h in hybrids
union!(services, PSY.get_services(h))
@@ -250,7 +261,7 @@ function PSI.build_impl!(decision_model::PSI.DecisionModel{MerchantHybridBilevel
container,
RenewablePowerTimeSeries(),
_hybrids_with_renewable,
- "RenewableDispatch__max_active_power_da",
+ "RenewableDispatch__max_active_power",
)
PSI.add_variables!(
container,
diff --git a/src/decision_models/cooptimizer_decision_model.jl b/src/decision_models/cooptimizer_decision_model.jl
index c47090d6..bc6395b0 100644
--- a/src/decision_models/cooptimizer_decision_model.jl
+++ b/src/decision_models/cooptimizer_decision_model.jl
@@ -19,20 +19,36 @@ function PSI.build_impl!(decision_model::PSI.DecisionModel{MerchantHybridCooptim
sys,
)
PSI.init_model_store_params!(decision_model)
-
- # Create Multiple Time Horizons based on ext horizons
- ext = PSY.get_ext(sys)
- dates_da = ext["λ_da_df"][!, "DateTime"]
- dates_rt = ext["λ_rt_df"][!, "DateTime"]
- len_DA = get(ext, "horizon_DA", length(dates_da))
- len_RT = get(ext, "horizon_RT", length(dates_rt))
- T_da = 1:len_DA
- T_rt = 1:len_RT
- container.time_steps = T_rt
+ set_time_series_keys!(container, decision_model)
+
+ da_key = get_day_ahead_time_series_key(decision_model)
+ rt_key = get_real_time_time_series_key(decision_model)
+ hybrid_ref = first(collect(PSY.get_components(PSY.HybridSystem, sys)))
+ da_metadata = first_matching_hybrid_scalar_metadata(
+ hybrid_ref,
+ hybrid_energy_price_time_series_name(da_key),
+ )
+ rt_metadata = first_matching_hybrid_scalar_metadata(
+ hybrid_ref,
+ hybrid_energy_price_time_series_name(rt_key),
+ )
+ len_DA_meta = time_series_metadata_horizon_steps(da_metadata)
+ len_RT_meta = time_series_metadata_horizon_steps(rt_metadata)
+ settings = PSI.get_settings(container)
+ h_ms = Dates.value(PSI.get_horizon(settings))
+ da_slot_ms = Dates.value(Dates.Millisecond(Dates.Hour(1)))
+ n_DA = max(1, div(h_ms, da_slot_ms))
+ T_da = 1:min(n_DA, len_DA_meta)
+
+ T_rt = PSI.get_time_steps(container)
+ len_RT = length(T_rt)
+ len_RT_meta < len_RT && error(
+ "Hybrid RT energy price series ($(len_RT_meta) pts) is shorter than model horizon ($(len_RT) steps).",
+ )
time_steps = T_rt
# Map for DA to RT
- tmap = [div(k - 1, Int(length(T_rt) / length(T_da))) + 1 for k in T_rt]
+ tmap = merchant_rt_to_da_tmap(len_RT, length(T_da))
###############################
######## Parameters ###########
@@ -40,11 +56,6 @@ function PSI.build_impl!(decision_model::PSI.DecisionModel{MerchantHybridCooptim
hybrids = collect(PSY.get_components(PSY.HybridSystem, sys))
h_names = PSY.get_name.(hybrids)
- for h in hybrids
- PSY.get_ext(h)["T_da"] = T_da
- PSY.get_ext(h)["tmap"] = tmap
- end
-
services = Set()
for h in hybrids
union!(services, PSY.get_services(h))
@@ -52,7 +63,7 @@ function PSI.build_impl!(decision_model::PSI.DecisionModel{MerchantHybridCooptim
if !isempty(services)
PSI.add_variables!(container, TotalReserve, hybrids, MerchantModelWithReserves())
- if len_DA == 24
+ if length(T_da) == 24
PSI.add_variables!(
container,
SlackReserveUp,
@@ -273,21 +284,12 @@ function PSI.build_impl!(decision_model::PSI.DecisionModel{MerchantHybridCooptim
_hybrids_with_renewable,
MerchantModelWithReserves(),
)
- if get(decision_model.ext, "RT", false)
- add_time_series_parameters!(
- container,
- RenewablePowerTimeSeries(),
- _hybrids_with_renewable,
- "RenewableDispatch__max_active_power",
- )
- else
- add_time_series_parameters!(
- container,
- RenewablePowerTimeSeries(),
- _hybrids_with_renewable,
- "RenewableDispatch__max_active_power_da",
- )
- end
+ add_time_series_parameters!(
+ container,
+ RenewablePowerTimeSeries(),
+ _hybrids_with_renewable,
+ "RenewableDispatch__max_active_power",
+ )
PSI.add_variables!(
container,
RenewableReserveVariable,
@@ -828,7 +830,7 @@ function PSI.build_impl!(decision_model::PSI.DecisionModel{MerchantHybridCooptim
end
end
- if len_DA == 24 && !isempty(services)
+ if length(T_da) == 24 && !isempty(services)
res_slack_up = PSI.get_variable(container, SlackReserveUp(), PSY.HybridSystem)
res_slack_dn = PSI.get_variable(container, SlackReserveDown(), PSY.HybridSystem)
end
@@ -862,7 +864,7 @@ function PSI.build_impl!(decision_model::PSI.DecisionModel{MerchantHybridCooptim
PSI.add_to_objective_invariant_expression!(container, lin_cost_p_ch)
PSI.add_to_objective_invariant_expression!(container, lin_cost_p_ds)
end
- if len_DA == 24 && !isempty(services)
+ if length(T_da) == 24 && !isempty(services)
dev_services = PSY.get_services(dev)
for service in dev_services
service_name = PSY.get_name(service)
diff --git a/src/decision_models/only_energy_decision_model.jl b/src/decision_models/only_energy_decision_model.jl
index f19be73d..f2865d85 100644
--- a/src/decision_models/only_energy_decision_model.jl
+++ b/src/decision_models/only_energy_decision_model.jl
@@ -17,19 +17,31 @@ function PSI.build_impl!(decision_model::PSI.DecisionModel{MerchantHybridEnergyC
sys,
)
PSI.init_model_store_params!(decision_model)
-
- # Create Multiple Time Horizons based on ext horizons
- ext = PSY.get_ext(sys)
- dates_da = ext["λ_da_df"][!, "DateTime"]
- dates_rt = ext["λ_rt_df"][!, "DateTime"]
- len_DA = get(ext, "horizon_DA", length(dates_da))
- len_RT = get(ext, "horizon_RT", length(dates_rt))
- T_da = 1:len_DA
- T_rt = 1:len_RT
- container.time_steps = T_rt
+ set_time_series_keys!(container, decision_model)
+
+ da_key = get_day_ahead_time_series_key(decision_model)
+ rt_key = get_real_time_time_series_key(decision_model)
+ hybrid_ref = first(collect(PSY.get_components(PSY.HybridSystem, sys)))
+ da_metadata = first_matching_hybrid_scalar_metadata(
+ hybrid_ref,
+ hybrid_energy_price_time_series_name(da_key),
+ )
+ rt_metadata = first_matching_hybrid_scalar_metadata(
+ hybrid_ref,
+ hybrid_energy_price_time_series_name(rt_key),
+ )
+ len_DA_meta = time_series_metadata_horizon_steps(da_metadata)
+ len_RT_meta = time_series_metadata_horizon_steps(rt_metadata)
+ settings = PSI.get_settings(container)
+ T_rt = PSI.get_time_steps(container)
+ len_RT = length(T_rt)
+ T_da = merchant_da_time_step_range(container, hybrid_ref)
+ len_RT_meta < len_RT && error(
+ "Hybrid RT energy price series ($(len_RT_meta) pts) is shorter than model horizon ($(len_RT) steps).",
+ )
# Map for DA to RT
- tmap = [div(k - 1, Int(length(T_rt) / length(T_da))) + 1 for k in T_rt]
+ tmap = merchant_rt_to_da_tmap(len_RT, length(T_da))
###############################
######## Parameters ###########
@@ -37,11 +49,6 @@ function PSI.build_impl!(decision_model::PSI.DecisionModel{MerchantHybridEnergyC
hybrids = collect(PSY.get_components(PSY.HybridSystem, sys))
h_names = PSY.get_name.(hybrids)
- for h in hybrids
- PSY.get_ext(h)["T_da"] = T_da
- PSY.get_ext(h)["tmap"] = tmap
- end
-
services = Set()
for d in hybrids
union!(services, PSY.get_services(d))
@@ -86,21 +93,12 @@ function PSI.build_impl!(decision_model::PSI.DecisionModel{MerchantHybridEnergyC
_hybrids_with_renewable,
MerchantModelEnergyOnly(),
)
- if get(decision_model.ext, "RT", false)
- add_time_series_parameters!(
- container,
- RenewablePowerTimeSeries(),
- _hybrids_with_renewable,
- "RenewableDispatch__max_active_power",
- )
- else
- add_time_series_parameters!(
- container,
- RenewablePowerTimeSeries(),
- _hybrids_with_renewable,
- "RenewableDispatch__max_active_power_da",
- )
- end
+ add_time_series_parameters!(
+ container,
+ RenewablePowerTimeSeries(),
+ _hybrids_with_renewable,
+ "RenewableDispatch__max_active_power",
+ )
end
if !isempty(_hybrids_with_loads)
diff --git a/src/hybrid_system_decision_models.jl b/src/hybrid_system_decision_models.jl
index 5de7d202..fae181d2 100644
--- a/src/hybrid_system_decision_models.jl
+++ b/src/hybrid_system_decision_models.jl
@@ -212,9 +212,11 @@ function PSI.update_decision_state!(
offset = resolution_ratio - 1
result_time_index = axes(store_data)[2]
+ max_state_index = PSI.get_num_rows(state_data)
PSI.set_update_timestamp!(state_data, simulation_time)
for t in result_time_index
- state_range = state_data_index:(state_data_index + offset)
+ state_data_index > max_state_index && break
+ state_range = state_data_index:min(max_state_index, state_data_index + offset)
for name in axes(state_data.values)[1], i in state_range
# TODO: We could also interpolate here
state_data.values[name, i] = store_data[name, t]
diff --git a/test/inputs/chuhsi_DA_prices.csv b/test/inputs/chuhsi_DA_prices.csv
index 2a5c6bf1..59c3772d 100644
--- a/test/inputs/chuhsi_DA_prices.csv
+++ b/test/inputs/chuhsi_DA_prices.csv
@@ -1,73 +1,73 @@
-DateTime,Chuhsi
-2020-10-03T00:00:00.0,30.27755657999998
-2020-10-03T01:00:00.0,27.754750800000032
-2020-10-03T02:00:00.0,30.277556579999924
-2020-10-03T03:00:00.0,26.790720239999825
-2020-10-03T04:00:00.0,26.790720240000102
-2020-10-03T05:00:00.0,30.277556579999988
-2020-10-03T06:00:00.0,-15.00000000000001
-2020-10-03T07:00:00.0,-15.00000000000025
-2020-10-03T08:00:00.0,-15.000000000000242
-2020-10-03T09:00:00.0,-15.000000000000043
-2020-10-03T10:00:00.0,23.168325362718395
-2020-10-03T11:00:00.0,-14.999999999999993
-2020-10-03T12:00:00.0,19.663893987708512
-2020-10-03T13:00:00.0,25.908321300000022
-2020-10-03T14:00:00.0,24.621651480000004
-2020-10-03T15:00:00.0,26.429208780000078
-2020-10-03T16:00:00.0,30.277556579999796
-2020-10-03T17:00:00.0,31.727489639999945
-2020-10-03T18:00:00.0,128.76811444827337
-2020-10-03T19:00:00.0,31.72748963999999
-2020-10-03T20:00:00.0,27.128908380000027
-2020-10-03T21:00:00.0,23.206703399999807
-2020-10-03T22:00:00.0,22.732462559999984
-2020-10-03T23:00:00.0,20.004236639100032
-2020-10-04T00:00:00.0,26.324253840000047
-2020-10-04T01:00:00.0,25.90832130000032
-2020-10-04T02:00:00.0,22.732462560000013
-2020-10-04T03:00:00.0,23.206703400000016
-2020-10-04T04:00:00.0,26.283342074228177
-2020-10-04T05:00:00.0,23.12895899999998
-2020-10-04T06:00:00.0,-15.000000000000002
-2020-10-04T07:00:00.0,-14.999999999999986
-2020-10-04T08:00:00.0,-14.999999999999677
-2020-10-04T09:00:00.0,21.338117861855576
-2020-10-04T10:00:00.0,22.732462559999984
-2020-10-04T11:00:00.0,19.661306868143946
-2020-10-04T12:00:00.0,23.20670340000006
-2020-10-04T13:00:00.0,30.530225880000188
-2020-10-04T14:00:00.0,27.128908380000027
-2020-10-04T15:00:00.0,26.755735259999923
-2020-10-04T16:00:00.0,30.27755657999988
-2020-10-04T17:00:00.0,78.35360939288634
-2020-10-04T18:00:00.0,78.35360939288614
-2020-10-04T19:00:00.0,67.48390376638841
-2020-10-04T20:00:00.0,31.727489639999774
-2020-10-04T21:00:00.0,27.754750800000046
-2020-10-04T22:00:00.0,27.754750800000025
-2020-10-04T23:00:00.0,26.755735259999923
-2020-10-05T00:00:00.0,30.277556579999967
-2020-10-05T01:00:00.0,27.441105789635966
-2020-10-05T02:00:00.0,27.441105789635955
-2020-10-05T03:00:00.0,27.754750800000025
-2020-10-05T04:00:00.0,30.277556579999906
-2020-10-05T05:00:00.0,31.727489640000023
-2020-10-05T06:00:00.0,-14.999999999999957
-2020-10-05T07:00:00.0,-14.999999999999936
-2020-10-05T08:00:00.0,-14.999999999999986
-2020-10-05T09:00:00.0,-15.00000000000001
-2020-10-05T10:00:00.0,-15.00000000000003
-2020-10-05T11:00:00.0,22.57697376
-2020-10-05T12:00:00.0,25.908321299999994
-2020-10-05T13:00:00.0,26.42920877999987
-2020-10-05T14:00:00.0,27.754750800000057
-2020-10-05T15:00:00.0,30.53022587999992
-2020-10-05T16:00:00.0,62.305558237152425
-2020-10-05T17:00:00.0,62.305558237152354
-2020-10-05T18:00:00.0,62.305558237152376
-2020-10-05T19:00:00.0,33.4523008697569
-2020-10-05T20:00:00.0,30.530225880000092
-2020-10-05T21:00:00.0,26.845141320000025
-2020-10-05T22:00:00.0,26.32425383999998
-2020-10-05T23:00:00.0,26.429208779999886
+DateTime,Chuhsi
+2020-10-03T00:00:00.0,30.27755657999998
+2020-10-03T01:00:00.0,27.754750800000032
+2020-10-03T02:00:00.0,30.277556579999924
+2020-10-03T03:00:00.0,26.790720239999825
+2020-10-03T04:00:00.0,26.790720240000102
+2020-10-03T05:00:00.0,30.277556579999988
+2020-10-03T06:00:00.0,-15.00000000000001
+2020-10-03T07:00:00.0,-15.00000000000025
+2020-10-03T08:00:00.0,-15.000000000000242
+2020-10-03T09:00:00.0,-15.000000000000043
+2020-10-03T10:00:00.0,23.168325362718395
+2020-10-03T11:00:00.0,-14.999999999999993
+2020-10-03T12:00:00.0,19.663893987708512
+2020-10-03T13:00:00.0,25.908321300000022
+2020-10-03T14:00:00.0,24.621651480000004
+2020-10-03T15:00:00.0,26.429208780000078
+2020-10-03T16:00:00.0,30.277556579999796
+2020-10-03T17:00:00.0,31.727489639999945
+2020-10-03T18:00:00.0,128.76811444827337
+2020-10-03T19:00:00.0,31.72748963999999
+2020-10-03T20:00:00.0,27.128908380000027
+2020-10-03T21:00:00.0,23.206703399999807
+2020-10-03T22:00:00.0,22.732462559999984
+2020-10-03T23:00:00.0,20.004236639100032
+2020-10-04T00:00:00.0,26.324253840000047
+2020-10-04T01:00:00.0,25.90832130000032
+2020-10-04T02:00:00.0,22.732462560000013
+2020-10-04T03:00:00.0,23.206703400000016
+2020-10-04T04:00:00.0,26.283342074228177
+2020-10-04T05:00:00.0,23.12895899999998
+2020-10-04T06:00:00.0,-15.000000000000002
+2020-10-04T07:00:00.0,-14.999999999999986
+2020-10-04T08:00:00.0,-14.999999999999677
+2020-10-04T09:00:00.0,21.338117861855576
+2020-10-04T10:00:00.0,22.732462559999984
+2020-10-04T11:00:00.0,19.661306868143946
+2020-10-04T12:00:00.0,23.20670340000006
+2020-10-04T13:00:00.0,30.530225880000188
+2020-10-04T14:00:00.0,27.128908380000027
+2020-10-04T15:00:00.0,26.755735259999923
+2020-10-04T16:00:00.0,30.27755657999988
+2020-10-04T17:00:00.0,78.35360939288634
+2020-10-04T18:00:00.0,78.35360939288614
+2020-10-04T19:00:00.0,67.48390376638841
+2020-10-04T20:00:00.0,31.727489639999774
+2020-10-04T21:00:00.0,27.754750800000046
+2020-10-04T22:00:00.0,27.754750800000025
+2020-10-04T23:00:00.0,26.755735259999923
+2020-10-05T00:00:00.0,30.277556579999967
+2020-10-05T01:00:00.0,27.441105789635966
+2020-10-05T02:00:00.0,27.441105789635955
+2020-10-05T03:00:00.0,27.754750800000025
+2020-10-05T04:00:00.0,30.277556579999906
+2020-10-05T05:00:00.0,31.727489640000023
+2020-10-05T06:00:00.0,-14.999999999999957
+2020-10-05T07:00:00.0,-14.999999999999936
+2020-10-05T08:00:00.0,-14.999999999999986
+2020-10-05T09:00:00.0,-15.00000000000001
+2020-10-05T10:00:00.0,-15.00000000000003
+2020-10-05T11:00:00.0,22.57697376
+2020-10-05T12:00:00.0,25.908321299999994
+2020-10-05T13:00:00.0,26.42920877999987
+2020-10-05T14:00:00.0,27.754750800000057
+2020-10-05T15:00:00.0,30.53022587999992
+2020-10-05T16:00:00.0,62.305558237152425
+2020-10-05T17:00:00.0,62.305558237152354
+2020-10-05T18:00:00.0,62.305558237152376
+2020-10-05T19:00:00.0,33.4523008697569
+2020-10-05T20:00:00.0,30.530225880000092
+2020-10-05T21:00:00.0,26.845141320000025
+2020-10-05T22:00:00.0,26.32425383999998
+2020-10-05T23:00:00.0,26.429208779999886
diff --git a/test/inputs/chuhsi_DA_prices_24.csv b/test/inputs/chuhsi_DA_prices_24.csv
new file mode 100644
index 00000000..377f8d45
--- /dev/null
+++ b/test/inputs/chuhsi_DA_prices_24.csv
@@ -0,0 +1,25 @@
+DateTime,Chuhsi
+2020-10-03T00:00:00.0,30.27755657999998
+2020-10-03T01:00:00.0,27.754750800000032
+2020-10-03T02:00:00.0,30.277556579999924
+2020-10-03T03:00:00.0,26.790720239999825
+2020-10-03T04:00:00.0,26.790720240000102
+2020-10-03T05:00:00.0,30.277556579999988
+2020-10-03T06:00:00.0,-15.00000000000001
+2020-10-03T07:00:00.0,-15.00000000000025
+2020-10-03T08:00:00.0,-15.000000000000242
+2020-10-03T09:00:00.0,-15.000000000000043
+2020-10-03T10:00:00.0,23.168325362718395
+2020-10-03T11:00:00.0,-14.999999999999993
+2020-10-03T12:00:00.0,19.663893987708512
+2020-10-03T13:00:00.0,25.908321300000022
+2020-10-03T14:00:00.0,24.621651480000004
+2020-10-03T15:00:00.0,26.429208780000078
+2020-10-03T16:00:00.0,30.277556579999796
+2020-10-03T17:00:00.0,31.727489639999945
+2020-10-03T18:00:00.0,128.76811444827337
+2020-10-03T19:00:00.0,31.72748963999999
+2020-10-03T20:00:00.0,27.128908380000027
+2020-10-03T21:00:00.0,23.206703399999807
+2020-10-03T22:00:00.0,22.732462559999984
+2020-10-03T23:00:00.0,20.004236639100032
diff --git a/test/inputs/chuhsi_DA_prices_5min.csv b/test/inputs/chuhsi_DA_prices_5min.csv
new file mode 100644
index 00000000..b6ec5cea
--- /dev/null
+++ b/test/inputs/chuhsi_DA_prices_5min.csv
@@ -0,0 +1,865 @@
+DateTime,Chuhsi
+2020-10-03T00:00:00.0,30.27755657999998
+2020-10-03T00:05:00.0,30.27755657999998
+2020-10-03T00:10:00.0,30.27755657999998
+2020-10-03T00:15:00.0,30.27755657999998
+2020-10-03T00:20:00.0,30.27755657999998
+2020-10-03T00:25:00.0,30.27755657999998
+2020-10-03T00:30:00.0,30.27755657999998
+2020-10-03T00:35:00.0,30.27755657999998
+2020-10-03T00:40:00.0,30.27755657999998
+2020-10-03T00:45:00.0,30.27755657999998
+2020-10-03T00:50:00.0,30.27755657999998
+2020-10-03T00:55:00.0,30.27755657999998
+2020-10-03T01:00:00.0,27.754750800000032
+2020-10-03T01:05:00.0,27.754750800000032
+2020-10-03T01:10:00.0,27.754750800000032
+2020-10-03T01:15:00.0,27.754750800000032
+2020-10-03T01:20:00.0,27.754750800000032
+2020-10-03T01:25:00.0,27.754750800000032
+2020-10-03T01:30:00.0,27.754750800000032
+2020-10-03T01:35:00.0,27.754750800000032
+2020-10-03T01:40:00.0,27.754750800000032
+2020-10-03T01:45:00.0,27.754750800000032
+2020-10-03T01:50:00.0,27.754750800000032
+2020-10-03T01:55:00.0,27.754750800000032
+2020-10-03T02:00:00.0,30.277556579999924
+2020-10-03T02:05:00.0,30.277556579999924
+2020-10-03T02:10:00.0,30.277556579999924
+2020-10-03T02:15:00.0,30.277556579999924
+2020-10-03T02:20:00.0,30.277556579999924
+2020-10-03T02:25:00.0,30.277556579999924
+2020-10-03T02:30:00.0,30.277556579999924
+2020-10-03T02:35:00.0,30.277556579999924
+2020-10-03T02:40:00.0,30.277556579999924
+2020-10-03T02:45:00.0,30.277556579999924
+2020-10-03T02:50:00.0,30.277556579999924
+2020-10-03T02:55:00.0,30.277556579999924
+2020-10-03T03:00:00.0,26.790720239999825
+2020-10-03T03:05:00.0,26.790720239999825
+2020-10-03T03:10:00.0,26.790720239999825
+2020-10-03T03:15:00.0,26.790720239999825
+2020-10-03T03:20:00.0,26.790720239999825
+2020-10-03T03:25:00.0,26.790720239999825
+2020-10-03T03:30:00.0,26.790720239999825
+2020-10-03T03:35:00.0,26.790720239999825
+2020-10-03T03:40:00.0,26.790720239999825
+2020-10-03T03:45:00.0,26.790720239999825
+2020-10-03T03:50:00.0,26.790720239999825
+2020-10-03T03:55:00.0,26.790720239999825
+2020-10-03T04:00:00.0,26.790720240000102
+2020-10-03T04:05:00.0,26.790720240000102
+2020-10-03T04:10:00.0,26.790720240000102
+2020-10-03T04:15:00.0,26.790720240000102
+2020-10-03T04:20:00.0,26.790720240000102
+2020-10-03T04:25:00.0,26.790720240000102
+2020-10-03T04:30:00.0,26.790720240000102
+2020-10-03T04:35:00.0,26.790720240000102
+2020-10-03T04:40:00.0,26.790720240000102
+2020-10-03T04:45:00.0,26.790720240000102
+2020-10-03T04:50:00.0,26.790720240000102
+2020-10-03T04:55:00.0,26.790720240000102
+2020-10-03T05:00:00.0,30.277556579999988
+2020-10-03T05:05:00.0,30.277556579999988
+2020-10-03T05:10:00.0,30.277556579999988
+2020-10-03T05:15:00.0,30.277556579999988
+2020-10-03T05:20:00.0,30.277556579999988
+2020-10-03T05:25:00.0,30.277556579999988
+2020-10-03T05:30:00.0,30.277556579999988
+2020-10-03T05:35:00.0,30.277556579999988
+2020-10-03T05:40:00.0,30.277556579999988
+2020-10-03T05:45:00.0,30.277556579999988
+2020-10-03T05:50:00.0,30.277556579999988
+2020-10-03T05:55:00.0,30.277556579999988
+2020-10-03T06:00:00.0,-15.00000000000001
+2020-10-03T06:05:00.0,-15.00000000000001
+2020-10-03T06:10:00.0,-15.00000000000001
+2020-10-03T06:15:00.0,-15.00000000000001
+2020-10-03T06:20:00.0,-15.00000000000001
+2020-10-03T06:25:00.0,-15.00000000000001
+2020-10-03T06:30:00.0,-15.00000000000001
+2020-10-03T06:35:00.0,-15.00000000000001
+2020-10-03T06:40:00.0,-15.00000000000001
+2020-10-03T06:45:00.0,-15.00000000000001
+2020-10-03T06:50:00.0,-15.00000000000001
+2020-10-03T06:55:00.0,-15.00000000000001
+2020-10-03T07:00:00.0,-15.00000000000025
+2020-10-03T07:05:00.0,-15.00000000000025
+2020-10-03T07:10:00.0,-15.00000000000025
+2020-10-03T07:15:00.0,-15.00000000000025
+2020-10-03T07:20:00.0,-15.00000000000025
+2020-10-03T07:25:00.0,-15.00000000000025
+2020-10-03T07:30:00.0,-15.00000000000025
+2020-10-03T07:35:00.0,-15.00000000000025
+2020-10-03T07:40:00.0,-15.00000000000025
+2020-10-03T07:45:00.0,-15.00000000000025
+2020-10-03T07:50:00.0,-15.00000000000025
+2020-10-03T07:55:00.0,-15.00000000000025
+2020-10-03T08:00:00.0,-15.000000000000242
+2020-10-03T08:05:00.0,-15.000000000000242
+2020-10-03T08:10:00.0,-15.000000000000242
+2020-10-03T08:15:00.0,-15.000000000000242
+2020-10-03T08:20:00.0,-15.000000000000242
+2020-10-03T08:25:00.0,-15.000000000000242
+2020-10-03T08:30:00.0,-15.000000000000242
+2020-10-03T08:35:00.0,-15.000000000000242
+2020-10-03T08:40:00.0,-15.000000000000242
+2020-10-03T08:45:00.0,-15.000000000000242
+2020-10-03T08:50:00.0,-15.000000000000242
+2020-10-03T08:55:00.0,-15.000000000000242
+2020-10-03T09:00:00.0,-15.000000000000043
+2020-10-03T09:05:00.0,-15.000000000000043
+2020-10-03T09:10:00.0,-15.000000000000043
+2020-10-03T09:15:00.0,-15.000000000000043
+2020-10-03T09:20:00.0,-15.000000000000043
+2020-10-03T09:25:00.0,-15.000000000000043
+2020-10-03T09:30:00.0,-15.000000000000043
+2020-10-03T09:35:00.0,-15.000000000000043
+2020-10-03T09:40:00.0,-15.000000000000043
+2020-10-03T09:45:00.0,-15.000000000000043
+2020-10-03T09:50:00.0,-15.000000000000043
+2020-10-03T09:55:00.0,-15.000000000000043
+2020-10-03T10:00:00.0,23.168325362718395
+2020-10-03T10:05:00.0,23.168325362718395
+2020-10-03T10:10:00.0,23.168325362718395
+2020-10-03T10:15:00.0,23.168325362718395
+2020-10-03T10:20:00.0,23.168325362718395
+2020-10-03T10:25:00.0,23.168325362718395
+2020-10-03T10:30:00.0,23.168325362718395
+2020-10-03T10:35:00.0,23.168325362718395
+2020-10-03T10:40:00.0,23.168325362718395
+2020-10-03T10:45:00.0,23.168325362718395
+2020-10-03T10:50:00.0,23.168325362718395
+2020-10-03T10:55:00.0,23.168325362718395
+2020-10-03T11:00:00.0,-14.999999999999993
+2020-10-03T11:05:00.0,-14.999999999999993
+2020-10-03T11:10:00.0,-14.999999999999993
+2020-10-03T11:15:00.0,-14.999999999999993
+2020-10-03T11:20:00.0,-14.999999999999993
+2020-10-03T11:25:00.0,-14.999999999999993
+2020-10-03T11:30:00.0,-14.999999999999993
+2020-10-03T11:35:00.0,-14.999999999999993
+2020-10-03T11:40:00.0,-14.999999999999993
+2020-10-03T11:45:00.0,-14.999999999999993
+2020-10-03T11:50:00.0,-14.999999999999993
+2020-10-03T11:55:00.0,-14.999999999999993
+2020-10-03T12:00:00.0,19.663893987708512
+2020-10-03T12:05:00.0,19.663893987708512
+2020-10-03T12:10:00.0,19.663893987708512
+2020-10-03T12:15:00.0,19.663893987708512
+2020-10-03T12:20:00.0,19.663893987708512
+2020-10-03T12:25:00.0,19.663893987708512
+2020-10-03T12:30:00.0,19.663893987708512
+2020-10-03T12:35:00.0,19.663893987708512
+2020-10-03T12:40:00.0,19.663893987708512
+2020-10-03T12:45:00.0,19.663893987708512
+2020-10-03T12:50:00.0,19.663893987708512
+2020-10-03T12:55:00.0,19.663893987708512
+2020-10-03T13:00:00.0,25.908321300000022
+2020-10-03T13:05:00.0,25.908321300000022
+2020-10-03T13:10:00.0,25.908321300000022
+2020-10-03T13:15:00.0,25.908321300000022
+2020-10-03T13:20:00.0,25.908321300000022
+2020-10-03T13:25:00.0,25.908321300000022
+2020-10-03T13:30:00.0,25.908321300000022
+2020-10-03T13:35:00.0,25.908321300000022
+2020-10-03T13:40:00.0,25.908321300000022
+2020-10-03T13:45:00.0,25.908321300000022
+2020-10-03T13:50:00.0,25.908321300000022
+2020-10-03T13:55:00.0,25.908321300000022
+2020-10-03T14:00:00.0,24.621651480000004
+2020-10-03T14:05:00.0,24.621651480000004
+2020-10-03T14:10:00.0,24.621651480000004
+2020-10-03T14:15:00.0,24.621651480000004
+2020-10-03T14:20:00.0,24.621651480000004
+2020-10-03T14:25:00.0,24.621651480000004
+2020-10-03T14:30:00.0,24.621651480000004
+2020-10-03T14:35:00.0,24.621651480000004
+2020-10-03T14:40:00.0,24.621651480000004
+2020-10-03T14:45:00.0,24.621651480000004
+2020-10-03T14:50:00.0,24.621651480000004
+2020-10-03T14:55:00.0,24.621651480000004
+2020-10-03T15:00:00.0,26.429208780000078
+2020-10-03T15:05:00.0,26.429208780000078
+2020-10-03T15:10:00.0,26.429208780000078
+2020-10-03T15:15:00.0,26.429208780000078
+2020-10-03T15:20:00.0,26.429208780000078
+2020-10-03T15:25:00.0,26.429208780000078
+2020-10-03T15:30:00.0,26.429208780000078
+2020-10-03T15:35:00.0,26.429208780000078
+2020-10-03T15:40:00.0,26.429208780000078
+2020-10-03T15:45:00.0,26.429208780000078
+2020-10-03T15:50:00.0,26.429208780000078
+2020-10-03T15:55:00.0,26.429208780000078
+2020-10-03T16:00:00.0,30.277556579999796
+2020-10-03T16:05:00.0,30.277556579999796
+2020-10-03T16:10:00.0,30.277556579999796
+2020-10-03T16:15:00.0,30.277556579999796
+2020-10-03T16:20:00.0,30.277556579999796
+2020-10-03T16:25:00.0,30.277556579999796
+2020-10-03T16:30:00.0,30.277556579999796
+2020-10-03T16:35:00.0,30.277556579999796
+2020-10-03T16:40:00.0,30.277556579999796
+2020-10-03T16:45:00.0,30.277556579999796
+2020-10-03T16:50:00.0,30.277556579999796
+2020-10-03T16:55:00.0,30.277556579999796
+2020-10-03T17:00:00.0,31.727489639999945
+2020-10-03T17:05:00.0,31.727489639999945
+2020-10-03T17:10:00.0,31.727489639999945
+2020-10-03T17:15:00.0,31.727489639999945
+2020-10-03T17:20:00.0,31.727489639999945
+2020-10-03T17:25:00.0,31.727489639999945
+2020-10-03T17:30:00.0,31.727489639999945
+2020-10-03T17:35:00.0,31.727489639999945
+2020-10-03T17:40:00.0,31.727489639999945
+2020-10-03T17:45:00.0,31.727489639999945
+2020-10-03T17:50:00.0,31.727489639999945
+2020-10-03T17:55:00.0,31.727489639999945
+2020-10-03T18:00:00.0,128.76811444827337
+2020-10-03T18:05:00.0,128.76811444827337
+2020-10-03T18:10:00.0,128.76811444827337
+2020-10-03T18:15:00.0,128.76811444827337
+2020-10-03T18:20:00.0,128.76811444827337
+2020-10-03T18:25:00.0,128.76811444827337
+2020-10-03T18:30:00.0,128.76811444827337
+2020-10-03T18:35:00.0,128.76811444827337
+2020-10-03T18:40:00.0,128.76811444827337
+2020-10-03T18:45:00.0,128.76811444827337
+2020-10-03T18:50:00.0,128.76811444827337
+2020-10-03T18:55:00.0,128.76811444827337
+2020-10-03T19:00:00.0,31.72748963999999
+2020-10-03T19:05:00.0,31.72748963999999
+2020-10-03T19:10:00.0,31.72748963999999
+2020-10-03T19:15:00.0,31.72748963999999
+2020-10-03T19:20:00.0,31.72748963999999
+2020-10-03T19:25:00.0,31.72748963999999
+2020-10-03T19:30:00.0,31.72748963999999
+2020-10-03T19:35:00.0,31.72748963999999
+2020-10-03T19:40:00.0,31.72748963999999
+2020-10-03T19:45:00.0,31.72748963999999
+2020-10-03T19:50:00.0,31.72748963999999
+2020-10-03T19:55:00.0,31.72748963999999
+2020-10-03T20:00:00.0,27.128908380000027
+2020-10-03T20:05:00.0,27.128908380000027
+2020-10-03T20:10:00.0,27.128908380000027
+2020-10-03T20:15:00.0,27.128908380000027
+2020-10-03T20:20:00.0,27.128908380000027
+2020-10-03T20:25:00.0,27.128908380000027
+2020-10-03T20:30:00.0,27.128908380000027
+2020-10-03T20:35:00.0,27.128908380000027
+2020-10-03T20:40:00.0,27.128908380000027
+2020-10-03T20:45:00.0,27.128908380000027
+2020-10-03T20:50:00.0,27.128908380000027
+2020-10-03T20:55:00.0,27.128908380000027
+2020-10-03T21:00:00.0,23.206703399999807
+2020-10-03T21:05:00.0,23.206703399999807
+2020-10-03T21:10:00.0,23.206703399999807
+2020-10-03T21:15:00.0,23.206703399999807
+2020-10-03T21:20:00.0,23.206703399999807
+2020-10-03T21:25:00.0,23.206703399999807
+2020-10-03T21:30:00.0,23.206703399999807
+2020-10-03T21:35:00.0,23.206703399999807
+2020-10-03T21:40:00.0,23.206703399999807
+2020-10-03T21:45:00.0,23.206703399999807
+2020-10-03T21:50:00.0,23.206703399999807
+2020-10-03T21:55:00.0,23.206703399999807
+2020-10-03T22:00:00.0,22.732462559999984
+2020-10-03T22:05:00.0,22.732462559999984
+2020-10-03T22:10:00.0,22.732462559999984
+2020-10-03T22:15:00.0,22.732462559999984
+2020-10-03T22:20:00.0,22.732462559999984
+2020-10-03T22:25:00.0,22.732462559999984
+2020-10-03T22:30:00.0,22.732462559999984
+2020-10-03T22:35:00.0,22.732462559999984
+2020-10-03T22:40:00.0,22.732462559999984
+2020-10-03T22:45:00.0,22.732462559999984
+2020-10-03T22:50:00.0,22.732462559999984
+2020-10-03T22:55:00.0,22.732462559999984
+2020-10-03T23:00:00.0,20.004236639100032
+2020-10-03T23:05:00.0,20.004236639100032
+2020-10-03T23:10:00.0,20.004236639100032
+2020-10-03T23:15:00.0,20.004236639100032
+2020-10-03T23:20:00.0,20.004236639100032
+2020-10-03T23:25:00.0,20.004236639100032
+2020-10-03T23:30:00.0,20.004236639100032
+2020-10-03T23:35:00.0,20.004236639100032
+2020-10-03T23:40:00.0,20.004236639100032
+2020-10-03T23:45:00.0,20.004236639100032
+2020-10-03T23:50:00.0,20.004236639100032
+2020-10-03T23:55:00.0,20.004236639100032
+2020-10-04T00:00:00.0,26.324253840000047
+2020-10-04T00:05:00.0,26.324253840000047
+2020-10-04T00:10:00.0,26.324253840000047
+2020-10-04T00:15:00.0,26.324253840000047
+2020-10-04T00:20:00.0,26.324253840000047
+2020-10-04T00:25:00.0,26.324253840000047
+2020-10-04T00:30:00.0,26.324253840000047
+2020-10-04T00:35:00.0,26.324253840000047
+2020-10-04T00:40:00.0,26.324253840000047
+2020-10-04T00:45:00.0,26.324253840000047
+2020-10-04T00:50:00.0,26.324253840000047
+2020-10-04T00:55:00.0,26.324253840000047
+2020-10-04T01:00:00.0,25.90832130000032
+2020-10-04T01:05:00.0,25.90832130000032
+2020-10-04T01:10:00.0,25.90832130000032
+2020-10-04T01:15:00.0,25.90832130000032
+2020-10-04T01:20:00.0,25.90832130000032
+2020-10-04T01:25:00.0,25.90832130000032
+2020-10-04T01:30:00.0,25.90832130000032
+2020-10-04T01:35:00.0,25.90832130000032
+2020-10-04T01:40:00.0,25.90832130000032
+2020-10-04T01:45:00.0,25.90832130000032
+2020-10-04T01:50:00.0,25.90832130000032
+2020-10-04T01:55:00.0,25.90832130000032
+2020-10-04T02:00:00.0,22.732462560000013
+2020-10-04T02:05:00.0,22.732462560000013
+2020-10-04T02:10:00.0,22.732462560000013
+2020-10-04T02:15:00.0,22.732462560000013
+2020-10-04T02:20:00.0,22.732462560000013
+2020-10-04T02:25:00.0,22.732462560000013
+2020-10-04T02:30:00.0,22.732462560000013
+2020-10-04T02:35:00.0,22.732462560000013
+2020-10-04T02:40:00.0,22.732462560000013
+2020-10-04T02:45:00.0,22.732462560000013
+2020-10-04T02:50:00.0,22.732462560000013
+2020-10-04T02:55:00.0,22.732462560000013
+2020-10-04T03:00:00.0,23.206703400000016
+2020-10-04T03:05:00.0,23.206703400000016
+2020-10-04T03:10:00.0,23.206703400000016
+2020-10-04T03:15:00.0,23.206703400000016
+2020-10-04T03:20:00.0,23.206703400000016
+2020-10-04T03:25:00.0,23.206703400000016
+2020-10-04T03:30:00.0,23.206703400000016
+2020-10-04T03:35:00.0,23.206703400000016
+2020-10-04T03:40:00.0,23.206703400000016
+2020-10-04T03:45:00.0,23.206703400000016
+2020-10-04T03:50:00.0,23.206703400000016
+2020-10-04T03:55:00.0,23.206703400000016
+2020-10-04T04:00:00.0,26.283342074228177
+2020-10-04T04:05:00.0,26.283342074228177
+2020-10-04T04:10:00.0,26.283342074228177
+2020-10-04T04:15:00.0,26.283342074228177
+2020-10-04T04:20:00.0,26.283342074228177
+2020-10-04T04:25:00.0,26.283342074228177
+2020-10-04T04:30:00.0,26.283342074228177
+2020-10-04T04:35:00.0,26.283342074228177
+2020-10-04T04:40:00.0,26.283342074228177
+2020-10-04T04:45:00.0,26.283342074228177
+2020-10-04T04:50:00.0,26.283342074228177
+2020-10-04T04:55:00.0,26.283342074228177
+2020-10-04T05:00:00.0,23.12895899999998
+2020-10-04T05:05:00.0,23.12895899999998
+2020-10-04T05:10:00.0,23.12895899999998
+2020-10-04T05:15:00.0,23.12895899999998
+2020-10-04T05:20:00.0,23.12895899999998
+2020-10-04T05:25:00.0,23.12895899999998
+2020-10-04T05:30:00.0,23.12895899999998
+2020-10-04T05:35:00.0,23.12895899999998
+2020-10-04T05:40:00.0,23.12895899999998
+2020-10-04T05:45:00.0,23.12895899999998
+2020-10-04T05:50:00.0,23.12895899999998
+2020-10-04T05:55:00.0,23.12895899999998
+2020-10-04T06:00:00.0,-15.000000000000002
+2020-10-04T06:05:00.0,-15.000000000000002
+2020-10-04T06:10:00.0,-15.000000000000002
+2020-10-04T06:15:00.0,-15.000000000000002
+2020-10-04T06:20:00.0,-15.000000000000002
+2020-10-04T06:25:00.0,-15.000000000000002
+2020-10-04T06:30:00.0,-15.000000000000002
+2020-10-04T06:35:00.0,-15.000000000000002
+2020-10-04T06:40:00.0,-15.000000000000002
+2020-10-04T06:45:00.0,-15.000000000000002
+2020-10-04T06:50:00.0,-15.000000000000002
+2020-10-04T06:55:00.0,-15.000000000000002
+2020-10-04T07:00:00.0,-14.999999999999986
+2020-10-04T07:05:00.0,-14.999999999999986
+2020-10-04T07:10:00.0,-14.999999999999986
+2020-10-04T07:15:00.0,-14.999999999999986
+2020-10-04T07:20:00.0,-14.999999999999986
+2020-10-04T07:25:00.0,-14.999999999999986
+2020-10-04T07:30:00.0,-14.999999999999986
+2020-10-04T07:35:00.0,-14.999999999999986
+2020-10-04T07:40:00.0,-14.999999999999986
+2020-10-04T07:45:00.0,-14.999999999999986
+2020-10-04T07:50:00.0,-14.999999999999986
+2020-10-04T07:55:00.0,-14.999999999999986
+2020-10-04T08:00:00.0,-14.999999999999677
+2020-10-04T08:05:00.0,-14.999999999999677
+2020-10-04T08:10:00.0,-14.999999999999677
+2020-10-04T08:15:00.0,-14.999999999999677
+2020-10-04T08:20:00.0,-14.999999999999677
+2020-10-04T08:25:00.0,-14.999999999999677
+2020-10-04T08:30:00.0,-14.999999999999677
+2020-10-04T08:35:00.0,-14.999999999999677
+2020-10-04T08:40:00.0,-14.999999999999677
+2020-10-04T08:45:00.0,-14.999999999999677
+2020-10-04T08:50:00.0,-14.999999999999677
+2020-10-04T08:55:00.0,-14.999999999999677
+2020-10-04T09:00:00.0,21.338117861855576
+2020-10-04T09:05:00.0,21.338117861855576
+2020-10-04T09:10:00.0,21.338117861855576
+2020-10-04T09:15:00.0,21.338117861855576
+2020-10-04T09:20:00.0,21.338117861855576
+2020-10-04T09:25:00.0,21.338117861855576
+2020-10-04T09:30:00.0,21.338117861855576
+2020-10-04T09:35:00.0,21.338117861855576
+2020-10-04T09:40:00.0,21.338117861855576
+2020-10-04T09:45:00.0,21.338117861855576
+2020-10-04T09:50:00.0,21.338117861855576
+2020-10-04T09:55:00.0,21.338117861855576
+2020-10-04T10:00:00.0,22.732462559999984
+2020-10-04T10:05:00.0,22.732462559999984
+2020-10-04T10:10:00.0,22.732462559999984
+2020-10-04T10:15:00.0,22.732462559999984
+2020-10-04T10:20:00.0,22.732462559999984
+2020-10-04T10:25:00.0,22.732462559999984
+2020-10-04T10:30:00.0,22.732462559999984
+2020-10-04T10:35:00.0,22.732462559999984
+2020-10-04T10:40:00.0,22.732462559999984
+2020-10-04T10:45:00.0,22.732462559999984
+2020-10-04T10:50:00.0,22.732462559999984
+2020-10-04T10:55:00.0,22.732462559999984
+2020-10-04T11:00:00.0,19.661306868143946
+2020-10-04T11:05:00.0,19.661306868143946
+2020-10-04T11:10:00.0,19.661306868143946
+2020-10-04T11:15:00.0,19.661306868143946
+2020-10-04T11:20:00.0,19.661306868143946
+2020-10-04T11:25:00.0,19.661306868143946
+2020-10-04T11:30:00.0,19.661306868143946
+2020-10-04T11:35:00.0,19.661306868143946
+2020-10-04T11:40:00.0,19.661306868143946
+2020-10-04T11:45:00.0,19.661306868143946
+2020-10-04T11:50:00.0,19.661306868143946
+2020-10-04T11:55:00.0,19.661306868143946
+2020-10-04T12:00:00.0,23.20670340000006
+2020-10-04T12:05:00.0,23.20670340000006
+2020-10-04T12:10:00.0,23.20670340000006
+2020-10-04T12:15:00.0,23.20670340000006
+2020-10-04T12:20:00.0,23.20670340000006
+2020-10-04T12:25:00.0,23.20670340000006
+2020-10-04T12:30:00.0,23.20670340000006
+2020-10-04T12:35:00.0,23.20670340000006
+2020-10-04T12:40:00.0,23.20670340000006
+2020-10-04T12:45:00.0,23.20670340000006
+2020-10-04T12:50:00.0,23.20670340000006
+2020-10-04T12:55:00.0,23.20670340000006
+2020-10-04T13:00:00.0,30.530225880000188
+2020-10-04T13:05:00.0,30.530225880000188
+2020-10-04T13:10:00.0,30.530225880000188
+2020-10-04T13:15:00.0,30.530225880000188
+2020-10-04T13:20:00.0,30.530225880000188
+2020-10-04T13:25:00.0,30.530225880000188
+2020-10-04T13:30:00.0,30.530225880000188
+2020-10-04T13:35:00.0,30.530225880000188
+2020-10-04T13:40:00.0,30.530225880000188
+2020-10-04T13:45:00.0,30.530225880000188
+2020-10-04T13:50:00.0,30.530225880000188
+2020-10-04T13:55:00.0,30.530225880000188
+2020-10-04T14:00:00.0,27.128908380000027
+2020-10-04T14:05:00.0,27.128908380000027
+2020-10-04T14:10:00.0,27.128908380000027
+2020-10-04T14:15:00.0,27.128908380000027
+2020-10-04T14:20:00.0,27.128908380000027
+2020-10-04T14:25:00.0,27.128908380000027
+2020-10-04T14:30:00.0,27.128908380000027
+2020-10-04T14:35:00.0,27.128908380000027
+2020-10-04T14:40:00.0,27.128908380000027
+2020-10-04T14:45:00.0,27.128908380000027
+2020-10-04T14:50:00.0,27.128908380000027
+2020-10-04T14:55:00.0,27.128908380000027
+2020-10-04T15:00:00.0,26.755735259999923
+2020-10-04T15:05:00.0,26.755735259999923
+2020-10-04T15:10:00.0,26.755735259999923
+2020-10-04T15:15:00.0,26.755735259999923
+2020-10-04T15:20:00.0,26.755735259999923
+2020-10-04T15:25:00.0,26.755735259999923
+2020-10-04T15:30:00.0,26.755735259999923
+2020-10-04T15:35:00.0,26.755735259999923
+2020-10-04T15:40:00.0,26.755735259999923
+2020-10-04T15:45:00.0,26.755735259999923
+2020-10-04T15:50:00.0,26.755735259999923
+2020-10-04T15:55:00.0,26.755735259999923
+2020-10-04T16:00:00.0,30.27755657999988
+2020-10-04T16:05:00.0,30.27755657999988
+2020-10-04T16:10:00.0,30.27755657999988
+2020-10-04T16:15:00.0,30.27755657999988
+2020-10-04T16:20:00.0,30.27755657999988
+2020-10-04T16:25:00.0,30.27755657999988
+2020-10-04T16:30:00.0,30.27755657999988
+2020-10-04T16:35:00.0,30.27755657999988
+2020-10-04T16:40:00.0,30.27755657999988
+2020-10-04T16:45:00.0,30.27755657999988
+2020-10-04T16:50:00.0,30.27755657999988
+2020-10-04T16:55:00.0,30.27755657999988
+2020-10-04T17:00:00.0,78.35360939288634
+2020-10-04T17:05:00.0,78.35360939288634
+2020-10-04T17:10:00.0,78.35360939288634
+2020-10-04T17:15:00.0,78.35360939288634
+2020-10-04T17:20:00.0,78.35360939288634
+2020-10-04T17:25:00.0,78.35360939288634
+2020-10-04T17:30:00.0,78.35360939288634
+2020-10-04T17:35:00.0,78.35360939288634
+2020-10-04T17:40:00.0,78.35360939288634
+2020-10-04T17:45:00.0,78.35360939288634
+2020-10-04T17:50:00.0,78.35360939288634
+2020-10-04T17:55:00.0,78.35360939288634
+2020-10-04T18:00:00.0,78.35360939288614
+2020-10-04T18:05:00.0,78.35360939288614
+2020-10-04T18:10:00.0,78.35360939288614
+2020-10-04T18:15:00.0,78.35360939288614
+2020-10-04T18:20:00.0,78.35360939288614
+2020-10-04T18:25:00.0,78.35360939288614
+2020-10-04T18:30:00.0,78.35360939288614
+2020-10-04T18:35:00.0,78.35360939288614
+2020-10-04T18:40:00.0,78.35360939288614
+2020-10-04T18:45:00.0,78.35360939288614
+2020-10-04T18:50:00.0,78.35360939288614
+2020-10-04T18:55:00.0,78.35360939288614
+2020-10-04T19:00:00.0,67.48390376638841
+2020-10-04T19:05:00.0,67.48390376638841
+2020-10-04T19:10:00.0,67.48390376638841
+2020-10-04T19:15:00.0,67.48390376638841
+2020-10-04T19:20:00.0,67.48390376638841
+2020-10-04T19:25:00.0,67.48390376638841
+2020-10-04T19:30:00.0,67.48390376638841
+2020-10-04T19:35:00.0,67.48390376638841
+2020-10-04T19:40:00.0,67.48390376638841
+2020-10-04T19:45:00.0,67.48390376638841
+2020-10-04T19:50:00.0,67.48390376638841
+2020-10-04T19:55:00.0,67.48390376638841
+2020-10-04T20:00:00.0,31.727489639999774
+2020-10-04T20:05:00.0,31.727489639999774
+2020-10-04T20:10:00.0,31.727489639999774
+2020-10-04T20:15:00.0,31.727489639999774
+2020-10-04T20:20:00.0,31.727489639999774
+2020-10-04T20:25:00.0,31.727489639999774
+2020-10-04T20:30:00.0,31.727489639999774
+2020-10-04T20:35:00.0,31.727489639999774
+2020-10-04T20:40:00.0,31.727489639999774
+2020-10-04T20:45:00.0,31.727489639999774
+2020-10-04T20:50:00.0,31.727489639999774
+2020-10-04T20:55:00.0,31.727489639999774
+2020-10-04T21:00:00.0,27.754750800000046
+2020-10-04T21:05:00.0,27.754750800000046
+2020-10-04T21:10:00.0,27.754750800000046
+2020-10-04T21:15:00.0,27.754750800000046
+2020-10-04T21:20:00.0,27.754750800000046
+2020-10-04T21:25:00.0,27.754750800000046
+2020-10-04T21:30:00.0,27.754750800000046
+2020-10-04T21:35:00.0,27.754750800000046
+2020-10-04T21:40:00.0,27.754750800000046
+2020-10-04T21:45:00.0,27.754750800000046
+2020-10-04T21:50:00.0,27.754750800000046
+2020-10-04T21:55:00.0,27.754750800000046
+2020-10-04T22:00:00.0,27.754750800000025
+2020-10-04T22:05:00.0,27.754750800000025
+2020-10-04T22:10:00.0,27.754750800000025
+2020-10-04T22:15:00.0,27.754750800000025
+2020-10-04T22:20:00.0,27.754750800000025
+2020-10-04T22:25:00.0,27.754750800000025
+2020-10-04T22:30:00.0,27.754750800000025
+2020-10-04T22:35:00.0,27.754750800000025
+2020-10-04T22:40:00.0,27.754750800000025
+2020-10-04T22:45:00.0,27.754750800000025
+2020-10-04T22:50:00.0,27.754750800000025
+2020-10-04T22:55:00.0,27.754750800000025
+2020-10-04T23:00:00.0,26.755735259999923
+2020-10-04T23:05:00.0,26.755735259999923
+2020-10-04T23:10:00.0,26.755735259999923
+2020-10-04T23:15:00.0,26.755735259999923
+2020-10-04T23:20:00.0,26.755735259999923
+2020-10-04T23:25:00.0,26.755735259999923
+2020-10-04T23:30:00.0,26.755735259999923
+2020-10-04T23:35:00.0,26.755735259999923
+2020-10-04T23:40:00.0,26.755735259999923
+2020-10-04T23:45:00.0,26.755735259999923
+2020-10-04T23:50:00.0,26.755735259999923
+2020-10-04T23:55:00.0,26.755735259999923
+2020-10-05T00:00:00.0,30.277556579999967
+2020-10-05T00:05:00.0,30.277556579999967
+2020-10-05T00:10:00.0,30.277556579999967
+2020-10-05T00:15:00.0,30.277556579999967
+2020-10-05T00:20:00.0,30.277556579999967
+2020-10-05T00:25:00.0,30.277556579999967
+2020-10-05T00:30:00.0,30.277556579999967
+2020-10-05T00:35:00.0,30.277556579999967
+2020-10-05T00:40:00.0,30.277556579999967
+2020-10-05T00:45:00.0,30.277556579999967
+2020-10-05T00:50:00.0,30.277556579999967
+2020-10-05T00:55:00.0,30.277556579999967
+2020-10-05T01:00:00.0,27.441105789635966
+2020-10-05T01:05:00.0,27.441105789635966
+2020-10-05T01:10:00.0,27.441105789635966
+2020-10-05T01:15:00.0,27.441105789635966
+2020-10-05T01:20:00.0,27.441105789635966
+2020-10-05T01:25:00.0,27.441105789635966
+2020-10-05T01:30:00.0,27.441105789635966
+2020-10-05T01:35:00.0,27.441105789635966
+2020-10-05T01:40:00.0,27.441105789635966
+2020-10-05T01:45:00.0,27.441105789635966
+2020-10-05T01:50:00.0,27.441105789635966
+2020-10-05T01:55:00.0,27.441105789635966
+2020-10-05T02:00:00.0,27.441105789635955
+2020-10-05T02:05:00.0,27.441105789635955
+2020-10-05T02:10:00.0,27.441105789635955
+2020-10-05T02:15:00.0,27.441105789635955
+2020-10-05T02:20:00.0,27.441105789635955
+2020-10-05T02:25:00.0,27.441105789635955
+2020-10-05T02:30:00.0,27.441105789635955
+2020-10-05T02:35:00.0,27.441105789635955
+2020-10-05T02:40:00.0,27.441105789635955
+2020-10-05T02:45:00.0,27.441105789635955
+2020-10-05T02:50:00.0,27.441105789635955
+2020-10-05T02:55:00.0,27.441105789635955
+2020-10-05T03:00:00.0,27.754750800000025
+2020-10-05T03:05:00.0,27.754750800000025
+2020-10-05T03:10:00.0,27.754750800000025
+2020-10-05T03:15:00.0,27.754750800000025
+2020-10-05T03:20:00.0,27.754750800000025
+2020-10-05T03:25:00.0,27.754750800000025
+2020-10-05T03:30:00.0,27.754750800000025
+2020-10-05T03:35:00.0,27.754750800000025
+2020-10-05T03:40:00.0,27.754750800000025
+2020-10-05T03:45:00.0,27.754750800000025
+2020-10-05T03:50:00.0,27.754750800000025
+2020-10-05T03:55:00.0,27.754750800000025
+2020-10-05T04:00:00.0,30.277556579999906
+2020-10-05T04:05:00.0,30.277556579999906
+2020-10-05T04:10:00.0,30.277556579999906
+2020-10-05T04:15:00.0,30.277556579999906
+2020-10-05T04:20:00.0,30.277556579999906
+2020-10-05T04:25:00.0,30.277556579999906
+2020-10-05T04:30:00.0,30.277556579999906
+2020-10-05T04:35:00.0,30.277556579999906
+2020-10-05T04:40:00.0,30.277556579999906
+2020-10-05T04:45:00.0,30.277556579999906
+2020-10-05T04:50:00.0,30.277556579999906
+2020-10-05T04:55:00.0,30.277556579999906
+2020-10-05T05:00:00.0,31.727489640000023
+2020-10-05T05:05:00.0,31.727489640000023
+2020-10-05T05:10:00.0,31.727489640000023
+2020-10-05T05:15:00.0,31.727489640000023
+2020-10-05T05:20:00.0,31.727489640000023
+2020-10-05T05:25:00.0,31.727489640000023
+2020-10-05T05:30:00.0,31.727489640000023
+2020-10-05T05:35:00.0,31.727489640000023
+2020-10-05T05:40:00.0,31.727489640000023
+2020-10-05T05:45:00.0,31.727489640000023
+2020-10-05T05:50:00.0,31.727489640000023
+2020-10-05T05:55:00.0,31.727489640000023
+2020-10-05T06:00:00.0,-14.999999999999957
+2020-10-05T06:05:00.0,-14.999999999999957
+2020-10-05T06:10:00.0,-14.999999999999957
+2020-10-05T06:15:00.0,-14.999999999999957
+2020-10-05T06:20:00.0,-14.999999999999957
+2020-10-05T06:25:00.0,-14.999999999999957
+2020-10-05T06:30:00.0,-14.999999999999957
+2020-10-05T06:35:00.0,-14.999999999999957
+2020-10-05T06:40:00.0,-14.999999999999957
+2020-10-05T06:45:00.0,-14.999999999999957
+2020-10-05T06:50:00.0,-14.999999999999957
+2020-10-05T06:55:00.0,-14.999999999999957
+2020-10-05T07:00:00.0,-14.999999999999936
+2020-10-05T07:05:00.0,-14.999999999999936
+2020-10-05T07:10:00.0,-14.999999999999936
+2020-10-05T07:15:00.0,-14.999999999999936
+2020-10-05T07:20:00.0,-14.999999999999936
+2020-10-05T07:25:00.0,-14.999999999999936
+2020-10-05T07:30:00.0,-14.999999999999936
+2020-10-05T07:35:00.0,-14.999999999999936
+2020-10-05T07:40:00.0,-14.999999999999936
+2020-10-05T07:45:00.0,-14.999999999999936
+2020-10-05T07:50:00.0,-14.999999999999936
+2020-10-05T07:55:00.0,-14.999999999999936
+2020-10-05T08:00:00.0,-14.999999999999986
+2020-10-05T08:05:00.0,-14.999999999999986
+2020-10-05T08:10:00.0,-14.999999999999986
+2020-10-05T08:15:00.0,-14.999999999999986
+2020-10-05T08:20:00.0,-14.999999999999986
+2020-10-05T08:25:00.0,-14.999999999999986
+2020-10-05T08:30:00.0,-14.999999999999986
+2020-10-05T08:35:00.0,-14.999999999999986
+2020-10-05T08:40:00.0,-14.999999999999986
+2020-10-05T08:45:00.0,-14.999999999999986
+2020-10-05T08:50:00.0,-14.999999999999986
+2020-10-05T08:55:00.0,-14.999999999999986
+2020-10-05T09:00:00.0,-15.00000000000001
+2020-10-05T09:05:00.0,-15.00000000000001
+2020-10-05T09:10:00.0,-15.00000000000001
+2020-10-05T09:15:00.0,-15.00000000000001
+2020-10-05T09:20:00.0,-15.00000000000001
+2020-10-05T09:25:00.0,-15.00000000000001
+2020-10-05T09:30:00.0,-15.00000000000001
+2020-10-05T09:35:00.0,-15.00000000000001
+2020-10-05T09:40:00.0,-15.00000000000001
+2020-10-05T09:45:00.0,-15.00000000000001
+2020-10-05T09:50:00.0,-15.00000000000001
+2020-10-05T09:55:00.0,-15.00000000000001
+2020-10-05T10:00:00.0,-15.00000000000003
+2020-10-05T10:05:00.0,-15.00000000000003
+2020-10-05T10:10:00.0,-15.00000000000003
+2020-10-05T10:15:00.0,-15.00000000000003
+2020-10-05T10:20:00.0,-15.00000000000003
+2020-10-05T10:25:00.0,-15.00000000000003
+2020-10-05T10:30:00.0,-15.00000000000003
+2020-10-05T10:35:00.0,-15.00000000000003
+2020-10-05T10:40:00.0,-15.00000000000003
+2020-10-05T10:45:00.0,-15.00000000000003
+2020-10-05T10:50:00.0,-15.00000000000003
+2020-10-05T10:55:00.0,-15.00000000000003
+2020-10-05T11:00:00.0,22.57697376
+2020-10-05T11:05:00.0,22.57697376
+2020-10-05T11:10:00.0,22.57697376
+2020-10-05T11:15:00.0,22.57697376
+2020-10-05T11:20:00.0,22.57697376
+2020-10-05T11:25:00.0,22.57697376
+2020-10-05T11:30:00.0,22.57697376
+2020-10-05T11:35:00.0,22.57697376
+2020-10-05T11:40:00.0,22.57697376
+2020-10-05T11:45:00.0,22.57697376
+2020-10-05T11:50:00.0,22.57697376
+2020-10-05T11:55:00.0,22.57697376
+2020-10-05T12:00:00.0,25.908321299999994
+2020-10-05T12:05:00.0,25.908321299999994
+2020-10-05T12:10:00.0,25.908321299999994
+2020-10-05T12:15:00.0,25.908321299999994
+2020-10-05T12:20:00.0,25.908321299999994
+2020-10-05T12:25:00.0,25.908321299999994
+2020-10-05T12:30:00.0,25.908321299999994
+2020-10-05T12:35:00.0,25.908321299999994
+2020-10-05T12:40:00.0,25.908321299999994
+2020-10-05T12:45:00.0,25.908321299999994
+2020-10-05T12:50:00.0,25.908321299999994
+2020-10-05T12:55:00.0,25.908321299999994
+2020-10-05T13:00:00.0,26.42920877999987
+2020-10-05T13:05:00.0,26.42920877999987
+2020-10-05T13:10:00.0,26.42920877999987
+2020-10-05T13:15:00.0,26.42920877999987
+2020-10-05T13:20:00.0,26.42920877999987
+2020-10-05T13:25:00.0,26.42920877999987
+2020-10-05T13:30:00.0,26.42920877999987
+2020-10-05T13:35:00.0,26.42920877999987
+2020-10-05T13:40:00.0,26.42920877999987
+2020-10-05T13:45:00.0,26.42920877999987
+2020-10-05T13:50:00.0,26.42920877999987
+2020-10-05T13:55:00.0,26.42920877999987
+2020-10-05T14:00:00.0,27.754750800000057
+2020-10-05T14:05:00.0,27.754750800000057
+2020-10-05T14:10:00.0,27.754750800000057
+2020-10-05T14:15:00.0,27.754750800000057
+2020-10-05T14:20:00.0,27.754750800000057
+2020-10-05T14:25:00.0,27.754750800000057
+2020-10-05T14:30:00.0,27.754750800000057
+2020-10-05T14:35:00.0,27.754750800000057
+2020-10-05T14:40:00.0,27.754750800000057
+2020-10-05T14:45:00.0,27.754750800000057
+2020-10-05T14:50:00.0,27.754750800000057
+2020-10-05T14:55:00.0,27.754750800000057
+2020-10-05T15:00:00.0,30.53022587999992
+2020-10-05T15:05:00.0,30.53022587999992
+2020-10-05T15:10:00.0,30.53022587999992
+2020-10-05T15:15:00.0,30.53022587999992
+2020-10-05T15:20:00.0,30.53022587999992
+2020-10-05T15:25:00.0,30.53022587999992
+2020-10-05T15:30:00.0,30.53022587999992
+2020-10-05T15:35:00.0,30.53022587999992
+2020-10-05T15:40:00.0,30.53022587999992
+2020-10-05T15:45:00.0,30.53022587999992
+2020-10-05T15:50:00.0,30.53022587999992
+2020-10-05T15:55:00.0,30.53022587999992
+2020-10-05T16:00:00.0,62.305558237152425
+2020-10-05T16:05:00.0,62.305558237152425
+2020-10-05T16:10:00.0,62.305558237152425
+2020-10-05T16:15:00.0,62.305558237152425
+2020-10-05T16:20:00.0,62.305558237152425
+2020-10-05T16:25:00.0,62.305558237152425
+2020-10-05T16:30:00.0,62.305558237152425
+2020-10-05T16:35:00.0,62.305558237152425
+2020-10-05T16:40:00.0,62.305558237152425
+2020-10-05T16:45:00.0,62.305558237152425
+2020-10-05T16:50:00.0,62.305558237152425
+2020-10-05T16:55:00.0,62.305558237152425
+2020-10-05T17:00:00.0,62.305558237152354
+2020-10-05T17:05:00.0,62.305558237152354
+2020-10-05T17:10:00.0,62.305558237152354
+2020-10-05T17:15:00.0,62.305558237152354
+2020-10-05T17:20:00.0,62.305558237152354
+2020-10-05T17:25:00.0,62.305558237152354
+2020-10-05T17:30:00.0,62.305558237152354
+2020-10-05T17:35:00.0,62.305558237152354
+2020-10-05T17:40:00.0,62.305558237152354
+2020-10-05T17:45:00.0,62.305558237152354
+2020-10-05T17:50:00.0,62.305558237152354
+2020-10-05T17:55:00.0,62.305558237152354
+2020-10-05T18:00:00.0,62.305558237152376
+2020-10-05T18:05:00.0,62.305558237152376
+2020-10-05T18:10:00.0,62.305558237152376
+2020-10-05T18:15:00.0,62.305558237152376
+2020-10-05T18:20:00.0,62.305558237152376
+2020-10-05T18:25:00.0,62.305558237152376
+2020-10-05T18:30:00.0,62.305558237152376
+2020-10-05T18:35:00.0,62.305558237152376
+2020-10-05T18:40:00.0,62.305558237152376
+2020-10-05T18:45:00.0,62.305558237152376
+2020-10-05T18:50:00.0,62.305558237152376
+2020-10-05T18:55:00.0,62.305558237152376
+2020-10-05T19:00:00.0,33.4523008697569
+2020-10-05T19:05:00.0,33.4523008697569
+2020-10-05T19:10:00.0,33.4523008697569
+2020-10-05T19:15:00.0,33.4523008697569
+2020-10-05T19:20:00.0,33.4523008697569
+2020-10-05T19:25:00.0,33.4523008697569
+2020-10-05T19:30:00.0,33.4523008697569
+2020-10-05T19:35:00.0,33.4523008697569
+2020-10-05T19:40:00.0,33.4523008697569
+2020-10-05T19:45:00.0,33.4523008697569
+2020-10-05T19:50:00.0,33.4523008697569
+2020-10-05T19:55:00.0,33.4523008697569
+2020-10-05T20:00:00.0,30.530225880000092
+2020-10-05T20:05:00.0,30.530225880000092
+2020-10-05T20:10:00.0,30.530225880000092
+2020-10-05T20:15:00.0,30.530225880000092
+2020-10-05T20:20:00.0,30.530225880000092
+2020-10-05T20:25:00.0,30.530225880000092
+2020-10-05T20:30:00.0,30.530225880000092
+2020-10-05T20:35:00.0,30.530225880000092
+2020-10-05T20:40:00.0,30.530225880000092
+2020-10-05T20:45:00.0,30.530225880000092
+2020-10-05T20:50:00.0,30.530225880000092
+2020-10-05T20:55:00.0,30.530225880000092
+2020-10-05T21:00:00.0,26.845141320000025
+2020-10-05T21:05:00.0,26.845141320000025
+2020-10-05T21:10:00.0,26.845141320000025
+2020-10-05T21:15:00.0,26.845141320000025
+2020-10-05T21:20:00.0,26.845141320000025
+2020-10-05T21:25:00.0,26.845141320000025
+2020-10-05T21:30:00.0,26.845141320000025
+2020-10-05T21:35:00.0,26.845141320000025
+2020-10-05T21:40:00.0,26.845141320000025
+2020-10-05T21:45:00.0,26.845141320000025
+2020-10-05T21:50:00.0,26.845141320000025
+2020-10-05T21:55:00.0,26.845141320000025
+2020-10-05T22:00:00.0,26.32425383999998
+2020-10-05T22:05:00.0,26.32425383999998
+2020-10-05T22:10:00.0,26.32425383999998
+2020-10-05T22:15:00.0,26.32425383999998
+2020-10-05T22:20:00.0,26.32425383999998
+2020-10-05T22:25:00.0,26.32425383999998
+2020-10-05T22:30:00.0,26.32425383999998
+2020-10-05T22:35:00.0,26.32425383999998
+2020-10-05T22:40:00.0,26.32425383999998
+2020-10-05T22:45:00.0,26.32425383999998
+2020-10-05T22:50:00.0,26.32425383999998
+2020-10-05T22:55:00.0,26.32425383999998
+2020-10-05T23:00:00.0,26.429208779999886
+2020-10-05T23:05:00.0,26.429208779999886
+2020-10-05T23:10:00.0,26.429208779999886
+2020-10-05T23:15:00.0,26.429208779999886
+2020-10-05T23:20:00.0,26.429208779999886
+2020-10-05T23:25:00.0,26.429208779999886
+2020-10-05T23:30:00.0,26.429208779999886
+2020-10-05T23:35:00.0,26.429208779999886
+2020-10-05T23:40:00.0,26.429208779999886
+2020-10-05T23:45:00.0,26.429208779999886
+2020-10-05T23:50:00.0,26.429208779999886
+2020-10-05T23:55:00.0,26.429208779999886
diff --git a/test/inputs/chuhsi_DA_prices_5min_300.csv b/test/inputs/chuhsi_DA_prices_5min_300.csv
new file mode 100644
index 00000000..65a8c160
--- /dev/null
+++ b/test/inputs/chuhsi_DA_prices_5min_300.csv
@@ -0,0 +1,301 @@
+DateTime,Chuhsi
+2020-10-03T00:00:00.0,30.27755657999998
+2020-10-03T00:05:00.0,30.27755657999998
+2020-10-03T00:10:00.0,30.27755657999998
+2020-10-03T00:15:00.0,30.27755657999998
+2020-10-03T00:20:00.0,30.27755657999998
+2020-10-03T00:25:00.0,30.27755657999998
+2020-10-03T00:30:00.0,30.27755657999998
+2020-10-03T00:35:00.0,30.27755657999998
+2020-10-03T00:40:00.0,30.27755657999998
+2020-10-03T00:45:00.0,30.27755657999998
+2020-10-03T00:50:00.0,30.27755657999998
+2020-10-03T00:55:00.0,30.27755657999998
+2020-10-03T01:00:00.0,27.754750800000032
+2020-10-03T01:05:00.0,27.754750800000032
+2020-10-03T01:10:00.0,27.754750800000032
+2020-10-03T01:15:00.0,27.754750800000032
+2020-10-03T01:20:00.0,27.754750800000032
+2020-10-03T01:25:00.0,27.754750800000032
+2020-10-03T01:30:00.0,27.754750800000032
+2020-10-03T01:35:00.0,27.754750800000032
+2020-10-03T01:40:00.0,27.754750800000032
+2020-10-03T01:45:00.0,27.754750800000032
+2020-10-03T01:50:00.0,27.754750800000032
+2020-10-03T01:55:00.0,27.754750800000032
+2020-10-03T02:00:00.0,30.277556579999924
+2020-10-03T02:05:00.0,30.277556579999924
+2020-10-03T02:10:00.0,30.277556579999924
+2020-10-03T02:15:00.0,30.277556579999924
+2020-10-03T02:20:00.0,30.277556579999924
+2020-10-03T02:25:00.0,30.277556579999924
+2020-10-03T02:30:00.0,30.277556579999924
+2020-10-03T02:35:00.0,30.277556579999924
+2020-10-03T02:40:00.0,30.277556579999924
+2020-10-03T02:45:00.0,30.277556579999924
+2020-10-03T02:50:00.0,30.277556579999924
+2020-10-03T02:55:00.0,30.277556579999924
+2020-10-03T03:00:00.0,26.790720239999825
+2020-10-03T03:05:00.0,26.790720239999825
+2020-10-03T03:10:00.0,26.790720239999825
+2020-10-03T03:15:00.0,26.790720239999825
+2020-10-03T03:20:00.0,26.790720239999825
+2020-10-03T03:25:00.0,26.790720239999825
+2020-10-03T03:30:00.0,26.790720239999825
+2020-10-03T03:35:00.0,26.790720239999825
+2020-10-03T03:40:00.0,26.790720239999825
+2020-10-03T03:45:00.0,26.790720239999825
+2020-10-03T03:50:00.0,26.790720239999825
+2020-10-03T03:55:00.0,26.790720239999825
+2020-10-03T04:00:00.0,26.790720240000102
+2020-10-03T04:05:00.0,26.790720240000102
+2020-10-03T04:10:00.0,26.790720240000102
+2020-10-03T04:15:00.0,26.790720240000102
+2020-10-03T04:20:00.0,26.790720240000102
+2020-10-03T04:25:00.0,26.790720240000102
+2020-10-03T04:30:00.0,26.790720240000102
+2020-10-03T04:35:00.0,26.790720240000102
+2020-10-03T04:40:00.0,26.790720240000102
+2020-10-03T04:45:00.0,26.790720240000102
+2020-10-03T04:50:00.0,26.790720240000102
+2020-10-03T04:55:00.0,26.790720240000102
+2020-10-03T05:00:00.0,30.277556579999988
+2020-10-03T05:05:00.0,30.277556579999988
+2020-10-03T05:10:00.0,30.277556579999988
+2020-10-03T05:15:00.0,30.277556579999988
+2020-10-03T05:20:00.0,30.277556579999988
+2020-10-03T05:25:00.0,30.277556579999988
+2020-10-03T05:30:00.0,30.277556579999988
+2020-10-03T05:35:00.0,30.277556579999988
+2020-10-03T05:40:00.0,30.277556579999988
+2020-10-03T05:45:00.0,30.277556579999988
+2020-10-03T05:50:00.0,30.277556579999988
+2020-10-03T05:55:00.0,30.277556579999988
+2020-10-03T06:00:00.0,-15.00000000000001
+2020-10-03T06:05:00.0,-15.00000000000001
+2020-10-03T06:10:00.0,-15.00000000000001
+2020-10-03T06:15:00.0,-15.00000000000001
+2020-10-03T06:20:00.0,-15.00000000000001
+2020-10-03T06:25:00.0,-15.00000000000001
+2020-10-03T06:30:00.0,-15.00000000000001
+2020-10-03T06:35:00.0,-15.00000000000001
+2020-10-03T06:40:00.0,-15.00000000000001
+2020-10-03T06:45:00.0,-15.00000000000001
+2020-10-03T06:50:00.0,-15.00000000000001
+2020-10-03T06:55:00.0,-15.00000000000001
+2020-10-03T07:00:00.0,-15.00000000000025
+2020-10-03T07:05:00.0,-15.00000000000025
+2020-10-03T07:10:00.0,-15.00000000000025
+2020-10-03T07:15:00.0,-15.00000000000025
+2020-10-03T07:20:00.0,-15.00000000000025
+2020-10-03T07:25:00.0,-15.00000000000025
+2020-10-03T07:30:00.0,-15.00000000000025
+2020-10-03T07:35:00.0,-15.00000000000025
+2020-10-03T07:40:00.0,-15.00000000000025
+2020-10-03T07:45:00.0,-15.00000000000025
+2020-10-03T07:50:00.0,-15.00000000000025
+2020-10-03T07:55:00.0,-15.00000000000025
+2020-10-03T08:00:00.0,-15.000000000000242
+2020-10-03T08:05:00.0,-15.000000000000242
+2020-10-03T08:10:00.0,-15.000000000000242
+2020-10-03T08:15:00.0,-15.000000000000242
+2020-10-03T08:20:00.0,-15.000000000000242
+2020-10-03T08:25:00.0,-15.000000000000242
+2020-10-03T08:30:00.0,-15.000000000000242
+2020-10-03T08:35:00.0,-15.000000000000242
+2020-10-03T08:40:00.0,-15.000000000000242
+2020-10-03T08:45:00.0,-15.000000000000242
+2020-10-03T08:50:00.0,-15.000000000000242
+2020-10-03T08:55:00.0,-15.000000000000242
+2020-10-03T09:00:00.0,-15.000000000000043
+2020-10-03T09:05:00.0,-15.000000000000043
+2020-10-03T09:10:00.0,-15.000000000000043
+2020-10-03T09:15:00.0,-15.000000000000043
+2020-10-03T09:20:00.0,-15.000000000000043
+2020-10-03T09:25:00.0,-15.000000000000043
+2020-10-03T09:30:00.0,-15.000000000000043
+2020-10-03T09:35:00.0,-15.000000000000043
+2020-10-03T09:40:00.0,-15.000000000000043
+2020-10-03T09:45:00.0,-15.000000000000043
+2020-10-03T09:50:00.0,-15.000000000000043
+2020-10-03T09:55:00.0,-15.000000000000043
+2020-10-03T10:00:00.0,23.168325362718395
+2020-10-03T10:05:00.0,23.168325362718395
+2020-10-03T10:10:00.0,23.168325362718395
+2020-10-03T10:15:00.0,23.168325362718395
+2020-10-03T10:20:00.0,23.168325362718395
+2020-10-03T10:25:00.0,23.168325362718395
+2020-10-03T10:30:00.0,23.168325362718395
+2020-10-03T10:35:00.0,23.168325362718395
+2020-10-03T10:40:00.0,23.168325362718395
+2020-10-03T10:45:00.0,23.168325362718395
+2020-10-03T10:50:00.0,23.168325362718395
+2020-10-03T10:55:00.0,23.168325362718395
+2020-10-03T11:00:00.0,-14.999999999999993
+2020-10-03T11:05:00.0,-14.999999999999993
+2020-10-03T11:10:00.0,-14.999999999999993
+2020-10-03T11:15:00.0,-14.999999999999993
+2020-10-03T11:20:00.0,-14.999999999999993
+2020-10-03T11:25:00.0,-14.999999999999993
+2020-10-03T11:30:00.0,-14.999999999999993
+2020-10-03T11:35:00.0,-14.999999999999993
+2020-10-03T11:40:00.0,-14.999999999999993
+2020-10-03T11:45:00.0,-14.999999999999993
+2020-10-03T11:50:00.0,-14.999999999999993
+2020-10-03T11:55:00.0,-14.999999999999993
+2020-10-03T12:00:00.0,19.663893987708512
+2020-10-03T12:05:00.0,19.663893987708512
+2020-10-03T12:10:00.0,19.663893987708512
+2020-10-03T12:15:00.0,19.663893987708512
+2020-10-03T12:20:00.0,19.663893987708512
+2020-10-03T12:25:00.0,19.663893987708512
+2020-10-03T12:30:00.0,19.663893987708512
+2020-10-03T12:35:00.0,19.663893987708512
+2020-10-03T12:40:00.0,19.663893987708512
+2020-10-03T12:45:00.0,19.663893987708512
+2020-10-03T12:50:00.0,19.663893987708512
+2020-10-03T12:55:00.0,19.663893987708512
+2020-10-03T13:00:00.0,25.908321300000022
+2020-10-03T13:05:00.0,25.908321300000022
+2020-10-03T13:10:00.0,25.908321300000022
+2020-10-03T13:15:00.0,25.908321300000022
+2020-10-03T13:20:00.0,25.908321300000022
+2020-10-03T13:25:00.0,25.908321300000022
+2020-10-03T13:30:00.0,25.908321300000022
+2020-10-03T13:35:00.0,25.908321300000022
+2020-10-03T13:40:00.0,25.908321300000022
+2020-10-03T13:45:00.0,25.908321300000022
+2020-10-03T13:50:00.0,25.908321300000022
+2020-10-03T13:55:00.0,25.908321300000022
+2020-10-03T14:00:00.0,24.621651480000004
+2020-10-03T14:05:00.0,24.621651480000004
+2020-10-03T14:10:00.0,24.621651480000004
+2020-10-03T14:15:00.0,24.621651480000004
+2020-10-03T14:20:00.0,24.621651480000004
+2020-10-03T14:25:00.0,24.621651480000004
+2020-10-03T14:30:00.0,24.621651480000004
+2020-10-03T14:35:00.0,24.621651480000004
+2020-10-03T14:40:00.0,24.621651480000004
+2020-10-03T14:45:00.0,24.621651480000004
+2020-10-03T14:50:00.0,24.621651480000004
+2020-10-03T14:55:00.0,24.621651480000004
+2020-10-03T15:00:00.0,26.429208780000078
+2020-10-03T15:05:00.0,26.429208780000078
+2020-10-03T15:10:00.0,26.429208780000078
+2020-10-03T15:15:00.0,26.429208780000078
+2020-10-03T15:20:00.0,26.429208780000078
+2020-10-03T15:25:00.0,26.429208780000078
+2020-10-03T15:30:00.0,26.429208780000078
+2020-10-03T15:35:00.0,26.429208780000078
+2020-10-03T15:40:00.0,26.429208780000078
+2020-10-03T15:45:00.0,26.429208780000078
+2020-10-03T15:50:00.0,26.429208780000078
+2020-10-03T15:55:00.0,26.429208780000078
+2020-10-03T16:00:00.0,30.277556579999796
+2020-10-03T16:05:00.0,30.277556579999796
+2020-10-03T16:10:00.0,30.277556579999796
+2020-10-03T16:15:00.0,30.277556579999796
+2020-10-03T16:20:00.0,30.277556579999796
+2020-10-03T16:25:00.0,30.277556579999796
+2020-10-03T16:30:00.0,30.277556579999796
+2020-10-03T16:35:00.0,30.277556579999796
+2020-10-03T16:40:00.0,30.277556579999796
+2020-10-03T16:45:00.0,30.277556579999796
+2020-10-03T16:50:00.0,30.277556579999796
+2020-10-03T16:55:00.0,30.277556579999796
+2020-10-03T17:00:00.0,31.727489639999945
+2020-10-03T17:05:00.0,31.727489639999945
+2020-10-03T17:10:00.0,31.727489639999945
+2020-10-03T17:15:00.0,31.727489639999945
+2020-10-03T17:20:00.0,31.727489639999945
+2020-10-03T17:25:00.0,31.727489639999945
+2020-10-03T17:30:00.0,31.727489639999945
+2020-10-03T17:35:00.0,31.727489639999945
+2020-10-03T17:40:00.0,31.727489639999945
+2020-10-03T17:45:00.0,31.727489639999945
+2020-10-03T17:50:00.0,31.727489639999945
+2020-10-03T17:55:00.0,31.727489639999945
+2020-10-03T18:00:00.0,128.76811444827337
+2020-10-03T18:05:00.0,128.76811444827337
+2020-10-03T18:10:00.0,128.76811444827337
+2020-10-03T18:15:00.0,128.76811444827337
+2020-10-03T18:20:00.0,128.76811444827337
+2020-10-03T18:25:00.0,128.76811444827337
+2020-10-03T18:30:00.0,128.76811444827337
+2020-10-03T18:35:00.0,128.76811444827337
+2020-10-03T18:40:00.0,128.76811444827337
+2020-10-03T18:45:00.0,128.76811444827337
+2020-10-03T18:50:00.0,128.76811444827337
+2020-10-03T18:55:00.0,128.76811444827337
+2020-10-03T19:00:00.0,31.72748963999999
+2020-10-03T19:05:00.0,31.72748963999999
+2020-10-03T19:10:00.0,31.72748963999999
+2020-10-03T19:15:00.0,31.72748963999999
+2020-10-03T19:20:00.0,31.72748963999999
+2020-10-03T19:25:00.0,31.72748963999999
+2020-10-03T19:30:00.0,31.72748963999999
+2020-10-03T19:35:00.0,31.72748963999999
+2020-10-03T19:40:00.0,31.72748963999999
+2020-10-03T19:45:00.0,31.72748963999999
+2020-10-03T19:50:00.0,31.72748963999999
+2020-10-03T19:55:00.0,31.72748963999999
+2020-10-03T20:00:00.0,27.128908380000027
+2020-10-03T20:05:00.0,27.128908380000027
+2020-10-03T20:10:00.0,27.128908380000027
+2020-10-03T20:15:00.0,27.128908380000027
+2020-10-03T20:20:00.0,27.128908380000027
+2020-10-03T20:25:00.0,27.128908380000027
+2020-10-03T20:30:00.0,27.128908380000027
+2020-10-03T20:35:00.0,27.128908380000027
+2020-10-03T20:40:00.0,27.128908380000027
+2020-10-03T20:45:00.0,27.128908380000027
+2020-10-03T20:50:00.0,27.128908380000027
+2020-10-03T20:55:00.0,27.128908380000027
+2020-10-03T21:00:00.0,23.206703399999807
+2020-10-03T21:05:00.0,23.206703399999807
+2020-10-03T21:10:00.0,23.206703399999807
+2020-10-03T21:15:00.0,23.206703399999807
+2020-10-03T21:20:00.0,23.206703399999807
+2020-10-03T21:25:00.0,23.206703399999807
+2020-10-03T21:30:00.0,23.206703399999807
+2020-10-03T21:35:00.0,23.206703399999807
+2020-10-03T21:40:00.0,23.206703399999807
+2020-10-03T21:45:00.0,23.206703399999807
+2020-10-03T21:50:00.0,23.206703399999807
+2020-10-03T21:55:00.0,23.206703399999807
+2020-10-03T22:00:00.0,22.732462559999984
+2020-10-03T22:05:00.0,22.732462559999984
+2020-10-03T22:10:00.0,22.732462559999984
+2020-10-03T22:15:00.0,22.732462559999984
+2020-10-03T22:20:00.0,22.732462559999984
+2020-10-03T22:25:00.0,22.732462559999984
+2020-10-03T22:30:00.0,22.732462559999984
+2020-10-03T22:35:00.0,22.732462559999984
+2020-10-03T22:40:00.0,22.732462559999984
+2020-10-03T22:45:00.0,22.732462559999984
+2020-10-03T22:50:00.0,22.732462559999984
+2020-10-03T22:55:00.0,22.732462559999984
+2020-10-03T23:00:00.0,20.004236639100032
+2020-10-03T23:05:00.0,20.004236639100032
+2020-10-03T23:10:00.0,20.004236639100032
+2020-10-03T23:15:00.0,20.004236639100032
+2020-10-03T23:20:00.0,20.004236639100032
+2020-10-03T23:25:00.0,20.004236639100032
+2020-10-03T23:30:00.0,20.004236639100032
+2020-10-03T23:35:00.0,20.004236639100032
+2020-10-03T23:40:00.0,20.004236639100032
+2020-10-03T23:45:00.0,20.004236639100032
+2020-10-03T23:50:00.0,20.004236639100032
+2020-10-03T23:55:00.0,20.004236639100032
+2020-10-04T00:00:00.0,26.324253840000047
+2020-10-04T00:05:00.0,26.324253840000047
+2020-10-04T00:10:00.0,26.324253840000047
+2020-10-04T00:15:00.0,26.324253840000047
+2020-10-04T00:20:00.0,26.324253840000047
+2020-10-04T00:25:00.0,26.324253840000047
+2020-10-04T00:30:00.0,26.324253840000047
+2020-10-04T00:35:00.0,26.324253840000047
+2020-10-04T00:40:00.0,26.324253840000047
+2020-10-04T00:45:00.0,26.324253840000047
+2020-10-04T00:50:00.0,26.324253840000047
+2020-10-04T00:55:00.0,26.324253840000047
diff --git a/test/inputs/chuhsi_RT_prices_300.csv b/test/inputs/chuhsi_RT_prices_300.csv
new file mode 100644
index 00000000..43f3d9f0
--- /dev/null
+++ b/test/inputs/chuhsi_RT_prices_300.csv
@@ -0,0 +1,301 @@
+DateTime,Chuhsi
+2020-10-03T00:00:00.0,26.79072023999998
+2020-10-03T00:05:00.0,26.755735260000076
+2020-10-03T00:10:00.0,26.755735259999984
+2020-10-03T00:15:00.0,26.755735259999952
+2020-10-03T00:20:00.0,26.755735260000648
+2020-10-03T00:25:00.0,26.755735260000016
+2020-10-03T00:30:00.0,26.755735259999952
+2020-10-03T00:35:00.0,26.755735259999962
+2020-10-03T00:40:00.0,26.755735259999962
+2020-10-03T00:45:00.0,26.755735259999977
+2020-10-03T00:50:00.0,26.755735259999955
+2020-10-03T00:55:00.0,26.75573525999995
+2020-10-03T01:00:00.0,26.755735259999945
+2020-10-03T01:05:00.0,26.75573525999995
+2020-10-03T01:10:00.0,26.755735259999955
+2020-10-03T01:15:00.0,26.790720239999715
+2020-10-03T01:20:00.0,26.79072023999977
+2020-10-03T01:25:00.0,26.755735260000012
+2020-10-03T01:30:00.0,26.755735260000012
+2020-10-03T01:35:00.0,26.75573525999985
+2020-10-03T01:40:00.0,26.75573525999997
+2020-10-03T01:45:00.0,26.429208779999936
+2020-10-03T01:50:00.0,26.429208780000177
+2020-10-03T01:55:00.0,26.42920878000017
+2020-10-03T02:00:00.0,27.754750799999986
+2020-10-03T02:05:00.0,27.75475080000023
+2020-10-03T02:10:00.0,27.7547508
+2020-10-03T02:15:00.0,27.754750799999943
+2020-10-03T02:20:00.0,27.754750800000004
+2020-10-03T02:25:00.0,27.75475080000004
+2020-10-03T02:30:00.0,27.75475080000002
+2020-10-03T02:35:00.0,27.75475080000004
+2020-10-03T02:40:00.0,27.754750799999997
+2020-10-03T02:45:00.0,27.7547508
+2020-10-03T02:50:00.0,26.790720239999985
+2020-10-03T02:55:00.0,27.75475080000005
+2020-10-03T03:00:00.0,26.79072024000011
+2020-10-03T03:05:00.0,26.790720239999818
+2020-10-03T03:10:00.0,26.79072024000011
+2020-10-03T03:15:00.0,26.790720240000073
+2020-10-03T03:20:00.0,26.79072024000011
+2020-10-03T03:25:00.0,26.79072024000021
+2020-10-03T03:30:00.0,26.790720240000105
+2020-10-03T03:35:00.0,26.79072023999981
+2020-10-03T03:40:00.0,26.790720239999985
+2020-10-03T03:45:00.0,26.790720240000077
+2020-10-03T03:50:00.0,26.79072023999981
+2020-10-03T03:55:00.0,26.790720239999626
+2020-10-03T04:00:00.0,26.755735260000026
+2020-10-03T04:05:00.0,26.790720239999985
+2020-10-03T04:10:00.0,26.79072023999971
+2020-10-03T04:15:00.0,27.754750799999986
+2020-10-03T04:20:00.0,26.79072023999978
+2020-10-03T04:25:00.0,26.790720239999768
+2020-10-03T04:30:00.0,28.076534482877953
+2020-10-03T04:35:00.0,26.79072024000004
+2020-10-03T04:40:00.0,27.754750800000053
+2020-10-03T04:45:00.0,28.076534482877992
+2020-10-03T04:50:00.0,27.75475080000003
+2020-10-03T04:55:00.0,27.75475080000003
+2020-10-03T05:00:00.0,31.727489639999742
+2020-10-03T05:05:00.0,32.462174220000016
+2020-10-03T05:10:00.0,32.46217422000002
+2020-10-03T05:15:00.0,200.000000000
+2020-10-03T05:20:00.0,200.000000000
+2020-10-03T05:25:00.0,200.000000000
+2020-10-03T05:30:00.0,200.000000000
+2020-10-03T05:35:00.0,200.000000000
+2020-10-03T05:40:00.0,200.000000000
+2020-10-03T05:45:00.0,200.000000000
+2020-10-03T05:50:00.0,200.000000000
+2020-10-03T05:55:00.0,200.000000000
+2020-10-03T06:00:00.0,200.000000000
+2020-10-03T06:05:00.0,200.000000000
+2020-10-03T06:10:00.0,200.000000000
+2020-10-03T06:15:00.0,200.000000000
+2020-10-03T06:20:00.0,200.0000000000
+2020-10-03T06:25:00.0,30.277556580000173
+2020-10-03T06:30:00.0,26.429208780000305
+2020-10-03T06:35:00.0,25.908321300000047
+2020-10-03T06:40:00.0,24.617413550000002
+2020-10-03T06:45:00.0,23.437807129999936
+2020-10-03T06:50:00.0,23.128959000000002
+2020-10-03T06:55:00.0,23.069972870000015
+2020-10-03T07:00:00.0,23.069972870000008
+2020-10-03T07:05:00.0,23.069972870000015
+2020-10-03T07:10:00.0,22.732462559999984
+2020-10-03T07:15:00.0,21.647257600000064
+2020-10-03T07:20:00.0,19.983547469999976
+2020-10-03T07:25:00.0,21.647257600000064
+2020-10-03T07:30:00.0,21.647257600000327
+2020-10-03T07:35:00.0,19.983547470000413
+2020-10-03T07:40:00.0,18.861018779999995
+2020-10-03T07:45:00.0,18.861018779999995
+2020-10-03T07:50:00.0,0.0
+2020-10-03T07:55:00.0,0.0
+2020-10-03T08:00:00.0,0.0
+2020-10-03T08:05:00.0,0.0
+2020-10-03T08:10:00.0,0.0
+2020-10-03T08:15:00.0,-15.000000000000012
+2020-10-03T08:20:00.0,-15.000000000000048
+2020-10-03T08:25:00.0,0.0
+2020-10-03T08:30:00.0,0.0
+2020-10-03T08:35:00.0,-14.999999999999943
+2020-10-03T08:40:00.0,0.0
+2020-10-03T08:45:00.0,0.0
+2020-10-03T08:50:00.0,0.0
+2020-10-03T08:55:00.0,-15.00000000000001
+2020-10-03T09:00:00.0,-15.000000000000012
+2020-10-03T09:05:00.0,0.0
+2020-10-03T09:10:00.0,0.0
+2020-10-03T09:15:00.0,0.0
+2020-10-03T09:20:00.0,0.0
+2020-10-03T09:25:00.0,0.0
+2020-10-03T09:30:00.0,0.0
+2020-10-03T09:35:00.0,0.43287156724080805
+2020-10-03T09:40:00.0,0.4328715672408083
+2020-10-03T09:45:00.0,18.82779927822689
+2020-10-03T09:50:00.0,18.82779927822689
+2020-10-03T09:55:00.0,18.827799278226884
+2020-10-03T10:00:00.0,21.647257600000035
+2020-10-03T10:05:00.0,21.647257600000046
+2020-10-03T10:10:00.0,21.647257600000042
+2020-10-03T10:15:00.0,21.647257600000042
+2020-10-03T10:20:00.0,21.64725760000003
+2020-10-03T10:25:00.0,21.64725760000004
+2020-10-03T10:30:00.0,21.647257600000035
+2020-10-03T10:35:00.0,21.647257600000025
+2020-10-03T10:40:00.0,21.64725760000002
+2020-10-03T10:45:00.0,21.647257600000046
+2020-10-03T10:50:00.0,21.647257600000003
+2020-10-03T10:55:00.0,21.647257599999985
+2020-10-03T11:00:00.0,19.689702860000015
+2020-10-03T11:05:00.0,18.861018779999988
+2020-10-03T11:10:00.0,18.861018779999988
+2020-10-03T11:15:00.0,18.86101877999998
+2020-10-03T11:20:00.0,18.57351613999999
+2020-10-03T11:25:00.0,18.57351614
+2020-10-03T11:30:00.0,18.57351614000001
+2020-10-03T11:35:00.0,16.312895142822025
+2020-10-03T11:40:00.0,16.312895142821983
+2020-10-03T11:45:00.0,18.57351613999999
+2020-10-03T11:50:00.0,18.86101878
+2020-10-03T11:55:00.0,18.86101877999998
+2020-10-03T12:00:00.0,18.861018779999977
+2020-10-03T12:05:00.0,19.034365959999995
+2020-10-03T12:10:00.0,18.86101877999997
+2020-10-03T12:15:00.0,18.861018779999977
+2020-10-03T12:20:00.0,19.034365959999995
+2020-10-03T12:25:00.0,19.429682089999986
+2020-10-03T12:30:00.0,19.429682089999996
+2020-10-03T12:35:00.0,19.429682090000078
+2020-10-03T12:40:00.0,19.68970286000002
+2020-10-03T12:45:00.0,19.983547469999547
+2020-10-03T12:50:00.0,19.983547469999543
+2020-10-03T12:55:00.0,20.400003500000004
+2020-10-03T13:00:00.0,21.116646110000016
+2020-10-03T13:05:00.0,21.647257599999733
+2020-10-03T13:10:00.0,21.647257600000252
+2020-10-03T13:15:00.0,21.2878793
+2020-10-03T13:20:00.0,21.647257600000025
+2020-10-03T13:25:00.0,21.64725759999998
+2020-10-03T13:30:00.0,22.492853600000025
+2020-10-03T13:35:00.0,22.516107490000028
+2020-10-03T13:40:00.0,22.516107490000028
+2020-10-03T13:45:00.0,22.492853600000025
+2020-10-03T13:50:00.0,22.51610749000002
+2020-10-03T13:55:00.0,22.49285360000002
+2020-10-03T14:00:00.0,19.983547469999813
+2020-10-03T14:05:00.0,20.400003500000008
+2020-10-03T14:10:00.0,20.419029409999997
+2020-10-03T14:15:00.0,21.11664611000001
+2020-10-03T14:20:00.0,21.287879300000018
+2020-10-03T14:25:00.0,21.84385867000002
+2020-10-03T14:30:00.0,22.49285360000002
+2020-10-03T14:35:00.0,22.576973760000005
+2020-10-03T14:40:00.0,22.576973759999994
+2020-10-03T14:45:00.0,22.576973760000055
+2020-10-03T14:50:00.0,22.73246256000001
+2020-10-03T14:55:00.0,22.73246256000001
+2020-10-03T15:00:00.0,22.576973759999987
+2020-10-03T15:05:00.0,22.732462559999995
+2020-10-03T15:10:00.0,22.732462560000002
+2020-10-03T15:15:00.0,22.9685013500001
+2020-10-03T15:20:00.0,23.06997287000001
+2020-10-03T15:25:00.0,23.06997286999999
+2020-10-03T15:30:00.0,23.128959000000023
+2020-10-03T15:35:00.0,23.128959000000002
+2020-10-03T15:40:00.0,23.437807130000046
+2020-10-03T15:45:00.0,24.617413550000006
+2020-10-03T15:50:00.0,24.617413550000002
+2020-10-03T15:55:00.0,24.617413550000002
+2020-10-03T16:00:00.0,22.732462559999984
+2020-10-03T16:05:00.0,23.069972870000015
+2020-10-03T16:10:00.0,23.206703399999956
+2020-10-03T16:15:00.0,23.20670339999998
+2020-10-03T16:20:00.0,23.20670339999979
+2020-10-03T16:25:00.0,23.43780713000002
+2020-10-03T16:30:00.0,23.437807130000092
+2020-10-03T16:35:00.0,24.61741355000001
+2020-10-03T16:40:00.0,24.617413550000013
+2020-10-03T16:45:00.0,24.62165147999999
+2020-10-03T16:50:00.0,24.62165147999998
+2020-10-03T16:55:00.0,24.62165147999997
+2020-10-03T17:00:00.0,25.908321300000036
+2020-10-03T17:05:00.0,25.908321299999997
+2020-10-03T17:10:00.0,24.62165147999999
+2020-10-03T17:15:00.0,24.621651479999983
+2020-10-03T17:20:00.0,24.62165147999998
+2020-10-03T17:25:00.0,24.617413550000002
+2020-10-03T17:30:00.0,24.617413549999984
+2020-10-03T17:35:00.0,23.65766208999997
+2020-10-03T17:40:00.0,23.437807130000017
+2020-10-03T17:45:00.0,23.437807130000095
+2020-10-03T17:50:00.0,23.437807130000092
+2020-10-03T17:55:00.0,23.437807130000063
+2020-10-03T18:00:00.0,23.437807130000095
+2020-10-03T18:05:00.0,23.437807130000053
+2020-10-03T18:10:00.0,23.20670339999976
+2020-10-03T18:15:00.0,23.206703400000134
+2020-10-03T18:20:00.0,23.20670339999975
+2020-10-03T18:25:00.0,23.437807130000063
+2020-10-03T18:30:00.0,23.437807130000138
+2020-10-03T18:35:00.0,23.437807130000188
+2020-10-03T18:40:00.0,23.437807129999882
+2020-10-03T18:45:00.0,23.43780713000062
+2020-10-03T18:50:00.0,23.43780712999985
+2020-10-03T18:55:00.0,23.437807129999847
+2020-10-03T19:00:00.0,23.437807130000092
+2020-10-03T19:05:00.0,23.206703399999736
+2020-10-03T19:10:00.0,23.206703399999785
+2020-10-03T19:15:00.0,23.128959000000023
+2020-10-03T19:20:00.0,23.128959000000023
+2020-10-03T19:25:00.0,23.06997287000001
+2020-10-03T19:30:00.0,22.968501350000363
+2020-10-03T19:35:00.0,22.732462560000002
+2020-10-03T19:40:00.0,22.732462559999984
+2020-10-03T19:45:00.0,22.732462560000002
+2020-10-03T19:50:00.0,22.576973759999973
+2020-10-03T19:55:00.0,22.516107490000046
+2020-10-03T20:00:00.0,22.968501350000036
+2020-10-03T20:05:00.0,22.73246256000001
+2020-10-03T20:10:00.0,22.73246255999999
+2020-10-03T20:15:00.0,22.576973759999976
+2020-10-03T20:20:00.0,22.51610749000007
+2020-10-03T20:25:00.0,22.49285360000003
+2020-10-03T20:30:00.0,21.84385867000001
+2020-10-03T20:35:00.0,21.647257600000028
+2020-10-03T20:40:00.0,21.64725759999995
+2020-10-03T20:45:00.0,21.11664611000001
+2020-10-03T20:50:00.0,20.846055390000103
+2020-10-03T20:55:00.0,20.419029409999993
+2020-10-03T21:00:00.0,20.400003499999997
+2020-10-03T21:05:00.0,19.983547469999994
+2020-10-03T21:10:00.0,19.689702860000022
+2020-10-03T21:15:00.0,19.42968209000004
+2020-10-03T21:20:00.0,19.03436595999999
+2020-10-03T21:25:00.0,18.86101877999998
+2020-10-03T21:30:00.0,18.86101877999998
+2020-10-03T21:35:00.0,18.57351614
+2020-10-03T21:40:00.0,18.46358866000004
+2020-10-03T21:45:00.0,18.463588659999985
+2020-10-03T21:50:00.0,18.072500510000033
+2020-10-03T21:55:00.0,18.072500510000033
+2020-10-03T22:00:00.0,19.98354747000004
+2020-10-03T22:05:00.0,19.983547470000214
+2020-10-03T22:10:00.0,19.983547470000047
+2020-10-03T22:15:00.0,19.98354747000005
+2020-10-03T22:20:00.0,19.68970286000002
+2020-10-03T22:25:00.0,19.68970286
+2020-10-03T22:30:00.0,19.68970286000002
+2020-10-03T22:35:00.0,19.689702859999997
+2020-10-03T22:40:00.0,19.68970285999999
+2020-10-03T22:45:00.0,19.429682090000107
+2020-10-03T22:50:00.0,19.429682090000075
+2020-10-03T22:55:00.0,19.03436595999998
+2020-10-03T23:00:00.0,19.034365959999977
+2020-10-03T23:05:00.0,19.03436595999999
+2020-10-03T23:10:00.0,19.034365959999977
+2020-10-03T23:15:00.0,18.861018779999988
+2020-10-03T23:20:00.0,18.86101877999998
+2020-10-03T23:25:00.0,18.86101877999998
+2020-10-03T23:30:00.0,18.861018779999974
+2020-10-03T23:35:00.0,18.861018779999988
+2020-10-03T23:40:00.0,18.861018779999974
+2020-10-03T23:45:00.0,18.861018779999995
+2020-10-03T23:50:00.0,18.57351613999999
+2020-10-03T23:55:00.0,18.57351614
+2020-10-04T00:00:00.0,18.57351614000006
+2020-10-04T00:05:00.0,18.463588660000035
+2020-10-04T00:10:00.0,18.07250051000029
+2020-10-04T00:15:00.0,18.072500510000282
+2020-10-04T00:20:00.0,0.0
+2020-10-04T00:25:00.0,0.0
+2020-10-04T00:30:00.0,0.0
+2020-10-04T00:35:00.0,0.0
+2020-10-04T00:40:00.0,0.0
+2020-10-04T00:45:00.0,15.630905691098986
+2020-10-04T00:50:00.0,15.630905691098986
+2020-10-04T00:55:00.0,15.630905691099
diff --git a/test/inputs/chuhsi_RegDown_prices.csv b/test/inputs/chuhsi_RegDown_prices.csv
index 27e30613..4ea70254 100644
--- a/test/inputs/chuhsi_RegDown_prices.csv
+++ b/test/inputs/chuhsi_RegDown_prices.csv
@@ -1,73 +1,73 @@
-DateTime,Chuhsi
-2020-10-03T00:00:00.0,2.5231297150000005
-2020-10-03T01:00:00.0,2.3128959000000022
-2020-10-03T02:00:00.0,2.312895900000002
-2020-10-03T03:00:00.0,2.232560020000001
-2020-10-03T04:00:00.0,2.232560020000001
-2020-10-03T05:00:00.0,2.2296446050000003
-2020-10-03T06:00:00.0,1.25
-2020-10-03T07:00:00.0,1.25
-2020-10-03T08:00:00.0,1.25
-2020-10-03T09:00:00.0,1.25
-2020-10-03T10:00:00.0,1.5717515649999987
-2020-10-03T11:00:00.0,1.6652956224999993
-2020-10-03T12:00:00.0,1.7015857841666662
-2020-10-03T13:00:00.0,1.803938133333334
-2020-10-03T14:00:00.0,1.8943718800000007
-2020-10-03T15:00:00.0,1.922497739166667
-2020-10-03T16:00:00.0,2.2370951100000003
-2020-10-03T17:00:00.0,2.3128959000000022
-2020-10-03T18:00:00.0,2.7051811849999985
-2020-10-03T19:00:00.0,2.3128959000000022
-2020-10-03T20:00:00.0,2.2296446050000003
-2020-10-03T21:00:00.0,1.9274132499999992
-2020-10-03T22:00:00.0,1.7597205091666657
-2020-10-03T23:00:00.0,1.7000002916666659
-2020-10-04T00:00:00.0,1.6652956224999993
-2020-10-04T01:00:00.0,1.6652956224999993
-2020-10-04T02:00:00.0,1.5861971633333318
-2020-10-04T03:00:00.0,1.6408085716666676
-2020-10-04T04:00:00.0,1.6652956224999993
-2020-10-04T05:00:00.0,1.5717515649999987
-2020-10-04T06:00:00.0,1.25
-2020-10-04T07:00:00.0,1.25
-2020-10-04T08:00:00.0,1.25
-2020-10-04T09:00:00.0,1.619140174166666
-2020-10-04T10:00:00.0,1.6652956224999993
-2020-10-04T11:00:00.0,1.6652956224999993
-2020-10-04T12:00:00.0,1.803938133333334
-2020-10-04T13:00:00.0,1.9274132499999992
-2020-10-04T14:00:00.0,2.2296446050000003
-2020-10-04T15:00:00.0,2.2296446050000003
-2020-10-04T16:00:00.0,2.5441854899999994
-2020-10-04T17:00:00.0,2.627436784999998
-2020-10-04T18:00:00.0,2.627436784999998
-2020-10-04T19:00:00.0,2.627436784999998
-2020-10-04T20:00:00.0,2.312895900000002
-2020-10-04T21:00:00.0,2.2296446050000003
-2020-10-04T22:00:00.0,2.193687820000001
-2020-10-04T23:00:00.0,1.9896169216666653
-2020-10-05T00:00:00.0,1.927413249999999
-2020-10-05T01:00:00.0,1.9140417791666675
-2020-10-05T02:00:00.0,1.8943718800000002
-2020-10-05T03:00:00.0,1.922497739166667
-2020-10-05T04:00:00.0,1.922497739166667
-2020-10-05T05:00:00.0,1.9531505941666651
-2020-10-05T06:00:00.0,1.5717515649999987
-2020-10-05T07:00:00.0,1.25
-2020-10-05T08:00:00.0,1.25
-2020-10-05T09:00:00.0,1.5386323883333326
-2020-10-05T10:00:00.0,1.5861971633333318
-2020-10-05T11:00:00.0,1.7371712824999994
-2020-10-05T12:00:00.0,1.8814144800000001
-2020-10-05T13:00:00.0,1.9274132499999992
-2020-10-05T14:00:00.0,2.051451129166666
-2020-10-05T15:00:00.0,2.5441854899999994
-2020-10-05T16:00:00.0,2.627436784999998
-2020-10-05T17:00:00.0,2.6439574699999993
-2020-10-05T18:00:00.0,2.6439574699999993
-2020-10-05T19:00:00.0,2.6439574699999993
-2020-10-05T20:00:00.0,2.5231297150000005
-2020-10-05T21:00:00.0,2.2728915816666664
-2020-10-05T22:00:00.0,2.2024340650000003
-2020-10-05T23:00:00.0,1.9714718408333318
+DateTime,Chuhsi
+2020-10-03T00:00:00.0,2.5231297150000005
+2020-10-03T01:00:00.0,2.3128959000000022
+2020-10-03T02:00:00.0,2.312895900000002
+2020-10-03T03:00:00.0,2.232560020000001
+2020-10-03T04:00:00.0,2.232560020000001
+2020-10-03T05:00:00.0,2.2296446050000003
+2020-10-03T06:00:00.0,1.25
+2020-10-03T07:00:00.0,1.25
+2020-10-03T08:00:00.0,1.25
+2020-10-03T09:00:00.0,1.25
+2020-10-03T10:00:00.0,1.5717515649999987
+2020-10-03T11:00:00.0,1.6652956224999993
+2020-10-03T12:00:00.0,1.7015857841666662
+2020-10-03T13:00:00.0,1.803938133333334
+2020-10-03T14:00:00.0,1.8943718800000007
+2020-10-03T15:00:00.0,1.922497739166667
+2020-10-03T16:00:00.0,2.2370951100000003
+2020-10-03T17:00:00.0,2.3128959000000022
+2020-10-03T18:00:00.0,2.7051811849999985
+2020-10-03T19:00:00.0,2.3128959000000022
+2020-10-03T20:00:00.0,2.2296446050000003
+2020-10-03T21:00:00.0,1.9274132499999992
+2020-10-03T22:00:00.0,1.7597205091666657
+2020-10-03T23:00:00.0,1.7000002916666659
+2020-10-04T00:00:00.0,1.6652956224999993
+2020-10-04T01:00:00.0,1.6652956224999993
+2020-10-04T02:00:00.0,1.5861971633333318
+2020-10-04T03:00:00.0,1.6408085716666676
+2020-10-04T04:00:00.0,1.6652956224999993
+2020-10-04T05:00:00.0,1.5717515649999987
+2020-10-04T06:00:00.0,1.25
+2020-10-04T07:00:00.0,1.25
+2020-10-04T08:00:00.0,1.25
+2020-10-04T09:00:00.0,1.619140174166666
+2020-10-04T10:00:00.0,1.6652956224999993
+2020-10-04T11:00:00.0,1.6652956224999993
+2020-10-04T12:00:00.0,1.803938133333334
+2020-10-04T13:00:00.0,1.9274132499999992
+2020-10-04T14:00:00.0,2.2296446050000003
+2020-10-04T15:00:00.0,2.2296446050000003
+2020-10-04T16:00:00.0,2.5441854899999994
+2020-10-04T17:00:00.0,2.627436784999998
+2020-10-04T18:00:00.0,2.627436784999998
+2020-10-04T19:00:00.0,2.627436784999998
+2020-10-04T20:00:00.0,2.312895900000002
+2020-10-04T21:00:00.0,2.2296446050000003
+2020-10-04T22:00:00.0,2.193687820000001
+2020-10-04T23:00:00.0,1.9896169216666653
+2020-10-05T00:00:00.0,1.927413249999999
+2020-10-05T01:00:00.0,1.9140417791666675
+2020-10-05T02:00:00.0,1.8943718800000002
+2020-10-05T03:00:00.0,1.922497739166667
+2020-10-05T04:00:00.0,1.922497739166667
+2020-10-05T05:00:00.0,1.9531505941666651
+2020-10-05T06:00:00.0,1.5717515649999987
+2020-10-05T07:00:00.0,1.25
+2020-10-05T08:00:00.0,1.25
+2020-10-05T09:00:00.0,1.5386323883333326
+2020-10-05T10:00:00.0,1.5861971633333318
+2020-10-05T11:00:00.0,1.7371712824999994
+2020-10-05T12:00:00.0,1.8814144800000001
+2020-10-05T13:00:00.0,1.9274132499999992
+2020-10-05T14:00:00.0,2.051451129166666
+2020-10-05T15:00:00.0,2.5441854899999994
+2020-10-05T16:00:00.0,2.627436784999998
+2020-10-05T17:00:00.0,2.6439574699999993
+2020-10-05T18:00:00.0,2.6439574699999993
+2020-10-05T19:00:00.0,2.6439574699999993
+2020-10-05T20:00:00.0,2.5231297150000005
+2020-10-05T21:00:00.0,2.2728915816666664
+2020-10-05T22:00:00.0,2.2024340650000003
+2020-10-05T23:00:00.0,1.9714718408333318
diff --git a/test/inputs/chuhsi_RegDown_prices_24.csv b/test/inputs/chuhsi_RegDown_prices_24.csv
new file mode 100644
index 00000000..12c65939
--- /dev/null
+++ b/test/inputs/chuhsi_RegDown_prices_24.csv
@@ -0,0 +1,25 @@
+DateTime,Chuhsi
+2020-10-03T00:00:00.0,2.5231297150000005
+2020-10-03T01:00:00.0,2.3128959000000022
+2020-10-03T02:00:00.0,2.312895900000002
+2020-10-03T03:00:00.0,2.232560020000001
+2020-10-03T04:00:00.0,2.232560020000001
+2020-10-03T05:00:00.0,2.2296446050000003
+2020-10-03T06:00:00.0,1.25
+2020-10-03T07:00:00.0,1.25
+2020-10-03T08:00:00.0,1.25
+2020-10-03T09:00:00.0,1.25
+2020-10-03T10:00:00.0,1.5717515649999987
+2020-10-03T11:00:00.0,1.6652956224999993
+2020-10-03T12:00:00.0,1.7015857841666662
+2020-10-03T13:00:00.0,1.803938133333334
+2020-10-03T14:00:00.0,1.8943718800000007
+2020-10-03T15:00:00.0,1.922497739166667
+2020-10-03T16:00:00.0,2.2370951100000003
+2020-10-03T17:00:00.0,2.3128959000000022
+2020-10-03T18:00:00.0,2.7051811849999985
+2020-10-03T19:00:00.0,2.3128959000000022
+2020-10-03T20:00:00.0,2.2296446050000003
+2020-10-03T21:00:00.0,1.9274132499999992
+2020-10-03T22:00:00.0,1.7597205091666657
+2020-10-03T23:00:00.0,1.7000002916666659
diff --git a/test/inputs/chuhsi_RegDown_prices_5min.csv b/test/inputs/chuhsi_RegDown_prices_5min.csv
new file mode 100644
index 00000000..ee81bf1b
--- /dev/null
+++ b/test/inputs/chuhsi_RegDown_prices_5min.csv
@@ -0,0 +1,865 @@
+DateTime,Chuhsi
+2020-10-03T00:00:00.0,2.5231297150000005
+2020-10-03T00:05:00.0,2.5231297150000005
+2020-10-03T00:10:00.0,2.5231297150000005
+2020-10-03T00:15:00.0,2.5231297150000005
+2020-10-03T00:20:00.0,2.5231297150000005
+2020-10-03T00:25:00.0,2.5231297150000005
+2020-10-03T00:30:00.0,2.5231297150000005
+2020-10-03T00:35:00.0,2.5231297150000005
+2020-10-03T00:40:00.0,2.5231297150000005
+2020-10-03T00:45:00.0,2.5231297150000005
+2020-10-03T00:50:00.0,2.5231297150000005
+2020-10-03T00:55:00.0,2.5231297150000005
+2020-10-03T01:00:00.0,2.3128959000000022
+2020-10-03T01:05:00.0,2.3128959000000022
+2020-10-03T01:10:00.0,2.3128959000000022
+2020-10-03T01:15:00.0,2.3128959000000022
+2020-10-03T01:20:00.0,2.3128959000000022
+2020-10-03T01:25:00.0,2.3128959000000022
+2020-10-03T01:30:00.0,2.3128959000000022
+2020-10-03T01:35:00.0,2.3128959000000022
+2020-10-03T01:40:00.0,2.3128959000000022
+2020-10-03T01:45:00.0,2.3128959000000022
+2020-10-03T01:50:00.0,2.3128959000000022
+2020-10-03T01:55:00.0,2.3128959000000022
+2020-10-03T02:00:00.0,2.312895900000002
+2020-10-03T02:05:00.0,2.312895900000002
+2020-10-03T02:10:00.0,2.312895900000002
+2020-10-03T02:15:00.0,2.312895900000002
+2020-10-03T02:20:00.0,2.312895900000002
+2020-10-03T02:25:00.0,2.312895900000002
+2020-10-03T02:30:00.0,2.312895900000002
+2020-10-03T02:35:00.0,2.312895900000002
+2020-10-03T02:40:00.0,2.312895900000002
+2020-10-03T02:45:00.0,2.312895900000002
+2020-10-03T02:50:00.0,2.312895900000002
+2020-10-03T02:55:00.0,2.312895900000002
+2020-10-03T03:00:00.0,2.232560020000001
+2020-10-03T03:05:00.0,2.232560020000001
+2020-10-03T03:10:00.0,2.232560020000001
+2020-10-03T03:15:00.0,2.232560020000001
+2020-10-03T03:20:00.0,2.232560020000001
+2020-10-03T03:25:00.0,2.232560020000001
+2020-10-03T03:30:00.0,2.232560020000001
+2020-10-03T03:35:00.0,2.232560020000001
+2020-10-03T03:40:00.0,2.232560020000001
+2020-10-03T03:45:00.0,2.232560020000001
+2020-10-03T03:50:00.0,2.232560020000001
+2020-10-03T03:55:00.0,2.232560020000001
+2020-10-03T04:00:00.0,2.232560020000001
+2020-10-03T04:05:00.0,2.232560020000001
+2020-10-03T04:10:00.0,2.232560020000001
+2020-10-03T04:15:00.0,2.232560020000001
+2020-10-03T04:20:00.0,2.232560020000001
+2020-10-03T04:25:00.0,2.232560020000001
+2020-10-03T04:30:00.0,2.232560020000001
+2020-10-03T04:35:00.0,2.232560020000001
+2020-10-03T04:40:00.0,2.232560020000001
+2020-10-03T04:45:00.0,2.232560020000001
+2020-10-03T04:50:00.0,2.232560020000001
+2020-10-03T04:55:00.0,2.232560020000001
+2020-10-03T05:00:00.0,2.2296446050000003
+2020-10-03T05:05:00.0,2.2296446050000003
+2020-10-03T05:10:00.0,2.2296446050000003
+2020-10-03T05:15:00.0,2.2296446050000003
+2020-10-03T05:20:00.0,2.2296446050000003
+2020-10-03T05:25:00.0,2.2296446050000003
+2020-10-03T05:30:00.0,2.2296446050000003
+2020-10-03T05:35:00.0,2.2296446050000003
+2020-10-03T05:40:00.0,2.2296446050000003
+2020-10-03T05:45:00.0,2.2296446050000003
+2020-10-03T05:50:00.0,2.2296446050000003
+2020-10-03T05:55:00.0,2.2296446050000003
+2020-10-03T06:00:00.0,1.25
+2020-10-03T06:05:00.0,1.25
+2020-10-03T06:10:00.0,1.25
+2020-10-03T06:15:00.0,1.25
+2020-10-03T06:20:00.0,1.25
+2020-10-03T06:25:00.0,1.25
+2020-10-03T06:30:00.0,1.25
+2020-10-03T06:35:00.0,1.25
+2020-10-03T06:40:00.0,1.25
+2020-10-03T06:45:00.0,1.25
+2020-10-03T06:50:00.0,1.25
+2020-10-03T06:55:00.0,1.25
+2020-10-03T07:00:00.0,1.25
+2020-10-03T07:05:00.0,1.25
+2020-10-03T07:10:00.0,1.25
+2020-10-03T07:15:00.0,1.25
+2020-10-03T07:20:00.0,1.25
+2020-10-03T07:25:00.0,1.25
+2020-10-03T07:30:00.0,1.25
+2020-10-03T07:35:00.0,1.25
+2020-10-03T07:40:00.0,1.25
+2020-10-03T07:45:00.0,1.25
+2020-10-03T07:50:00.0,1.25
+2020-10-03T07:55:00.0,1.25
+2020-10-03T08:00:00.0,1.25
+2020-10-03T08:05:00.0,1.25
+2020-10-03T08:10:00.0,1.25
+2020-10-03T08:15:00.0,1.25
+2020-10-03T08:20:00.0,1.25
+2020-10-03T08:25:00.0,1.25
+2020-10-03T08:30:00.0,1.25
+2020-10-03T08:35:00.0,1.25
+2020-10-03T08:40:00.0,1.25
+2020-10-03T08:45:00.0,1.25
+2020-10-03T08:50:00.0,1.25
+2020-10-03T08:55:00.0,1.25
+2020-10-03T09:00:00.0,1.25
+2020-10-03T09:05:00.0,1.25
+2020-10-03T09:10:00.0,1.25
+2020-10-03T09:15:00.0,1.25
+2020-10-03T09:20:00.0,1.25
+2020-10-03T09:25:00.0,1.25
+2020-10-03T09:30:00.0,1.25
+2020-10-03T09:35:00.0,1.25
+2020-10-03T09:40:00.0,1.25
+2020-10-03T09:45:00.0,1.25
+2020-10-03T09:50:00.0,1.25
+2020-10-03T09:55:00.0,1.25
+2020-10-03T10:00:00.0,1.5717515649999987
+2020-10-03T10:05:00.0,1.5717515649999987
+2020-10-03T10:10:00.0,1.5717515649999987
+2020-10-03T10:15:00.0,1.5717515649999987
+2020-10-03T10:20:00.0,1.5717515649999987
+2020-10-03T10:25:00.0,1.5717515649999987
+2020-10-03T10:30:00.0,1.5717515649999987
+2020-10-03T10:35:00.0,1.5717515649999987
+2020-10-03T10:40:00.0,1.5717515649999987
+2020-10-03T10:45:00.0,1.5717515649999987
+2020-10-03T10:50:00.0,1.5717515649999987
+2020-10-03T10:55:00.0,1.5717515649999987
+2020-10-03T11:00:00.0,1.6652956224999993
+2020-10-03T11:05:00.0,1.6652956224999993
+2020-10-03T11:10:00.0,1.6652956224999993
+2020-10-03T11:15:00.0,1.6652956224999993
+2020-10-03T11:20:00.0,1.6652956224999993
+2020-10-03T11:25:00.0,1.6652956224999993
+2020-10-03T11:30:00.0,1.6652956224999993
+2020-10-03T11:35:00.0,1.6652956224999993
+2020-10-03T11:40:00.0,1.6652956224999993
+2020-10-03T11:45:00.0,1.6652956224999993
+2020-10-03T11:50:00.0,1.6652956224999993
+2020-10-03T11:55:00.0,1.6652956224999993
+2020-10-03T12:00:00.0,1.7015857841666662
+2020-10-03T12:05:00.0,1.7015857841666662
+2020-10-03T12:10:00.0,1.7015857841666662
+2020-10-03T12:15:00.0,1.7015857841666662
+2020-10-03T12:20:00.0,1.7015857841666662
+2020-10-03T12:25:00.0,1.7015857841666662
+2020-10-03T12:30:00.0,1.7015857841666662
+2020-10-03T12:35:00.0,1.7015857841666662
+2020-10-03T12:40:00.0,1.7015857841666662
+2020-10-03T12:45:00.0,1.7015857841666662
+2020-10-03T12:50:00.0,1.7015857841666662
+2020-10-03T12:55:00.0,1.7015857841666662
+2020-10-03T13:00:00.0,1.803938133333334
+2020-10-03T13:05:00.0,1.803938133333334
+2020-10-03T13:10:00.0,1.803938133333334
+2020-10-03T13:15:00.0,1.803938133333334
+2020-10-03T13:20:00.0,1.803938133333334
+2020-10-03T13:25:00.0,1.803938133333334
+2020-10-03T13:30:00.0,1.803938133333334
+2020-10-03T13:35:00.0,1.803938133333334
+2020-10-03T13:40:00.0,1.803938133333334
+2020-10-03T13:45:00.0,1.803938133333334
+2020-10-03T13:50:00.0,1.803938133333334
+2020-10-03T13:55:00.0,1.803938133333334
+2020-10-03T14:00:00.0,1.8943718800000007
+2020-10-03T14:05:00.0,1.8943718800000007
+2020-10-03T14:10:00.0,1.8943718800000007
+2020-10-03T14:15:00.0,1.8943718800000007
+2020-10-03T14:20:00.0,1.8943718800000007
+2020-10-03T14:25:00.0,1.8943718800000007
+2020-10-03T14:30:00.0,1.8943718800000007
+2020-10-03T14:35:00.0,1.8943718800000007
+2020-10-03T14:40:00.0,1.8943718800000007
+2020-10-03T14:45:00.0,1.8943718800000007
+2020-10-03T14:50:00.0,1.8943718800000007
+2020-10-03T14:55:00.0,1.8943718800000007
+2020-10-03T15:00:00.0,1.922497739166667
+2020-10-03T15:05:00.0,1.922497739166667
+2020-10-03T15:10:00.0,1.922497739166667
+2020-10-03T15:15:00.0,1.922497739166667
+2020-10-03T15:20:00.0,1.922497739166667
+2020-10-03T15:25:00.0,1.922497739166667
+2020-10-03T15:30:00.0,1.922497739166667
+2020-10-03T15:35:00.0,1.922497739166667
+2020-10-03T15:40:00.0,1.922497739166667
+2020-10-03T15:45:00.0,1.922497739166667
+2020-10-03T15:50:00.0,1.922497739166667
+2020-10-03T15:55:00.0,1.922497739166667
+2020-10-03T16:00:00.0,2.2370951100000003
+2020-10-03T16:05:00.0,2.2370951100000003
+2020-10-03T16:10:00.0,2.2370951100000003
+2020-10-03T16:15:00.0,2.2370951100000003
+2020-10-03T16:20:00.0,2.2370951100000003
+2020-10-03T16:25:00.0,2.2370951100000003
+2020-10-03T16:30:00.0,2.2370951100000003
+2020-10-03T16:35:00.0,2.2370951100000003
+2020-10-03T16:40:00.0,2.2370951100000003
+2020-10-03T16:45:00.0,2.2370951100000003
+2020-10-03T16:50:00.0,2.2370951100000003
+2020-10-03T16:55:00.0,2.2370951100000003
+2020-10-03T17:00:00.0,2.3128959000000022
+2020-10-03T17:05:00.0,2.3128959000000022
+2020-10-03T17:10:00.0,2.3128959000000022
+2020-10-03T17:15:00.0,2.3128959000000022
+2020-10-03T17:20:00.0,2.3128959000000022
+2020-10-03T17:25:00.0,2.3128959000000022
+2020-10-03T17:30:00.0,2.3128959000000022
+2020-10-03T17:35:00.0,2.3128959000000022
+2020-10-03T17:40:00.0,2.3128959000000022
+2020-10-03T17:45:00.0,2.3128959000000022
+2020-10-03T17:50:00.0,2.3128959000000022
+2020-10-03T17:55:00.0,2.3128959000000022
+2020-10-03T18:00:00.0,2.7051811849999985
+2020-10-03T18:05:00.0,2.7051811849999985
+2020-10-03T18:10:00.0,2.7051811849999985
+2020-10-03T18:15:00.0,2.7051811849999985
+2020-10-03T18:20:00.0,2.7051811849999985
+2020-10-03T18:25:00.0,2.7051811849999985
+2020-10-03T18:30:00.0,2.7051811849999985
+2020-10-03T18:35:00.0,2.7051811849999985
+2020-10-03T18:40:00.0,2.7051811849999985
+2020-10-03T18:45:00.0,2.7051811849999985
+2020-10-03T18:50:00.0,2.7051811849999985
+2020-10-03T18:55:00.0,2.7051811849999985
+2020-10-03T19:00:00.0,2.3128959000000022
+2020-10-03T19:05:00.0,2.3128959000000022
+2020-10-03T19:10:00.0,2.3128959000000022
+2020-10-03T19:15:00.0,2.3128959000000022
+2020-10-03T19:20:00.0,2.3128959000000022
+2020-10-03T19:25:00.0,2.3128959000000022
+2020-10-03T19:30:00.0,2.3128959000000022
+2020-10-03T19:35:00.0,2.3128959000000022
+2020-10-03T19:40:00.0,2.3128959000000022
+2020-10-03T19:45:00.0,2.3128959000000022
+2020-10-03T19:50:00.0,2.3128959000000022
+2020-10-03T19:55:00.0,2.3128959000000022
+2020-10-03T20:00:00.0,2.2296446050000003
+2020-10-03T20:05:00.0,2.2296446050000003
+2020-10-03T20:10:00.0,2.2296446050000003
+2020-10-03T20:15:00.0,2.2296446050000003
+2020-10-03T20:20:00.0,2.2296446050000003
+2020-10-03T20:25:00.0,2.2296446050000003
+2020-10-03T20:30:00.0,2.2296446050000003
+2020-10-03T20:35:00.0,2.2296446050000003
+2020-10-03T20:40:00.0,2.2296446050000003
+2020-10-03T20:45:00.0,2.2296446050000003
+2020-10-03T20:50:00.0,2.2296446050000003
+2020-10-03T20:55:00.0,2.2296446050000003
+2020-10-03T21:00:00.0,1.9274132499999992
+2020-10-03T21:05:00.0,1.9274132499999992
+2020-10-03T21:10:00.0,1.9274132499999992
+2020-10-03T21:15:00.0,1.9274132499999992
+2020-10-03T21:20:00.0,1.9274132499999992
+2020-10-03T21:25:00.0,1.9274132499999992
+2020-10-03T21:30:00.0,1.9274132499999992
+2020-10-03T21:35:00.0,1.9274132499999992
+2020-10-03T21:40:00.0,1.9274132499999992
+2020-10-03T21:45:00.0,1.9274132499999992
+2020-10-03T21:50:00.0,1.9274132499999992
+2020-10-03T21:55:00.0,1.9274132499999992
+2020-10-03T22:00:00.0,1.7597205091666657
+2020-10-03T22:05:00.0,1.7597205091666657
+2020-10-03T22:10:00.0,1.7597205091666657
+2020-10-03T22:15:00.0,1.7597205091666657
+2020-10-03T22:20:00.0,1.7597205091666657
+2020-10-03T22:25:00.0,1.7597205091666657
+2020-10-03T22:30:00.0,1.7597205091666657
+2020-10-03T22:35:00.0,1.7597205091666657
+2020-10-03T22:40:00.0,1.7597205091666657
+2020-10-03T22:45:00.0,1.7597205091666657
+2020-10-03T22:50:00.0,1.7597205091666657
+2020-10-03T22:55:00.0,1.7597205091666657
+2020-10-03T23:00:00.0,1.7000002916666659
+2020-10-03T23:05:00.0,1.7000002916666659
+2020-10-03T23:10:00.0,1.7000002916666659
+2020-10-03T23:15:00.0,1.7000002916666659
+2020-10-03T23:20:00.0,1.7000002916666659
+2020-10-03T23:25:00.0,1.7000002916666659
+2020-10-03T23:30:00.0,1.7000002916666659
+2020-10-03T23:35:00.0,1.7000002916666659
+2020-10-03T23:40:00.0,1.7000002916666659
+2020-10-03T23:45:00.0,1.7000002916666659
+2020-10-03T23:50:00.0,1.7000002916666659
+2020-10-03T23:55:00.0,1.7000002916666659
+2020-10-04T00:00:00.0,1.6652956224999993
+2020-10-04T00:05:00.0,1.6652956224999993
+2020-10-04T00:10:00.0,1.6652956224999993
+2020-10-04T00:15:00.0,1.6652956224999993
+2020-10-04T00:20:00.0,1.6652956224999993
+2020-10-04T00:25:00.0,1.6652956224999993
+2020-10-04T00:30:00.0,1.6652956224999993
+2020-10-04T00:35:00.0,1.6652956224999993
+2020-10-04T00:40:00.0,1.6652956224999993
+2020-10-04T00:45:00.0,1.6652956224999993
+2020-10-04T00:50:00.0,1.6652956224999993
+2020-10-04T00:55:00.0,1.6652956224999993
+2020-10-04T01:00:00.0,1.6652956224999993
+2020-10-04T01:05:00.0,1.6652956224999993
+2020-10-04T01:10:00.0,1.6652956224999993
+2020-10-04T01:15:00.0,1.6652956224999993
+2020-10-04T01:20:00.0,1.6652956224999993
+2020-10-04T01:25:00.0,1.6652956224999993
+2020-10-04T01:30:00.0,1.6652956224999993
+2020-10-04T01:35:00.0,1.6652956224999993
+2020-10-04T01:40:00.0,1.6652956224999993
+2020-10-04T01:45:00.0,1.6652956224999993
+2020-10-04T01:50:00.0,1.6652956224999993
+2020-10-04T01:55:00.0,1.6652956224999993
+2020-10-04T02:00:00.0,1.5861971633333318
+2020-10-04T02:05:00.0,1.5861971633333318
+2020-10-04T02:10:00.0,1.5861971633333318
+2020-10-04T02:15:00.0,1.5861971633333318
+2020-10-04T02:20:00.0,1.5861971633333318
+2020-10-04T02:25:00.0,1.5861971633333318
+2020-10-04T02:30:00.0,1.5861971633333318
+2020-10-04T02:35:00.0,1.5861971633333318
+2020-10-04T02:40:00.0,1.5861971633333318
+2020-10-04T02:45:00.0,1.5861971633333318
+2020-10-04T02:50:00.0,1.5861971633333318
+2020-10-04T02:55:00.0,1.5861971633333318
+2020-10-04T03:00:00.0,1.6408085716666676
+2020-10-04T03:05:00.0,1.6408085716666676
+2020-10-04T03:10:00.0,1.6408085716666676
+2020-10-04T03:15:00.0,1.6408085716666676
+2020-10-04T03:20:00.0,1.6408085716666676
+2020-10-04T03:25:00.0,1.6408085716666676
+2020-10-04T03:30:00.0,1.6408085716666676
+2020-10-04T03:35:00.0,1.6408085716666676
+2020-10-04T03:40:00.0,1.6408085716666676
+2020-10-04T03:45:00.0,1.6408085716666676
+2020-10-04T03:50:00.0,1.6408085716666676
+2020-10-04T03:55:00.0,1.6408085716666676
+2020-10-04T04:00:00.0,1.6652956224999993
+2020-10-04T04:05:00.0,1.6652956224999993
+2020-10-04T04:10:00.0,1.6652956224999993
+2020-10-04T04:15:00.0,1.6652956224999993
+2020-10-04T04:20:00.0,1.6652956224999993
+2020-10-04T04:25:00.0,1.6652956224999993
+2020-10-04T04:30:00.0,1.6652956224999993
+2020-10-04T04:35:00.0,1.6652956224999993
+2020-10-04T04:40:00.0,1.6652956224999993
+2020-10-04T04:45:00.0,1.6652956224999993
+2020-10-04T04:50:00.0,1.6652956224999993
+2020-10-04T04:55:00.0,1.6652956224999993
+2020-10-04T05:00:00.0,1.5717515649999987
+2020-10-04T05:05:00.0,1.5717515649999987
+2020-10-04T05:10:00.0,1.5717515649999987
+2020-10-04T05:15:00.0,1.5717515649999987
+2020-10-04T05:20:00.0,1.5717515649999987
+2020-10-04T05:25:00.0,1.5717515649999987
+2020-10-04T05:30:00.0,1.5717515649999987
+2020-10-04T05:35:00.0,1.5717515649999987
+2020-10-04T05:40:00.0,1.5717515649999987
+2020-10-04T05:45:00.0,1.5717515649999987
+2020-10-04T05:50:00.0,1.5717515649999987
+2020-10-04T05:55:00.0,1.5717515649999987
+2020-10-04T06:00:00.0,1.25
+2020-10-04T06:05:00.0,1.25
+2020-10-04T06:10:00.0,1.25
+2020-10-04T06:15:00.0,1.25
+2020-10-04T06:20:00.0,1.25
+2020-10-04T06:25:00.0,1.25
+2020-10-04T06:30:00.0,1.25
+2020-10-04T06:35:00.0,1.25
+2020-10-04T06:40:00.0,1.25
+2020-10-04T06:45:00.0,1.25
+2020-10-04T06:50:00.0,1.25
+2020-10-04T06:55:00.0,1.25
+2020-10-04T07:00:00.0,1.25
+2020-10-04T07:05:00.0,1.25
+2020-10-04T07:10:00.0,1.25
+2020-10-04T07:15:00.0,1.25
+2020-10-04T07:20:00.0,1.25
+2020-10-04T07:25:00.0,1.25
+2020-10-04T07:30:00.0,1.25
+2020-10-04T07:35:00.0,1.25
+2020-10-04T07:40:00.0,1.25
+2020-10-04T07:45:00.0,1.25
+2020-10-04T07:50:00.0,1.25
+2020-10-04T07:55:00.0,1.25
+2020-10-04T08:00:00.0,1.25
+2020-10-04T08:05:00.0,1.25
+2020-10-04T08:10:00.0,1.25
+2020-10-04T08:15:00.0,1.25
+2020-10-04T08:20:00.0,1.25
+2020-10-04T08:25:00.0,1.25
+2020-10-04T08:30:00.0,1.25
+2020-10-04T08:35:00.0,1.25
+2020-10-04T08:40:00.0,1.25
+2020-10-04T08:45:00.0,1.25
+2020-10-04T08:50:00.0,1.25
+2020-10-04T08:55:00.0,1.25
+2020-10-04T09:00:00.0,1.619140174166666
+2020-10-04T09:05:00.0,1.619140174166666
+2020-10-04T09:10:00.0,1.619140174166666
+2020-10-04T09:15:00.0,1.619140174166666
+2020-10-04T09:20:00.0,1.619140174166666
+2020-10-04T09:25:00.0,1.619140174166666
+2020-10-04T09:30:00.0,1.619140174166666
+2020-10-04T09:35:00.0,1.619140174166666
+2020-10-04T09:40:00.0,1.619140174166666
+2020-10-04T09:45:00.0,1.619140174166666
+2020-10-04T09:50:00.0,1.619140174166666
+2020-10-04T09:55:00.0,1.619140174166666
+2020-10-04T10:00:00.0,1.6652956224999993
+2020-10-04T10:05:00.0,1.6652956224999993
+2020-10-04T10:10:00.0,1.6652956224999993
+2020-10-04T10:15:00.0,1.6652956224999993
+2020-10-04T10:20:00.0,1.6652956224999993
+2020-10-04T10:25:00.0,1.6652956224999993
+2020-10-04T10:30:00.0,1.6652956224999993
+2020-10-04T10:35:00.0,1.6652956224999993
+2020-10-04T10:40:00.0,1.6652956224999993
+2020-10-04T10:45:00.0,1.6652956224999993
+2020-10-04T10:50:00.0,1.6652956224999993
+2020-10-04T10:55:00.0,1.6652956224999993
+2020-10-04T11:00:00.0,1.6652956224999993
+2020-10-04T11:05:00.0,1.6652956224999993
+2020-10-04T11:10:00.0,1.6652956224999993
+2020-10-04T11:15:00.0,1.6652956224999993
+2020-10-04T11:20:00.0,1.6652956224999993
+2020-10-04T11:25:00.0,1.6652956224999993
+2020-10-04T11:30:00.0,1.6652956224999993
+2020-10-04T11:35:00.0,1.6652956224999993
+2020-10-04T11:40:00.0,1.6652956224999993
+2020-10-04T11:45:00.0,1.6652956224999993
+2020-10-04T11:50:00.0,1.6652956224999993
+2020-10-04T11:55:00.0,1.6652956224999993
+2020-10-04T12:00:00.0,1.803938133333334
+2020-10-04T12:05:00.0,1.803938133333334
+2020-10-04T12:10:00.0,1.803938133333334
+2020-10-04T12:15:00.0,1.803938133333334
+2020-10-04T12:20:00.0,1.803938133333334
+2020-10-04T12:25:00.0,1.803938133333334
+2020-10-04T12:30:00.0,1.803938133333334
+2020-10-04T12:35:00.0,1.803938133333334
+2020-10-04T12:40:00.0,1.803938133333334
+2020-10-04T12:45:00.0,1.803938133333334
+2020-10-04T12:50:00.0,1.803938133333334
+2020-10-04T12:55:00.0,1.803938133333334
+2020-10-04T13:00:00.0,1.9274132499999992
+2020-10-04T13:05:00.0,1.9274132499999992
+2020-10-04T13:10:00.0,1.9274132499999992
+2020-10-04T13:15:00.0,1.9274132499999992
+2020-10-04T13:20:00.0,1.9274132499999992
+2020-10-04T13:25:00.0,1.9274132499999992
+2020-10-04T13:30:00.0,1.9274132499999992
+2020-10-04T13:35:00.0,1.9274132499999992
+2020-10-04T13:40:00.0,1.9274132499999992
+2020-10-04T13:45:00.0,1.9274132499999992
+2020-10-04T13:50:00.0,1.9274132499999992
+2020-10-04T13:55:00.0,1.9274132499999992
+2020-10-04T14:00:00.0,2.2296446050000003
+2020-10-04T14:05:00.0,2.2296446050000003
+2020-10-04T14:10:00.0,2.2296446050000003
+2020-10-04T14:15:00.0,2.2296446050000003
+2020-10-04T14:20:00.0,2.2296446050000003
+2020-10-04T14:25:00.0,2.2296446050000003
+2020-10-04T14:30:00.0,2.2296446050000003
+2020-10-04T14:35:00.0,2.2296446050000003
+2020-10-04T14:40:00.0,2.2296446050000003
+2020-10-04T14:45:00.0,2.2296446050000003
+2020-10-04T14:50:00.0,2.2296446050000003
+2020-10-04T14:55:00.0,2.2296446050000003
+2020-10-04T15:00:00.0,2.2296446050000003
+2020-10-04T15:05:00.0,2.2296446050000003
+2020-10-04T15:10:00.0,2.2296446050000003
+2020-10-04T15:15:00.0,2.2296446050000003
+2020-10-04T15:20:00.0,2.2296446050000003
+2020-10-04T15:25:00.0,2.2296446050000003
+2020-10-04T15:30:00.0,2.2296446050000003
+2020-10-04T15:35:00.0,2.2296446050000003
+2020-10-04T15:40:00.0,2.2296446050000003
+2020-10-04T15:45:00.0,2.2296446050000003
+2020-10-04T15:50:00.0,2.2296446050000003
+2020-10-04T15:55:00.0,2.2296446050000003
+2020-10-04T16:00:00.0,2.5441854899999994
+2020-10-04T16:05:00.0,2.5441854899999994
+2020-10-04T16:10:00.0,2.5441854899999994
+2020-10-04T16:15:00.0,2.5441854899999994
+2020-10-04T16:20:00.0,2.5441854899999994
+2020-10-04T16:25:00.0,2.5441854899999994
+2020-10-04T16:30:00.0,2.5441854899999994
+2020-10-04T16:35:00.0,2.5441854899999994
+2020-10-04T16:40:00.0,2.5441854899999994
+2020-10-04T16:45:00.0,2.5441854899999994
+2020-10-04T16:50:00.0,2.5441854899999994
+2020-10-04T16:55:00.0,2.5441854899999994
+2020-10-04T17:00:00.0,2.627436784999998
+2020-10-04T17:05:00.0,2.627436784999998
+2020-10-04T17:10:00.0,2.627436784999998
+2020-10-04T17:15:00.0,2.627436784999998
+2020-10-04T17:20:00.0,2.627436784999998
+2020-10-04T17:25:00.0,2.627436784999998
+2020-10-04T17:30:00.0,2.627436784999998
+2020-10-04T17:35:00.0,2.627436784999998
+2020-10-04T17:40:00.0,2.627436784999998
+2020-10-04T17:45:00.0,2.627436784999998
+2020-10-04T17:50:00.0,2.627436784999998
+2020-10-04T17:55:00.0,2.627436784999998
+2020-10-04T18:00:00.0,2.627436784999998
+2020-10-04T18:05:00.0,2.627436784999998
+2020-10-04T18:10:00.0,2.627436784999998
+2020-10-04T18:15:00.0,2.627436784999998
+2020-10-04T18:20:00.0,2.627436784999998
+2020-10-04T18:25:00.0,2.627436784999998
+2020-10-04T18:30:00.0,2.627436784999998
+2020-10-04T18:35:00.0,2.627436784999998
+2020-10-04T18:40:00.0,2.627436784999998
+2020-10-04T18:45:00.0,2.627436784999998
+2020-10-04T18:50:00.0,2.627436784999998
+2020-10-04T18:55:00.0,2.627436784999998
+2020-10-04T19:00:00.0,2.627436784999998
+2020-10-04T19:05:00.0,2.627436784999998
+2020-10-04T19:10:00.0,2.627436784999998
+2020-10-04T19:15:00.0,2.627436784999998
+2020-10-04T19:20:00.0,2.627436784999998
+2020-10-04T19:25:00.0,2.627436784999998
+2020-10-04T19:30:00.0,2.627436784999998
+2020-10-04T19:35:00.0,2.627436784999998
+2020-10-04T19:40:00.0,2.627436784999998
+2020-10-04T19:45:00.0,2.627436784999998
+2020-10-04T19:50:00.0,2.627436784999998
+2020-10-04T19:55:00.0,2.627436784999998
+2020-10-04T20:00:00.0,2.312895900000002
+2020-10-04T20:05:00.0,2.312895900000002
+2020-10-04T20:10:00.0,2.312895900000002
+2020-10-04T20:15:00.0,2.312895900000002
+2020-10-04T20:20:00.0,2.312895900000002
+2020-10-04T20:25:00.0,2.312895900000002
+2020-10-04T20:30:00.0,2.312895900000002
+2020-10-04T20:35:00.0,2.312895900000002
+2020-10-04T20:40:00.0,2.312895900000002
+2020-10-04T20:45:00.0,2.312895900000002
+2020-10-04T20:50:00.0,2.312895900000002
+2020-10-04T20:55:00.0,2.312895900000002
+2020-10-04T21:00:00.0,2.2296446050000003
+2020-10-04T21:05:00.0,2.2296446050000003
+2020-10-04T21:10:00.0,2.2296446050000003
+2020-10-04T21:15:00.0,2.2296446050000003
+2020-10-04T21:20:00.0,2.2296446050000003
+2020-10-04T21:25:00.0,2.2296446050000003
+2020-10-04T21:30:00.0,2.2296446050000003
+2020-10-04T21:35:00.0,2.2296446050000003
+2020-10-04T21:40:00.0,2.2296446050000003
+2020-10-04T21:45:00.0,2.2296446050000003
+2020-10-04T21:50:00.0,2.2296446050000003
+2020-10-04T21:55:00.0,2.2296446050000003
+2020-10-04T22:00:00.0,2.193687820000001
+2020-10-04T22:05:00.0,2.193687820000001
+2020-10-04T22:10:00.0,2.193687820000001
+2020-10-04T22:15:00.0,2.193687820000001
+2020-10-04T22:20:00.0,2.193687820000001
+2020-10-04T22:25:00.0,2.193687820000001
+2020-10-04T22:30:00.0,2.193687820000001
+2020-10-04T22:35:00.0,2.193687820000001
+2020-10-04T22:40:00.0,2.193687820000001
+2020-10-04T22:45:00.0,2.193687820000001
+2020-10-04T22:50:00.0,2.193687820000001
+2020-10-04T22:55:00.0,2.193687820000001
+2020-10-04T23:00:00.0,1.9896169216666653
+2020-10-04T23:05:00.0,1.9896169216666653
+2020-10-04T23:10:00.0,1.9896169216666653
+2020-10-04T23:15:00.0,1.9896169216666653
+2020-10-04T23:20:00.0,1.9896169216666653
+2020-10-04T23:25:00.0,1.9896169216666653
+2020-10-04T23:30:00.0,1.9896169216666653
+2020-10-04T23:35:00.0,1.9896169216666653
+2020-10-04T23:40:00.0,1.9896169216666653
+2020-10-04T23:45:00.0,1.9896169216666653
+2020-10-04T23:50:00.0,1.9896169216666653
+2020-10-04T23:55:00.0,1.9896169216666653
+2020-10-05T00:00:00.0,1.927413249999999
+2020-10-05T00:05:00.0,1.927413249999999
+2020-10-05T00:10:00.0,1.927413249999999
+2020-10-05T00:15:00.0,1.927413249999999
+2020-10-05T00:20:00.0,1.927413249999999
+2020-10-05T00:25:00.0,1.927413249999999
+2020-10-05T00:30:00.0,1.927413249999999
+2020-10-05T00:35:00.0,1.927413249999999
+2020-10-05T00:40:00.0,1.927413249999999
+2020-10-05T00:45:00.0,1.927413249999999
+2020-10-05T00:50:00.0,1.927413249999999
+2020-10-05T00:55:00.0,1.927413249999999
+2020-10-05T01:00:00.0,1.9140417791666675
+2020-10-05T01:05:00.0,1.9140417791666675
+2020-10-05T01:10:00.0,1.9140417791666675
+2020-10-05T01:15:00.0,1.9140417791666675
+2020-10-05T01:20:00.0,1.9140417791666675
+2020-10-05T01:25:00.0,1.9140417791666675
+2020-10-05T01:30:00.0,1.9140417791666675
+2020-10-05T01:35:00.0,1.9140417791666675
+2020-10-05T01:40:00.0,1.9140417791666675
+2020-10-05T01:45:00.0,1.9140417791666675
+2020-10-05T01:50:00.0,1.9140417791666675
+2020-10-05T01:55:00.0,1.9140417791666675
+2020-10-05T02:00:00.0,1.8943718800000002
+2020-10-05T02:05:00.0,1.8943718800000002
+2020-10-05T02:10:00.0,1.8943718800000002
+2020-10-05T02:15:00.0,1.8943718800000002
+2020-10-05T02:20:00.0,1.8943718800000002
+2020-10-05T02:25:00.0,1.8943718800000002
+2020-10-05T02:30:00.0,1.8943718800000002
+2020-10-05T02:35:00.0,1.8943718800000002
+2020-10-05T02:40:00.0,1.8943718800000002
+2020-10-05T02:45:00.0,1.8943718800000002
+2020-10-05T02:50:00.0,1.8943718800000002
+2020-10-05T02:55:00.0,1.8943718800000002
+2020-10-05T03:00:00.0,1.922497739166667
+2020-10-05T03:05:00.0,1.922497739166667
+2020-10-05T03:10:00.0,1.922497739166667
+2020-10-05T03:15:00.0,1.922497739166667
+2020-10-05T03:20:00.0,1.922497739166667
+2020-10-05T03:25:00.0,1.922497739166667
+2020-10-05T03:30:00.0,1.922497739166667
+2020-10-05T03:35:00.0,1.922497739166667
+2020-10-05T03:40:00.0,1.922497739166667
+2020-10-05T03:45:00.0,1.922497739166667
+2020-10-05T03:50:00.0,1.922497739166667
+2020-10-05T03:55:00.0,1.922497739166667
+2020-10-05T04:00:00.0,1.922497739166667
+2020-10-05T04:05:00.0,1.922497739166667
+2020-10-05T04:10:00.0,1.922497739166667
+2020-10-05T04:15:00.0,1.922497739166667
+2020-10-05T04:20:00.0,1.922497739166667
+2020-10-05T04:25:00.0,1.922497739166667
+2020-10-05T04:30:00.0,1.922497739166667
+2020-10-05T04:35:00.0,1.922497739166667
+2020-10-05T04:40:00.0,1.922497739166667
+2020-10-05T04:45:00.0,1.922497739166667
+2020-10-05T04:50:00.0,1.922497739166667
+2020-10-05T04:55:00.0,1.922497739166667
+2020-10-05T05:00:00.0,1.9531505941666651
+2020-10-05T05:05:00.0,1.9531505941666651
+2020-10-05T05:10:00.0,1.9531505941666651
+2020-10-05T05:15:00.0,1.9531505941666651
+2020-10-05T05:20:00.0,1.9531505941666651
+2020-10-05T05:25:00.0,1.9531505941666651
+2020-10-05T05:30:00.0,1.9531505941666651
+2020-10-05T05:35:00.0,1.9531505941666651
+2020-10-05T05:40:00.0,1.9531505941666651
+2020-10-05T05:45:00.0,1.9531505941666651
+2020-10-05T05:50:00.0,1.9531505941666651
+2020-10-05T05:55:00.0,1.9531505941666651
+2020-10-05T06:00:00.0,1.5717515649999987
+2020-10-05T06:05:00.0,1.5717515649999987
+2020-10-05T06:10:00.0,1.5717515649999987
+2020-10-05T06:15:00.0,1.5717515649999987
+2020-10-05T06:20:00.0,1.5717515649999987
+2020-10-05T06:25:00.0,1.5717515649999987
+2020-10-05T06:30:00.0,1.5717515649999987
+2020-10-05T06:35:00.0,1.5717515649999987
+2020-10-05T06:40:00.0,1.5717515649999987
+2020-10-05T06:45:00.0,1.5717515649999987
+2020-10-05T06:50:00.0,1.5717515649999987
+2020-10-05T06:55:00.0,1.5717515649999987
+2020-10-05T07:00:00.0,1.25
+2020-10-05T07:05:00.0,1.25
+2020-10-05T07:10:00.0,1.25
+2020-10-05T07:15:00.0,1.25
+2020-10-05T07:20:00.0,1.25
+2020-10-05T07:25:00.0,1.25
+2020-10-05T07:30:00.0,1.25
+2020-10-05T07:35:00.0,1.25
+2020-10-05T07:40:00.0,1.25
+2020-10-05T07:45:00.0,1.25
+2020-10-05T07:50:00.0,1.25
+2020-10-05T07:55:00.0,1.25
+2020-10-05T08:00:00.0,1.25
+2020-10-05T08:05:00.0,1.25
+2020-10-05T08:10:00.0,1.25
+2020-10-05T08:15:00.0,1.25
+2020-10-05T08:20:00.0,1.25
+2020-10-05T08:25:00.0,1.25
+2020-10-05T08:30:00.0,1.25
+2020-10-05T08:35:00.0,1.25
+2020-10-05T08:40:00.0,1.25
+2020-10-05T08:45:00.0,1.25
+2020-10-05T08:50:00.0,1.25
+2020-10-05T08:55:00.0,1.25
+2020-10-05T09:00:00.0,1.5386323883333326
+2020-10-05T09:05:00.0,1.5386323883333326
+2020-10-05T09:10:00.0,1.5386323883333326
+2020-10-05T09:15:00.0,1.5386323883333326
+2020-10-05T09:20:00.0,1.5386323883333326
+2020-10-05T09:25:00.0,1.5386323883333326
+2020-10-05T09:30:00.0,1.5386323883333326
+2020-10-05T09:35:00.0,1.5386323883333326
+2020-10-05T09:40:00.0,1.5386323883333326
+2020-10-05T09:45:00.0,1.5386323883333326
+2020-10-05T09:50:00.0,1.5386323883333326
+2020-10-05T09:55:00.0,1.5386323883333326
+2020-10-05T10:00:00.0,1.5861971633333318
+2020-10-05T10:05:00.0,1.5861971633333318
+2020-10-05T10:10:00.0,1.5861971633333318
+2020-10-05T10:15:00.0,1.5861971633333318
+2020-10-05T10:20:00.0,1.5861971633333318
+2020-10-05T10:25:00.0,1.5861971633333318
+2020-10-05T10:30:00.0,1.5861971633333318
+2020-10-05T10:35:00.0,1.5861971633333318
+2020-10-05T10:40:00.0,1.5861971633333318
+2020-10-05T10:45:00.0,1.5861971633333318
+2020-10-05T10:50:00.0,1.5861971633333318
+2020-10-05T10:55:00.0,1.5861971633333318
+2020-10-05T11:00:00.0,1.7371712824999994
+2020-10-05T11:05:00.0,1.7371712824999994
+2020-10-05T11:10:00.0,1.7371712824999994
+2020-10-05T11:15:00.0,1.7371712824999994
+2020-10-05T11:20:00.0,1.7371712824999994
+2020-10-05T11:25:00.0,1.7371712824999994
+2020-10-05T11:30:00.0,1.7371712824999994
+2020-10-05T11:35:00.0,1.7371712824999994
+2020-10-05T11:40:00.0,1.7371712824999994
+2020-10-05T11:45:00.0,1.7371712824999994
+2020-10-05T11:50:00.0,1.7371712824999994
+2020-10-05T11:55:00.0,1.7371712824999994
+2020-10-05T12:00:00.0,1.8814144800000001
+2020-10-05T12:05:00.0,1.8814144800000001
+2020-10-05T12:10:00.0,1.8814144800000001
+2020-10-05T12:15:00.0,1.8814144800000001
+2020-10-05T12:20:00.0,1.8814144800000001
+2020-10-05T12:25:00.0,1.8814144800000001
+2020-10-05T12:30:00.0,1.8814144800000001
+2020-10-05T12:35:00.0,1.8814144800000001
+2020-10-05T12:40:00.0,1.8814144800000001
+2020-10-05T12:45:00.0,1.8814144800000001
+2020-10-05T12:50:00.0,1.8814144800000001
+2020-10-05T12:55:00.0,1.8814144800000001
+2020-10-05T13:00:00.0,1.9274132499999992
+2020-10-05T13:05:00.0,1.9274132499999992
+2020-10-05T13:10:00.0,1.9274132499999992
+2020-10-05T13:15:00.0,1.9274132499999992
+2020-10-05T13:20:00.0,1.9274132499999992
+2020-10-05T13:25:00.0,1.9274132499999992
+2020-10-05T13:30:00.0,1.9274132499999992
+2020-10-05T13:35:00.0,1.9274132499999992
+2020-10-05T13:40:00.0,1.9274132499999992
+2020-10-05T13:45:00.0,1.9274132499999992
+2020-10-05T13:50:00.0,1.9274132499999992
+2020-10-05T13:55:00.0,1.9274132499999992
+2020-10-05T14:00:00.0,2.051451129166666
+2020-10-05T14:05:00.0,2.051451129166666
+2020-10-05T14:10:00.0,2.051451129166666
+2020-10-05T14:15:00.0,2.051451129166666
+2020-10-05T14:20:00.0,2.051451129166666
+2020-10-05T14:25:00.0,2.051451129166666
+2020-10-05T14:30:00.0,2.051451129166666
+2020-10-05T14:35:00.0,2.051451129166666
+2020-10-05T14:40:00.0,2.051451129166666
+2020-10-05T14:45:00.0,2.051451129166666
+2020-10-05T14:50:00.0,2.051451129166666
+2020-10-05T14:55:00.0,2.051451129166666
+2020-10-05T15:00:00.0,2.5441854899999994
+2020-10-05T15:05:00.0,2.5441854899999994
+2020-10-05T15:10:00.0,2.5441854899999994
+2020-10-05T15:15:00.0,2.5441854899999994
+2020-10-05T15:20:00.0,2.5441854899999994
+2020-10-05T15:25:00.0,2.5441854899999994
+2020-10-05T15:30:00.0,2.5441854899999994
+2020-10-05T15:35:00.0,2.5441854899999994
+2020-10-05T15:40:00.0,2.5441854899999994
+2020-10-05T15:45:00.0,2.5441854899999994
+2020-10-05T15:50:00.0,2.5441854899999994
+2020-10-05T15:55:00.0,2.5441854899999994
+2020-10-05T16:00:00.0,2.627436784999998
+2020-10-05T16:05:00.0,2.627436784999998
+2020-10-05T16:10:00.0,2.627436784999998
+2020-10-05T16:15:00.0,2.627436784999998
+2020-10-05T16:20:00.0,2.627436784999998
+2020-10-05T16:25:00.0,2.627436784999998
+2020-10-05T16:30:00.0,2.627436784999998
+2020-10-05T16:35:00.0,2.627436784999998
+2020-10-05T16:40:00.0,2.627436784999998
+2020-10-05T16:45:00.0,2.627436784999998
+2020-10-05T16:50:00.0,2.627436784999998
+2020-10-05T16:55:00.0,2.627436784999998
+2020-10-05T17:00:00.0,2.6439574699999993
+2020-10-05T17:05:00.0,2.6439574699999993
+2020-10-05T17:10:00.0,2.6439574699999993
+2020-10-05T17:15:00.0,2.6439574699999993
+2020-10-05T17:20:00.0,2.6439574699999993
+2020-10-05T17:25:00.0,2.6439574699999993
+2020-10-05T17:30:00.0,2.6439574699999993
+2020-10-05T17:35:00.0,2.6439574699999993
+2020-10-05T17:40:00.0,2.6439574699999993
+2020-10-05T17:45:00.0,2.6439574699999993
+2020-10-05T17:50:00.0,2.6439574699999993
+2020-10-05T17:55:00.0,2.6439574699999993
+2020-10-05T18:00:00.0,2.6439574699999993
+2020-10-05T18:05:00.0,2.6439574699999993
+2020-10-05T18:10:00.0,2.6439574699999993
+2020-10-05T18:15:00.0,2.6439574699999993
+2020-10-05T18:20:00.0,2.6439574699999993
+2020-10-05T18:25:00.0,2.6439574699999993
+2020-10-05T18:30:00.0,2.6439574699999993
+2020-10-05T18:35:00.0,2.6439574699999993
+2020-10-05T18:40:00.0,2.6439574699999993
+2020-10-05T18:45:00.0,2.6439574699999993
+2020-10-05T18:50:00.0,2.6439574699999993
+2020-10-05T18:55:00.0,2.6439574699999993
+2020-10-05T19:00:00.0,2.6439574699999993
+2020-10-05T19:05:00.0,2.6439574699999993
+2020-10-05T19:10:00.0,2.6439574699999993
+2020-10-05T19:15:00.0,2.6439574699999993
+2020-10-05T19:20:00.0,2.6439574699999993
+2020-10-05T19:25:00.0,2.6439574699999993
+2020-10-05T19:30:00.0,2.6439574699999993
+2020-10-05T19:35:00.0,2.6439574699999993
+2020-10-05T19:40:00.0,2.6439574699999993
+2020-10-05T19:45:00.0,2.6439574699999993
+2020-10-05T19:50:00.0,2.6439574699999993
+2020-10-05T19:55:00.0,2.6439574699999993
+2020-10-05T20:00:00.0,2.5231297150000005
+2020-10-05T20:05:00.0,2.5231297150000005
+2020-10-05T20:10:00.0,2.5231297150000005
+2020-10-05T20:15:00.0,2.5231297150000005
+2020-10-05T20:20:00.0,2.5231297150000005
+2020-10-05T20:25:00.0,2.5231297150000005
+2020-10-05T20:30:00.0,2.5231297150000005
+2020-10-05T20:35:00.0,2.5231297150000005
+2020-10-05T20:40:00.0,2.5231297150000005
+2020-10-05T20:45:00.0,2.5231297150000005
+2020-10-05T20:50:00.0,2.5231297150000005
+2020-10-05T20:55:00.0,2.5231297150000005
+2020-10-05T21:00:00.0,2.2728915816666664
+2020-10-05T21:05:00.0,2.2728915816666664
+2020-10-05T21:10:00.0,2.2728915816666664
+2020-10-05T21:15:00.0,2.2728915816666664
+2020-10-05T21:20:00.0,2.2728915816666664
+2020-10-05T21:25:00.0,2.2728915816666664
+2020-10-05T21:30:00.0,2.2728915816666664
+2020-10-05T21:35:00.0,2.2728915816666664
+2020-10-05T21:40:00.0,2.2728915816666664
+2020-10-05T21:45:00.0,2.2728915816666664
+2020-10-05T21:50:00.0,2.2728915816666664
+2020-10-05T21:55:00.0,2.2728915816666664
+2020-10-05T22:00:00.0,2.2024340650000003
+2020-10-05T22:05:00.0,2.2024340650000003
+2020-10-05T22:10:00.0,2.2024340650000003
+2020-10-05T22:15:00.0,2.2024340650000003
+2020-10-05T22:20:00.0,2.2024340650000003
+2020-10-05T22:25:00.0,2.2024340650000003
+2020-10-05T22:30:00.0,2.2024340650000003
+2020-10-05T22:35:00.0,2.2024340650000003
+2020-10-05T22:40:00.0,2.2024340650000003
+2020-10-05T22:45:00.0,2.2024340650000003
+2020-10-05T22:50:00.0,2.2024340650000003
+2020-10-05T22:55:00.0,2.2024340650000003
+2020-10-05T23:00:00.0,1.9714718408333318
+2020-10-05T23:05:00.0,1.9714718408333318
+2020-10-05T23:10:00.0,1.9714718408333318
+2020-10-05T23:15:00.0,1.9714718408333318
+2020-10-05T23:20:00.0,1.9714718408333318
+2020-10-05T23:25:00.0,1.9714718408333318
+2020-10-05T23:30:00.0,1.9714718408333318
+2020-10-05T23:35:00.0,1.9714718408333318
+2020-10-05T23:40:00.0,1.9714718408333318
+2020-10-05T23:45:00.0,1.9714718408333318
+2020-10-05T23:50:00.0,1.9714718408333318
+2020-10-05T23:55:00.0,1.9714718408333318
diff --git a/test/inputs/chuhsi_RegDown_prices_5min_300.csv b/test/inputs/chuhsi_RegDown_prices_5min_300.csv
new file mode 100644
index 00000000..61168558
--- /dev/null
+++ b/test/inputs/chuhsi_RegDown_prices_5min_300.csv
@@ -0,0 +1,301 @@
+DateTime,Chuhsi
+2020-10-03T00:00:00.0,2.5231297150000005
+2020-10-03T00:05:00.0,2.5231297150000005
+2020-10-03T00:10:00.0,2.5231297150000005
+2020-10-03T00:15:00.0,2.5231297150000005
+2020-10-03T00:20:00.0,2.5231297150000005
+2020-10-03T00:25:00.0,2.5231297150000005
+2020-10-03T00:30:00.0,2.5231297150000005
+2020-10-03T00:35:00.0,2.5231297150000005
+2020-10-03T00:40:00.0,2.5231297150000005
+2020-10-03T00:45:00.0,2.5231297150000005
+2020-10-03T00:50:00.0,2.5231297150000005
+2020-10-03T00:55:00.0,2.5231297150000005
+2020-10-03T01:00:00.0,2.3128959000000022
+2020-10-03T01:05:00.0,2.3128959000000022
+2020-10-03T01:10:00.0,2.3128959000000022
+2020-10-03T01:15:00.0,2.3128959000000022
+2020-10-03T01:20:00.0,2.3128959000000022
+2020-10-03T01:25:00.0,2.3128959000000022
+2020-10-03T01:30:00.0,2.3128959000000022
+2020-10-03T01:35:00.0,2.3128959000000022
+2020-10-03T01:40:00.0,2.3128959000000022
+2020-10-03T01:45:00.0,2.3128959000000022
+2020-10-03T01:50:00.0,2.3128959000000022
+2020-10-03T01:55:00.0,2.3128959000000022
+2020-10-03T02:00:00.0,2.312895900000002
+2020-10-03T02:05:00.0,2.312895900000002
+2020-10-03T02:10:00.0,2.312895900000002
+2020-10-03T02:15:00.0,2.312895900000002
+2020-10-03T02:20:00.0,2.312895900000002
+2020-10-03T02:25:00.0,2.312895900000002
+2020-10-03T02:30:00.0,2.312895900000002
+2020-10-03T02:35:00.0,2.312895900000002
+2020-10-03T02:40:00.0,2.312895900000002
+2020-10-03T02:45:00.0,2.312895900000002
+2020-10-03T02:50:00.0,2.312895900000002
+2020-10-03T02:55:00.0,2.312895900000002
+2020-10-03T03:00:00.0,2.232560020000001
+2020-10-03T03:05:00.0,2.232560020000001
+2020-10-03T03:10:00.0,2.232560020000001
+2020-10-03T03:15:00.0,2.232560020000001
+2020-10-03T03:20:00.0,2.232560020000001
+2020-10-03T03:25:00.0,2.232560020000001
+2020-10-03T03:30:00.0,2.232560020000001
+2020-10-03T03:35:00.0,2.232560020000001
+2020-10-03T03:40:00.0,2.232560020000001
+2020-10-03T03:45:00.0,2.232560020000001
+2020-10-03T03:50:00.0,2.232560020000001
+2020-10-03T03:55:00.0,2.232560020000001
+2020-10-03T04:00:00.0,2.232560020000001
+2020-10-03T04:05:00.0,2.232560020000001
+2020-10-03T04:10:00.0,2.232560020000001
+2020-10-03T04:15:00.0,2.232560020000001
+2020-10-03T04:20:00.0,2.232560020000001
+2020-10-03T04:25:00.0,2.232560020000001
+2020-10-03T04:30:00.0,2.232560020000001
+2020-10-03T04:35:00.0,2.232560020000001
+2020-10-03T04:40:00.0,2.232560020000001
+2020-10-03T04:45:00.0,2.232560020000001
+2020-10-03T04:50:00.0,2.232560020000001
+2020-10-03T04:55:00.0,2.232560020000001
+2020-10-03T05:00:00.0,2.2296446050000003
+2020-10-03T05:05:00.0,2.2296446050000003
+2020-10-03T05:10:00.0,2.2296446050000003
+2020-10-03T05:15:00.0,2.2296446050000003
+2020-10-03T05:20:00.0,2.2296446050000003
+2020-10-03T05:25:00.0,2.2296446050000003
+2020-10-03T05:30:00.0,2.2296446050000003
+2020-10-03T05:35:00.0,2.2296446050000003
+2020-10-03T05:40:00.0,2.2296446050000003
+2020-10-03T05:45:00.0,2.2296446050000003
+2020-10-03T05:50:00.0,2.2296446050000003
+2020-10-03T05:55:00.0,2.2296446050000003
+2020-10-03T06:00:00.0,1.25
+2020-10-03T06:05:00.0,1.25
+2020-10-03T06:10:00.0,1.25
+2020-10-03T06:15:00.0,1.25
+2020-10-03T06:20:00.0,1.25
+2020-10-03T06:25:00.0,1.25
+2020-10-03T06:30:00.0,1.25
+2020-10-03T06:35:00.0,1.25
+2020-10-03T06:40:00.0,1.25
+2020-10-03T06:45:00.0,1.25
+2020-10-03T06:50:00.0,1.25
+2020-10-03T06:55:00.0,1.25
+2020-10-03T07:00:00.0,1.25
+2020-10-03T07:05:00.0,1.25
+2020-10-03T07:10:00.0,1.25
+2020-10-03T07:15:00.0,1.25
+2020-10-03T07:20:00.0,1.25
+2020-10-03T07:25:00.0,1.25
+2020-10-03T07:30:00.0,1.25
+2020-10-03T07:35:00.0,1.25
+2020-10-03T07:40:00.0,1.25
+2020-10-03T07:45:00.0,1.25
+2020-10-03T07:50:00.0,1.25
+2020-10-03T07:55:00.0,1.25
+2020-10-03T08:00:00.0,1.25
+2020-10-03T08:05:00.0,1.25
+2020-10-03T08:10:00.0,1.25
+2020-10-03T08:15:00.0,1.25
+2020-10-03T08:20:00.0,1.25
+2020-10-03T08:25:00.0,1.25
+2020-10-03T08:30:00.0,1.25
+2020-10-03T08:35:00.0,1.25
+2020-10-03T08:40:00.0,1.25
+2020-10-03T08:45:00.0,1.25
+2020-10-03T08:50:00.0,1.25
+2020-10-03T08:55:00.0,1.25
+2020-10-03T09:00:00.0,1.25
+2020-10-03T09:05:00.0,1.25
+2020-10-03T09:10:00.0,1.25
+2020-10-03T09:15:00.0,1.25
+2020-10-03T09:20:00.0,1.25
+2020-10-03T09:25:00.0,1.25
+2020-10-03T09:30:00.0,1.25
+2020-10-03T09:35:00.0,1.25
+2020-10-03T09:40:00.0,1.25
+2020-10-03T09:45:00.0,1.25
+2020-10-03T09:50:00.0,1.25
+2020-10-03T09:55:00.0,1.25
+2020-10-03T10:00:00.0,1.5717515649999987
+2020-10-03T10:05:00.0,1.5717515649999987
+2020-10-03T10:10:00.0,1.5717515649999987
+2020-10-03T10:15:00.0,1.5717515649999987
+2020-10-03T10:20:00.0,1.5717515649999987
+2020-10-03T10:25:00.0,1.5717515649999987
+2020-10-03T10:30:00.0,1.5717515649999987
+2020-10-03T10:35:00.0,1.5717515649999987
+2020-10-03T10:40:00.0,1.5717515649999987
+2020-10-03T10:45:00.0,1.5717515649999987
+2020-10-03T10:50:00.0,1.5717515649999987
+2020-10-03T10:55:00.0,1.5717515649999987
+2020-10-03T11:00:00.0,1.6652956224999993
+2020-10-03T11:05:00.0,1.6652956224999993
+2020-10-03T11:10:00.0,1.6652956224999993
+2020-10-03T11:15:00.0,1.6652956224999993
+2020-10-03T11:20:00.0,1.6652956224999993
+2020-10-03T11:25:00.0,1.6652956224999993
+2020-10-03T11:30:00.0,1.6652956224999993
+2020-10-03T11:35:00.0,1.6652956224999993
+2020-10-03T11:40:00.0,1.6652956224999993
+2020-10-03T11:45:00.0,1.6652956224999993
+2020-10-03T11:50:00.0,1.6652956224999993
+2020-10-03T11:55:00.0,1.6652956224999993
+2020-10-03T12:00:00.0,1.7015857841666662
+2020-10-03T12:05:00.0,1.7015857841666662
+2020-10-03T12:10:00.0,1.7015857841666662
+2020-10-03T12:15:00.0,1.7015857841666662
+2020-10-03T12:20:00.0,1.7015857841666662
+2020-10-03T12:25:00.0,1.7015857841666662
+2020-10-03T12:30:00.0,1.7015857841666662
+2020-10-03T12:35:00.0,1.7015857841666662
+2020-10-03T12:40:00.0,1.7015857841666662
+2020-10-03T12:45:00.0,1.7015857841666662
+2020-10-03T12:50:00.0,1.7015857841666662
+2020-10-03T12:55:00.0,1.7015857841666662
+2020-10-03T13:00:00.0,1.803938133333334
+2020-10-03T13:05:00.0,1.803938133333334
+2020-10-03T13:10:00.0,1.803938133333334
+2020-10-03T13:15:00.0,1.803938133333334
+2020-10-03T13:20:00.0,1.803938133333334
+2020-10-03T13:25:00.0,1.803938133333334
+2020-10-03T13:30:00.0,1.803938133333334
+2020-10-03T13:35:00.0,1.803938133333334
+2020-10-03T13:40:00.0,1.803938133333334
+2020-10-03T13:45:00.0,1.803938133333334
+2020-10-03T13:50:00.0,1.803938133333334
+2020-10-03T13:55:00.0,1.803938133333334
+2020-10-03T14:00:00.0,1.8943718800000007
+2020-10-03T14:05:00.0,1.8943718800000007
+2020-10-03T14:10:00.0,1.8943718800000007
+2020-10-03T14:15:00.0,1.8943718800000007
+2020-10-03T14:20:00.0,1.8943718800000007
+2020-10-03T14:25:00.0,1.8943718800000007
+2020-10-03T14:30:00.0,1.8943718800000007
+2020-10-03T14:35:00.0,1.8943718800000007
+2020-10-03T14:40:00.0,1.8943718800000007
+2020-10-03T14:45:00.0,1.8943718800000007
+2020-10-03T14:50:00.0,1.8943718800000007
+2020-10-03T14:55:00.0,1.8943718800000007
+2020-10-03T15:00:00.0,1.922497739166667
+2020-10-03T15:05:00.0,1.922497739166667
+2020-10-03T15:10:00.0,1.922497739166667
+2020-10-03T15:15:00.0,1.922497739166667
+2020-10-03T15:20:00.0,1.922497739166667
+2020-10-03T15:25:00.0,1.922497739166667
+2020-10-03T15:30:00.0,1.922497739166667
+2020-10-03T15:35:00.0,1.922497739166667
+2020-10-03T15:40:00.0,1.922497739166667
+2020-10-03T15:45:00.0,1.922497739166667
+2020-10-03T15:50:00.0,1.922497739166667
+2020-10-03T15:55:00.0,1.922497739166667
+2020-10-03T16:00:00.0,2.2370951100000003
+2020-10-03T16:05:00.0,2.2370951100000003
+2020-10-03T16:10:00.0,2.2370951100000003
+2020-10-03T16:15:00.0,2.2370951100000003
+2020-10-03T16:20:00.0,2.2370951100000003
+2020-10-03T16:25:00.0,2.2370951100000003
+2020-10-03T16:30:00.0,2.2370951100000003
+2020-10-03T16:35:00.0,2.2370951100000003
+2020-10-03T16:40:00.0,2.2370951100000003
+2020-10-03T16:45:00.0,2.2370951100000003
+2020-10-03T16:50:00.0,2.2370951100000003
+2020-10-03T16:55:00.0,2.2370951100000003
+2020-10-03T17:00:00.0,2.3128959000000022
+2020-10-03T17:05:00.0,2.3128959000000022
+2020-10-03T17:10:00.0,2.3128959000000022
+2020-10-03T17:15:00.0,2.3128959000000022
+2020-10-03T17:20:00.0,2.3128959000000022
+2020-10-03T17:25:00.0,2.3128959000000022
+2020-10-03T17:30:00.0,2.3128959000000022
+2020-10-03T17:35:00.0,2.3128959000000022
+2020-10-03T17:40:00.0,2.3128959000000022
+2020-10-03T17:45:00.0,2.3128959000000022
+2020-10-03T17:50:00.0,2.3128959000000022
+2020-10-03T17:55:00.0,2.3128959000000022
+2020-10-03T18:00:00.0,2.7051811849999985
+2020-10-03T18:05:00.0,2.7051811849999985
+2020-10-03T18:10:00.0,2.7051811849999985
+2020-10-03T18:15:00.0,2.7051811849999985
+2020-10-03T18:20:00.0,2.7051811849999985
+2020-10-03T18:25:00.0,2.7051811849999985
+2020-10-03T18:30:00.0,2.7051811849999985
+2020-10-03T18:35:00.0,2.7051811849999985
+2020-10-03T18:40:00.0,2.7051811849999985
+2020-10-03T18:45:00.0,2.7051811849999985
+2020-10-03T18:50:00.0,2.7051811849999985
+2020-10-03T18:55:00.0,2.7051811849999985
+2020-10-03T19:00:00.0,2.3128959000000022
+2020-10-03T19:05:00.0,2.3128959000000022
+2020-10-03T19:10:00.0,2.3128959000000022
+2020-10-03T19:15:00.0,2.3128959000000022
+2020-10-03T19:20:00.0,2.3128959000000022
+2020-10-03T19:25:00.0,2.3128959000000022
+2020-10-03T19:30:00.0,2.3128959000000022
+2020-10-03T19:35:00.0,2.3128959000000022
+2020-10-03T19:40:00.0,2.3128959000000022
+2020-10-03T19:45:00.0,2.3128959000000022
+2020-10-03T19:50:00.0,2.3128959000000022
+2020-10-03T19:55:00.0,2.3128959000000022
+2020-10-03T20:00:00.0,2.2296446050000003
+2020-10-03T20:05:00.0,2.2296446050000003
+2020-10-03T20:10:00.0,2.2296446050000003
+2020-10-03T20:15:00.0,2.2296446050000003
+2020-10-03T20:20:00.0,2.2296446050000003
+2020-10-03T20:25:00.0,2.2296446050000003
+2020-10-03T20:30:00.0,2.2296446050000003
+2020-10-03T20:35:00.0,2.2296446050000003
+2020-10-03T20:40:00.0,2.2296446050000003
+2020-10-03T20:45:00.0,2.2296446050000003
+2020-10-03T20:50:00.0,2.2296446050000003
+2020-10-03T20:55:00.0,2.2296446050000003
+2020-10-03T21:00:00.0,1.9274132499999992
+2020-10-03T21:05:00.0,1.9274132499999992
+2020-10-03T21:10:00.0,1.9274132499999992
+2020-10-03T21:15:00.0,1.9274132499999992
+2020-10-03T21:20:00.0,1.9274132499999992
+2020-10-03T21:25:00.0,1.9274132499999992
+2020-10-03T21:30:00.0,1.9274132499999992
+2020-10-03T21:35:00.0,1.9274132499999992
+2020-10-03T21:40:00.0,1.9274132499999992
+2020-10-03T21:45:00.0,1.9274132499999992
+2020-10-03T21:50:00.0,1.9274132499999992
+2020-10-03T21:55:00.0,1.9274132499999992
+2020-10-03T22:00:00.0,1.7597205091666657
+2020-10-03T22:05:00.0,1.7597205091666657
+2020-10-03T22:10:00.0,1.7597205091666657
+2020-10-03T22:15:00.0,1.7597205091666657
+2020-10-03T22:20:00.0,1.7597205091666657
+2020-10-03T22:25:00.0,1.7597205091666657
+2020-10-03T22:30:00.0,1.7597205091666657
+2020-10-03T22:35:00.0,1.7597205091666657
+2020-10-03T22:40:00.0,1.7597205091666657
+2020-10-03T22:45:00.0,1.7597205091666657
+2020-10-03T22:50:00.0,1.7597205091666657
+2020-10-03T22:55:00.0,1.7597205091666657
+2020-10-03T23:00:00.0,1.7000002916666659
+2020-10-03T23:05:00.0,1.7000002916666659
+2020-10-03T23:10:00.0,1.7000002916666659
+2020-10-03T23:15:00.0,1.7000002916666659
+2020-10-03T23:20:00.0,1.7000002916666659
+2020-10-03T23:25:00.0,1.7000002916666659
+2020-10-03T23:30:00.0,1.7000002916666659
+2020-10-03T23:35:00.0,1.7000002916666659
+2020-10-03T23:40:00.0,1.7000002916666659
+2020-10-03T23:45:00.0,1.7000002916666659
+2020-10-03T23:50:00.0,1.7000002916666659
+2020-10-03T23:55:00.0,1.7000002916666659
+2020-10-04T00:00:00.0,1.6652956224999993
+2020-10-04T00:05:00.0,1.6652956224999993
+2020-10-04T00:10:00.0,1.6652956224999993
+2020-10-04T00:15:00.0,1.6652956224999993
+2020-10-04T00:20:00.0,1.6652956224999993
+2020-10-04T00:25:00.0,1.6652956224999993
+2020-10-04T00:30:00.0,1.6652956224999993
+2020-10-04T00:35:00.0,1.6652956224999993
+2020-10-04T00:40:00.0,1.6652956224999993
+2020-10-04T00:45:00.0,1.6652956224999993
+2020-10-04T00:50:00.0,1.6652956224999993
+2020-10-04T00:55:00.0,1.6652956224999993
diff --git a/test/inputs/chuhsi_RegUp_prices.csv b/test/inputs/chuhsi_RegUp_prices.csv
index f86e0de9..d49d7b07 100644
--- a/test/inputs/chuhsi_RegUp_prices.csv
+++ b/test/inputs/chuhsi_RegUp_prices.csv
@@ -1,73 +1,73 @@
-DateTime,Chuhsi
-2020-10-03T00:00:00.0,3.0277556580000007
-2020-10-03T01:00:00.0,2.7754750800000023
-2020-10-03T02:00:00.0,2.7754750800000023
-2020-10-03T03:00:00.0,2.679072024000001
-2020-10-03T04:00:00.0,2.679072024000001
-2020-10-03T05:00:00.0,2.6755735260000004
-2020-10-03T06:00:00.0,1.5
-2020-10-03T07:00:00.0,1.5
-2020-10-03T08:00:00.0,1.5
-2020-10-03T09:00:00.0,1.5
-2020-10-03T10:00:00.0,1.8861018779999985
-2020-10-03T11:00:00.0,1.9983547469999992
-2020-10-03T12:00:00.0,2.041902940999999
-2020-10-03T13:00:00.0,2.1647257600000005
-2020-10-03T14:00:00.0,2.273246256000001
-2020-10-03T15:00:00.0,2.3069972870000006
-2020-10-03T16:00:00.0,2.6845141320000003
-2020-10-03T17:00:00.0,2.7754750800000023
-2020-10-03T18:00:00.0,3.246217421999998
-2020-10-03T19:00:00.0,2.7754750800000023
-2020-10-03T20:00:00.0,2.675573526
-2020-10-03T21:00:00.0,2.312895899999999
-2020-10-03T22:00:00.0,2.111664610999999
-2020-10-03T23:00:00.0,2.040000349999999
-2020-10-04T00:00:00.0,1.9983547469999992
-2020-10-04T01:00:00.0,1.9983547469999992
-2020-10-04T02:00:00.0,1.9034365959999981
-2020-10-04T03:00:00.0,1.9689702860000011
-2020-10-04T04:00:00.0,1.9983547469999992
-2020-10-04T05:00:00.0,1.8861018779999985
-2020-10-04T06:00:00.0,1.5
-2020-10-04T07:00:00.0,1.5
-2020-10-04T08:00:00.0,1.5
-2020-10-04T09:00:00.0,1.9429682089999993
-2020-10-04T10:00:00.0,1.9983547469999992
-2020-10-04T11:00:00.0,1.9983547469999992
-2020-10-04T12:00:00.0,2.1647257600000005
-2020-10-04T13:00:00.0,2.312895899999999
-2020-10-04T14:00:00.0,2.6755735260000004
-2020-10-04T15:00:00.0,2.6755735260000004
-2020-10-04T16:00:00.0,3.0530225879999993
-2020-10-04T17:00:00.0,3.152924141999998
-2020-10-04T18:00:00.0,3.152924141999998
-2020-10-04T19:00:00.0,3.152924141999998
-2020-10-04T20:00:00.0,2.7754750800000023
-2020-10-04T21:00:00.0,2.675573526
-2020-10-04T22:00:00.0,2.632425384000001
-2020-10-04T23:00:00.0,2.3875403059999982
-2020-10-05T00:00:00.0,2.3128958999999987
-2020-10-05T01:00:00.0,2.296850135000001
-2020-10-05T02:00:00.0,2.273246256
-2020-10-05T03:00:00.0,2.3069972870000006
-2020-10-05T04:00:00.0,2.3069972870000006
-2020-10-05T05:00:00.0,2.3437807129999984
-2020-10-05T06:00:00.0,1.8861018779999985
-2020-10-05T07:00:00.0,1.5
-2020-10-05T08:00:00.0,1.5
-2020-10-05T09:00:00.0,1.8463588659999992
-2020-10-05T10:00:00.0,1.9034365959999981
-2020-10-05T11:00:00.0,2.084605538999999
-2020-10-05T12:00:00.0,2.2576973760000003
-2020-10-05T13:00:00.0,2.312895899999999
-2020-10-05T14:00:00.0,2.461741354999999
-2020-10-05T15:00:00.0,3.0530225879999993
-2020-10-05T16:00:00.0,3.152924141999998
-2020-10-05T17:00:00.0,3.1727489639999993
-2020-10-05T18:00:00.0,3.1727489639999993
-2020-10-05T19:00:00.0,3.1727489639999993
-2020-10-05T20:00:00.0,3.0277556580000007
-2020-10-05T21:00:00.0,2.727469898
-2020-10-05T22:00:00.0,2.6429208780000004
-2020-10-05T23:00:00.0,2.365766208999998
+DateTime,Chuhsi
+2020-10-03T00:00:00.0,3.0277556580000007
+2020-10-03T01:00:00.0,2.7754750800000023
+2020-10-03T02:00:00.0,2.7754750800000023
+2020-10-03T03:00:00.0,2.679072024000001
+2020-10-03T04:00:00.0,2.679072024000001
+2020-10-03T05:00:00.0,2.6755735260000004
+2020-10-03T06:00:00.0,1.5
+2020-10-03T07:00:00.0,1.5
+2020-10-03T08:00:00.0,1.5
+2020-10-03T09:00:00.0,1.5
+2020-10-03T10:00:00.0,1.8861018779999985
+2020-10-03T11:00:00.0,1.9983547469999992
+2020-10-03T12:00:00.0,2.041902940999999
+2020-10-03T13:00:00.0,2.1647257600000005
+2020-10-03T14:00:00.0,2.273246256000001
+2020-10-03T15:00:00.0,2.3069972870000006
+2020-10-03T16:00:00.0,2.6845141320000003
+2020-10-03T17:00:00.0,2.7754750800000023
+2020-10-03T18:00:00.0,3.246217421999998
+2020-10-03T19:00:00.0,2.7754750800000023
+2020-10-03T20:00:00.0,2.675573526
+2020-10-03T21:00:00.0,2.312895899999999
+2020-10-03T22:00:00.0,2.111664610999999
+2020-10-03T23:00:00.0,2.040000349999999
+2020-10-04T00:00:00.0,1.9983547469999992
+2020-10-04T01:00:00.0,1.9983547469999992
+2020-10-04T02:00:00.0,1.9034365959999981
+2020-10-04T03:00:00.0,1.9689702860000011
+2020-10-04T04:00:00.0,1.9983547469999992
+2020-10-04T05:00:00.0,1.8861018779999985
+2020-10-04T06:00:00.0,1.5
+2020-10-04T07:00:00.0,1.5
+2020-10-04T08:00:00.0,1.5
+2020-10-04T09:00:00.0,1.9429682089999993
+2020-10-04T10:00:00.0,1.9983547469999992
+2020-10-04T11:00:00.0,1.9983547469999992
+2020-10-04T12:00:00.0,2.1647257600000005
+2020-10-04T13:00:00.0,2.312895899999999
+2020-10-04T14:00:00.0,2.6755735260000004
+2020-10-04T15:00:00.0,2.6755735260000004
+2020-10-04T16:00:00.0,3.0530225879999993
+2020-10-04T17:00:00.0,3.152924141999998
+2020-10-04T18:00:00.0,3.152924141999998
+2020-10-04T19:00:00.0,3.152924141999998
+2020-10-04T20:00:00.0,2.7754750800000023
+2020-10-04T21:00:00.0,2.675573526
+2020-10-04T22:00:00.0,2.632425384000001
+2020-10-04T23:00:00.0,2.3875403059999982
+2020-10-05T00:00:00.0,2.3128958999999987
+2020-10-05T01:00:00.0,2.296850135000001
+2020-10-05T02:00:00.0,2.273246256
+2020-10-05T03:00:00.0,2.3069972870000006
+2020-10-05T04:00:00.0,2.3069972870000006
+2020-10-05T05:00:00.0,2.3437807129999984
+2020-10-05T06:00:00.0,1.8861018779999985
+2020-10-05T07:00:00.0,1.5
+2020-10-05T08:00:00.0,1.5
+2020-10-05T09:00:00.0,1.8463588659999992
+2020-10-05T10:00:00.0,1.9034365959999981
+2020-10-05T11:00:00.0,2.084605538999999
+2020-10-05T12:00:00.0,2.2576973760000003
+2020-10-05T13:00:00.0,2.312895899999999
+2020-10-05T14:00:00.0,2.461741354999999
+2020-10-05T15:00:00.0,3.0530225879999993
+2020-10-05T16:00:00.0,3.152924141999998
+2020-10-05T17:00:00.0,3.1727489639999993
+2020-10-05T18:00:00.0,3.1727489639999993
+2020-10-05T19:00:00.0,3.1727489639999993
+2020-10-05T20:00:00.0,3.0277556580000007
+2020-10-05T21:00:00.0,2.727469898
+2020-10-05T22:00:00.0,2.6429208780000004
+2020-10-05T23:00:00.0,2.365766208999998
diff --git a/test/inputs/chuhsi_RegUp_prices_24.csv b/test/inputs/chuhsi_RegUp_prices_24.csv
new file mode 100644
index 00000000..79b1ece7
--- /dev/null
+++ b/test/inputs/chuhsi_RegUp_prices_24.csv
@@ -0,0 +1,25 @@
+DateTime,Chuhsi
+2020-10-03T00:00:00.0,3.0277556580000007
+2020-10-03T01:00:00.0,2.7754750800000023
+2020-10-03T02:00:00.0,2.7754750800000023
+2020-10-03T03:00:00.0,2.679072024000001
+2020-10-03T04:00:00.0,2.679072024000001
+2020-10-03T05:00:00.0,2.6755735260000004
+2020-10-03T06:00:00.0,1.5
+2020-10-03T07:00:00.0,1.5
+2020-10-03T08:00:00.0,1.5
+2020-10-03T09:00:00.0,1.5
+2020-10-03T10:00:00.0,1.8861018779999985
+2020-10-03T11:00:00.0,1.9983547469999992
+2020-10-03T12:00:00.0,2.041902940999999
+2020-10-03T13:00:00.0,2.1647257600000005
+2020-10-03T14:00:00.0,2.273246256000001
+2020-10-03T15:00:00.0,2.3069972870000006
+2020-10-03T16:00:00.0,2.6845141320000003
+2020-10-03T17:00:00.0,2.7754750800000023
+2020-10-03T18:00:00.0,3.246217421999998
+2020-10-03T19:00:00.0,2.7754750800000023
+2020-10-03T20:00:00.0,2.675573526
+2020-10-03T21:00:00.0,2.312895899999999
+2020-10-03T22:00:00.0,2.111664610999999
+2020-10-03T23:00:00.0,2.040000349999999
diff --git a/test/inputs/chuhsi_RegUp_prices_5min.csv b/test/inputs/chuhsi_RegUp_prices_5min.csv
new file mode 100644
index 00000000..7072e1e3
--- /dev/null
+++ b/test/inputs/chuhsi_RegUp_prices_5min.csv
@@ -0,0 +1,865 @@
+DateTime,Chuhsi
+2020-10-03T00:00:00.0,3.0277556580000007
+2020-10-03T00:05:00.0,3.0277556580000007
+2020-10-03T00:10:00.0,3.0277556580000007
+2020-10-03T00:15:00.0,3.0277556580000007
+2020-10-03T00:20:00.0,3.0277556580000007
+2020-10-03T00:25:00.0,3.0277556580000007
+2020-10-03T00:30:00.0,3.0277556580000007
+2020-10-03T00:35:00.0,3.0277556580000007
+2020-10-03T00:40:00.0,3.0277556580000007
+2020-10-03T00:45:00.0,3.0277556580000007
+2020-10-03T00:50:00.0,3.0277556580000007
+2020-10-03T00:55:00.0,3.0277556580000007
+2020-10-03T01:00:00.0,2.7754750800000023
+2020-10-03T01:05:00.0,2.7754750800000023
+2020-10-03T01:10:00.0,2.7754750800000023
+2020-10-03T01:15:00.0,2.7754750800000023
+2020-10-03T01:20:00.0,2.7754750800000023
+2020-10-03T01:25:00.0,2.7754750800000023
+2020-10-03T01:30:00.0,2.7754750800000023
+2020-10-03T01:35:00.0,2.7754750800000023
+2020-10-03T01:40:00.0,2.7754750800000023
+2020-10-03T01:45:00.0,2.7754750800000023
+2020-10-03T01:50:00.0,2.7754750800000023
+2020-10-03T01:55:00.0,2.7754750800000023
+2020-10-03T02:00:00.0,2.7754750800000023
+2020-10-03T02:05:00.0,2.7754750800000023
+2020-10-03T02:10:00.0,2.7754750800000023
+2020-10-03T02:15:00.0,2.7754750800000023
+2020-10-03T02:20:00.0,2.7754750800000023
+2020-10-03T02:25:00.0,2.7754750800000023
+2020-10-03T02:30:00.0,2.7754750800000023
+2020-10-03T02:35:00.0,2.7754750800000023
+2020-10-03T02:40:00.0,2.7754750800000023
+2020-10-03T02:45:00.0,2.7754750800000023
+2020-10-03T02:50:00.0,2.7754750800000023
+2020-10-03T02:55:00.0,2.7754750800000023
+2020-10-03T03:00:00.0,2.679072024000001
+2020-10-03T03:05:00.0,2.679072024000001
+2020-10-03T03:10:00.0,2.679072024000001
+2020-10-03T03:15:00.0,2.679072024000001
+2020-10-03T03:20:00.0,2.679072024000001
+2020-10-03T03:25:00.0,2.679072024000001
+2020-10-03T03:30:00.0,2.679072024000001
+2020-10-03T03:35:00.0,2.679072024000001
+2020-10-03T03:40:00.0,2.679072024000001
+2020-10-03T03:45:00.0,2.679072024000001
+2020-10-03T03:50:00.0,2.679072024000001
+2020-10-03T03:55:00.0,2.679072024000001
+2020-10-03T04:00:00.0,2.679072024000001
+2020-10-03T04:05:00.0,2.679072024000001
+2020-10-03T04:10:00.0,2.679072024000001
+2020-10-03T04:15:00.0,2.679072024000001
+2020-10-03T04:20:00.0,2.679072024000001
+2020-10-03T04:25:00.0,2.679072024000001
+2020-10-03T04:30:00.0,2.679072024000001
+2020-10-03T04:35:00.0,2.679072024000001
+2020-10-03T04:40:00.0,2.679072024000001
+2020-10-03T04:45:00.0,2.679072024000001
+2020-10-03T04:50:00.0,2.679072024000001
+2020-10-03T04:55:00.0,2.679072024000001
+2020-10-03T05:00:00.0,2.6755735260000004
+2020-10-03T05:05:00.0,2.6755735260000004
+2020-10-03T05:10:00.0,2.6755735260000004
+2020-10-03T05:15:00.0,2.6755735260000004
+2020-10-03T05:20:00.0,2.6755735260000004
+2020-10-03T05:25:00.0,2.6755735260000004
+2020-10-03T05:30:00.0,2.6755735260000004
+2020-10-03T05:35:00.0,2.6755735260000004
+2020-10-03T05:40:00.0,2.6755735260000004
+2020-10-03T05:45:00.0,2.6755735260000004
+2020-10-03T05:50:00.0,2.6755735260000004
+2020-10-03T05:55:00.0,2.6755735260000004
+2020-10-03T06:00:00.0,1.5
+2020-10-03T06:05:00.0,1.5
+2020-10-03T06:10:00.0,1.5
+2020-10-03T06:15:00.0,1.5
+2020-10-03T06:20:00.0,1.5
+2020-10-03T06:25:00.0,1.5
+2020-10-03T06:30:00.0,1.5
+2020-10-03T06:35:00.0,1.5
+2020-10-03T06:40:00.0,1.5
+2020-10-03T06:45:00.0,1.5
+2020-10-03T06:50:00.0,1.5
+2020-10-03T06:55:00.0,1.5
+2020-10-03T07:00:00.0,1.5
+2020-10-03T07:05:00.0,1.5
+2020-10-03T07:10:00.0,1.5
+2020-10-03T07:15:00.0,1.5
+2020-10-03T07:20:00.0,1.5
+2020-10-03T07:25:00.0,1.5
+2020-10-03T07:30:00.0,1.5
+2020-10-03T07:35:00.0,1.5
+2020-10-03T07:40:00.0,1.5
+2020-10-03T07:45:00.0,1.5
+2020-10-03T07:50:00.0,1.5
+2020-10-03T07:55:00.0,1.5
+2020-10-03T08:00:00.0,1.5
+2020-10-03T08:05:00.0,1.5
+2020-10-03T08:10:00.0,1.5
+2020-10-03T08:15:00.0,1.5
+2020-10-03T08:20:00.0,1.5
+2020-10-03T08:25:00.0,1.5
+2020-10-03T08:30:00.0,1.5
+2020-10-03T08:35:00.0,1.5
+2020-10-03T08:40:00.0,1.5
+2020-10-03T08:45:00.0,1.5
+2020-10-03T08:50:00.0,1.5
+2020-10-03T08:55:00.0,1.5
+2020-10-03T09:00:00.0,1.5
+2020-10-03T09:05:00.0,1.5
+2020-10-03T09:10:00.0,1.5
+2020-10-03T09:15:00.0,1.5
+2020-10-03T09:20:00.0,1.5
+2020-10-03T09:25:00.0,1.5
+2020-10-03T09:30:00.0,1.5
+2020-10-03T09:35:00.0,1.5
+2020-10-03T09:40:00.0,1.5
+2020-10-03T09:45:00.0,1.5
+2020-10-03T09:50:00.0,1.5
+2020-10-03T09:55:00.0,1.5
+2020-10-03T10:00:00.0,1.8861018779999985
+2020-10-03T10:05:00.0,1.8861018779999985
+2020-10-03T10:10:00.0,1.8861018779999985
+2020-10-03T10:15:00.0,1.8861018779999985
+2020-10-03T10:20:00.0,1.8861018779999985
+2020-10-03T10:25:00.0,1.8861018779999985
+2020-10-03T10:30:00.0,1.8861018779999985
+2020-10-03T10:35:00.0,1.8861018779999985
+2020-10-03T10:40:00.0,1.8861018779999985
+2020-10-03T10:45:00.0,1.8861018779999985
+2020-10-03T10:50:00.0,1.8861018779999985
+2020-10-03T10:55:00.0,1.8861018779999985
+2020-10-03T11:00:00.0,1.9983547469999992
+2020-10-03T11:05:00.0,1.9983547469999992
+2020-10-03T11:10:00.0,1.9983547469999992
+2020-10-03T11:15:00.0,1.9983547469999992
+2020-10-03T11:20:00.0,1.9983547469999992
+2020-10-03T11:25:00.0,1.9983547469999992
+2020-10-03T11:30:00.0,1.9983547469999992
+2020-10-03T11:35:00.0,1.9983547469999992
+2020-10-03T11:40:00.0,1.9983547469999992
+2020-10-03T11:45:00.0,1.9983547469999992
+2020-10-03T11:50:00.0,1.9983547469999992
+2020-10-03T11:55:00.0,1.9983547469999992
+2020-10-03T12:00:00.0,2.041902940999999
+2020-10-03T12:05:00.0,2.041902940999999
+2020-10-03T12:10:00.0,2.041902940999999
+2020-10-03T12:15:00.0,2.041902940999999
+2020-10-03T12:20:00.0,2.041902940999999
+2020-10-03T12:25:00.0,2.041902940999999
+2020-10-03T12:30:00.0,2.041902940999999
+2020-10-03T12:35:00.0,2.041902940999999
+2020-10-03T12:40:00.0,2.041902940999999
+2020-10-03T12:45:00.0,2.041902940999999
+2020-10-03T12:50:00.0,2.041902940999999
+2020-10-03T12:55:00.0,2.041902940999999
+2020-10-03T13:00:00.0,2.1647257600000005
+2020-10-03T13:05:00.0,2.1647257600000005
+2020-10-03T13:10:00.0,2.1647257600000005
+2020-10-03T13:15:00.0,2.1647257600000005
+2020-10-03T13:20:00.0,2.1647257600000005
+2020-10-03T13:25:00.0,2.1647257600000005
+2020-10-03T13:30:00.0,2.1647257600000005
+2020-10-03T13:35:00.0,2.1647257600000005
+2020-10-03T13:40:00.0,2.1647257600000005
+2020-10-03T13:45:00.0,2.1647257600000005
+2020-10-03T13:50:00.0,2.1647257600000005
+2020-10-03T13:55:00.0,2.1647257600000005
+2020-10-03T14:00:00.0,2.273246256000001
+2020-10-03T14:05:00.0,2.273246256000001
+2020-10-03T14:10:00.0,2.273246256000001
+2020-10-03T14:15:00.0,2.273246256000001
+2020-10-03T14:20:00.0,2.273246256000001
+2020-10-03T14:25:00.0,2.273246256000001
+2020-10-03T14:30:00.0,2.273246256000001
+2020-10-03T14:35:00.0,2.273246256000001
+2020-10-03T14:40:00.0,2.273246256000001
+2020-10-03T14:45:00.0,2.273246256000001
+2020-10-03T14:50:00.0,2.273246256000001
+2020-10-03T14:55:00.0,2.273246256000001
+2020-10-03T15:00:00.0,2.3069972870000006
+2020-10-03T15:05:00.0,2.3069972870000006
+2020-10-03T15:10:00.0,2.3069972870000006
+2020-10-03T15:15:00.0,2.3069972870000006
+2020-10-03T15:20:00.0,2.3069972870000006
+2020-10-03T15:25:00.0,2.3069972870000006
+2020-10-03T15:30:00.0,2.3069972870000006
+2020-10-03T15:35:00.0,2.3069972870000006
+2020-10-03T15:40:00.0,2.3069972870000006
+2020-10-03T15:45:00.0,2.3069972870000006
+2020-10-03T15:50:00.0,2.3069972870000006
+2020-10-03T15:55:00.0,2.3069972870000006
+2020-10-03T16:00:00.0,2.6845141320000003
+2020-10-03T16:05:00.0,2.6845141320000003
+2020-10-03T16:10:00.0,2.6845141320000003
+2020-10-03T16:15:00.0,2.6845141320000003
+2020-10-03T16:20:00.0,2.6845141320000003
+2020-10-03T16:25:00.0,2.6845141320000003
+2020-10-03T16:30:00.0,2.6845141320000003
+2020-10-03T16:35:00.0,2.6845141320000003
+2020-10-03T16:40:00.0,2.6845141320000003
+2020-10-03T16:45:00.0,2.6845141320000003
+2020-10-03T16:50:00.0,2.6845141320000003
+2020-10-03T16:55:00.0,2.6845141320000003
+2020-10-03T17:00:00.0,2.7754750800000023
+2020-10-03T17:05:00.0,2.7754750800000023
+2020-10-03T17:10:00.0,2.7754750800000023
+2020-10-03T17:15:00.0,2.7754750800000023
+2020-10-03T17:20:00.0,2.7754750800000023
+2020-10-03T17:25:00.0,2.7754750800000023
+2020-10-03T17:30:00.0,2.7754750800000023
+2020-10-03T17:35:00.0,2.7754750800000023
+2020-10-03T17:40:00.0,2.7754750800000023
+2020-10-03T17:45:00.0,2.7754750800000023
+2020-10-03T17:50:00.0,2.7754750800000023
+2020-10-03T17:55:00.0,2.7754750800000023
+2020-10-03T18:00:00.0,3.246217421999998
+2020-10-03T18:05:00.0,3.246217421999998
+2020-10-03T18:10:00.0,3.246217421999998
+2020-10-03T18:15:00.0,3.246217421999998
+2020-10-03T18:20:00.0,3.246217421999998
+2020-10-03T18:25:00.0,3.246217421999998
+2020-10-03T18:30:00.0,3.246217421999998
+2020-10-03T18:35:00.0,3.246217421999998
+2020-10-03T18:40:00.0,3.246217421999998
+2020-10-03T18:45:00.0,3.246217421999998
+2020-10-03T18:50:00.0,3.246217421999998
+2020-10-03T18:55:00.0,3.246217421999998
+2020-10-03T19:00:00.0,2.7754750800000023
+2020-10-03T19:05:00.0,2.7754750800000023
+2020-10-03T19:10:00.0,2.7754750800000023
+2020-10-03T19:15:00.0,2.7754750800000023
+2020-10-03T19:20:00.0,2.7754750800000023
+2020-10-03T19:25:00.0,2.7754750800000023
+2020-10-03T19:30:00.0,2.7754750800000023
+2020-10-03T19:35:00.0,2.7754750800000023
+2020-10-03T19:40:00.0,2.7754750800000023
+2020-10-03T19:45:00.0,2.7754750800000023
+2020-10-03T19:50:00.0,2.7754750800000023
+2020-10-03T19:55:00.0,2.7754750800000023
+2020-10-03T20:00:00.0,2.675573526
+2020-10-03T20:05:00.0,2.675573526
+2020-10-03T20:10:00.0,2.675573526
+2020-10-03T20:15:00.0,2.675573526
+2020-10-03T20:20:00.0,2.675573526
+2020-10-03T20:25:00.0,2.675573526
+2020-10-03T20:30:00.0,2.675573526
+2020-10-03T20:35:00.0,2.675573526
+2020-10-03T20:40:00.0,2.675573526
+2020-10-03T20:45:00.0,2.675573526
+2020-10-03T20:50:00.0,2.675573526
+2020-10-03T20:55:00.0,2.675573526
+2020-10-03T21:00:00.0,2.312895899999999
+2020-10-03T21:05:00.0,2.312895899999999
+2020-10-03T21:10:00.0,2.312895899999999
+2020-10-03T21:15:00.0,2.312895899999999
+2020-10-03T21:20:00.0,2.312895899999999
+2020-10-03T21:25:00.0,2.312895899999999
+2020-10-03T21:30:00.0,2.312895899999999
+2020-10-03T21:35:00.0,2.312895899999999
+2020-10-03T21:40:00.0,2.312895899999999
+2020-10-03T21:45:00.0,2.312895899999999
+2020-10-03T21:50:00.0,2.312895899999999
+2020-10-03T21:55:00.0,2.312895899999999
+2020-10-03T22:00:00.0,2.111664610999999
+2020-10-03T22:05:00.0,2.111664610999999
+2020-10-03T22:10:00.0,2.111664610999999
+2020-10-03T22:15:00.0,2.111664610999999
+2020-10-03T22:20:00.0,2.111664610999999
+2020-10-03T22:25:00.0,2.111664610999999
+2020-10-03T22:30:00.0,2.111664610999999
+2020-10-03T22:35:00.0,2.111664610999999
+2020-10-03T22:40:00.0,2.111664610999999
+2020-10-03T22:45:00.0,2.111664610999999
+2020-10-03T22:50:00.0,2.111664610999999
+2020-10-03T22:55:00.0,2.111664610999999
+2020-10-03T23:00:00.0,2.040000349999999
+2020-10-03T23:05:00.0,2.040000349999999
+2020-10-03T23:10:00.0,2.040000349999999
+2020-10-03T23:15:00.0,2.040000349999999
+2020-10-03T23:20:00.0,2.040000349999999
+2020-10-03T23:25:00.0,2.040000349999999
+2020-10-03T23:30:00.0,2.040000349999999
+2020-10-03T23:35:00.0,2.040000349999999
+2020-10-03T23:40:00.0,2.040000349999999
+2020-10-03T23:45:00.0,2.040000349999999
+2020-10-03T23:50:00.0,2.040000349999999
+2020-10-03T23:55:00.0,2.040000349999999
+2020-10-04T00:00:00.0,1.9983547469999992
+2020-10-04T00:05:00.0,1.9983547469999992
+2020-10-04T00:10:00.0,1.9983547469999992
+2020-10-04T00:15:00.0,1.9983547469999992
+2020-10-04T00:20:00.0,1.9983547469999992
+2020-10-04T00:25:00.0,1.9983547469999992
+2020-10-04T00:30:00.0,1.9983547469999992
+2020-10-04T00:35:00.0,1.9983547469999992
+2020-10-04T00:40:00.0,1.9983547469999992
+2020-10-04T00:45:00.0,1.9983547469999992
+2020-10-04T00:50:00.0,1.9983547469999992
+2020-10-04T00:55:00.0,1.9983547469999992
+2020-10-04T01:00:00.0,1.9983547469999992
+2020-10-04T01:05:00.0,1.9983547469999992
+2020-10-04T01:10:00.0,1.9983547469999992
+2020-10-04T01:15:00.0,1.9983547469999992
+2020-10-04T01:20:00.0,1.9983547469999992
+2020-10-04T01:25:00.0,1.9983547469999992
+2020-10-04T01:30:00.0,1.9983547469999992
+2020-10-04T01:35:00.0,1.9983547469999992
+2020-10-04T01:40:00.0,1.9983547469999992
+2020-10-04T01:45:00.0,1.9983547469999992
+2020-10-04T01:50:00.0,1.9983547469999992
+2020-10-04T01:55:00.0,1.9983547469999992
+2020-10-04T02:00:00.0,1.9034365959999981
+2020-10-04T02:05:00.0,1.9034365959999981
+2020-10-04T02:10:00.0,1.9034365959999981
+2020-10-04T02:15:00.0,1.9034365959999981
+2020-10-04T02:20:00.0,1.9034365959999981
+2020-10-04T02:25:00.0,1.9034365959999981
+2020-10-04T02:30:00.0,1.9034365959999981
+2020-10-04T02:35:00.0,1.9034365959999981
+2020-10-04T02:40:00.0,1.9034365959999981
+2020-10-04T02:45:00.0,1.9034365959999981
+2020-10-04T02:50:00.0,1.9034365959999981
+2020-10-04T02:55:00.0,1.9034365959999981
+2020-10-04T03:00:00.0,1.9689702860000011
+2020-10-04T03:05:00.0,1.9689702860000011
+2020-10-04T03:10:00.0,1.9689702860000011
+2020-10-04T03:15:00.0,1.9689702860000011
+2020-10-04T03:20:00.0,1.9689702860000011
+2020-10-04T03:25:00.0,1.9689702860000011
+2020-10-04T03:30:00.0,1.9689702860000011
+2020-10-04T03:35:00.0,1.9689702860000011
+2020-10-04T03:40:00.0,1.9689702860000011
+2020-10-04T03:45:00.0,1.9689702860000011
+2020-10-04T03:50:00.0,1.9689702860000011
+2020-10-04T03:55:00.0,1.9689702860000011
+2020-10-04T04:00:00.0,1.9983547469999992
+2020-10-04T04:05:00.0,1.9983547469999992
+2020-10-04T04:10:00.0,1.9983547469999992
+2020-10-04T04:15:00.0,1.9983547469999992
+2020-10-04T04:20:00.0,1.9983547469999992
+2020-10-04T04:25:00.0,1.9983547469999992
+2020-10-04T04:30:00.0,1.9983547469999992
+2020-10-04T04:35:00.0,1.9983547469999992
+2020-10-04T04:40:00.0,1.9983547469999992
+2020-10-04T04:45:00.0,1.9983547469999992
+2020-10-04T04:50:00.0,1.9983547469999992
+2020-10-04T04:55:00.0,1.9983547469999992
+2020-10-04T05:00:00.0,1.8861018779999985
+2020-10-04T05:05:00.0,1.8861018779999985
+2020-10-04T05:10:00.0,1.8861018779999985
+2020-10-04T05:15:00.0,1.8861018779999985
+2020-10-04T05:20:00.0,1.8861018779999985
+2020-10-04T05:25:00.0,1.8861018779999985
+2020-10-04T05:30:00.0,1.8861018779999985
+2020-10-04T05:35:00.0,1.8861018779999985
+2020-10-04T05:40:00.0,1.8861018779999985
+2020-10-04T05:45:00.0,1.8861018779999985
+2020-10-04T05:50:00.0,1.8861018779999985
+2020-10-04T05:55:00.0,1.8861018779999985
+2020-10-04T06:00:00.0,1.5
+2020-10-04T06:05:00.0,1.5
+2020-10-04T06:10:00.0,1.5
+2020-10-04T06:15:00.0,1.5
+2020-10-04T06:20:00.0,1.5
+2020-10-04T06:25:00.0,1.5
+2020-10-04T06:30:00.0,1.5
+2020-10-04T06:35:00.0,1.5
+2020-10-04T06:40:00.0,1.5
+2020-10-04T06:45:00.0,1.5
+2020-10-04T06:50:00.0,1.5
+2020-10-04T06:55:00.0,1.5
+2020-10-04T07:00:00.0,1.5
+2020-10-04T07:05:00.0,1.5
+2020-10-04T07:10:00.0,1.5
+2020-10-04T07:15:00.0,1.5
+2020-10-04T07:20:00.0,1.5
+2020-10-04T07:25:00.0,1.5
+2020-10-04T07:30:00.0,1.5
+2020-10-04T07:35:00.0,1.5
+2020-10-04T07:40:00.0,1.5
+2020-10-04T07:45:00.0,1.5
+2020-10-04T07:50:00.0,1.5
+2020-10-04T07:55:00.0,1.5
+2020-10-04T08:00:00.0,1.5
+2020-10-04T08:05:00.0,1.5
+2020-10-04T08:10:00.0,1.5
+2020-10-04T08:15:00.0,1.5
+2020-10-04T08:20:00.0,1.5
+2020-10-04T08:25:00.0,1.5
+2020-10-04T08:30:00.0,1.5
+2020-10-04T08:35:00.0,1.5
+2020-10-04T08:40:00.0,1.5
+2020-10-04T08:45:00.0,1.5
+2020-10-04T08:50:00.0,1.5
+2020-10-04T08:55:00.0,1.5
+2020-10-04T09:00:00.0,1.9429682089999993
+2020-10-04T09:05:00.0,1.9429682089999993
+2020-10-04T09:10:00.0,1.9429682089999993
+2020-10-04T09:15:00.0,1.9429682089999993
+2020-10-04T09:20:00.0,1.9429682089999993
+2020-10-04T09:25:00.0,1.9429682089999993
+2020-10-04T09:30:00.0,1.9429682089999993
+2020-10-04T09:35:00.0,1.9429682089999993
+2020-10-04T09:40:00.0,1.9429682089999993
+2020-10-04T09:45:00.0,1.9429682089999993
+2020-10-04T09:50:00.0,1.9429682089999993
+2020-10-04T09:55:00.0,1.9429682089999993
+2020-10-04T10:00:00.0,1.9983547469999992
+2020-10-04T10:05:00.0,1.9983547469999992
+2020-10-04T10:10:00.0,1.9983547469999992
+2020-10-04T10:15:00.0,1.9983547469999992
+2020-10-04T10:20:00.0,1.9983547469999992
+2020-10-04T10:25:00.0,1.9983547469999992
+2020-10-04T10:30:00.0,1.9983547469999992
+2020-10-04T10:35:00.0,1.9983547469999992
+2020-10-04T10:40:00.0,1.9983547469999992
+2020-10-04T10:45:00.0,1.9983547469999992
+2020-10-04T10:50:00.0,1.9983547469999992
+2020-10-04T10:55:00.0,1.9983547469999992
+2020-10-04T11:00:00.0,1.9983547469999992
+2020-10-04T11:05:00.0,1.9983547469999992
+2020-10-04T11:10:00.0,1.9983547469999992
+2020-10-04T11:15:00.0,1.9983547469999992
+2020-10-04T11:20:00.0,1.9983547469999992
+2020-10-04T11:25:00.0,1.9983547469999992
+2020-10-04T11:30:00.0,1.9983547469999992
+2020-10-04T11:35:00.0,1.9983547469999992
+2020-10-04T11:40:00.0,1.9983547469999992
+2020-10-04T11:45:00.0,1.9983547469999992
+2020-10-04T11:50:00.0,1.9983547469999992
+2020-10-04T11:55:00.0,1.9983547469999992
+2020-10-04T12:00:00.0,2.1647257600000005
+2020-10-04T12:05:00.0,2.1647257600000005
+2020-10-04T12:10:00.0,2.1647257600000005
+2020-10-04T12:15:00.0,2.1647257600000005
+2020-10-04T12:20:00.0,2.1647257600000005
+2020-10-04T12:25:00.0,2.1647257600000005
+2020-10-04T12:30:00.0,2.1647257600000005
+2020-10-04T12:35:00.0,2.1647257600000005
+2020-10-04T12:40:00.0,2.1647257600000005
+2020-10-04T12:45:00.0,2.1647257600000005
+2020-10-04T12:50:00.0,2.1647257600000005
+2020-10-04T12:55:00.0,2.1647257600000005
+2020-10-04T13:00:00.0,2.312895899999999
+2020-10-04T13:05:00.0,2.312895899999999
+2020-10-04T13:10:00.0,2.312895899999999
+2020-10-04T13:15:00.0,2.312895899999999
+2020-10-04T13:20:00.0,2.312895899999999
+2020-10-04T13:25:00.0,2.312895899999999
+2020-10-04T13:30:00.0,2.312895899999999
+2020-10-04T13:35:00.0,2.312895899999999
+2020-10-04T13:40:00.0,2.312895899999999
+2020-10-04T13:45:00.0,2.312895899999999
+2020-10-04T13:50:00.0,2.312895899999999
+2020-10-04T13:55:00.0,2.312895899999999
+2020-10-04T14:00:00.0,2.6755735260000004
+2020-10-04T14:05:00.0,2.6755735260000004
+2020-10-04T14:10:00.0,2.6755735260000004
+2020-10-04T14:15:00.0,2.6755735260000004
+2020-10-04T14:20:00.0,2.6755735260000004
+2020-10-04T14:25:00.0,2.6755735260000004
+2020-10-04T14:30:00.0,2.6755735260000004
+2020-10-04T14:35:00.0,2.6755735260000004
+2020-10-04T14:40:00.0,2.6755735260000004
+2020-10-04T14:45:00.0,2.6755735260000004
+2020-10-04T14:50:00.0,2.6755735260000004
+2020-10-04T14:55:00.0,2.6755735260000004
+2020-10-04T15:00:00.0,2.6755735260000004
+2020-10-04T15:05:00.0,2.6755735260000004
+2020-10-04T15:10:00.0,2.6755735260000004
+2020-10-04T15:15:00.0,2.6755735260000004
+2020-10-04T15:20:00.0,2.6755735260000004
+2020-10-04T15:25:00.0,2.6755735260000004
+2020-10-04T15:30:00.0,2.6755735260000004
+2020-10-04T15:35:00.0,2.6755735260000004
+2020-10-04T15:40:00.0,2.6755735260000004
+2020-10-04T15:45:00.0,2.6755735260000004
+2020-10-04T15:50:00.0,2.6755735260000004
+2020-10-04T15:55:00.0,2.6755735260000004
+2020-10-04T16:00:00.0,3.0530225879999993
+2020-10-04T16:05:00.0,3.0530225879999993
+2020-10-04T16:10:00.0,3.0530225879999993
+2020-10-04T16:15:00.0,3.0530225879999993
+2020-10-04T16:20:00.0,3.0530225879999993
+2020-10-04T16:25:00.0,3.0530225879999993
+2020-10-04T16:30:00.0,3.0530225879999993
+2020-10-04T16:35:00.0,3.0530225879999993
+2020-10-04T16:40:00.0,3.0530225879999993
+2020-10-04T16:45:00.0,3.0530225879999993
+2020-10-04T16:50:00.0,3.0530225879999993
+2020-10-04T16:55:00.0,3.0530225879999993
+2020-10-04T17:00:00.0,3.152924141999998
+2020-10-04T17:05:00.0,3.152924141999998
+2020-10-04T17:10:00.0,3.152924141999998
+2020-10-04T17:15:00.0,3.152924141999998
+2020-10-04T17:20:00.0,3.152924141999998
+2020-10-04T17:25:00.0,3.152924141999998
+2020-10-04T17:30:00.0,3.152924141999998
+2020-10-04T17:35:00.0,3.152924141999998
+2020-10-04T17:40:00.0,3.152924141999998
+2020-10-04T17:45:00.0,3.152924141999998
+2020-10-04T17:50:00.0,3.152924141999998
+2020-10-04T17:55:00.0,3.152924141999998
+2020-10-04T18:00:00.0,3.152924141999998
+2020-10-04T18:05:00.0,3.152924141999998
+2020-10-04T18:10:00.0,3.152924141999998
+2020-10-04T18:15:00.0,3.152924141999998
+2020-10-04T18:20:00.0,3.152924141999998
+2020-10-04T18:25:00.0,3.152924141999998
+2020-10-04T18:30:00.0,3.152924141999998
+2020-10-04T18:35:00.0,3.152924141999998
+2020-10-04T18:40:00.0,3.152924141999998
+2020-10-04T18:45:00.0,3.152924141999998
+2020-10-04T18:50:00.0,3.152924141999998
+2020-10-04T18:55:00.0,3.152924141999998
+2020-10-04T19:00:00.0,3.152924141999998
+2020-10-04T19:05:00.0,3.152924141999998
+2020-10-04T19:10:00.0,3.152924141999998
+2020-10-04T19:15:00.0,3.152924141999998
+2020-10-04T19:20:00.0,3.152924141999998
+2020-10-04T19:25:00.0,3.152924141999998
+2020-10-04T19:30:00.0,3.152924141999998
+2020-10-04T19:35:00.0,3.152924141999998
+2020-10-04T19:40:00.0,3.152924141999998
+2020-10-04T19:45:00.0,3.152924141999998
+2020-10-04T19:50:00.0,3.152924141999998
+2020-10-04T19:55:00.0,3.152924141999998
+2020-10-04T20:00:00.0,2.7754750800000023
+2020-10-04T20:05:00.0,2.7754750800000023
+2020-10-04T20:10:00.0,2.7754750800000023
+2020-10-04T20:15:00.0,2.7754750800000023
+2020-10-04T20:20:00.0,2.7754750800000023
+2020-10-04T20:25:00.0,2.7754750800000023
+2020-10-04T20:30:00.0,2.7754750800000023
+2020-10-04T20:35:00.0,2.7754750800000023
+2020-10-04T20:40:00.0,2.7754750800000023
+2020-10-04T20:45:00.0,2.7754750800000023
+2020-10-04T20:50:00.0,2.7754750800000023
+2020-10-04T20:55:00.0,2.7754750800000023
+2020-10-04T21:00:00.0,2.675573526
+2020-10-04T21:05:00.0,2.675573526
+2020-10-04T21:10:00.0,2.675573526
+2020-10-04T21:15:00.0,2.675573526
+2020-10-04T21:20:00.0,2.675573526
+2020-10-04T21:25:00.0,2.675573526
+2020-10-04T21:30:00.0,2.675573526
+2020-10-04T21:35:00.0,2.675573526
+2020-10-04T21:40:00.0,2.675573526
+2020-10-04T21:45:00.0,2.675573526
+2020-10-04T21:50:00.0,2.675573526
+2020-10-04T21:55:00.0,2.675573526
+2020-10-04T22:00:00.0,2.632425384000001
+2020-10-04T22:05:00.0,2.632425384000001
+2020-10-04T22:10:00.0,2.632425384000001
+2020-10-04T22:15:00.0,2.632425384000001
+2020-10-04T22:20:00.0,2.632425384000001
+2020-10-04T22:25:00.0,2.632425384000001
+2020-10-04T22:30:00.0,2.632425384000001
+2020-10-04T22:35:00.0,2.632425384000001
+2020-10-04T22:40:00.0,2.632425384000001
+2020-10-04T22:45:00.0,2.632425384000001
+2020-10-04T22:50:00.0,2.632425384000001
+2020-10-04T22:55:00.0,2.632425384000001
+2020-10-04T23:00:00.0,2.3875403059999982
+2020-10-04T23:05:00.0,2.3875403059999982
+2020-10-04T23:10:00.0,2.3875403059999982
+2020-10-04T23:15:00.0,2.3875403059999982
+2020-10-04T23:20:00.0,2.3875403059999982
+2020-10-04T23:25:00.0,2.3875403059999982
+2020-10-04T23:30:00.0,2.3875403059999982
+2020-10-04T23:35:00.0,2.3875403059999982
+2020-10-04T23:40:00.0,2.3875403059999982
+2020-10-04T23:45:00.0,2.3875403059999982
+2020-10-04T23:50:00.0,2.3875403059999982
+2020-10-04T23:55:00.0,2.3875403059999982
+2020-10-05T00:00:00.0,2.3128958999999987
+2020-10-05T00:05:00.0,2.3128958999999987
+2020-10-05T00:10:00.0,2.3128958999999987
+2020-10-05T00:15:00.0,2.3128958999999987
+2020-10-05T00:20:00.0,2.3128958999999987
+2020-10-05T00:25:00.0,2.3128958999999987
+2020-10-05T00:30:00.0,2.3128958999999987
+2020-10-05T00:35:00.0,2.3128958999999987
+2020-10-05T00:40:00.0,2.3128958999999987
+2020-10-05T00:45:00.0,2.3128958999999987
+2020-10-05T00:50:00.0,2.3128958999999987
+2020-10-05T00:55:00.0,2.3128958999999987
+2020-10-05T01:00:00.0,2.296850135000001
+2020-10-05T01:05:00.0,2.296850135000001
+2020-10-05T01:10:00.0,2.296850135000001
+2020-10-05T01:15:00.0,2.296850135000001
+2020-10-05T01:20:00.0,2.296850135000001
+2020-10-05T01:25:00.0,2.296850135000001
+2020-10-05T01:30:00.0,2.296850135000001
+2020-10-05T01:35:00.0,2.296850135000001
+2020-10-05T01:40:00.0,2.296850135000001
+2020-10-05T01:45:00.0,2.296850135000001
+2020-10-05T01:50:00.0,2.296850135000001
+2020-10-05T01:55:00.0,2.296850135000001
+2020-10-05T02:00:00.0,2.273246256
+2020-10-05T02:05:00.0,2.273246256
+2020-10-05T02:10:00.0,2.273246256
+2020-10-05T02:15:00.0,2.273246256
+2020-10-05T02:20:00.0,2.273246256
+2020-10-05T02:25:00.0,2.273246256
+2020-10-05T02:30:00.0,2.273246256
+2020-10-05T02:35:00.0,2.273246256
+2020-10-05T02:40:00.0,2.273246256
+2020-10-05T02:45:00.0,2.273246256
+2020-10-05T02:50:00.0,2.273246256
+2020-10-05T02:55:00.0,2.273246256
+2020-10-05T03:00:00.0,2.3069972870000006
+2020-10-05T03:05:00.0,2.3069972870000006
+2020-10-05T03:10:00.0,2.3069972870000006
+2020-10-05T03:15:00.0,2.3069972870000006
+2020-10-05T03:20:00.0,2.3069972870000006
+2020-10-05T03:25:00.0,2.3069972870000006
+2020-10-05T03:30:00.0,2.3069972870000006
+2020-10-05T03:35:00.0,2.3069972870000006
+2020-10-05T03:40:00.0,2.3069972870000006
+2020-10-05T03:45:00.0,2.3069972870000006
+2020-10-05T03:50:00.0,2.3069972870000006
+2020-10-05T03:55:00.0,2.3069972870000006
+2020-10-05T04:00:00.0,2.3069972870000006
+2020-10-05T04:05:00.0,2.3069972870000006
+2020-10-05T04:10:00.0,2.3069972870000006
+2020-10-05T04:15:00.0,2.3069972870000006
+2020-10-05T04:20:00.0,2.3069972870000006
+2020-10-05T04:25:00.0,2.3069972870000006
+2020-10-05T04:30:00.0,2.3069972870000006
+2020-10-05T04:35:00.0,2.3069972870000006
+2020-10-05T04:40:00.0,2.3069972870000006
+2020-10-05T04:45:00.0,2.3069972870000006
+2020-10-05T04:50:00.0,2.3069972870000006
+2020-10-05T04:55:00.0,2.3069972870000006
+2020-10-05T05:00:00.0,2.3437807129999984
+2020-10-05T05:05:00.0,2.3437807129999984
+2020-10-05T05:10:00.0,2.3437807129999984
+2020-10-05T05:15:00.0,2.3437807129999984
+2020-10-05T05:20:00.0,2.3437807129999984
+2020-10-05T05:25:00.0,2.3437807129999984
+2020-10-05T05:30:00.0,2.3437807129999984
+2020-10-05T05:35:00.0,2.3437807129999984
+2020-10-05T05:40:00.0,2.3437807129999984
+2020-10-05T05:45:00.0,2.3437807129999984
+2020-10-05T05:50:00.0,2.3437807129999984
+2020-10-05T05:55:00.0,2.3437807129999984
+2020-10-05T06:00:00.0,1.8861018779999985
+2020-10-05T06:05:00.0,1.8861018779999985
+2020-10-05T06:10:00.0,1.8861018779999985
+2020-10-05T06:15:00.0,1.8861018779999985
+2020-10-05T06:20:00.0,1.8861018779999985
+2020-10-05T06:25:00.0,1.8861018779999985
+2020-10-05T06:30:00.0,1.8861018779999985
+2020-10-05T06:35:00.0,1.8861018779999985
+2020-10-05T06:40:00.0,1.8861018779999985
+2020-10-05T06:45:00.0,1.8861018779999985
+2020-10-05T06:50:00.0,1.8861018779999985
+2020-10-05T06:55:00.0,1.8861018779999985
+2020-10-05T07:00:00.0,1.5
+2020-10-05T07:05:00.0,1.5
+2020-10-05T07:10:00.0,1.5
+2020-10-05T07:15:00.0,1.5
+2020-10-05T07:20:00.0,1.5
+2020-10-05T07:25:00.0,1.5
+2020-10-05T07:30:00.0,1.5
+2020-10-05T07:35:00.0,1.5
+2020-10-05T07:40:00.0,1.5
+2020-10-05T07:45:00.0,1.5
+2020-10-05T07:50:00.0,1.5
+2020-10-05T07:55:00.0,1.5
+2020-10-05T08:00:00.0,1.5
+2020-10-05T08:05:00.0,1.5
+2020-10-05T08:10:00.0,1.5
+2020-10-05T08:15:00.0,1.5
+2020-10-05T08:20:00.0,1.5
+2020-10-05T08:25:00.0,1.5
+2020-10-05T08:30:00.0,1.5
+2020-10-05T08:35:00.0,1.5
+2020-10-05T08:40:00.0,1.5
+2020-10-05T08:45:00.0,1.5
+2020-10-05T08:50:00.0,1.5
+2020-10-05T08:55:00.0,1.5
+2020-10-05T09:00:00.0,1.8463588659999992
+2020-10-05T09:05:00.0,1.8463588659999992
+2020-10-05T09:10:00.0,1.8463588659999992
+2020-10-05T09:15:00.0,1.8463588659999992
+2020-10-05T09:20:00.0,1.8463588659999992
+2020-10-05T09:25:00.0,1.8463588659999992
+2020-10-05T09:30:00.0,1.8463588659999992
+2020-10-05T09:35:00.0,1.8463588659999992
+2020-10-05T09:40:00.0,1.8463588659999992
+2020-10-05T09:45:00.0,1.8463588659999992
+2020-10-05T09:50:00.0,1.8463588659999992
+2020-10-05T09:55:00.0,1.8463588659999992
+2020-10-05T10:00:00.0,1.9034365959999981
+2020-10-05T10:05:00.0,1.9034365959999981
+2020-10-05T10:10:00.0,1.9034365959999981
+2020-10-05T10:15:00.0,1.9034365959999981
+2020-10-05T10:20:00.0,1.9034365959999981
+2020-10-05T10:25:00.0,1.9034365959999981
+2020-10-05T10:30:00.0,1.9034365959999981
+2020-10-05T10:35:00.0,1.9034365959999981
+2020-10-05T10:40:00.0,1.9034365959999981
+2020-10-05T10:45:00.0,1.9034365959999981
+2020-10-05T10:50:00.0,1.9034365959999981
+2020-10-05T10:55:00.0,1.9034365959999981
+2020-10-05T11:00:00.0,2.084605538999999
+2020-10-05T11:05:00.0,2.084605538999999
+2020-10-05T11:10:00.0,2.084605538999999
+2020-10-05T11:15:00.0,2.084605538999999
+2020-10-05T11:20:00.0,2.084605538999999
+2020-10-05T11:25:00.0,2.084605538999999
+2020-10-05T11:30:00.0,2.084605538999999
+2020-10-05T11:35:00.0,2.084605538999999
+2020-10-05T11:40:00.0,2.084605538999999
+2020-10-05T11:45:00.0,2.084605538999999
+2020-10-05T11:50:00.0,2.084605538999999
+2020-10-05T11:55:00.0,2.084605538999999
+2020-10-05T12:00:00.0,2.2576973760000003
+2020-10-05T12:05:00.0,2.2576973760000003
+2020-10-05T12:10:00.0,2.2576973760000003
+2020-10-05T12:15:00.0,2.2576973760000003
+2020-10-05T12:20:00.0,2.2576973760000003
+2020-10-05T12:25:00.0,2.2576973760000003
+2020-10-05T12:30:00.0,2.2576973760000003
+2020-10-05T12:35:00.0,2.2576973760000003
+2020-10-05T12:40:00.0,2.2576973760000003
+2020-10-05T12:45:00.0,2.2576973760000003
+2020-10-05T12:50:00.0,2.2576973760000003
+2020-10-05T12:55:00.0,2.2576973760000003
+2020-10-05T13:00:00.0,2.312895899999999
+2020-10-05T13:05:00.0,2.312895899999999
+2020-10-05T13:10:00.0,2.312895899999999
+2020-10-05T13:15:00.0,2.312895899999999
+2020-10-05T13:20:00.0,2.312895899999999
+2020-10-05T13:25:00.0,2.312895899999999
+2020-10-05T13:30:00.0,2.312895899999999
+2020-10-05T13:35:00.0,2.312895899999999
+2020-10-05T13:40:00.0,2.312895899999999
+2020-10-05T13:45:00.0,2.312895899999999
+2020-10-05T13:50:00.0,2.312895899999999
+2020-10-05T13:55:00.0,2.312895899999999
+2020-10-05T14:00:00.0,2.461741354999999
+2020-10-05T14:05:00.0,2.461741354999999
+2020-10-05T14:10:00.0,2.461741354999999
+2020-10-05T14:15:00.0,2.461741354999999
+2020-10-05T14:20:00.0,2.461741354999999
+2020-10-05T14:25:00.0,2.461741354999999
+2020-10-05T14:30:00.0,2.461741354999999
+2020-10-05T14:35:00.0,2.461741354999999
+2020-10-05T14:40:00.0,2.461741354999999
+2020-10-05T14:45:00.0,2.461741354999999
+2020-10-05T14:50:00.0,2.461741354999999
+2020-10-05T14:55:00.0,2.461741354999999
+2020-10-05T15:00:00.0,3.0530225879999993
+2020-10-05T15:05:00.0,3.0530225879999993
+2020-10-05T15:10:00.0,3.0530225879999993
+2020-10-05T15:15:00.0,3.0530225879999993
+2020-10-05T15:20:00.0,3.0530225879999993
+2020-10-05T15:25:00.0,3.0530225879999993
+2020-10-05T15:30:00.0,3.0530225879999993
+2020-10-05T15:35:00.0,3.0530225879999993
+2020-10-05T15:40:00.0,3.0530225879999993
+2020-10-05T15:45:00.0,3.0530225879999993
+2020-10-05T15:50:00.0,3.0530225879999993
+2020-10-05T15:55:00.0,3.0530225879999993
+2020-10-05T16:00:00.0,3.152924141999998
+2020-10-05T16:05:00.0,3.152924141999998
+2020-10-05T16:10:00.0,3.152924141999998
+2020-10-05T16:15:00.0,3.152924141999998
+2020-10-05T16:20:00.0,3.152924141999998
+2020-10-05T16:25:00.0,3.152924141999998
+2020-10-05T16:30:00.0,3.152924141999998
+2020-10-05T16:35:00.0,3.152924141999998
+2020-10-05T16:40:00.0,3.152924141999998
+2020-10-05T16:45:00.0,3.152924141999998
+2020-10-05T16:50:00.0,3.152924141999998
+2020-10-05T16:55:00.0,3.152924141999998
+2020-10-05T17:00:00.0,3.1727489639999993
+2020-10-05T17:05:00.0,3.1727489639999993
+2020-10-05T17:10:00.0,3.1727489639999993
+2020-10-05T17:15:00.0,3.1727489639999993
+2020-10-05T17:20:00.0,3.1727489639999993
+2020-10-05T17:25:00.0,3.1727489639999993
+2020-10-05T17:30:00.0,3.1727489639999993
+2020-10-05T17:35:00.0,3.1727489639999993
+2020-10-05T17:40:00.0,3.1727489639999993
+2020-10-05T17:45:00.0,3.1727489639999993
+2020-10-05T17:50:00.0,3.1727489639999993
+2020-10-05T17:55:00.0,3.1727489639999993
+2020-10-05T18:00:00.0,3.1727489639999993
+2020-10-05T18:05:00.0,3.1727489639999993
+2020-10-05T18:10:00.0,3.1727489639999993
+2020-10-05T18:15:00.0,3.1727489639999993
+2020-10-05T18:20:00.0,3.1727489639999993
+2020-10-05T18:25:00.0,3.1727489639999993
+2020-10-05T18:30:00.0,3.1727489639999993
+2020-10-05T18:35:00.0,3.1727489639999993
+2020-10-05T18:40:00.0,3.1727489639999993
+2020-10-05T18:45:00.0,3.1727489639999993
+2020-10-05T18:50:00.0,3.1727489639999993
+2020-10-05T18:55:00.0,3.1727489639999993
+2020-10-05T19:00:00.0,3.1727489639999993
+2020-10-05T19:05:00.0,3.1727489639999993
+2020-10-05T19:10:00.0,3.1727489639999993
+2020-10-05T19:15:00.0,3.1727489639999993
+2020-10-05T19:20:00.0,3.1727489639999993
+2020-10-05T19:25:00.0,3.1727489639999993
+2020-10-05T19:30:00.0,3.1727489639999993
+2020-10-05T19:35:00.0,3.1727489639999993
+2020-10-05T19:40:00.0,3.1727489639999993
+2020-10-05T19:45:00.0,3.1727489639999993
+2020-10-05T19:50:00.0,3.1727489639999993
+2020-10-05T19:55:00.0,3.1727489639999993
+2020-10-05T20:00:00.0,3.0277556580000007
+2020-10-05T20:05:00.0,3.0277556580000007
+2020-10-05T20:10:00.0,3.0277556580000007
+2020-10-05T20:15:00.0,3.0277556580000007
+2020-10-05T20:20:00.0,3.0277556580000007
+2020-10-05T20:25:00.0,3.0277556580000007
+2020-10-05T20:30:00.0,3.0277556580000007
+2020-10-05T20:35:00.0,3.0277556580000007
+2020-10-05T20:40:00.0,3.0277556580000007
+2020-10-05T20:45:00.0,3.0277556580000007
+2020-10-05T20:50:00.0,3.0277556580000007
+2020-10-05T20:55:00.0,3.0277556580000007
+2020-10-05T21:00:00.0,2.727469898
+2020-10-05T21:05:00.0,2.727469898
+2020-10-05T21:10:00.0,2.727469898
+2020-10-05T21:15:00.0,2.727469898
+2020-10-05T21:20:00.0,2.727469898
+2020-10-05T21:25:00.0,2.727469898
+2020-10-05T21:30:00.0,2.727469898
+2020-10-05T21:35:00.0,2.727469898
+2020-10-05T21:40:00.0,2.727469898
+2020-10-05T21:45:00.0,2.727469898
+2020-10-05T21:50:00.0,2.727469898
+2020-10-05T21:55:00.0,2.727469898
+2020-10-05T22:00:00.0,2.6429208780000004
+2020-10-05T22:05:00.0,2.6429208780000004
+2020-10-05T22:10:00.0,2.6429208780000004
+2020-10-05T22:15:00.0,2.6429208780000004
+2020-10-05T22:20:00.0,2.6429208780000004
+2020-10-05T22:25:00.0,2.6429208780000004
+2020-10-05T22:30:00.0,2.6429208780000004
+2020-10-05T22:35:00.0,2.6429208780000004
+2020-10-05T22:40:00.0,2.6429208780000004
+2020-10-05T22:45:00.0,2.6429208780000004
+2020-10-05T22:50:00.0,2.6429208780000004
+2020-10-05T22:55:00.0,2.6429208780000004
+2020-10-05T23:00:00.0,2.365766208999998
+2020-10-05T23:05:00.0,2.365766208999998
+2020-10-05T23:10:00.0,2.365766208999998
+2020-10-05T23:15:00.0,2.365766208999998
+2020-10-05T23:20:00.0,2.365766208999998
+2020-10-05T23:25:00.0,2.365766208999998
+2020-10-05T23:30:00.0,2.365766208999998
+2020-10-05T23:35:00.0,2.365766208999998
+2020-10-05T23:40:00.0,2.365766208999998
+2020-10-05T23:45:00.0,2.365766208999998
+2020-10-05T23:50:00.0,2.365766208999998
+2020-10-05T23:55:00.0,2.365766208999998
diff --git a/test/inputs/chuhsi_RegUp_prices_5min_300.csv b/test/inputs/chuhsi_RegUp_prices_5min_300.csv
new file mode 100644
index 00000000..571a308d
--- /dev/null
+++ b/test/inputs/chuhsi_RegUp_prices_5min_300.csv
@@ -0,0 +1,301 @@
+DateTime,Chuhsi
+2020-10-03T00:00:00.0,3.0277556580000007
+2020-10-03T00:05:00.0,3.0277556580000007
+2020-10-03T00:10:00.0,3.0277556580000007
+2020-10-03T00:15:00.0,3.0277556580000007
+2020-10-03T00:20:00.0,3.0277556580000007
+2020-10-03T00:25:00.0,3.0277556580000007
+2020-10-03T00:30:00.0,3.0277556580000007
+2020-10-03T00:35:00.0,3.0277556580000007
+2020-10-03T00:40:00.0,3.0277556580000007
+2020-10-03T00:45:00.0,3.0277556580000007
+2020-10-03T00:50:00.0,3.0277556580000007
+2020-10-03T00:55:00.0,3.0277556580000007
+2020-10-03T01:00:00.0,2.7754750800000023
+2020-10-03T01:05:00.0,2.7754750800000023
+2020-10-03T01:10:00.0,2.7754750800000023
+2020-10-03T01:15:00.0,2.7754750800000023
+2020-10-03T01:20:00.0,2.7754750800000023
+2020-10-03T01:25:00.0,2.7754750800000023
+2020-10-03T01:30:00.0,2.7754750800000023
+2020-10-03T01:35:00.0,2.7754750800000023
+2020-10-03T01:40:00.0,2.7754750800000023
+2020-10-03T01:45:00.0,2.7754750800000023
+2020-10-03T01:50:00.0,2.7754750800000023
+2020-10-03T01:55:00.0,2.7754750800000023
+2020-10-03T02:00:00.0,2.7754750800000023
+2020-10-03T02:05:00.0,2.7754750800000023
+2020-10-03T02:10:00.0,2.7754750800000023
+2020-10-03T02:15:00.0,2.7754750800000023
+2020-10-03T02:20:00.0,2.7754750800000023
+2020-10-03T02:25:00.0,2.7754750800000023
+2020-10-03T02:30:00.0,2.7754750800000023
+2020-10-03T02:35:00.0,2.7754750800000023
+2020-10-03T02:40:00.0,2.7754750800000023
+2020-10-03T02:45:00.0,2.7754750800000023
+2020-10-03T02:50:00.0,2.7754750800000023
+2020-10-03T02:55:00.0,2.7754750800000023
+2020-10-03T03:00:00.0,2.679072024000001
+2020-10-03T03:05:00.0,2.679072024000001
+2020-10-03T03:10:00.0,2.679072024000001
+2020-10-03T03:15:00.0,2.679072024000001
+2020-10-03T03:20:00.0,2.679072024000001
+2020-10-03T03:25:00.0,2.679072024000001
+2020-10-03T03:30:00.0,2.679072024000001
+2020-10-03T03:35:00.0,2.679072024000001
+2020-10-03T03:40:00.0,2.679072024000001
+2020-10-03T03:45:00.0,2.679072024000001
+2020-10-03T03:50:00.0,2.679072024000001
+2020-10-03T03:55:00.0,2.679072024000001
+2020-10-03T04:00:00.0,2.679072024000001
+2020-10-03T04:05:00.0,2.679072024000001
+2020-10-03T04:10:00.0,2.679072024000001
+2020-10-03T04:15:00.0,2.679072024000001
+2020-10-03T04:20:00.0,2.679072024000001
+2020-10-03T04:25:00.0,2.679072024000001
+2020-10-03T04:30:00.0,2.679072024000001
+2020-10-03T04:35:00.0,2.679072024000001
+2020-10-03T04:40:00.0,2.679072024000001
+2020-10-03T04:45:00.0,2.679072024000001
+2020-10-03T04:50:00.0,2.679072024000001
+2020-10-03T04:55:00.0,2.679072024000001
+2020-10-03T05:00:00.0,2.6755735260000004
+2020-10-03T05:05:00.0,2.6755735260000004
+2020-10-03T05:10:00.0,2.6755735260000004
+2020-10-03T05:15:00.0,2.6755735260000004
+2020-10-03T05:20:00.0,2.6755735260000004
+2020-10-03T05:25:00.0,2.6755735260000004
+2020-10-03T05:30:00.0,2.6755735260000004
+2020-10-03T05:35:00.0,2.6755735260000004
+2020-10-03T05:40:00.0,2.6755735260000004
+2020-10-03T05:45:00.0,2.6755735260000004
+2020-10-03T05:50:00.0,2.6755735260000004
+2020-10-03T05:55:00.0,2.6755735260000004
+2020-10-03T06:00:00.0,1.5
+2020-10-03T06:05:00.0,1.5
+2020-10-03T06:10:00.0,1.5
+2020-10-03T06:15:00.0,1.5
+2020-10-03T06:20:00.0,1.5
+2020-10-03T06:25:00.0,1.5
+2020-10-03T06:30:00.0,1.5
+2020-10-03T06:35:00.0,1.5
+2020-10-03T06:40:00.0,1.5
+2020-10-03T06:45:00.0,1.5
+2020-10-03T06:50:00.0,1.5
+2020-10-03T06:55:00.0,1.5
+2020-10-03T07:00:00.0,1.5
+2020-10-03T07:05:00.0,1.5
+2020-10-03T07:10:00.0,1.5
+2020-10-03T07:15:00.0,1.5
+2020-10-03T07:20:00.0,1.5
+2020-10-03T07:25:00.0,1.5
+2020-10-03T07:30:00.0,1.5
+2020-10-03T07:35:00.0,1.5
+2020-10-03T07:40:00.0,1.5
+2020-10-03T07:45:00.0,1.5
+2020-10-03T07:50:00.0,1.5
+2020-10-03T07:55:00.0,1.5
+2020-10-03T08:00:00.0,1.5
+2020-10-03T08:05:00.0,1.5
+2020-10-03T08:10:00.0,1.5
+2020-10-03T08:15:00.0,1.5
+2020-10-03T08:20:00.0,1.5
+2020-10-03T08:25:00.0,1.5
+2020-10-03T08:30:00.0,1.5
+2020-10-03T08:35:00.0,1.5
+2020-10-03T08:40:00.0,1.5
+2020-10-03T08:45:00.0,1.5
+2020-10-03T08:50:00.0,1.5
+2020-10-03T08:55:00.0,1.5
+2020-10-03T09:00:00.0,1.5
+2020-10-03T09:05:00.0,1.5
+2020-10-03T09:10:00.0,1.5
+2020-10-03T09:15:00.0,1.5
+2020-10-03T09:20:00.0,1.5
+2020-10-03T09:25:00.0,1.5
+2020-10-03T09:30:00.0,1.5
+2020-10-03T09:35:00.0,1.5
+2020-10-03T09:40:00.0,1.5
+2020-10-03T09:45:00.0,1.5
+2020-10-03T09:50:00.0,1.5
+2020-10-03T09:55:00.0,1.5
+2020-10-03T10:00:00.0,1.8861018779999985
+2020-10-03T10:05:00.0,1.8861018779999985
+2020-10-03T10:10:00.0,1.8861018779999985
+2020-10-03T10:15:00.0,1.8861018779999985
+2020-10-03T10:20:00.0,1.8861018779999985
+2020-10-03T10:25:00.0,1.8861018779999985
+2020-10-03T10:30:00.0,1.8861018779999985
+2020-10-03T10:35:00.0,1.8861018779999985
+2020-10-03T10:40:00.0,1.8861018779999985
+2020-10-03T10:45:00.0,1.8861018779999985
+2020-10-03T10:50:00.0,1.8861018779999985
+2020-10-03T10:55:00.0,1.8861018779999985
+2020-10-03T11:00:00.0,1.9983547469999992
+2020-10-03T11:05:00.0,1.9983547469999992
+2020-10-03T11:10:00.0,1.9983547469999992
+2020-10-03T11:15:00.0,1.9983547469999992
+2020-10-03T11:20:00.0,1.9983547469999992
+2020-10-03T11:25:00.0,1.9983547469999992
+2020-10-03T11:30:00.0,1.9983547469999992
+2020-10-03T11:35:00.0,1.9983547469999992
+2020-10-03T11:40:00.0,1.9983547469999992
+2020-10-03T11:45:00.0,1.9983547469999992
+2020-10-03T11:50:00.0,1.9983547469999992
+2020-10-03T11:55:00.0,1.9983547469999992
+2020-10-03T12:00:00.0,2.041902940999999
+2020-10-03T12:05:00.0,2.041902940999999
+2020-10-03T12:10:00.0,2.041902940999999
+2020-10-03T12:15:00.0,2.041902940999999
+2020-10-03T12:20:00.0,2.041902940999999
+2020-10-03T12:25:00.0,2.041902940999999
+2020-10-03T12:30:00.0,2.041902940999999
+2020-10-03T12:35:00.0,2.041902940999999
+2020-10-03T12:40:00.0,2.041902940999999
+2020-10-03T12:45:00.0,2.041902940999999
+2020-10-03T12:50:00.0,2.041902940999999
+2020-10-03T12:55:00.0,2.041902940999999
+2020-10-03T13:00:00.0,2.1647257600000005
+2020-10-03T13:05:00.0,2.1647257600000005
+2020-10-03T13:10:00.0,2.1647257600000005
+2020-10-03T13:15:00.0,2.1647257600000005
+2020-10-03T13:20:00.0,2.1647257600000005
+2020-10-03T13:25:00.0,2.1647257600000005
+2020-10-03T13:30:00.0,2.1647257600000005
+2020-10-03T13:35:00.0,2.1647257600000005
+2020-10-03T13:40:00.0,2.1647257600000005
+2020-10-03T13:45:00.0,2.1647257600000005
+2020-10-03T13:50:00.0,2.1647257600000005
+2020-10-03T13:55:00.0,2.1647257600000005
+2020-10-03T14:00:00.0,2.273246256000001
+2020-10-03T14:05:00.0,2.273246256000001
+2020-10-03T14:10:00.0,2.273246256000001
+2020-10-03T14:15:00.0,2.273246256000001
+2020-10-03T14:20:00.0,2.273246256000001
+2020-10-03T14:25:00.0,2.273246256000001
+2020-10-03T14:30:00.0,2.273246256000001
+2020-10-03T14:35:00.0,2.273246256000001
+2020-10-03T14:40:00.0,2.273246256000001
+2020-10-03T14:45:00.0,2.273246256000001
+2020-10-03T14:50:00.0,2.273246256000001
+2020-10-03T14:55:00.0,2.273246256000001
+2020-10-03T15:00:00.0,2.3069972870000006
+2020-10-03T15:05:00.0,2.3069972870000006
+2020-10-03T15:10:00.0,2.3069972870000006
+2020-10-03T15:15:00.0,2.3069972870000006
+2020-10-03T15:20:00.0,2.3069972870000006
+2020-10-03T15:25:00.0,2.3069972870000006
+2020-10-03T15:30:00.0,2.3069972870000006
+2020-10-03T15:35:00.0,2.3069972870000006
+2020-10-03T15:40:00.0,2.3069972870000006
+2020-10-03T15:45:00.0,2.3069972870000006
+2020-10-03T15:50:00.0,2.3069972870000006
+2020-10-03T15:55:00.0,2.3069972870000006
+2020-10-03T16:00:00.0,2.6845141320000003
+2020-10-03T16:05:00.0,2.6845141320000003
+2020-10-03T16:10:00.0,2.6845141320000003
+2020-10-03T16:15:00.0,2.6845141320000003
+2020-10-03T16:20:00.0,2.6845141320000003
+2020-10-03T16:25:00.0,2.6845141320000003
+2020-10-03T16:30:00.0,2.6845141320000003
+2020-10-03T16:35:00.0,2.6845141320000003
+2020-10-03T16:40:00.0,2.6845141320000003
+2020-10-03T16:45:00.0,2.6845141320000003
+2020-10-03T16:50:00.0,2.6845141320000003
+2020-10-03T16:55:00.0,2.6845141320000003
+2020-10-03T17:00:00.0,2.7754750800000023
+2020-10-03T17:05:00.0,2.7754750800000023
+2020-10-03T17:10:00.0,2.7754750800000023
+2020-10-03T17:15:00.0,2.7754750800000023
+2020-10-03T17:20:00.0,2.7754750800000023
+2020-10-03T17:25:00.0,2.7754750800000023
+2020-10-03T17:30:00.0,2.7754750800000023
+2020-10-03T17:35:00.0,2.7754750800000023
+2020-10-03T17:40:00.0,2.7754750800000023
+2020-10-03T17:45:00.0,2.7754750800000023
+2020-10-03T17:50:00.0,2.7754750800000023
+2020-10-03T17:55:00.0,2.7754750800000023
+2020-10-03T18:00:00.0,3.246217421999998
+2020-10-03T18:05:00.0,3.246217421999998
+2020-10-03T18:10:00.0,3.246217421999998
+2020-10-03T18:15:00.0,3.246217421999998
+2020-10-03T18:20:00.0,3.246217421999998
+2020-10-03T18:25:00.0,3.246217421999998
+2020-10-03T18:30:00.0,3.246217421999998
+2020-10-03T18:35:00.0,3.246217421999998
+2020-10-03T18:40:00.0,3.246217421999998
+2020-10-03T18:45:00.0,3.246217421999998
+2020-10-03T18:50:00.0,3.246217421999998
+2020-10-03T18:55:00.0,3.246217421999998
+2020-10-03T19:00:00.0,2.7754750800000023
+2020-10-03T19:05:00.0,2.7754750800000023
+2020-10-03T19:10:00.0,2.7754750800000023
+2020-10-03T19:15:00.0,2.7754750800000023
+2020-10-03T19:20:00.0,2.7754750800000023
+2020-10-03T19:25:00.0,2.7754750800000023
+2020-10-03T19:30:00.0,2.7754750800000023
+2020-10-03T19:35:00.0,2.7754750800000023
+2020-10-03T19:40:00.0,2.7754750800000023
+2020-10-03T19:45:00.0,2.7754750800000023
+2020-10-03T19:50:00.0,2.7754750800000023
+2020-10-03T19:55:00.0,2.7754750800000023
+2020-10-03T20:00:00.0,2.675573526
+2020-10-03T20:05:00.0,2.675573526
+2020-10-03T20:10:00.0,2.675573526
+2020-10-03T20:15:00.0,2.675573526
+2020-10-03T20:20:00.0,2.675573526
+2020-10-03T20:25:00.0,2.675573526
+2020-10-03T20:30:00.0,2.675573526
+2020-10-03T20:35:00.0,2.675573526
+2020-10-03T20:40:00.0,2.675573526
+2020-10-03T20:45:00.0,2.675573526
+2020-10-03T20:50:00.0,2.675573526
+2020-10-03T20:55:00.0,2.675573526
+2020-10-03T21:00:00.0,2.312895899999999
+2020-10-03T21:05:00.0,2.312895899999999
+2020-10-03T21:10:00.0,2.312895899999999
+2020-10-03T21:15:00.0,2.312895899999999
+2020-10-03T21:20:00.0,2.312895899999999
+2020-10-03T21:25:00.0,2.312895899999999
+2020-10-03T21:30:00.0,2.312895899999999
+2020-10-03T21:35:00.0,2.312895899999999
+2020-10-03T21:40:00.0,2.312895899999999
+2020-10-03T21:45:00.0,2.312895899999999
+2020-10-03T21:50:00.0,2.312895899999999
+2020-10-03T21:55:00.0,2.312895899999999
+2020-10-03T22:00:00.0,2.111664610999999
+2020-10-03T22:05:00.0,2.111664610999999
+2020-10-03T22:10:00.0,2.111664610999999
+2020-10-03T22:15:00.0,2.111664610999999
+2020-10-03T22:20:00.0,2.111664610999999
+2020-10-03T22:25:00.0,2.111664610999999
+2020-10-03T22:30:00.0,2.111664610999999
+2020-10-03T22:35:00.0,2.111664610999999
+2020-10-03T22:40:00.0,2.111664610999999
+2020-10-03T22:45:00.0,2.111664610999999
+2020-10-03T22:50:00.0,2.111664610999999
+2020-10-03T22:55:00.0,2.111664610999999
+2020-10-03T23:00:00.0,2.040000349999999
+2020-10-03T23:05:00.0,2.040000349999999
+2020-10-03T23:10:00.0,2.040000349999999
+2020-10-03T23:15:00.0,2.040000349999999
+2020-10-03T23:20:00.0,2.040000349999999
+2020-10-03T23:25:00.0,2.040000349999999
+2020-10-03T23:30:00.0,2.040000349999999
+2020-10-03T23:35:00.0,2.040000349999999
+2020-10-03T23:40:00.0,2.040000349999999
+2020-10-03T23:45:00.0,2.040000349999999
+2020-10-03T23:50:00.0,2.040000349999999
+2020-10-03T23:55:00.0,2.040000349999999
+2020-10-04T00:00:00.0,1.9983547469999992
+2020-10-04T00:05:00.0,1.9983547469999992
+2020-10-04T00:10:00.0,1.9983547469999992
+2020-10-04T00:15:00.0,1.9983547469999992
+2020-10-04T00:20:00.0,1.9983547469999992
+2020-10-04T00:25:00.0,1.9983547469999992
+2020-10-04T00:30:00.0,1.9983547469999992
+2020-10-04T00:35:00.0,1.9983547469999992
+2020-10-04T00:40:00.0,1.9983547469999992
+2020-10-04T00:45:00.0,1.9983547469999992
+2020-10-04T00:50:00.0,1.9983547469999992
+2020-10-04T00:55:00.0,1.9983547469999992
diff --git a/test/inputs/chuhsi_Spin_prices.csv b/test/inputs/chuhsi_Spin_prices.csv
index 1a59dc29..9e067439 100644
--- a/test/inputs/chuhsi_Spin_prices.csv
+++ b/test/inputs/chuhsi_Spin_prices.csv
@@ -1,73 +1,73 @@
-DateTime,Chuhsi
-2020-10-03T00:00:00.0,1.2111022632000001
-2020-10-03T01:00:00.0,1.110190032000001
-2020-10-03T02:00:00.0,1.1101900320000009
-2020-10-03T03:00:00.0,1.0716288096000006
-2020-10-03T04:00:00.0,1.0716288096000006
-2020-10-03T05:00:00.0,1.0702294104000003
-2020-10-03T06:00:00.0,0.6
-2020-10-03T07:00:00.0,0.6
-2020-10-03T08:00:00.0,0.6
-2020-10-03T09:00:00.0,0.6
-2020-10-03T10:00:00.0,0.7544407511999993
-2020-10-03T11:00:00.0,0.7993418987999996
-2020-10-03T12:00:00.0,0.8167611763999997
-2020-10-03T13:00:00.0,0.8658903040000002
-2020-10-03T14:00:00.0,0.9092985024000003
-2020-10-03T15:00:00.0,0.9227989148000002
-2020-10-03T16:00:00.0,1.0738056528000002
-2020-10-03T17:00:00.0,1.110190032000001
-2020-10-03T18:00:00.0,1.2984869687999991
-2020-10-03T19:00:00.0,1.110190032000001
-2020-10-03T20:00:00.0,1.0702294104
-2020-10-03T21:00:00.0,0.9251583599999996
-2020-10-03T22:00:00.0,0.8446658443999995
-2020-10-03T23:00:00.0,0.8160001399999995
-2020-10-04T00:00:00.0,0.7993418987999996
-2020-10-04T01:00:00.0,0.7993418987999996
-2020-10-04T02:00:00.0,0.7613746383999992
-2020-10-04T03:00:00.0,0.7875881144000004
-2020-10-04T04:00:00.0,0.7993418987999996
-2020-10-04T05:00:00.0,0.7544407511999993
-2020-10-04T06:00:00.0,0.6
-2020-10-04T07:00:00.0,0.6
-2020-10-04T08:00:00.0,0.6
-2020-10-04T09:00:00.0,0.7771872835999997
-2020-10-04T10:00:00.0,0.7993418987999996
-2020-10-04T11:00:00.0,0.7993418987999996
-2020-10-04T12:00:00.0,0.8658903040000002
-2020-10-04T13:00:00.0,0.9251583599999996
-2020-10-04T14:00:00.0,1.0702294104000003
-2020-10-04T15:00:00.0,1.0702294104000003
-2020-10-04T16:00:00.0,1.2212090351999998
-2020-10-04T17:00:00.0,1.2611696567999993
-2020-10-04T18:00:00.0,1.2611696567999993
-2020-10-04T19:00:00.0,1.2611696567999993
-2020-10-04T20:00:00.0,1.1101900320000009
-2020-10-04T21:00:00.0,1.0702294104
-2020-10-04T22:00:00.0,1.0529701536000005
-2020-10-04T23:00:00.0,0.9550161223999993
-2020-10-05T00:00:00.0,0.9251583599999995
-2020-10-05T01:00:00.0,0.9187400540000005
-2020-10-05T02:00:00.0,0.9092985024000001
-2020-10-05T03:00:00.0,0.9227989148000002
-2020-10-05T04:00:00.0,0.9227989148000002
-2020-10-05T05:00:00.0,0.9375122851999993
-2020-10-05T06:00:00.0,0.7544407511999993
-2020-10-05T07:00:00.0,0.6
-2020-10-05T08:00:00.0,0.6
-2020-10-05T09:00:00.0,0.7385435463999996
-2020-10-05T10:00:00.0,0.7613746383999992
-2020-10-05T11:00:00.0,0.8338422155999997
-2020-10-05T12:00:00.0,0.9030789504000001
-2020-10-05T13:00:00.0,0.9251583599999996
-2020-10-05T14:00:00.0,0.9846965419999997
-2020-10-05T15:00:00.0,1.2212090351999998
-2020-10-05T16:00:00.0,1.2611696567999993
-2020-10-05T17:00:00.0,1.2690995855999996
-2020-10-05T18:00:00.0,1.2690995855999996
-2020-10-05T19:00:00.0,1.2690995855999996
-2020-10-05T20:00:00.0,1.2111022632000001
-2020-10-05T21:00:00.0,1.0909879591999998
-2020-10-05T22:00:00.0,1.0571683512
-2020-10-05T23:00:00.0,0.9463064835999992
+DateTime,Chuhsi
+2020-10-03T00:00:00.0,1.2111022632000001
+2020-10-03T01:00:00.0,1.110190032000001
+2020-10-03T02:00:00.0,1.1101900320000009
+2020-10-03T03:00:00.0,1.0716288096000006
+2020-10-03T04:00:00.0,1.0716288096000006
+2020-10-03T05:00:00.0,1.0702294104000003
+2020-10-03T06:00:00.0,0.6
+2020-10-03T07:00:00.0,0.6
+2020-10-03T08:00:00.0,0.6
+2020-10-03T09:00:00.0,0.6
+2020-10-03T10:00:00.0,0.7544407511999993
+2020-10-03T11:00:00.0,0.7993418987999996
+2020-10-03T12:00:00.0,0.8167611763999997
+2020-10-03T13:00:00.0,0.8658903040000002
+2020-10-03T14:00:00.0,0.9092985024000003
+2020-10-03T15:00:00.0,0.9227989148000002
+2020-10-03T16:00:00.0,1.0738056528000002
+2020-10-03T17:00:00.0,1.110190032000001
+2020-10-03T18:00:00.0,1.2984869687999991
+2020-10-03T19:00:00.0,1.110190032000001
+2020-10-03T20:00:00.0,1.0702294104
+2020-10-03T21:00:00.0,0.9251583599999996
+2020-10-03T22:00:00.0,0.8446658443999995
+2020-10-03T23:00:00.0,0.8160001399999995
+2020-10-04T00:00:00.0,0.7993418987999996
+2020-10-04T01:00:00.0,0.7993418987999996
+2020-10-04T02:00:00.0,0.7613746383999992
+2020-10-04T03:00:00.0,0.7875881144000004
+2020-10-04T04:00:00.0,0.7993418987999996
+2020-10-04T05:00:00.0,0.7544407511999993
+2020-10-04T06:00:00.0,0.6
+2020-10-04T07:00:00.0,0.6
+2020-10-04T08:00:00.0,0.6
+2020-10-04T09:00:00.0,0.7771872835999997
+2020-10-04T10:00:00.0,0.7993418987999996
+2020-10-04T11:00:00.0,0.7993418987999996
+2020-10-04T12:00:00.0,0.8658903040000002
+2020-10-04T13:00:00.0,0.9251583599999996
+2020-10-04T14:00:00.0,1.0702294104000003
+2020-10-04T15:00:00.0,1.0702294104000003
+2020-10-04T16:00:00.0,1.2212090351999998
+2020-10-04T17:00:00.0,1.2611696567999993
+2020-10-04T18:00:00.0,1.2611696567999993
+2020-10-04T19:00:00.0,1.2611696567999993
+2020-10-04T20:00:00.0,1.1101900320000009
+2020-10-04T21:00:00.0,1.0702294104
+2020-10-04T22:00:00.0,1.0529701536000005
+2020-10-04T23:00:00.0,0.9550161223999993
+2020-10-05T00:00:00.0,0.9251583599999995
+2020-10-05T01:00:00.0,0.9187400540000005
+2020-10-05T02:00:00.0,0.9092985024000001
+2020-10-05T03:00:00.0,0.9227989148000002
+2020-10-05T04:00:00.0,0.9227989148000002
+2020-10-05T05:00:00.0,0.9375122851999993
+2020-10-05T06:00:00.0,0.7544407511999993
+2020-10-05T07:00:00.0,0.6
+2020-10-05T08:00:00.0,0.6
+2020-10-05T09:00:00.0,0.7385435463999996
+2020-10-05T10:00:00.0,0.7613746383999992
+2020-10-05T11:00:00.0,0.8338422155999997
+2020-10-05T12:00:00.0,0.9030789504000001
+2020-10-05T13:00:00.0,0.9251583599999996
+2020-10-05T14:00:00.0,0.9846965419999997
+2020-10-05T15:00:00.0,1.2212090351999998
+2020-10-05T16:00:00.0,1.2611696567999993
+2020-10-05T17:00:00.0,1.2690995855999996
+2020-10-05T18:00:00.0,1.2690995855999996
+2020-10-05T19:00:00.0,1.2690995855999996
+2020-10-05T20:00:00.0,1.2111022632000001
+2020-10-05T21:00:00.0,1.0909879591999998
+2020-10-05T22:00:00.0,1.0571683512
+2020-10-05T23:00:00.0,0.9463064835999992
diff --git a/test/inputs/chuhsi_Spin_prices_24.csv b/test/inputs/chuhsi_Spin_prices_24.csv
new file mode 100644
index 00000000..5bff1bc8
--- /dev/null
+++ b/test/inputs/chuhsi_Spin_prices_24.csv
@@ -0,0 +1,25 @@
+DateTime,Chuhsi
+2020-10-03T00:00:00.0,1.2111022632000001
+2020-10-03T01:00:00.0,1.110190032000001
+2020-10-03T02:00:00.0,1.1101900320000009
+2020-10-03T03:00:00.0,1.0716288096000006
+2020-10-03T04:00:00.0,1.0716288096000006
+2020-10-03T05:00:00.0,1.0702294104000003
+2020-10-03T06:00:00.0,0.6
+2020-10-03T07:00:00.0,0.6
+2020-10-03T08:00:00.0,0.6
+2020-10-03T09:00:00.0,0.6
+2020-10-03T10:00:00.0,0.7544407511999993
+2020-10-03T11:00:00.0,0.7993418987999996
+2020-10-03T12:00:00.0,0.8167611763999997
+2020-10-03T13:00:00.0,0.8658903040000002
+2020-10-03T14:00:00.0,0.9092985024000003
+2020-10-03T15:00:00.0,0.9227989148000002
+2020-10-03T16:00:00.0,1.0738056528000002
+2020-10-03T17:00:00.0,1.110190032000001
+2020-10-03T18:00:00.0,1.2984869687999991
+2020-10-03T19:00:00.0,1.110190032000001
+2020-10-03T20:00:00.0,1.0702294104
+2020-10-03T21:00:00.0,0.9251583599999996
+2020-10-03T22:00:00.0,0.8446658443999995
+2020-10-03T23:00:00.0,0.8160001399999995
diff --git a/test/inputs/chuhsi_Spin_prices_5min.csv b/test/inputs/chuhsi_Spin_prices_5min.csv
new file mode 100644
index 00000000..96ecc7c9
--- /dev/null
+++ b/test/inputs/chuhsi_Spin_prices_5min.csv
@@ -0,0 +1,865 @@
+DateTime,Chuhsi
+2020-10-03T00:00:00.0,1.2111022632000001
+2020-10-03T00:05:00.0,1.2111022632000001
+2020-10-03T00:10:00.0,1.2111022632000001
+2020-10-03T00:15:00.0,1.2111022632000001
+2020-10-03T00:20:00.0,1.2111022632000001
+2020-10-03T00:25:00.0,1.2111022632000001
+2020-10-03T00:30:00.0,1.2111022632000001
+2020-10-03T00:35:00.0,1.2111022632000001
+2020-10-03T00:40:00.0,1.2111022632000001
+2020-10-03T00:45:00.0,1.2111022632000001
+2020-10-03T00:50:00.0,1.2111022632000001
+2020-10-03T00:55:00.0,1.2111022632000001
+2020-10-03T01:00:00.0,1.110190032000001
+2020-10-03T01:05:00.0,1.110190032000001
+2020-10-03T01:10:00.0,1.110190032000001
+2020-10-03T01:15:00.0,1.110190032000001
+2020-10-03T01:20:00.0,1.110190032000001
+2020-10-03T01:25:00.0,1.110190032000001
+2020-10-03T01:30:00.0,1.110190032000001
+2020-10-03T01:35:00.0,1.110190032000001
+2020-10-03T01:40:00.0,1.110190032000001
+2020-10-03T01:45:00.0,1.110190032000001
+2020-10-03T01:50:00.0,1.110190032000001
+2020-10-03T01:55:00.0,1.110190032000001
+2020-10-03T02:00:00.0,1.1101900320000009
+2020-10-03T02:05:00.0,1.1101900320000009
+2020-10-03T02:10:00.0,1.1101900320000009
+2020-10-03T02:15:00.0,1.1101900320000009
+2020-10-03T02:20:00.0,1.1101900320000009
+2020-10-03T02:25:00.0,1.1101900320000009
+2020-10-03T02:30:00.0,1.1101900320000009
+2020-10-03T02:35:00.0,1.1101900320000009
+2020-10-03T02:40:00.0,1.1101900320000009
+2020-10-03T02:45:00.0,1.1101900320000009
+2020-10-03T02:50:00.0,1.1101900320000009
+2020-10-03T02:55:00.0,1.1101900320000009
+2020-10-03T03:00:00.0,1.0716288096000006
+2020-10-03T03:05:00.0,1.0716288096000006
+2020-10-03T03:10:00.0,1.0716288096000006
+2020-10-03T03:15:00.0,1.0716288096000006
+2020-10-03T03:20:00.0,1.0716288096000006
+2020-10-03T03:25:00.0,1.0716288096000006
+2020-10-03T03:30:00.0,1.0716288096000006
+2020-10-03T03:35:00.0,1.0716288096000006
+2020-10-03T03:40:00.0,1.0716288096000006
+2020-10-03T03:45:00.0,1.0716288096000006
+2020-10-03T03:50:00.0,1.0716288096000006
+2020-10-03T03:55:00.0,1.0716288096000006
+2020-10-03T04:00:00.0,1.0716288096000006
+2020-10-03T04:05:00.0,1.0716288096000006
+2020-10-03T04:10:00.0,1.0716288096000006
+2020-10-03T04:15:00.0,1.0716288096000006
+2020-10-03T04:20:00.0,1.0716288096000006
+2020-10-03T04:25:00.0,1.0716288096000006
+2020-10-03T04:30:00.0,1.0716288096000006
+2020-10-03T04:35:00.0,1.0716288096000006
+2020-10-03T04:40:00.0,1.0716288096000006
+2020-10-03T04:45:00.0,1.0716288096000006
+2020-10-03T04:50:00.0,1.0716288096000006
+2020-10-03T04:55:00.0,1.0716288096000006
+2020-10-03T05:00:00.0,1.0702294104000003
+2020-10-03T05:05:00.0,1.0702294104000003
+2020-10-03T05:10:00.0,1.0702294104000003
+2020-10-03T05:15:00.0,1.0702294104000003
+2020-10-03T05:20:00.0,1.0702294104000003
+2020-10-03T05:25:00.0,1.0702294104000003
+2020-10-03T05:30:00.0,1.0702294104000003
+2020-10-03T05:35:00.0,1.0702294104000003
+2020-10-03T05:40:00.0,1.0702294104000003
+2020-10-03T05:45:00.0,1.0702294104000003
+2020-10-03T05:50:00.0,1.0702294104000003
+2020-10-03T05:55:00.0,1.0702294104000003
+2020-10-03T06:00:00.0,0.6
+2020-10-03T06:05:00.0,0.6
+2020-10-03T06:10:00.0,0.6
+2020-10-03T06:15:00.0,0.6
+2020-10-03T06:20:00.0,0.6
+2020-10-03T06:25:00.0,0.6
+2020-10-03T06:30:00.0,0.6
+2020-10-03T06:35:00.0,0.6
+2020-10-03T06:40:00.0,0.6
+2020-10-03T06:45:00.0,0.6
+2020-10-03T06:50:00.0,0.6
+2020-10-03T06:55:00.0,0.6
+2020-10-03T07:00:00.0,0.6
+2020-10-03T07:05:00.0,0.6
+2020-10-03T07:10:00.0,0.6
+2020-10-03T07:15:00.0,0.6
+2020-10-03T07:20:00.0,0.6
+2020-10-03T07:25:00.0,0.6
+2020-10-03T07:30:00.0,0.6
+2020-10-03T07:35:00.0,0.6
+2020-10-03T07:40:00.0,0.6
+2020-10-03T07:45:00.0,0.6
+2020-10-03T07:50:00.0,0.6
+2020-10-03T07:55:00.0,0.6
+2020-10-03T08:00:00.0,0.6
+2020-10-03T08:05:00.0,0.6
+2020-10-03T08:10:00.0,0.6
+2020-10-03T08:15:00.0,0.6
+2020-10-03T08:20:00.0,0.6
+2020-10-03T08:25:00.0,0.6
+2020-10-03T08:30:00.0,0.6
+2020-10-03T08:35:00.0,0.6
+2020-10-03T08:40:00.0,0.6
+2020-10-03T08:45:00.0,0.6
+2020-10-03T08:50:00.0,0.6
+2020-10-03T08:55:00.0,0.6
+2020-10-03T09:00:00.0,0.6
+2020-10-03T09:05:00.0,0.6
+2020-10-03T09:10:00.0,0.6
+2020-10-03T09:15:00.0,0.6
+2020-10-03T09:20:00.0,0.6
+2020-10-03T09:25:00.0,0.6
+2020-10-03T09:30:00.0,0.6
+2020-10-03T09:35:00.0,0.6
+2020-10-03T09:40:00.0,0.6
+2020-10-03T09:45:00.0,0.6
+2020-10-03T09:50:00.0,0.6
+2020-10-03T09:55:00.0,0.6
+2020-10-03T10:00:00.0,0.7544407511999993
+2020-10-03T10:05:00.0,0.7544407511999993
+2020-10-03T10:10:00.0,0.7544407511999993
+2020-10-03T10:15:00.0,0.7544407511999993
+2020-10-03T10:20:00.0,0.7544407511999993
+2020-10-03T10:25:00.0,0.7544407511999993
+2020-10-03T10:30:00.0,0.7544407511999993
+2020-10-03T10:35:00.0,0.7544407511999993
+2020-10-03T10:40:00.0,0.7544407511999993
+2020-10-03T10:45:00.0,0.7544407511999993
+2020-10-03T10:50:00.0,0.7544407511999993
+2020-10-03T10:55:00.0,0.7544407511999993
+2020-10-03T11:00:00.0,0.7993418987999996
+2020-10-03T11:05:00.0,0.7993418987999996
+2020-10-03T11:10:00.0,0.7993418987999996
+2020-10-03T11:15:00.0,0.7993418987999996
+2020-10-03T11:20:00.0,0.7993418987999996
+2020-10-03T11:25:00.0,0.7993418987999996
+2020-10-03T11:30:00.0,0.7993418987999996
+2020-10-03T11:35:00.0,0.7993418987999996
+2020-10-03T11:40:00.0,0.7993418987999996
+2020-10-03T11:45:00.0,0.7993418987999996
+2020-10-03T11:50:00.0,0.7993418987999996
+2020-10-03T11:55:00.0,0.7993418987999996
+2020-10-03T12:00:00.0,0.8167611763999997
+2020-10-03T12:05:00.0,0.8167611763999997
+2020-10-03T12:10:00.0,0.8167611763999997
+2020-10-03T12:15:00.0,0.8167611763999997
+2020-10-03T12:20:00.0,0.8167611763999997
+2020-10-03T12:25:00.0,0.8167611763999997
+2020-10-03T12:30:00.0,0.8167611763999997
+2020-10-03T12:35:00.0,0.8167611763999997
+2020-10-03T12:40:00.0,0.8167611763999997
+2020-10-03T12:45:00.0,0.8167611763999997
+2020-10-03T12:50:00.0,0.8167611763999997
+2020-10-03T12:55:00.0,0.8167611763999997
+2020-10-03T13:00:00.0,0.8658903040000002
+2020-10-03T13:05:00.0,0.8658903040000002
+2020-10-03T13:10:00.0,0.8658903040000002
+2020-10-03T13:15:00.0,0.8658903040000002
+2020-10-03T13:20:00.0,0.8658903040000002
+2020-10-03T13:25:00.0,0.8658903040000002
+2020-10-03T13:30:00.0,0.8658903040000002
+2020-10-03T13:35:00.0,0.8658903040000002
+2020-10-03T13:40:00.0,0.8658903040000002
+2020-10-03T13:45:00.0,0.8658903040000002
+2020-10-03T13:50:00.0,0.8658903040000002
+2020-10-03T13:55:00.0,0.8658903040000002
+2020-10-03T14:00:00.0,0.9092985024000003
+2020-10-03T14:05:00.0,0.9092985024000003
+2020-10-03T14:10:00.0,0.9092985024000003
+2020-10-03T14:15:00.0,0.9092985024000003
+2020-10-03T14:20:00.0,0.9092985024000003
+2020-10-03T14:25:00.0,0.9092985024000003
+2020-10-03T14:30:00.0,0.9092985024000003
+2020-10-03T14:35:00.0,0.9092985024000003
+2020-10-03T14:40:00.0,0.9092985024000003
+2020-10-03T14:45:00.0,0.9092985024000003
+2020-10-03T14:50:00.0,0.9092985024000003
+2020-10-03T14:55:00.0,0.9092985024000003
+2020-10-03T15:00:00.0,0.9227989148000002
+2020-10-03T15:05:00.0,0.9227989148000002
+2020-10-03T15:10:00.0,0.9227989148000002
+2020-10-03T15:15:00.0,0.9227989148000002
+2020-10-03T15:20:00.0,0.9227989148000002
+2020-10-03T15:25:00.0,0.9227989148000002
+2020-10-03T15:30:00.0,0.9227989148000002
+2020-10-03T15:35:00.0,0.9227989148000002
+2020-10-03T15:40:00.0,0.9227989148000002
+2020-10-03T15:45:00.0,0.9227989148000002
+2020-10-03T15:50:00.0,0.9227989148000002
+2020-10-03T15:55:00.0,0.9227989148000002
+2020-10-03T16:00:00.0,1.0738056528000002
+2020-10-03T16:05:00.0,1.0738056528000002
+2020-10-03T16:10:00.0,1.0738056528000002
+2020-10-03T16:15:00.0,1.0738056528000002
+2020-10-03T16:20:00.0,1.0738056528000002
+2020-10-03T16:25:00.0,1.0738056528000002
+2020-10-03T16:30:00.0,1.0738056528000002
+2020-10-03T16:35:00.0,1.0738056528000002
+2020-10-03T16:40:00.0,1.0738056528000002
+2020-10-03T16:45:00.0,1.0738056528000002
+2020-10-03T16:50:00.0,1.0738056528000002
+2020-10-03T16:55:00.0,1.0738056528000002
+2020-10-03T17:00:00.0,1.110190032000001
+2020-10-03T17:05:00.0,1.110190032000001
+2020-10-03T17:10:00.0,1.110190032000001
+2020-10-03T17:15:00.0,1.110190032000001
+2020-10-03T17:20:00.0,1.110190032000001
+2020-10-03T17:25:00.0,1.110190032000001
+2020-10-03T17:30:00.0,1.110190032000001
+2020-10-03T17:35:00.0,1.110190032000001
+2020-10-03T17:40:00.0,1.110190032000001
+2020-10-03T17:45:00.0,1.110190032000001
+2020-10-03T17:50:00.0,1.110190032000001
+2020-10-03T17:55:00.0,1.110190032000001
+2020-10-03T18:00:00.0,1.2984869687999991
+2020-10-03T18:05:00.0,1.2984869687999991
+2020-10-03T18:10:00.0,1.2984869687999991
+2020-10-03T18:15:00.0,1.2984869687999991
+2020-10-03T18:20:00.0,1.2984869687999991
+2020-10-03T18:25:00.0,1.2984869687999991
+2020-10-03T18:30:00.0,1.2984869687999991
+2020-10-03T18:35:00.0,1.2984869687999991
+2020-10-03T18:40:00.0,1.2984869687999991
+2020-10-03T18:45:00.0,1.2984869687999991
+2020-10-03T18:50:00.0,1.2984869687999991
+2020-10-03T18:55:00.0,1.2984869687999991
+2020-10-03T19:00:00.0,1.110190032000001
+2020-10-03T19:05:00.0,1.110190032000001
+2020-10-03T19:10:00.0,1.110190032000001
+2020-10-03T19:15:00.0,1.110190032000001
+2020-10-03T19:20:00.0,1.110190032000001
+2020-10-03T19:25:00.0,1.110190032000001
+2020-10-03T19:30:00.0,1.110190032000001
+2020-10-03T19:35:00.0,1.110190032000001
+2020-10-03T19:40:00.0,1.110190032000001
+2020-10-03T19:45:00.0,1.110190032000001
+2020-10-03T19:50:00.0,1.110190032000001
+2020-10-03T19:55:00.0,1.110190032000001
+2020-10-03T20:00:00.0,1.0702294104
+2020-10-03T20:05:00.0,1.0702294104
+2020-10-03T20:10:00.0,1.0702294104
+2020-10-03T20:15:00.0,1.0702294104
+2020-10-03T20:20:00.0,1.0702294104
+2020-10-03T20:25:00.0,1.0702294104
+2020-10-03T20:30:00.0,1.0702294104
+2020-10-03T20:35:00.0,1.0702294104
+2020-10-03T20:40:00.0,1.0702294104
+2020-10-03T20:45:00.0,1.0702294104
+2020-10-03T20:50:00.0,1.0702294104
+2020-10-03T20:55:00.0,1.0702294104
+2020-10-03T21:00:00.0,0.9251583599999996
+2020-10-03T21:05:00.0,0.9251583599999996
+2020-10-03T21:10:00.0,0.9251583599999996
+2020-10-03T21:15:00.0,0.9251583599999996
+2020-10-03T21:20:00.0,0.9251583599999996
+2020-10-03T21:25:00.0,0.9251583599999996
+2020-10-03T21:30:00.0,0.9251583599999996
+2020-10-03T21:35:00.0,0.9251583599999996
+2020-10-03T21:40:00.0,0.9251583599999996
+2020-10-03T21:45:00.0,0.9251583599999996
+2020-10-03T21:50:00.0,0.9251583599999996
+2020-10-03T21:55:00.0,0.9251583599999996
+2020-10-03T22:00:00.0,0.8446658443999995
+2020-10-03T22:05:00.0,0.8446658443999995
+2020-10-03T22:10:00.0,0.8446658443999995
+2020-10-03T22:15:00.0,0.8446658443999995
+2020-10-03T22:20:00.0,0.8446658443999995
+2020-10-03T22:25:00.0,0.8446658443999995
+2020-10-03T22:30:00.0,0.8446658443999995
+2020-10-03T22:35:00.0,0.8446658443999995
+2020-10-03T22:40:00.0,0.8446658443999995
+2020-10-03T22:45:00.0,0.8446658443999995
+2020-10-03T22:50:00.0,0.8446658443999995
+2020-10-03T22:55:00.0,0.8446658443999995
+2020-10-03T23:00:00.0,0.8160001399999995
+2020-10-03T23:05:00.0,0.8160001399999995
+2020-10-03T23:10:00.0,0.8160001399999995
+2020-10-03T23:15:00.0,0.8160001399999995
+2020-10-03T23:20:00.0,0.8160001399999995
+2020-10-03T23:25:00.0,0.8160001399999995
+2020-10-03T23:30:00.0,0.8160001399999995
+2020-10-03T23:35:00.0,0.8160001399999995
+2020-10-03T23:40:00.0,0.8160001399999995
+2020-10-03T23:45:00.0,0.8160001399999995
+2020-10-03T23:50:00.0,0.8160001399999995
+2020-10-03T23:55:00.0,0.8160001399999995
+2020-10-04T00:00:00.0,0.7993418987999996
+2020-10-04T00:05:00.0,0.7993418987999996
+2020-10-04T00:10:00.0,0.7993418987999996
+2020-10-04T00:15:00.0,0.7993418987999996
+2020-10-04T00:20:00.0,0.7993418987999996
+2020-10-04T00:25:00.0,0.7993418987999996
+2020-10-04T00:30:00.0,0.7993418987999996
+2020-10-04T00:35:00.0,0.7993418987999996
+2020-10-04T00:40:00.0,0.7993418987999996
+2020-10-04T00:45:00.0,0.7993418987999996
+2020-10-04T00:50:00.0,0.7993418987999996
+2020-10-04T00:55:00.0,0.7993418987999996
+2020-10-04T01:00:00.0,0.7993418987999996
+2020-10-04T01:05:00.0,0.7993418987999996
+2020-10-04T01:10:00.0,0.7993418987999996
+2020-10-04T01:15:00.0,0.7993418987999996
+2020-10-04T01:20:00.0,0.7993418987999996
+2020-10-04T01:25:00.0,0.7993418987999996
+2020-10-04T01:30:00.0,0.7993418987999996
+2020-10-04T01:35:00.0,0.7993418987999996
+2020-10-04T01:40:00.0,0.7993418987999996
+2020-10-04T01:45:00.0,0.7993418987999996
+2020-10-04T01:50:00.0,0.7993418987999996
+2020-10-04T01:55:00.0,0.7993418987999996
+2020-10-04T02:00:00.0,0.7613746383999992
+2020-10-04T02:05:00.0,0.7613746383999992
+2020-10-04T02:10:00.0,0.7613746383999992
+2020-10-04T02:15:00.0,0.7613746383999992
+2020-10-04T02:20:00.0,0.7613746383999992
+2020-10-04T02:25:00.0,0.7613746383999992
+2020-10-04T02:30:00.0,0.7613746383999992
+2020-10-04T02:35:00.0,0.7613746383999992
+2020-10-04T02:40:00.0,0.7613746383999992
+2020-10-04T02:45:00.0,0.7613746383999992
+2020-10-04T02:50:00.0,0.7613746383999992
+2020-10-04T02:55:00.0,0.7613746383999992
+2020-10-04T03:00:00.0,0.7875881144000004
+2020-10-04T03:05:00.0,0.7875881144000004
+2020-10-04T03:10:00.0,0.7875881144000004
+2020-10-04T03:15:00.0,0.7875881144000004
+2020-10-04T03:20:00.0,0.7875881144000004
+2020-10-04T03:25:00.0,0.7875881144000004
+2020-10-04T03:30:00.0,0.7875881144000004
+2020-10-04T03:35:00.0,0.7875881144000004
+2020-10-04T03:40:00.0,0.7875881144000004
+2020-10-04T03:45:00.0,0.7875881144000004
+2020-10-04T03:50:00.0,0.7875881144000004
+2020-10-04T03:55:00.0,0.7875881144000004
+2020-10-04T04:00:00.0,0.7993418987999996
+2020-10-04T04:05:00.0,0.7993418987999996
+2020-10-04T04:10:00.0,0.7993418987999996
+2020-10-04T04:15:00.0,0.7993418987999996
+2020-10-04T04:20:00.0,0.7993418987999996
+2020-10-04T04:25:00.0,0.7993418987999996
+2020-10-04T04:30:00.0,0.7993418987999996
+2020-10-04T04:35:00.0,0.7993418987999996
+2020-10-04T04:40:00.0,0.7993418987999996
+2020-10-04T04:45:00.0,0.7993418987999996
+2020-10-04T04:50:00.0,0.7993418987999996
+2020-10-04T04:55:00.0,0.7993418987999996
+2020-10-04T05:00:00.0,0.7544407511999993
+2020-10-04T05:05:00.0,0.7544407511999993
+2020-10-04T05:10:00.0,0.7544407511999993
+2020-10-04T05:15:00.0,0.7544407511999993
+2020-10-04T05:20:00.0,0.7544407511999993
+2020-10-04T05:25:00.0,0.7544407511999993
+2020-10-04T05:30:00.0,0.7544407511999993
+2020-10-04T05:35:00.0,0.7544407511999993
+2020-10-04T05:40:00.0,0.7544407511999993
+2020-10-04T05:45:00.0,0.7544407511999993
+2020-10-04T05:50:00.0,0.7544407511999993
+2020-10-04T05:55:00.0,0.7544407511999993
+2020-10-04T06:00:00.0,0.6
+2020-10-04T06:05:00.0,0.6
+2020-10-04T06:10:00.0,0.6
+2020-10-04T06:15:00.0,0.6
+2020-10-04T06:20:00.0,0.6
+2020-10-04T06:25:00.0,0.6
+2020-10-04T06:30:00.0,0.6
+2020-10-04T06:35:00.0,0.6
+2020-10-04T06:40:00.0,0.6
+2020-10-04T06:45:00.0,0.6
+2020-10-04T06:50:00.0,0.6
+2020-10-04T06:55:00.0,0.6
+2020-10-04T07:00:00.0,0.6
+2020-10-04T07:05:00.0,0.6
+2020-10-04T07:10:00.0,0.6
+2020-10-04T07:15:00.0,0.6
+2020-10-04T07:20:00.0,0.6
+2020-10-04T07:25:00.0,0.6
+2020-10-04T07:30:00.0,0.6
+2020-10-04T07:35:00.0,0.6
+2020-10-04T07:40:00.0,0.6
+2020-10-04T07:45:00.0,0.6
+2020-10-04T07:50:00.0,0.6
+2020-10-04T07:55:00.0,0.6
+2020-10-04T08:00:00.0,0.6
+2020-10-04T08:05:00.0,0.6
+2020-10-04T08:10:00.0,0.6
+2020-10-04T08:15:00.0,0.6
+2020-10-04T08:20:00.0,0.6
+2020-10-04T08:25:00.0,0.6
+2020-10-04T08:30:00.0,0.6
+2020-10-04T08:35:00.0,0.6
+2020-10-04T08:40:00.0,0.6
+2020-10-04T08:45:00.0,0.6
+2020-10-04T08:50:00.0,0.6
+2020-10-04T08:55:00.0,0.6
+2020-10-04T09:00:00.0,0.7771872835999997
+2020-10-04T09:05:00.0,0.7771872835999997
+2020-10-04T09:10:00.0,0.7771872835999997
+2020-10-04T09:15:00.0,0.7771872835999997
+2020-10-04T09:20:00.0,0.7771872835999997
+2020-10-04T09:25:00.0,0.7771872835999997
+2020-10-04T09:30:00.0,0.7771872835999997
+2020-10-04T09:35:00.0,0.7771872835999997
+2020-10-04T09:40:00.0,0.7771872835999997
+2020-10-04T09:45:00.0,0.7771872835999997
+2020-10-04T09:50:00.0,0.7771872835999997
+2020-10-04T09:55:00.0,0.7771872835999997
+2020-10-04T10:00:00.0,0.7993418987999996
+2020-10-04T10:05:00.0,0.7993418987999996
+2020-10-04T10:10:00.0,0.7993418987999996
+2020-10-04T10:15:00.0,0.7993418987999996
+2020-10-04T10:20:00.0,0.7993418987999996
+2020-10-04T10:25:00.0,0.7993418987999996
+2020-10-04T10:30:00.0,0.7993418987999996
+2020-10-04T10:35:00.0,0.7993418987999996
+2020-10-04T10:40:00.0,0.7993418987999996
+2020-10-04T10:45:00.0,0.7993418987999996
+2020-10-04T10:50:00.0,0.7993418987999996
+2020-10-04T10:55:00.0,0.7993418987999996
+2020-10-04T11:00:00.0,0.7993418987999996
+2020-10-04T11:05:00.0,0.7993418987999996
+2020-10-04T11:10:00.0,0.7993418987999996
+2020-10-04T11:15:00.0,0.7993418987999996
+2020-10-04T11:20:00.0,0.7993418987999996
+2020-10-04T11:25:00.0,0.7993418987999996
+2020-10-04T11:30:00.0,0.7993418987999996
+2020-10-04T11:35:00.0,0.7993418987999996
+2020-10-04T11:40:00.0,0.7993418987999996
+2020-10-04T11:45:00.0,0.7993418987999996
+2020-10-04T11:50:00.0,0.7993418987999996
+2020-10-04T11:55:00.0,0.7993418987999996
+2020-10-04T12:00:00.0,0.8658903040000002
+2020-10-04T12:05:00.0,0.8658903040000002
+2020-10-04T12:10:00.0,0.8658903040000002
+2020-10-04T12:15:00.0,0.8658903040000002
+2020-10-04T12:20:00.0,0.8658903040000002
+2020-10-04T12:25:00.0,0.8658903040000002
+2020-10-04T12:30:00.0,0.8658903040000002
+2020-10-04T12:35:00.0,0.8658903040000002
+2020-10-04T12:40:00.0,0.8658903040000002
+2020-10-04T12:45:00.0,0.8658903040000002
+2020-10-04T12:50:00.0,0.8658903040000002
+2020-10-04T12:55:00.0,0.8658903040000002
+2020-10-04T13:00:00.0,0.9251583599999996
+2020-10-04T13:05:00.0,0.9251583599999996
+2020-10-04T13:10:00.0,0.9251583599999996
+2020-10-04T13:15:00.0,0.9251583599999996
+2020-10-04T13:20:00.0,0.9251583599999996
+2020-10-04T13:25:00.0,0.9251583599999996
+2020-10-04T13:30:00.0,0.9251583599999996
+2020-10-04T13:35:00.0,0.9251583599999996
+2020-10-04T13:40:00.0,0.9251583599999996
+2020-10-04T13:45:00.0,0.9251583599999996
+2020-10-04T13:50:00.0,0.9251583599999996
+2020-10-04T13:55:00.0,0.9251583599999996
+2020-10-04T14:00:00.0,1.0702294104000003
+2020-10-04T14:05:00.0,1.0702294104000003
+2020-10-04T14:10:00.0,1.0702294104000003
+2020-10-04T14:15:00.0,1.0702294104000003
+2020-10-04T14:20:00.0,1.0702294104000003
+2020-10-04T14:25:00.0,1.0702294104000003
+2020-10-04T14:30:00.0,1.0702294104000003
+2020-10-04T14:35:00.0,1.0702294104000003
+2020-10-04T14:40:00.0,1.0702294104000003
+2020-10-04T14:45:00.0,1.0702294104000003
+2020-10-04T14:50:00.0,1.0702294104000003
+2020-10-04T14:55:00.0,1.0702294104000003
+2020-10-04T15:00:00.0,1.0702294104000003
+2020-10-04T15:05:00.0,1.0702294104000003
+2020-10-04T15:10:00.0,1.0702294104000003
+2020-10-04T15:15:00.0,1.0702294104000003
+2020-10-04T15:20:00.0,1.0702294104000003
+2020-10-04T15:25:00.0,1.0702294104000003
+2020-10-04T15:30:00.0,1.0702294104000003
+2020-10-04T15:35:00.0,1.0702294104000003
+2020-10-04T15:40:00.0,1.0702294104000003
+2020-10-04T15:45:00.0,1.0702294104000003
+2020-10-04T15:50:00.0,1.0702294104000003
+2020-10-04T15:55:00.0,1.0702294104000003
+2020-10-04T16:00:00.0,1.2212090351999998
+2020-10-04T16:05:00.0,1.2212090351999998
+2020-10-04T16:10:00.0,1.2212090351999998
+2020-10-04T16:15:00.0,1.2212090351999998
+2020-10-04T16:20:00.0,1.2212090351999998
+2020-10-04T16:25:00.0,1.2212090351999998
+2020-10-04T16:30:00.0,1.2212090351999998
+2020-10-04T16:35:00.0,1.2212090351999998
+2020-10-04T16:40:00.0,1.2212090351999998
+2020-10-04T16:45:00.0,1.2212090351999998
+2020-10-04T16:50:00.0,1.2212090351999998
+2020-10-04T16:55:00.0,1.2212090351999998
+2020-10-04T17:00:00.0,1.2611696567999993
+2020-10-04T17:05:00.0,1.2611696567999993
+2020-10-04T17:10:00.0,1.2611696567999993
+2020-10-04T17:15:00.0,1.2611696567999993
+2020-10-04T17:20:00.0,1.2611696567999993
+2020-10-04T17:25:00.0,1.2611696567999993
+2020-10-04T17:30:00.0,1.2611696567999993
+2020-10-04T17:35:00.0,1.2611696567999993
+2020-10-04T17:40:00.0,1.2611696567999993
+2020-10-04T17:45:00.0,1.2611696567999993
+2020-10-04T17:50:00.0,1.2611696567999993
+2020-10-04T17:55:00.0,1.2611696567999993
+2020-10-04T18:00:00.0,1.2611696567999993
+2020-10-04T18:05:00.0,1.2611696567999993
+2020-10-04T18:10:00.0,1.2611696567999993
+2020-10-04T18:15:00.0,1.2611696567999993
+2020-10-04T18:20:00.0,1.2611696567999993
+2020-10-04T18:25:00.0,1.2611696567999993
+2020-10-04T18:30:00.0,1.2611696567999993
+2020-10-04T18:35:00.0,1.2611696567999993
+2020-10-04T18:40:00.0,1.2611696567999993
+2020-10-04T18:45:00.0,1.2611696567999993
+2020-10-04T18:50:00.0,1.2611696567999993
+2020-10-04T18:55:00.0,1.2611696567999993
+2020-10-04T19:00:00.0,1.2611696567999993
+2020-10-04T19:05:00.0,1.2611696567999993
+2020-10-04T19:10:00.0,1.2611696567999993
+2020-10-04T19:15:00.0,1.2611696567999993
+2020-10-04T19:20:00.0,1.2611696567999993
+2020-10-04T19:25:00.0,1.2611696567999993
+2020-10-04T19:30:00.0,1.2611696567999993
+2020-10-04T19:35:00.0,1.2611696567999993
+2020-10-04T19:40:00.0,1.2611696567999993
+2020-10-04T19:45:00.0,1.2611696567999993
+2020-10-04T19:50:00.0,1.2611696567999993
+2020-10-04T19:55:00.0,1.2611696567999993
+2020-10-04T20:00:00.0,1.1101900320000009
+2020-10-04T20:05:00.0,1.1101900320000009
+2020-10-04T20:10:00.0,1.1101900320000009
+2020-10-04T20:15:00.0,1.1101900320000009
+2020-10-04T20:20:00.0,1.1101900320000009
+2020-10-04T20:25:00.0,1.1101900320000009
+2020-10-04T20:30:00.0,1.1101900320000009
+2020-10-04T20:35:00.0,1.1101900320000009
+2020-10-04T20:40:00.0,1.1101900320000009
+2020-10-04T20:45:00.0,1.1101900320000009
+2020-10-04T20:50:00.0,1.1101900320000009
+2020-10-04T20:55:00.0,1.1101900320000009
+2020-10-04T21:00:00.0,1.0702294104
+2020-10-04T21:05:00.0,1.0702294104
+2020-10-04T21:10:00.0,1.0702294104
+2020-10-04T21:15:00.0,1.0702294104
+2020-10-04T21:20:00.0,1.0702294104
+2020-10-04T21:25:00.0,1.0702294104
+2020-10-04T21:30:00.0,1.0702294104
+2020-10-04T21:35:00.0,1.0702294104
+2020-10-04T21:40:00.0,1.0702294104
+2020-10-04T21:45:00.0,1.0702294104
+2020-10-04T21:50:00.0,1.0702294104
+2020-10-04T21:55:00.0,1.0702294104
+2020-10-04T22:00:00.0,1.0529701536000005
+2020-10-04T22:05:00.0,1.0529701536000005
+2020-10-04T22:10:00.0,1.0529701536000005
+2020-10-04T22:15:00.0,1.0529701536000005
+2020-10-04T22:20:00.0,1.0529701536000005
+2020-10-04T22:25:00.0,1.0529701536000005
+2020-10-04T22:30:00.0,1.0529701536000005
+2020-10-04T22:35:00.0,1.0529701536000005
+2020-10-04T22:40:00.0,1.0529701536000005
+2020-10-04T22:45:00.0,1.0529701536000005
+2020-10-04T22:50:00.0,1.0529701536000005
+2020-10-04T22:55:00.0,1.0529701536000005
+2020-10-04T23:00:00.0,0.9550161223999993
+2020-10-04T23:05:00.0,0.9550161223999993
+2020-10-04T23:10:00.0,0.9550161223999993
+2020-10-04T23:15:00.0,0.9550161223999993
+2020-10-04T23:20:00.0,0.9550161223999993
+2020-10-04T23:25:00.0,0.9550161223999993
+2020-10-04T23:30:00.0,0.9550161223999993
+2020-10-04T23:35:00.0,0.9550161223999993
+2020-10-04T23:40:00.0,0.9550161223999993
+2020-10-04T23:45:00.0,0.9550161223999993
+2020-10-04T23:50:00.0,0.9550161223999993
+2020-10-04T23:55:00.0,0.9550161223999993
+2020-10-05T00:00:00.0,0.9251583599999995
+2020-10-05T00:05:00.0,0.9251583599999995
+2020-10-05T00:10:00.0,0.9251583599999995
+2020-10-05T00:15:00.0,0.9251583599999995
+2020-10-05T00:20:00.0,0.9251583599999995
+2020-10-05T00:25:00.0,0.9251583599999995
+2020-10-05T00:30:00.0,0.9251583599999995
+2020-10-05T00:35:00.0,0.9251583599999995
+2020-10-05T00:40:00.0,0.9251583599999995
+2020-10-05T00:45:00.0,0.9251583599999995
+2020-10-05T00:50:00.0,0.9251583599999995
+2020-10-05T00:55:00.0,0.9251583599999995
+2020-10-05T01:00:00.0,0.9187400540000005
+2020-10-05T01:05:00.0,0.9187400540000005
+2020-10-05T01:10:00.0,0.9187400540000005
+2020-10-05T01:15:00.0,0.9187400540000005
+2020-10-05T01:20:00.0,0.9187400540000005
+2020-10-05T01:25:00.0,0.9187400540000005
+2020-10-05T01:30:00.0,0.9187400540000005
+2020-10-05T01:35:00.0,0.9187400540000005
+2020-10-05T01:40:00.0,0.9187400540000005
+2020-10-05T01:45:00.0,0.9187400540000005
+2020-10-05T01:50:00.0,0.9187400540000005
+2020-10-05T01:55:00.0,0.9187400540000005
+2020-10-05T02:00:00.0,0.9092985024000001
+2020-10-05T02:05:00.0,0.9092985024000001
+2020-10-05T02:10:00.0,0.9092985024000001
+2020-10-05T02:15:00.0,0.9092985024000001
+2020-10-05T02:20:00.0,0.9092985024000001
+2020-10-05T02:25:00.0,0.9092985024000001
+2020-10-05T02:30:00.0,0.9092985024000001
+2020-10-05T02:35:00.0,0.9092985024000001
+2020-10-05T02:40:00.0,0.9092985024000001
+2020-10-05T02:45:00.0,0.9092985024000001
+2020-10-05T02:50:00.0,0.9092985024000001
+2020-10-05T02:55:00.0,0.9092985024000001
+2020-10-05T03:00:00.0,0.9227989148000002
+2020-10-05T03:05:00.0,0.9227989148000002
+2020-10-05T03:10:00.0,0.9227989148000002
+2020-10-05T03:15:00.0,0.9227989148000002
+2020-10-05T03:20:00.0,0.9227989148000002
+2020-10-05T03:25:00.0,0.9227989148000002
+2020-10-05T03:30:00.0,0.9227989148000002
+2020-10-05T03:35:00.0,0.9227989148000002
+2020-10-05T03:40:00.0,0.9227989148000002
+2020-10-05T03:45:00.0,0.9227989148000002
+2020-10-05T03:50:00.0,0.9227989148000002
+2020-10-05T03:55:00.0,0.9227989148000002
+2020-10-05T04:00:00.0,0.9227989148000002
+2020-10-05T04:05:00.0,0.9227989148000002
+2020-10-05T04:10:00.0,0.9227989148000002
+2020-10-05T04:15:00.0,0.9227989148000002
+2020-10-05T04:20:00.0,0.9227989148000002
+2020-10-05T04:25:00.0,0.9227989148000002
+2020-10-05T04:30:00.0,0.9227989148000002
+2020-10-05T04:35:00.0,0.9227989148000002
+2020-10-05T04:40:00.0,0.9227989148000002
+2020-10-05T04:45:00.0,0.9227989148000002
+2020-10-05T04:50:00.0,0.9227989148000002
+2020-10-05T04:55:00.0,0.9227989148000002
+2020-10-05T05:00:00.0,0.9375122851999993
+2020-10-05T05:05:00.0,0.9375122851999993
+2020-10-05T05:10:00.0,0.9375122851999993
+2020-10-05T05:15:00.0,0.9375122851999993
+2020-10-05T05:20:00.0,0.9375122851999993
+2020-10-05T05:25:00.0,0.9375122851999993
+2020-10-05T05:30:00.0,0.9375122851999993
+2020-10-05T05:35:00.0,0.9375122851999993
+2020-10-05T05:40:00.0,0.9375122851999993
+2020-10-05T05:45:00.0,0.9375122851999993
+2020-10-05T05:50:00.0,0.9375122851999993
+2020-10-05T05:55:00.0,0.9375122851999993
+2020-10-05T06:00:00.0,0.7544407511999993
+2020-10-05T06:05:00.0,0.7544407511999993
+2020-10-05T06:10:00.0,0.7544407511999993
+2020-10-05T06:15:00.0,0.7544407511999993
+2020-10-05T06:20:00.0,0.7544407511999993
+2020-10-05T06:25:00.0,0.7544407511999993
+2020-10-05T06:30:00.0,0.7544407511999993
+2020-10-05T06:35:00.0,0.7544407511999993
+2020-10-05T06:40:00.0,0.7544407511999993
+2020-10-05T06:45:00.0,0.7544407511999993
+2020-10-05T06:50:00.0,0.7544407511999993
+2020-10-05T06:55:00.0,0.7544407511999993
+2020-10-05T07:00:00.0,0.6
+2020-10-05T07:05:00.0,0.6
+2020-10-05T07:10:00.0,0.6
+2020-10-05T07:15:00.0,0.6
+2020-10-05T07:20:00.0,0.6
+2020-10-05T07:25:00.0,0.6
+2020-10-05T07:30:00.0,0.6
+2020-10-05T07:35:00.0,0.6
+2020-10-05T07:40:00.0,0.6
+2020-10-05T07:45:00.0,0.6
+2020-10-05T07:50:00.0,0.6
+2020-10-05T07:55:00.0,0.6
+2020-10-05T08:00:00.0,0.6
+2020-10-05T08:05:00.0,0.6
+2020-10-05T08:10:00.0,0.6
+2020-10-05T08:15:00.0,0.6
+2020-10-05T08:20:00.0,0.6
+2020-10-05T08:25:00.0,0.6
+2020-10-05T08:30:00.0,0.6
+2020-10-05T08:35:00.0,0.6
+2020-10-05T08:40:00.0,0.6
+2020-10-05T08:45:00.0,0.6
+2020-10-05T08:50:00.0,0.6
+2020-10-05T08:55:00.0,0.6
+2020-10-05T09:00:00.0,0.7385435463999996
+2020-10-05T09:05:00.0,0.7385435463999996
+2020-10-05T09:10:00.0,0.7385435463999996
+2020-10-05T09:15:00.0,0.7385435463999996
+2020-10-05T09:20:00.0,0.7385435463999996
+2020-10-05T09:25:00.0,0.7385435463999996
+2020-10-05T09:30:00.0,0.7385435463999996
+2020-10-05T09:35:00.0,0.7385435463999996
+2020-10-05T09:40:00.0,0.7385435463999996
+2020-10-05T09:45:00.0,0.7385435463999996
+2020-10-05T09:50:00.0,0.7385435463999996
+2020-10-05T09:55:00.0,0.7385435463999996
+2020-10-05T10:00:00.0,0.7613746383999992
+2020-10-05T10:05:00.0,0.7613746383999992
+2020-10-05T10:10:00.0,0.7613746383999992
+2020-10-05T10:15:00.0,0.7613746383999992
+2020-10-05T10:20:00.0,0.7613746383999992
+2020-10-05T10:25:00.0,0.7613746383999992
+2020-10-05T10:30:00.0,0.7613746383999992
+2020-10-05T10:35:00.0,0.7613746383999992
+2020-10-05T10:40:00.0,0.7613746383999992
+2020-10-05T10:45:00.0,0.7613746383999992
+2020-10-05T10:50:00.0,0.7613746383999992
+2020-10-05T10:55:00.0,0.7613746383999992
+2020-10-05T11:00:00.0,0.8338422155999997
+2020-10-05T11:05:00.0,0.8338422155999997
+2020-10-05T11:10:00.0,0.8338422155999997
+2020-10-05T11:15:00.0,0.8338422155999997
+2020-10-05T11:20:00.0,0.8338422155999997
+2020-10-05T11:25:00.0,0.8338422155999997
+2020-10-05T11:30:00.0,0.8338422155999997
+2020-10-05T11:35:00.0,0.8338422155999997
+2020-10-05T11:40:00.0,0.8338422155999997
+2020-10-05T11:45:00.0,0.8338422155999997
+2020-10-05T11:50:00.0,0.8338422155999997
+2020-10-05T11:55:00.0,0.8338422155999997
+2020-10-05T12:00:00.0,0.9030789504000001
+2020-10-05T12:05:00.0,0.9030789504000001
+2020-10-05T12:10:00.0,0.9030789504000001
+2020-10-05T12:15:00.0,0.9030789504000001
+2020-10-05T12:20:00.0,0.9030789504000001
+2020-10-05T12:25:00.0,0.9030789504000001
+2020-10-05T12:30:00.0,0.9030789504000001
+2020-10-05T12:35:00.0,0.9030789504000001
+2020-10-05T12:40:00.0,0.9030789504000001
+2020-10-05T12:45:00.0,0.9030789504000001
+2020-10-05T12:50:00.0,0.9030789504000001
+2020-10-05T12:55:00.0,0.9030789504000001
+2020-10-05T13:00:00.0,0.9251583599999996
+2020-10-05T13:05:00.0,0.9251583599999996
+2020-10-05T13:10:00.0,0.9251583599999996
+2020-10-05T13:15:00.0,0.9251583599999996
+2020-10-05T13:20:00.0,0.9251583599999996
+2020-10-05T13:25:00.0,0.9251583599999996
+2020-10-05T13:30:00.0,0.9251583599999996
+2020-10-05T13:35:00.0,0.9251583599999996
+2020-10-05T13:40:00.0,0.9251583599999996
+2020-10-05T13:45:00.0,0.9251583599999996
+2020-10-05T13:50:00.0,0.9251583599999996
+2020-10-05T13:55:00.0,0.9251583599999996
+2020-10-05T14:00:00.0,0.9846965419999997
+2020-10-05T14:05:00.0,0.9846965419999997
+2020-10-05T14:10:00.0,0.9846965419999997
+2020-10-05T14:15:00.0,0.9846965419999997
+2020-10-05T14:20:00.0,0.9846965419999997
+2020-10-05T14:25:00.0,0.9846965419999997
+2020-10-05T14:30:00.0,0.9846965419999997
+2020-10-05T14:35:00.0,0.9846965419999997
+2020-10-05T14:40:00.0,0.9846965419999997
+2020-10-05T14:45:00.0,0.9846965419999997
+2020-10-05T14:50:00.0,0.9846965419999997
+2020-10-05T14:55:00.0,0.9846965419999997
+2020-10-05T15:00:00.0,1.2212090351999998
+2020-10-05T15:05:00.0,1.2212090351999998
+2020-10-05T15:10:00.0,1.2212090351999998
+2020-10-05T15:15:00.0,1.2212090351999998
+2020-10-05T15:20:00.0,1.2212090351999998
+2020-10-05T15:25:00.0,1.2212090351999998
+2020-10-05T15:30:00.0,1.2212090351999998
+2020-10-05T15:35:00.0,1.2212090351999998
+2020-10-05T15:40:00.0,1.2212090351999998
+2020-10-05T15:45:00.0,1.2212090351999998
+2020-10-05T15:50:00.0,1.2212090351999998
+2020-10-05T15:55:00.0,1.2212090351999998
+2020-10-05T16:00:00.0,1.2611696567999993
+2020-10-05T16:05:00.0,1.2611696567999993
+2020-10-05T16:10:00.0,1.2611696567999993
+2020-10-05T16:15:00.0,1.2611696567999993
+2020-10-05T16:20:00.0,1.2611696567999993
+2020-10-05T16:25:00.0,1.2611696567999993
+2020-10-05T16:30:00.0,1.2611696567999993
+2020-10-05T16:35:00.0,1.2611696567999993
+2020-10-05T16:40:00.0,1.2611696567999993
+2020-10-05T16:45:00.0,1.2611696567999993
+2020-10-05T16:50:00.0,1.2611696567999993
+2020-10-05T16:55:00.0,1.2611696567999993
+2020-10-05T17:00:00.0,1.2690995855999996
+2020-10-05T17:05:00.0,1.2690995855999996
+2020-10-05T17:10:00.0,1.2690995855999996
+2020-10-05T17:15:00.0,1.2690995855999996
+2020-10-05T17:20:00.0,1.2690995855999996
+2020-10-05T17:25:00.0,1.2690995855999996
+2020-10-05T17:30:00.0,1.2690995855999996
+2020-10-05T17:35:00.0,1.2690995855999996
+2020-10-05T17:40:00.0,1.2690995855999996
+2020-10-05T17:45:00.0,1.2690995855999996
+2020-10-05T17:50:00.0,1.2690995855999996
+2020-10-05T17:55:00.0,1.2690995855999996
+2020-10-05T18:00:00.0,1.2690995855999996
+2020-10-05T18:05:00.0,1.2690995855999996
+2020-10-05T18:10:00.0,1.2690995855999996
+2020-10-05T18:15:00.0,1.2690995855999996
+2020-10-05T18:20:00.0,1.2690995855999996
+2020-10-05T18:25:00.0,1.2690995855999996
+2020-10-05T18:30:00.0,1.2690995855999996
+2020-10-05T18:35:00.0,1.2690995855999996
+2020-10-05T18:40:00.0,1.2690995855999996
+2020-10-05T18:45:00.0,1.2690995855999996
+2020-10-05T18:50:00.0,1.2690995855999996
+2020-10-05T18:55:00.0,1.2690995855999996
+2020-10-05T19:00:00.0,1.2690995855999996
+2020-10-05T19:05:00.0,1.2690995855999996
+2020-10-05T19:10:00.0,1.2690995855999996
+2020-10-05T19:15:00.0,1.2690995855999996
+2020-10-05T19:20:00.0,1.2690995855999996
+2020-10-05T19:25:00.0,1.2690995855999996
+2020-10-05T19:30:00.0,1.2690995855999996
+2020-10-05T19:35:00.0,1.2690995855999996
+2020-10-05T19:40:00.0,1.2690995855999996
+2020-10-05T19:45:00.0,1.2690995855999996
+2020-10-05T19:50:00.0,1.2690995855999996
+2020-10-05T19:55:00.0,1.2690995855999996
+2020-10-05T20:00:00.0,1.2111022632000001
+2020-10-05T20:05:00.0,1.2111022632000001
+2020-10-05T20:10:00.0,1.2111022632000001
+2020-10-05T20:15:00.0,1.2111022632000001
+2020-10-05T20:20:00.0,1.2111022632000001
+2020-10-05T20:25:00.0,1.2111022632000001
+2020-10-05T20:30:00.0,1.2111022632000001
+2020-10-05T20:35:00.0,1.2111022632000001
+2020-10-05T20:40:00.0,1.2111022632000001
+2020-10-05T20:45:00.0,1.2111022632000001
+2020-10-05T20:50:00.0,1.2111022632000001
+2020-10-05T20:55:00.0,1.2111022632000001
+2020-10-05T21:00:00.0,1.0909879591999998
+2020-10-05T21:05:00.0,1.0909879591999998
+2020-10-05T21:10:00.0,1.0909879591999998
+2020-10-05T21:15:00.0,1.0909879591999998
+2020-10-05T21:20:00.0,1.0909879591999998
+2020-10-05T21:25:00.0,1.0909879591999998
+2020-10-05T21:30:00.0,1.0909879591999998
+2020-10-05T21:35:00.0,1.0909879591999998
+2020-10-05T21:40:00.0,1.0909879591999998
+2020-10-05T21:45:00.0,1.0909879591999998
+2020-10-05T21:50:00.0,1.0909879591999998
+2020-10-05T21:55:00.0,1.0909879591999998
+2020-10-05T22:00:00.0,1.0571683512
+2020-10-05T22:05:00.0,1.0571683512
+2020-10-05T22:10:00.0,1.0571683512
+2020-10-05T22:15:00.0,1.0571683512
+2020-10-05T22:20:00.0,1.0571683512
+2020-10-05T22:25:00.0,1.0571683512
+2020-10-05T22:30:00.0,1.0571683512
+2020-10-05T22:35:00.0,1.0571683512
+2020-10-05T22:40:00.0,1.0571683512
+2020-10-05T22:45:00.0,1.0571683512
+2020-10-05T22:50:00.0,1.0571683512
+2020-10-05T22:55:00.0,1.0571683512
+2020-10-05T23:00:00.0,0.9463064835999992
+2020-10-05T23:05:00.0,0.9463064835999992
+2020-10-05T23:10:00.0,0.9463064835999992
+2020-10-05T23:15:00.0,0.9463064835999992
+2020-10-05T23:20:00.0,0.9463064835999992
+2020-10-05T23:25:00.0,0.9463064835999992
+2020-10-05T23:30:00.0,0.9463064835999992
+2020-10-05T23:35:00.0,0.9463064835999992
+2020-10-05T23:40:00.0,0.9463064835999992
+2020-10-05T23:45:00.0,0.9463064835999992
+2020-10-05T23:50:00.0,0.9463064835999992
+2020-10-05T23:55:00.0,0.9463064835999992
diff --git a/test/inputs/chuhsi_Spin_prices_5min_300.csv b/test/inputs/chuhsi_Spin_prices_5min_300.csv
new file mode 100644
index 00000000..4a6a4de1
--- /dev/null
+++ b/test/inputs/chuhsi_Spin_prices_5min_300.csv
@@ -0,0 +1,301 @@
+DateTime,Chuhsi
+2020-10-03T00:00:00.0,1.2111022632000001
+2020-10-03T00:05:00.0,1.2111022632000001
+2020-10-03T00:10:00.0,1.2111022632000001
+2020-10-03T00:15:00.0,1.2111022632000001
+2020-10-03T00:20:00.0,1.2111022632000001
+2020-10-03T00:25:00.0,1.2111022632000001
+2020-10-03T00:30:00.0,1.2111022632000001
+2020-10-03T00:35:00.0,1.2111022632000001
+2020-10-03T00:40:00.0,1.2111022632000001
+2020-10-03T00:45:00.0,1.2111022632000001
+2020-10-03T00:50:00.0,1.2111022632000001
+2020-10-03T00:55:00.0,1.2111022632000001
+2020-10-03T01:00:00.0,1.110190032000001
+2020-10-03T01:05:00.0,1.110190032000001
+2020-10-03T01:10:00.0,1.110190032000001
+2020-10-03T01:15:00.0,1.110190032000001
+2020-10-03T01:20:00.0,1.110190032000001
+2020-10-03T01:25:00.0,1.110190032000001
+2020-10-03T01:30:00.0,1.110190032000001
+2020-10-03T01:35:00.0,1.110190032000001
+2020-10-03T01:40:00.0,1.110190032000001
+2020-10-03T01:45:00.0,1.110190032000001
+2020-10-03T01:50:00.0,1.110190032000001
+2020-10-03T01:55:00.0,1.110190032000001
+2020-10-03T02:00:00.0,1.1101900320000009
+2020-10-03T02:05:00.0,1.1101900320000009
+2020-10-03T02:10:00.0,1.1101900320000009
+2020-10-03T02:15:00.0,1.1101900320000009
+2020-10-03T02:20:00.0,1.1101900320000009
+2020-10-03T02:25:00.0,1.1101900320000009
+2020-10-03T02:30:00.0,1.1101900320000009
+2020-10-03T02:35:00.0,1.1101900320000009
+2020-10-03T02:40:00.0,1.1101900320000009
+2020-10-03T02:45:00.0,1.1101900320000009
+2020-10-03T02:50:00.0,1.1101900320000009
+2020-10-03T02:55:00.0,1.1101900320000009
+2020-10-03T03:00:00.0,1.0716288096000006
+2020-10-03T03:05:00.0,1.0716288096000006
+2020-10-03T03:10:00.0,1.0716288096000006
+2020-10-03T03:15:00.0,1.0716288096000006
+2020-10-03T03:20:00.0,1.0716288096000006
+2020-10-03T03:25:00.0,1.0716288096000006
+2020-10-03T03:30:00.0,1.0716288096000006
+2020-10-03T03:35:00.0,1.0716288096000006
+2020-10-03T03:40:00.0,1.0716288096000006
+2020-10-03T03:45:00.0,1.0716288096000006
+2020-10-03T03:50:00.0,1.0716288096000006
+2020-10-03T03:55:00.0,1.0716288096000006
+2020-10-03T04:00:00.0,1.0716288096000006
+2020-10-03T04:05:00.0,1.0716288096000006
+2020-10-03T04:10:00.0,1.0716288096000006
+2020-10-03T04:15:00.0,1.0716288096000006
+2020-10-03T04:20:00.0,1.0716288096000006
+2020-10-03T04:25:00.0,1.0716288096000006
+2020-10-03T04:30:00.0,1.0716288096000006
+2020-10-03T04:35:00.0,1.0716288096000006
+2020-10-03T04:40:00.0,1.0716288096000006
+2020-10-03T04:45:00.0,1.0716288096000006
+2020-10-03T04:50:00.0,1.0716288096000006
+2020-10-03T04:55:00.0,1.0716288096000006
+2020-10-03T05:00:00.0,1.0702294104000003
+2020-10-03T05:05:00.0,1.0702294104000003
+2020-10-03T05:10:00.0,1.0702294104000003
+2020-10-03T05:15:00.0,1.0702294104000003
+2020-10-03T05:20:00.0,1.0702294104000003
+2020-10-03T05:25:00.0,1.0702294104000003
+2020-10-03T05:30:00.0,1.0702294104000003
+2020-10-03T05:35:00.0,1.0702294104000003
+2020-10-03T05:40:00.0,1.0702294104000003
+2020-10-03T05:45:00.0,1.0702294104000003
+2020-10-03T05:50:00.0,1.0702294104000003
+2020-10-03T05:55:00.0,1.0702294104000003
+2020-10-03T06:00:00.0,0.6
+2020-10-03T06:05:00.0,0.6
+2020-10-03T06:10:00.0,0.6
+2020-10-03T06:15:00.0,0.6
+2020-10-03T06:20:00.0,0.6
+2020-10-03T06:25:00.0,0.6
+2020-10-03T06:30:00.0,0.6
+2020-10-03T06:35:00.0,0.6
+2020-10-03T06:40:00.0,0.6
+2020-10-03T06:45:00.0,0.6
+2020-10-03T06:50:00.0,0.6
+2020-10-03T06:55:00.0,0.6
+2020-10-03T07:00:00.0,0.6
+2020-10-03T07:05:00.0,0.6
+2020-10-03T07:10:00.0,0.6
+2020-10-03T07:15:00.0,0.6
+2020-10-03T07:20:00.0,0.6
+2020-10-03T07:25:00.0,0.6
+2020-10-03T07:30:00.0,0.6
+2020-10-03T07:35:00.0,0.6
+2020-10-03T07:40:00.0,0.6
+2020-10-03T07:45:00.0,0.6
+2020-10-03T07:50:00.0,0.6
+2020-10-03T07:55:00.0,0.6
+2020-10-03T08:00:00.0,0.6
+2020-10-03T08:05:00.0,0.6
+2020-10-03T08:10:00.0,0.6
+2020-10-03T08:15:00.0,0.6
+2020-10-03T08:20:00.0,0.6
+2020-10-03T08:25:00.0,0.6
+2020-10-03T08:30:00.0,0.6
+2020-10-03T08:35:00.0,0.6
+2020-10-03T08:40:00.0,0.6
+2020-10-03T08:45:00.0,0.6
+2020-10-03T08:50:00.0,0.6
+2020-10-03T08:55:00.0,0.6
+2020-10-03T09:00:00.0,0.6
+2020-10-03T09:05:00.0,0.6
+2020-10-03T09:10:00.0,0.6
+2020-10-03T09:15:00.0,0.6
+2020-10-03T09:20:00.0,0.6
+2020-10-03T09:25:00.0,0.6
+2020-10-03T09:30:00.0,0.6
+2020-10-03T09:35:00.0,0.6
+2020-10-03T09:40:00.0,0.6
+2020-10-03T09:45:00.0,0.6
+2020-10-03T09:50:00.0,0.6
+2020-10-03T09:55:00.0,0.6
+2020-10-03T10:00:00.0,0.7544407511999993
+2020-10-03T10:05:00.0,0.7544407511999993
+2020-10-03T10:10:00.0,0.7544407511999993
+2020-10-03T10:15:00.0,0.7544407511999993
+2020-10-03T10:20:00.0,0.7544407511999993
+2020-10-03T10:25:00.0,0.7544407511999993
+2020-10-03T10:30:00.0,0.7544407511999993
+2020-10-03T10:35:00.0,0.7544407511999993
+2020-10-03T10:40:00.0,0.7544407511999993
+2020-10-03T10:45:00.0,0.7544407511999993
+2020-10-03T10:50:00.0,0.7544407511999993
+2020-10-03T10:55:00.0,0.7544407511999993
+2020-10-03T11:00:00.0,0.7993418987999996
+2020-10-03T11:05:00.0,0.7993418987999996
+2020-10-03T11:10:00.0,0.7993418987999996
+2020-10-03T11:15:00.0,0.7993418987999996
+2020-10-03T11:20:00.0,0.7993418987999996
+2020-10-03T11:25:00.0,0.7993418987999996
+2020-10-03T11:30:00.0,0.7993418987999996
+2020-10-03T11:35:00.0,0.7993418987999996
+2020-10-03T11:40:00.0,0.7993418987999996
+2020-10-03T11:45:00.0,0.7993418987999996
+2020-10-03T11:50:00.0,0.7993418987999996
+2020-10-03T11:55:00.0,0.7993418987999996
+2020-10-03T12:00:00.0,0.8167611763999997
+2020-10-03T12:05:00.0,0.8167611763999997
+2020-10-03T12:10:00.0,0.8167611763999997
+2020-10-03T12:15:00.0,0.8167611763999997
+2020-10-03T12:20:00.0,0.8167611763999997
+2020-10-03T12:25:00.0,0.8167611763999997
+2020-10-03T12:30:00.0,0.8167611763999997
+2020-10-03T12:35:00.0,0.8167611763999997
+2020-10-03T12:40:00.0,0.8167611763999997
+2020-10-03T12:45:00.0,0.8167611763999997
+2020-10-03T12:50:00.0,0.8167611763999997
+2020-10-03T12:55:00.0,0.8167611763999997
+2020-10-03T13:00:00.0,0.8658903040000002
+2020-10-03T13:05:00.0,0.8658903040000002
+2020-10-03T13:10:00.0,0.8658903040000002
+2020-10-03T13:15:00.0,0.8658903040000002
+2020-10-03T13:20:00.0,0.8658903040000002
+2020-10-03T13:25:00.0,0.8658903040000002
+2020-10-03T13:30:00.0,0.8658903040000002
+2020-10-03T13:35:00.0,0.8658903040000002
+2020-10-03T13:40:00.0,0.8658903040000002
+2020-10-03T13:45:00.0,0.8658903040000002
+2020-10-03T13:50:00.0,0.8658903040000002
+2020-10-03T13:55:00.0,0.8658903040000002
+2020-10-03T14:00:00.0,0.9092985024000003
+2020-10-03T14:05:00.0,0.9092985024000003
+2020-10-03T14:10:00.0,0.9092985024000003
+2020-10-03T14:15:00.0,0.9092985024000003
+2020-10-03T14:20:00.0,0.9092985024000003
+2020-10-03T14:25:00.0,0.9092985024000003
+2020-10-03T14:30:00.0,0.9092985024000003
+2020-10-03T14:35:00.0,0.9092985024000003
+2020-10-03T14:40:00.0,0.9092985024000003
+2020-10-03T14:45:00.0,0.9092985024000003
+2020-10-03T14:50:00.0,0.9092985024000003
+2020-10-03T14:55:00.0,0.9092985024000003
+2020-10-03T15:00:00.0,0.9227989148000002
+2020-10-03T15:05:00.0,0.9227989148000002
+2020-10-03T15:10:00.0,0.9227989148000002
+2020-10-03T15:15:00.0,0.9227989148000002
+2020-10-03T15:20:00.0,0.9227989148000002
+2020-10-03T15:25:00.0,0.9227989148000002
+2020-10-03T15:30:00.0,0.9227989148000002
+2020-10-03T15:35:00.0,0.9227989148000002
+2020-10-03T15:40:00.0,0.9227989148000002
+2020-10-03T15:45:00.0,0.9227989148000002
+2020-10-03T15:50:00.0,0.9227989148000002
+2020-10-03T15:55:00.0,0.9227989148000002
+2020-10-03T16:00:00.0,1.0738056528000002
+2020-10-03T16:05:00.0,1.0738056528000002
+2020-10-03T16:10:00.0,1.0738056528000002
+2020-10-03T16:15:00.0,1.0738056528000002
+2020-10-03T16:20:00.0,1.0738056528000002
+2020-10-03T16:25:00.0,1.0738056528000002
+2020-10-03T16:30:00.0,1.0738056528000002
+2020-10-03T16:35:00.0,1.0738056528000002
+2020-10-03T16:40:00.0,1.0738056528000002
+2020-10-03T16:45:00.0,1.0738056528000002
+2020-10-03T16:50:00.0,1.0738056528000002
+2020-10-03T16:55:00.0,1.0738056528000002
+2020-10-03T17:00:00.0,1.110190032000001
+2020-10-03T17:05:00.0,1.110190032000001
+2020-10-03T17:10:00.0,1.110190032000001
+2020-10-03T17:15:00.0,1.110190032000001
+2020-10-03T17:20:00.0,1.110190032000001
+2020-10-03T17:25:00.0,1.110190032000001
+2020-10-03T17:30:00.0,1.110190032000001
+2020-10-03T17:35:00.0,1.110190032000001
+2020-10-03T17:40:00.0,1.110190032000001
+2020-10-03T17:45:00.0,1.110190032000001
+2020-10-03T17:50:00.0,1.110190032000001
+2020-10-03T17:55:00.0,1.110190032000001
+2020-10-03T18:00:00.0,1.2984869687999991
+2020-10-03T18:05:00.0,1.2984869687999991
+2020-10-03T18:10:00.0,1.2984869687999991
+2020-10-03T18:15:00.0,1.2984869687999991
+2020-10-03T18:20:00.0,1.2984869687999991
+2020-10-03T18:25:00.0,1.2984869687999991
+2020-10-03T18:30:00.0,1.2984869687999991
+2020-10-03T18:35:00.0,1.2984869687999991
+2020-10-03T18:40:00.0,1.2984869687999991
+2020-10-03T18:45:00.0,1.2984869687999991
+2020-10-03T18:50:00.0,1.2984869687999991
+2020-10-03T18:55:00.0,1.2984869687999991
+2020-10-03T19:00:00.0,1.110190032000001
+2020-10-03T19:05:00.0,1.110190032000001
+2020-10-03T19:10:00.0,1.110190032000001
+2020-10-03T19:15:00.0,1.110190032000001
+2020-10-03T19:20:00.0,1.110190032000001
+2020-10-03T19:25:00.0,1.110190032000001
+2020-10-03T19:30:00.0,1.110190032000001
+2020-10-03T19:35:00.0,1.110190032000001
+2020-10-03T19:40:00.0,1.110190032000001
+2020-10-03T19:45:00.0,1.110190032000001
+2020-10-03T19:50:00.0,1.110190032000001
+2020-10-03T19:55:00.0,1.110190032000001
+2020-10-03T20:00:00.0,1.0702294104
+2020-10-03T20:05:00.0,1.0702294104
+2020-10-03T20:10:00.0,1.0702294104
+2020-10-03T20:15:00.0,1.0702294104
+2020-10-03T20:20:00.0,1.0702294104
+2020-10-03T20:25:00.0,1.0702294104
+2020-10-03T20:30:00.0,1.0702294104
+2020-10-03T20:35:00.0,1.0702294104
+2020-10-03T20:40:00.0,1.0702294104
+2020-10-03T20:45:00.0,1.0702294104
+2020-10-03T20:50:00.0,1.0702294104
+2020-10-03T20:55:00.0,1.0702294104
+2020-10-03T21:00:00.0,0.9251583599999996
+2020-10-03T21:05:00.0,0.9251583599999996
+2020-10-03T21:10:00.0,0.9251583599999996
+2020-10-03T21:15:00.0,0.9251583599999996
+2020-10-03T21:20:00.0,0.9251583599999996
+2020-10-03T21:25:00.0,0.9251583599999996
+2020-10-03T21:30:00.0,0.9251583599999996
+2020-10-03T21:35:00.0,0.9251583599999996
+2020-10-03T21:40:00.0,0.9251583599999996
+2020-10-03T21:45:00.0,0.9251583599999996
+2020-10-03T21:50:00.0,0.9251583599999996
+2020-10-03T21:55:00.0,0.9251583599999996
+2020-10-03T22:00:00.0,0.8446658443999995
+2020-10-03T22:05:00.0,0.8446658443999995
+2020-10-03T22:10:00.0,0.8446658443999995
+2020-10-03T22:15:00.0,0.8446658443999995
+2020-10-03T22:20:00.0,0.8446658443999995
+2020-10-03T22:25:00.0,0.8446658443999995
+2020-10-03T22:30:00.0,0.8446658443999995
+2020-10-03T22:35:00.0,0.8446658443999995
+2020-10-03T22:40:00.0,0.8446658443999995
+2020-10-03T22:45:00.0,0.8446658443999995
+2020-10-03T22:50:00.0,0.8446658443999995
+2020-10-03T22:55:00.0,0.8446658443999995
+2020-10-03T23:00:00.0,0.8160001399999995
+2020-10-03T23:05:00.0,0.8160001399999995
+2020-10-03T23:10:00.0,0.8160001399999995
+2020-10-03T23:15:00.0,0.8160001399999995
+2020-10-03T23:20:00.0,0.8160001399999995
+2020-10-03T23:25:00.0,0.8160001399999995
+2020-10-03T23:30:00.0,0.8160001399999995
+2020-10-03T23:35:00.0,0.8160001399999995
+2020-10-03T23:40:00.0,0.8160001399999995
+2020-10-03T23:45:00.0,0.8160001399999995
+2020-10-03T23:50:00.0,0.8160001399999995
+2020-10-03T23:55:00.0,0.8160001399999995
+2020-10-04T00:00:00.0,0.7993418987999996
+2020-10-04T00:05:00.0,0.7993418987999996
+2020-10-04T00:10:00.0,0.7993418987999996
+2020-10-04T00:15:00.0,0.7993418987999996
+2020-10-04T00:20:00.0,0.7993418987999996
+2020-10-04T00:25:00.0,0.7993418987999996
+2020-10-04T00:30:00.0,0.7993418987999996
+2020-10-04T00:35:00.0,0.7993418987999996
+2020-10-04T00:40:00.0,0.7993418987999996
+2020-10-04T00:45:00.0,0.7993418987999996
+2020-10-04T00:50:00.0,0.7993418987999996
+2020-10-04T00:55:00.0,0.7993418987999996
diff --git a/test/test_merchant_cooptimizer.jl b/test/test_merchant_cooptimizer.jl
index 49d8284c..d744fb4d 100644
--- a/test/test_merchant_cooptimizer.jl
+++ b/test/test_merchant_cooptimizer.jl
@@ -1,39 +1,34 @@
function _run_cooptimizer_case(with_services::Bool)
horizon_merchant_rt = 288
horizon_merchant_da = 24
+ injection_steps = max(horizon_merchant_rt, 300)
sys = PSB.build_RTS_GMLC_RT_sys(;
raw_data = PSB.RTS_DIR,
- horizon = horizon_merchant_rt,
- interval = Hour(24),
+ horizon = horizon_merchant_da,
+ interval = Hour(1),
)
modify_ren_curtailment_cost!(sys)
add_hybrid_to_chuhsi_bus!(sys; horizon_rt_steps = horizon_merchant_rt)
- sys.internal.ext = Dict{String, DataFrame}()
- dic = PSY.get_ext(sys)
- bus_name = "chuhsi"
- dic["λ_da_df"] =
- CSV.read(joinpath(TEST_DIR, "inputs/$(bus_name)_DA_prices.csv"), DataFrame)
- dic["λ_rt_df"] =
- CSV.read(joinpath(TEST_DIR, "inputs/$(bus_name)_RT_prices.csv"), DataFrame)
- dic["λ_Reg_Up"] =
- CSV.read(joinpath(TEST_DIR, "inputs/$(bus_name)_RegUp_prices.csv"), DataFrame)
- dic["λ_Reg_Down"] =
- CSV.read(joinpath(TEST_DIR, "inputs/$(bus_name)_RegDown_prices.csv"), DataFrame)
- dic["λ_Spin_Up_R3"] =
- CSV.read(joinpath(TEST_DIR, "inputs/$(bus_name)_Spin_prices.csv"), DataFrame)
- dic["horizon_RT"] = horizon_merchant_rt
- dic["horizon_DA"] = horizon_merchant_da
-
hy_sys = first(get_components(HybridSystem, sys))
- ts_rt =
- PSY.get_time_series(
- IS.DeterministicSingleTimeSeries,
- hy_sys,
- "RenewableDispatch__max_active_power",
- )
- @test IS.get_horizon(ts_rt) >= horizon_merchant_rt * IS.get_resolution(ts_rt)
+ attach_hybrid_market_time_series!(
+ sys,
+ hy_sys;
+ bus_name = "chuhsi",
+ attach_services = true,
+ rt_steps = horizon_merchant_rt,
+ da_steps = horizon_merchant_rt,
+ injection_rt_steps = injection_steps,
+ use_rt_resolution_for_da = true,
+ )
+ strip_non_hybrid_single_time_series!(sys)
+ ts_rt = PSY.get_time_series(
+ IS.SingleTimeSeries,
+ hy_sys,
+ "RenewableDispatch__max_active_power",
+ )
+ @test length(timestamp(IS.get_data(ts_rt))) >= horizon_merchant_rt
if with_services
services = get_components(VariableReserve, sys)
@@ -48,8 +43,6 @@ function _run_cooptimizer_case(with_services::Bool)
end
end
end
- PSY.set_ext!(hy_sys, deepcopy(dic))
-
template = ProblemTemplate(CopperPlatePowerModel)
set_device_model!(template, DeviceModel(PSY.HybridSystem, HybridDispatchWithReserves))
decision_optimizer_DA = DecisionModel(
@@ -58,9 +51,12 @@ function _run_cooptimizer_case(with_services::Bool)
sys;
optimizer = HiGHS_optimizer,
calculate_conflict = true,
- optimizer_solve_log_print = true,
+ optimizer_solve_log_print = false,
store_variable_names = true,
initial_time = DateTime("2020-10-03T00:00:00"),
+ resolution = Minute(5),
+ interval = Minute(5),
+ horizon = Hour(24),
name = "MerchantHybridCooptimizerCase_DA",
)
@@ -71,7 +67,7 @@ function _run_cooptimizer_case(with_services::Bool)
var_results = results.variable_values
rt_bid_out = read_variable(results, "EnergyRTBidOut__HybridSystem")
da_bid_out = var_results[PSI.VariableKey{HSS.EnergyDABidOut, HybridSystem}("")]
- @test length(da_bid_out[!, 1]) == 24
+ @test length(da_bid_out[!, 1]) == horizon_merchant_rt
@test length(rt_bid_out[!, 1]) == 288
if with_services
regup_bid_out =
@@ -81,7 +77,7 @@ function _run_cooptimizer_case(with_services::Bool)
}(
"Reg_Up",
)]
- @test length(regup_bid_out[!, 1]) == 24
+ @test length(regup_bid_out[!, 1]) == horizon_merchant_rt
end
end
diff --git a/test/test_merchant_only_energy.jl b/test/test_merchant_only_energy.jl
index 16342932..f7405d9d 100644
--- a/test/test_merchant_only_energy.jl
+++ b/test/test_merchant_only_energy.jl
@@ -1,37 +1,32 @@
function _run_only_energy_case(horizon_merchant_rt::Int, horizon_merchant_da::Int)
+ injection_steps = max(horizon_merchant_rt, 300)
sys = PSB.build_RTS_GMLC_RT_sys(;
raw_data = PSB.RTS_DIR,
- horizon = horizon_merchant_rt,
- interval = Hour(24),
+ horizon = horizon_merchant_da,
+ interval = Hour(1),
)
modify_ren_curtailment_cost!(sys)
add_hybrid_to_chuhsi_bus!(sys; horizon_rt_steps = horizon_merchant_rt)
- sys.internal.ext = Dict{String, DataFrame}()
- dic = PSY.get_ext(sys)
- bus_name = "chuhsi"
- dic["λ_da_df"] =
- CSV.read(joinpath(TEST_DIR, "inputs/$(bus_name)_DA_prices.csv"), DataFrame)
- dic["λ_rt_df"] =
- CSV.read(joinpath(TEST_DIR, "inputs/$(bus_name)_RT_prices.csv"), DataFrame)
- dic["horizon_RT"] = horizon_merchant_rt
- dic["horizon_DA"] = horizon_merchant_da
-
hy_sys = first(get_components(HybridSystem, sys))
- PSY.set_ext!(hy_sys, deepcopy(dic))
- ts_da = PSY.get_time_series(
+ attach_hybrid_market_time_series!(
+ sys,
+ hy_sys;
+ bus_name = "chuhsi",
+ attach_services = false,
+ rt_steps = horizon_merchant_rt,
+ da_steps = horizon_merchant_rt,
+ injection_rt_steps = injection_steps,
+ use_rt_resolution_for_da = true,
+ )
+ strip_non_hybrid_single_time_series!(sys)
+ ts_rt = PSY.get_time_series(
IS.SingleTimeSeries,
hy_sys,
- "RenewableDispatch__max_active_power_da",
+ "RenewableDispatch__max_active_power",
)
- ts_rt =
- PSY.get_time_series(
- IS.DeterministicSingleTimeSeries,
- hy_sys,
- "RenewableDispatch__max_active_power",
- )
- @test !isnothing(ts_da)
- @test IS.get_horizon(ts_rt) >= horizon_merchant_rt * IS.get_resolution(ts_rt)
+ @test !isnothing(ts_rt)
+ @test length(timestamp(IS.get_data(ts_rt))) >= horizon_merchant_rt
template = ProblemTemplate(CopperPlatePowerModel)
set_device_model!(template, DeviceModel(PSY.HybridSystem, HybridEnergyOnlyDispatch))
@@ -43,6 +38,9 @@ function _run_only_energy_case(horizon_merchant_rt::Int, horizon_merchant_da::In
calculate_conflict = true,
store_variable_names = true,
initial_time = DateTime("2020-10-03T00:00:00"),
+ resolution = Minute(5),
+ interval = Minute(5),
+ horizon = Hour(horizon_merchant_da),
name = "MerchantHybridEnergyCase_DA",
)
@@ -53,7 +51,7 @@ function _run_only_energy_case(horizon_merchant_rt::Int, horizon_merchant_da::In
var_results = results.variable_values
rt_bid_out = read_variable(results, "EnergyRTBidOut__HybridSystem")
da_bid_out = var_results[PSI.VariableKey{HSS.EnergyDABidOut, HybridSystem}("")]
- @test length(da_bid_out[!, 1]) == horizon_merchant_da
+ @test length(da_bid_out[!, 1]) == horizon_merchant_rt
@test length(rt_bid_out[!, 1]) == horizon_merchant_rt
end
diff --git a/test/test_merchant_sequence.jl b/test/test_merchant_sequence.jl
index bbdd81b7..078b1434 100644
--- a/test/test_merchant_sequence.jl
+++ b/test/test_merchant_sequence.jl
@@ -1,26 +1,30 @@
@testset "Test HybridSystem Merchant Optimizer Sequence Build" begin
+ # Fast dev loop: skip `execute!` (UC + merchant solves dominate runtime).
+ # HSS_MERCHANT_SEQUENCE_FAST=1 julia --project=. -e 'using Pkg; Pkg.test(; test_args=["test_merchant_sequence"])'
+ merchant_seq_fast_env =
+ lowercase(get(ENV, "HSS_MERCHANT_SEQUENCE_FAST", "0")) in ("1", "true", "yes")
+
sys_rts_da = PSB.build_RTS_GMLC_DA_sys(; raw_data = PSB.RTS_DIR, horizon = 24)
+ # Forecast horizon (hours) must match `DecisionModel` so device DST and the model agree.
sys_rts_rt = PSB.build_RTS_GMLC_RT_sys(;
raw_data = PSB.RTS_DIR,
- horizon = 288,
+ horizon = 24,
interval = Hour(1),
)
modify_ren_curtailment_cost!(sys_rts_rt)
add_hybrid_to_chuhsi_bus!(sys_rts_rt; horizon_rt_steps = 288)
-
- bus_name = "chuhsi"
- sys_rts_rt.internal.ext = Dict{String, DataFrame}()
- dic = get_ext(sys_rts_rt)
- dic["λ_da_df"] =
- CSV.read(joinpath(TEST_DIR, "inputs/$(bus_name)_DA_prices.csv"), DataFrame)
- dic["λ_rt_df"] =
- CSV.read(joinpath(TEST_DIR, "inputs/$(bus_name)_RT_prices.csv"), DataFrame)
- dic["horizon_RT"] = 288
- dic["horizon_DA"] = 24
-
hy_sys = first(get_components(HybridSystem, sys_rts_rt))
- PSY.set_ext!(hy_sys, deepcopy(dic))
+ attach_hybrid_market_time_series!(
+ sys_rts_rt,
+ hy_sys;
+ bus_name = "chuhsi",
+ rt_steps = 288,
+ da_steps = 288,
+ injection_rt_steps = max(288, 300),
+ use_rt_resolution_for_da = true,
+ )
+ strip_non_hybrid_single_time_series!(sys_rts_rt)
template = ProblemTemplate(CopperPlatePowerModel)
set_device_model!(template, DeviceModel(PSY.HybridSystem, HybridEnergyOnlyDispatch))
@@ -38,17 +42,23 @@
name = "MerchantHybridEnergyCase_Sequence",
)
+ # One simulation step: hybrid merchant fixtures leave a single forecast window that satisfies
+ # PowerSimulations `_check_steps` for this RT system (multi-step runs need longer horizons).
sim_optimizer = build_simulation_case_optimizer(
get_uc_dcp_template(),
decision_optimizer,
sys_rts_da,
sys_rts_rt,
- 2,
+ 1,
0.01,
DateTime("2020-10-03T00:00:00"),
)
@test build!(sim_optimizer) == PSI.SimulationBuildStatus.BUILT
- @test execute!(sim_optimizer; enable_progress_bar = false) ==
- PSI.RunStatus.SUCCESSFULLY_FINALIZED
+ if merchant_seq_fast_env
+ @info "HSS_MERCHANT_SEQUENCE_FAST: skipping execute! — unset env for full simulation run."
+ else
+ @test execute!(sim_optimizer; enable_progress_bar = false) ==
+ PSI.RunStatus.SUCCESSFULLY_FINALIZED
+ end
end
diff --git a/test/test_utils/function_utils.jl b/test/test_utils/function_utils.jl
index a90f005a..67880ae0 100644
--- a/test/test_utils/function_utils.jl
+++ b/test/test_utils/function_utils.jl
@@ -76,53 +76,404 @@ function add_hybrid_to_chuhsi_bus!(
)
# Add Hybrid (add_component! internally copies subcomponent time series to hybrid)
add_component!(sys, hybrid)
- # Ensure DA-named time series exists so merchant decision models that request
- # "RenewableDispatch__max_active_power_da" (DA path) find metadata on the hybrid.
- _add_hybrid_renewable_da_time_series!(sys, hybrid; horizon_rt_steps = horizon_rt_steps)
return
end
-function _add_hybrid_renewable_da_time_series!(
+function _build_scalar_time_series_from_csv(
+ path::String,
+ bus_name::String,
+ ts_name::String;
+ max_rows::Union{Nothing, Int} = nothing,
+)
+ df = CSV.read(path, DataFrame)
+ if max_rows !== nothing && nrow(df) > max_rows
+ df = df[1:max_rows, :]
+ end
+ col_match = findfirst(n -> lowercase(String(n)) == lowercase(bus_name), names(df))
+ isnothing(col_match) && error("No price column found for bus $(bus_name) in $(path)")
+ bus_col = names(df)[col_match]
+ timestamps = collect(df[!, "DateTime"])
+ values = collect(Float64, df[!, bus_col])
+ ts = TimeArray(timestamps, values, ["value"])
+ return PSY.SingleTimeSeries(ts_name, ts)
+end
+
+function _validate_scalar_series_contract(
+ ts::PSY.SingleTimeSeries;
+ min_length::Union{Nothing, Int} = nothing,
+ expected_resolution = nothing,
+ expected_start = nothing,
+ label::String = "series",
+)
+ data = IS.get_data(ts)
+ timestamps = collect(getfield(data, :timestamp))
+ values = getfield(data, :values)
+ n = size(values, 1)
+ if !isnothing(min_length) && n < min_length
+ error("$(label) has length $(n), but at least $(min_length) points are required")
+ end
+ if !isnothing(expected_resolution) && IS.get_resolution(ts) != expected_resolution
+ error(
+ "$(label) has resolution $(IS.get_resolution(ts)); expected $(expected_resolution). " *
+ "Update the fixture CSV to be directly PSY-ingestible.",
+ )
+ end
+ if !isnothing(expected_start) && first(timestamps) != expected_start
+ error("$(label) starts at $(first(timestamps)); expected $(expected_start)")
+ end
+ return
+end
+
+function _select_price_fixture(base_name::String, n_steps::Int)
+ dir = joinpath(TEST_DIR, "inputs")
+ short = joinpath(dir, base_name * "_300.csv")
+ # Prefer the 300-row harness when it covers the horizon so global `transform_single_time_series!`
+ # sees consistent metadata; `_build_scalar_time_series_from_csv(...; max_rows=...)` truncates
+ # (e.g. 300 → 288) so DA bid indices match RT steps without mixing full-RTS CSV counts.
+ if n_steps <= 300 && isfile(short)
+ return short
+ end
+ return joinpath(dir, base_name * ".csv")
+end
+
+function attach_hybrid_market_time_series!(
sys::PSY.System,
hybrid::PSY.HybridSystem;
- horizon_rt_steps::Union{Nothing, Int} = nothing,
+ bus_name::String = "chuhsi",
+ attach_services::Bool = false,
+ rt_steps::Int = 288,
+ da_steps::Int = 24,
+ injection_rt_steps::Int = rt_steps,
+ use_rt_resolution_for_da::Bool = false,
)
- try
- ts = PSY.get_time_series(
+ rt_price = _build_scalar_time_series_from_csv(
+ _select_price_fixture("$(bus_name)_RT_prices", rt_steps),
+ bus_name,
+ HSS.hybrid_energy_price_time_series_name(HSS.REAL_TIME_TIME_SERIES_KEY);
+ max_rows = rt_steps,
+ )
+ _validate_scalar_series_contract(
+ rt_price;
+ min_length = rt_steps,
+ label = "RT market price fixture",
+ )
+ rt_data = IS.get_data(rt_price)
+ rt_resolution = IS.get_resolution(rt_price)
+ expected_da_res = use_rt_resolution_for_da ? rt_resolution : Dates.Hour(1)
+ da_file = if use_rt_resolution_for_da
+ _select_price_fixture("$(bus_name)_DA_prices_5min", rt_steps)
+ else
+ joinpath(TEST_DIR, "inputs/$(bus_name)_DA_prices.csv")
+ end
+ da_price = _build_scalar_time_series_from_csv(
+ da_file,
+ bus_name,
+ HSS.hybrid_energy_price_time_series_name(HSS.DAY_AHEAD_TIME_SERIES_KEY);
+ max_rows = use_rt_resolution_for_da ? rt_steps : da_steps,
+ )
+ _validate_scalar_series_contract(
+ da_price;
+ min_length = use_rt_resolution_for_da ? rt_steps : da_steps,
+ expected_resolution = expected_da_res,
+ expected_start = first(getfield(rt_data, :timestamp)),
+ label = "DA market price fixture",
+ )
+ PSY.add_time_series!(sys, hybrid, da_price)
+ PSY.add_time_series!(sys, hybrid, rt_price)
+
+ if attach_services
+ reg_up_file = if use_rt_resolution_for_da
+ _select_price_fixture("$(bus_name)_RegUp_prices_5min", rt_steps)
+ else
+ joinpath(TEST_DIR, "inputs/$(bus_name)_RegUp_prices.csv")
+ end
+ reg_up = _build_scalar_time_series_from_csv(
+ reg_up_file,
+ bus_name,
+ HSS.hybrid_ancillary_service_price_time_series_name(
+ "Reg_Up",
+ HSS.DAY_AHEAD_TIME_SERIES_KEY,
+ );
+ max_rows = use_rt_resolution_for_da ? rt_steps : da_steps,
+ )
+ _validate_scalar_series_contract(
+ reg_up;
+ min_length = use_rt_resolution_for_da ? rt_steps : da_steps,
+ expected_resolution = expected_da_res,
+ expected_start = first(getfield(rt_data, :timestamp)),
+ label = "Reg_Up market price fixture",
+ )
+ reg_dn_file = if use_rt_resolution_for_da
+ _select_price_fixture("$(bus_name)_RegDown_prices_5min", rt_steps)
+ else
+ joinpath(TEST_DIR, "inputs/$(bus_name)_RegDown_prices.csv")
+ end
+ reg_dn = _build_scalar_time_series_from_csv(
+ reg_dn_file,
+ bus_name,
+ HSS.hybrid_ancillary_service_price_time_series_name(
+ "Reg_Down",
+ HSS.DAY_AHEAD_TIME_SERIES_KEY,
+ );
+ max_rows = use_rt_resolution_for_da ? rt_steps : da_steps,
+ )
+ _validate_scalar_series_contract(
+ reg_dn;
+ min_length = use_rt_resolution_for_da ? rt_steps : da_steps,
+ expected_resolution = expected_da_res,
+ expected_start = first(getfield(rt_data, :timestamp)),
+ label = "Reg_Down market price fixture",
+ )
+ spin_file = if use_rt_resolution_for_da
+ _select_price_fixture("$(bus_name)_Spin_prices_5min", rt_steps)
+ else
+ joinpath(TEST_DIR, "inputs/$(bus_name)_Spin_prices.csv")
+ end
+ spin = _build_scalar_time_series_from_csv(
+ spin_file,
+ bus_name,
+ HSS.hybrid_ancillary_service_price_time_series_name(
+ "Spin_Up_R3",
+ HSS.DAY_AHEAD_TIME_SERIES_KEY,
+ );
+ max_rows = use_rt_resolution_for_da ? rt_steps : da_steps,
+ )
+ _validate_scalar_series_contract(
+ spin;
+ min_length = use_rt_resolution_for_da ? rt_steps : da_steps,
+ expected_resolution = expected_da_res,
+ expected_start = first(getfield(rt_data, :timestamp)),
+ label = "Spin_Up_R3 market price fixture",
+ )
+ PSY.add_time_series!(sys, hybrid, reg_up)
+ PSY.add_time_series!(sys, hybrid, reg_dn)
+ PSY.add_time_series!(sys, hybrid, spin)
+ end
+ # Merchant models slice contiguous profile values from the wrapped SingleTimeSeries on the
+ # hybrid; elongate subcomponent copies so the stored series spans `rt_steps` at `step`.
+ # Cap profile length to the RT price series length so global transforms see one horizon count
+ # (caller may pass injection_rt_steps > rt_steps for legacy reasons).
+ profile_steps = min(injection_rt_steps, rt_steps)
+ ensure_hybrid_injection_profiles!(
+ sys,
+ hybrid,
+ profile_steps,
+ Dates.Minute(5);
+ start_time = first(getfield(rt_data, :timestamp)),
+ )
+ keep_names = Set([
+ "RenewableDispatch__max_active_power",
+ "PowerLoad__max_active_power",
+ HSS.hybrid_energy_price_time_series_name(HSS.DAY_AHEAD_TIME_SERIES_KEY),
+ HSS.hybrid_energy_price_time_series_name(HSS.REAL_TIME_TIME_SERIES_KEY),
+ ])
+ if attach_services
+ push!(
+ keep_names,
+ HSS.hybrid_ancillary_service_price_time_series_name(
+ "Reg_Up",
+ HSS.DAY_AHEAD_TIME_SERIES_KEY,
+ ),
+ )
+ push!(
+ keep_names,
+ HSS.hybrid_ancillary_service_price_time_series_name(
+ "Reg_Down",
+ HSS.DAY_AHEAD_TIME_SERIES_KEY,
+ ),
+ )
+ push!(
+ keep_names,
+ HSS.hybrid_ancillary_service_price_time_series_name(
+ "Spin_Up_R3",
+ HSS.DAY_AHEAD_TIME_SERIES_KEY,
+ ),
+ )
+ end
+ prune_hybrid_single_time_series!(sys, hybrid; keep_names)
+ kept_series = collect(PSY.get_time_series_multiple(hybrid; type = IS.SingleTimeSeries))
+ PSY.remove_time_series!(sys, IS.DeterministicSingleTimeSeries)
+ PSY.remove_time_series!(sys, IS.SingleTimeSeries)
+ for ts in kept_series
+ PSY.add_time_series!(sys, hybrid, ts)
+ end
+ return
+end
+
+function prune_hybrid_single_time_series!(
+ sys::PSY.System,
+ hybrid::PSY.HybridSystem;
+ keep_names::Set{String},
+)
+ for ts in collect(PSY.get_time_series_multiple(hybrid; type = IS.SingleTimeSeries))
+ nm = IS.get_name(ts)
+ nm in keep_names && continue
+ PSY.remove_time_series!(
+ sys,
IS.SingleTimeSeries,
hybrid,
- "RenewableDispatch__max_active_power",
+ nm;
+ resolution = IS.get_resolution(ts),
)
- single_da = IS.SingleTimeSeries(ts, "RenewableDispatch__max_active_power_da")
- PSY.add_time_series!(sys, hybrid, single_da)
- catch e
- e isa ArgumentError || rethrow()
end
+ return
+end
- # Force deterministic windows to exactly match the merchant RT horizon request
- # when provided (instead of only "at least as long"), so simulation updates
- # don't request out-of-window ranges.
+function _remove_all_hybrid_time_series_named!(
+ sys::PSY.System,
+ hybrid::PSY.HybridSystem,
+ ts_name::String,
+)
+ for ts in collect(PSY.get_time_series_multiple(hybrid; name = ts_name))
+ T = typeof(ts)
+ nm = IS.get_name(ts)
+ if ts isa IS.DeterministicSingleTimeSeries
+ PSY.remove_time_series!(
+ sys,
+ T,
+ hybrid,
+ nm;
+ resolution = IS.get_resolution(ts),
+ interval = IS.get_interval(ts),
+ )
+ else
+ PSY.remove_time_series!(
+ sys,
+ T,
+ hybrid,
+ nm;
+ resolution = IS.get_resolution(ts),
+ )
+ end
+ end
+ return
+end
+
+function _read_hybrid_profile_underlying_values(hybrid::PSY.HybridSystem, ts_name::String)
try
- ts_det = PSY.get_time_series(
- IS.DeterministicSingleTimeSeries,
- hybrid,
- "RenewableDispatch__max_active_power",
+ sts = PSY.get_time_series(IS.SingleTimeSeries, hybrid, ts_name)
+ ta = IS.get_data(sts)
+ ts = collect(getfield(ta, :timestamp))
+ vm = getfield(ta, :values)
+ vals = ndims(vm) == 1 ? Vector(vm) : vec(vm[:, 1])
+ return vals, first(ts)
+ catch
+ for ts in collect(
+ PSY.get_time_series_multiple(
+ hybrid;
+ name = ts_name,
+ type = IS.DeterministicSingleTimeSeries,
+ ),
)
- resolution = IS.get_resolution(ts_det)
- interval = IS.get_interval(ts_det)
- current_horizon = IS.get_horizon(ts_det)
+ sts = IS.get_single_time_series(ts)
+ ta = IS.get_data(sts)
+ tsvec = collect(getfield(ta, :timestamp))
+ vm = getfield(ta, :values)
+ vals = ndims(vm) == 1 ? Vector(vm) : vec(vm[:, 1])
+ return vals, first(tsvec)
+ end
+ end
+ return nothing, nothing
+end
- target_horizon =
- isnothing(horizon_rt_steps) ? current_horizon : (horizon_rt_steps * resolution)
+"""
+Remove every `InfrastructureSystems.SingleTimeSeries` from `sys` so a later
+`attach_hybrid_market_time_series!` call provides the only static series for
+`transform_single_time_series!`. This avoids `ConflictingInputsError` when RTS still carries
+unconverted static series whose forecast-window counts differ from the hybrid-attached CSVs.
+"""
+function _strip_single_time_series_from_owner!(sys::PSY.System, owner)
+ for ts in collect(PSY.get_time_series_multiple(owner; type = IS.SingleTimeSeries))
+ nm = IS.get_name(ts)
+ res = IS.get_resolution(ts)
+ try
+ PSY.remove_time_series!(sys, IS.SingleTimeSeries, owner, nm; resolution = res)
+ catch
+ end
+ end
+ return
+end
- PSY.transform_single_time_series!(
+function strip_all_single_time_series!(sys::PSY.System)
+ # Only visit IS time-series owners (excludes buses and other components without TS support).
+ for comp in IS.iterate_components_with_time_series(sys.data)
+ _strip_single_time_series_from_owner!(sys, comp)
+ end
+ for attr in IS.iterate_supplemental_attributes_with_time_series(sys.data)
+ _strip_single_time_series_from_owner!(sys, attr)
+ end
+ return
+end
+
+"""
+Remove `SingleTimeSeries` from non-hybrid owners only. Merchant tests rely on hybrid-attached
+series; pruning other static series avoids global transform conflicts at 5-minute model interval.
+"""
+function strip_non_hybrid_single_time_series!(sys::PSY.System)
+ for comp in IS.iterate_components_with_time_series(sys.data)
+ comp isa PSY.HybridSystem && continue
+ _strip_single_time_series_from_owner!(sys, comp)
+ end
+ for attr in IS.iterate_supplemental_attributes_with_time_series(sys.data)
+ _strip_single_time_series_from_owner!(sys, attr)
+ end
+ return
+end
+
+"""
+Ensure `RenewableDispatch__max_active_power` / `PowerLoad__max_active_power` on `hybrid` store
+exactly `target_steps` contiguous samples at `step`. Shorter series are tiled, longer series are
+truncated. This keeps hybrid-attached STS counts aligned for deterministic transforms.
+"""
+function ensure_hybrid_injection_profiles!(
+ sys::PSY.System,
+ hybrid::PSY.HybridSystem,
+ target_steps::Int,
+ step::Dates.Period = Dates.Minute(5);
+ start_time::Union{Nothing, Dates.DateTime} = nothing,
+)
+ if PSY.get_renewable_unit(hybrid) !== nothing
+ _ensure_one_hybrid_profile!(
+ sys,
+ hybrid,
+ "RenewableDispatch__max_active_power",
+ target_steps,
+ step,
+ start_time,
+ )
+ end
+ if PSY.get_electric_load(hybrid) !== nothing
+ _ensure_one_hybrid_profile!(
sys,
- target_horizon,
- interval;
- resolution = resolution,
+ hybrid,
+ "PowerLoad__max_active_power",
+ target_steps,
+ step,
+ start_time,
)
- catch e
- e isa ArgumentError || rethrow()
end
return
end
+
+function _ensure_one_hybrid_profile!(
+ sys::PSY.System,
+ hybrid::PSY.HybridSystem,
+ ts_name::String,
+ target_steps::Int,
+ step::Dates.Period,
+ start_time::Union{Nothing, Dates.DateTime},
+)
+ if isempty(collect(PSY.get_time_series_multiple(hybrid; name = ts_name)))
+ return
+ end
+ vals, t0 = _read_hybrid_profile_underlying_values(hybrid, ts_name)
+ (vals === nothing) && return
+ new_vals = repeat(vals, cld(target_steps, length(vals)))[1:target_steps]
+ t_start = isnothing(start_time) ? t0 : start_time
+ new_timestamps = [t_start + (i - 1) * step for i in 1:target_steps]
+ ta = TimeArray(new_timestamps, new_vals, ["value"])
+ new_sts = PSY.SingleTimeSeries(ts_name, ta)
+ _remove_all_hybrid_time_series_named!(sys, hybrid, ts_name)
+ PSY.add_time_series!(sys, hybrid, new_sts)
+ return
+end
From 345183f4e49783dc85d682882343c7b9b0f7d06d Mon Sep 17 00:00:00 2001
From: kdayday
Date: Tue, 5 May 2026 18:43:07 -0600
Subject: [PATCH 34/46] Fix docs links
---
docs/src/api/public.md | 2 +-
src/core/decision_models.jl | 5 ++---
2 files changed, 3 insertions(+), 4 deletions(-)
diff --git a/docs/src/api/public.md b/docs/src/api/public.md
index 115ea280..e76c0cc4 100644
--- a/docs/src/api/public.md
+++ b/docs/src/api/public.md
@@ -20,7 +20,7 @@ Depth = 3
## Device Formulations
Device formulations for hybrid systems (single PCC with renewable, thermal, and storage).
-Use with [`PowerSimulations.DeviceModel`](@extref PowerSimulations.DeviceModel) for unit
+Use with [`PowerSimulations.DeviceModel`](@extref) for unit
commitment or economic dispatch.
```@docs
diff --git a/src/core/decision_models.jl b/src/core/decision_models.jl
index 9e7434f6..0acb4bf3 100644
--- a/src/core/decision_models.jl
+++ b/src/core/decision_models.jl
@@ -5,7 +5,7 @@ const REAL_TIME_TIME_SERIES_KEY = "RT"
const HYBRID_TIME_SERIES_FEATURE_KEY = :timeseries_key
const ANCILLARY_PRICE_TIME_SERIES_PREFIX = "HybridSystem__ancillary_service_price__"
-"""Scalar energy price time series name for a given user key (e.g. [`DAY_AHEAD_TIME_SERIES_KEY`](@ref))."""
+"""Scalar energy price time series name for a given user key (e.g. `DAY_AHEAD_TIME_SERIES_KEY`)."""
function hybrid_energy_price_time_series_name(key::AbstractString)
return "HybridSystem__energy_price__" * string(key)
end
@@ -121,8 +121,7 @@ maximizes profit from energy (e.g. DA/RT spread) subject to internal asset limit
`InfrastructureSystems.SingleTimeSeries` objects with **distinct names** for each logical key
(defaults `"DA"` / `"RT"`): see [`hybrid_energy_price_time_series_name`](@ref). Profiles use the
standard renewable/load names below. Override keys via `model.ext["day_ahead_time_series_key"]`
- / `"real_time_time_series_key"` on the [`PowerSimulations.DecisionModel`](@extref PowerSimulations.DecisionModel)
- (propagated with [`set_time_series_keys!`](@ref)).
+ / `"real_time_time_series_key"` on the [`PowerSimulations.DecisionModel`](@extref).
| Role | Time series name |
| :--- | :--- |
From 6ab0ab636a78303204cb1da10cf07aa5ad3b5c74 Mon Sep 17 00:00:00 2001
From: kdayday
Date: Tue, 5 May 2026 19:30:30 -0600
Subject: [PATCH 35/46] Fix extref
---
src/core/decision_models.jl | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/core/decision_models.jl b/src/core/decision_models.jl
index 0acb4bf3..e8e38d92 100644
--- a/src/core/decision_models.jl
+++ b/src/core/decision_models.jl
@@ -121,7 +121,7 @@ maximizes profit from energy (e.g. DA/RT spread) subject to internal asset limit
`InfrastructureSystems.SingleTimeSeries` objects with **distinct names** for each logical key
(defaults `"DA"` / `"RT"`): see [`hybrid_energy_price_time_series_name`](@ref). Profiles use the
standard renewable/load names below. Override keys via `model.ext["day_ahead_time_series_key"]`
- / `"real_time_time_series_key"` on the [`PowerSimulations.DecisionModel`](@extref).
+ / `"real_time_time_series_key"` on the [`PowerSimulations.DecisionModel`](@extref `PowerSimulations.DecisionModel-Union{Tuple{M}, Tuple{PowerSimulations.AbstractProblemTemplate, System, PowerSimulations.Settings}, Tuple{PowerSimulations.AbstractProblemTemplate, System, PowerSimulations.Settings, Union{Nothing, JuMP.Model}}} where M<:PowerSimulations.DecisionProblem`).
| Role | Time series name |
| :--- | :--- |
From 2833d5baa5b62726feecb8b23ee56cdd96c03c12 Mon Sep 17 00:00:00 2001
From: kdayday
Date: Wed, 20 May 2026 12:37:54 -0600
Subject: [PATCH 36/46] PSI 0.35 compat and update cost definitions
---
Project.toml | 4 ++--
src/objective_function.jl | 10 ++++++----
test/Project.toml | 2 +-
3 files changed, 9 insertions(+), 7 deletions(-)
diff --git a/Project.toml b/Project.toml
index 183fdc10..7d2634a5 100644
--- a/Project.toml
+++ b/Project.toml
@@ -17,6 +17,6 @@ DataStructures = "~0.18, ^0.19"
DocStringExtensions = "0.8, 0.9.2"
JuMP = "^1.28"
MathOptInterface = "1"
-PowerSimulations = "^0.34"
-PowerSystems = "^5.8"
+PowerSimulations = "^0.35"
+PowerSystems = "^5.10"
julia = "^1.10"
diff --git a/src/objective_function.jl b/src/objective_function.jl
index 2e4fe668..80e8a485 100644
--- a/src/objective_function.jl
+++ b/src/objective_function.jl
@@ -102,7 +102,8 @@ function PSI.add_proportional_cost!(
cost_term = PSI.proportional_cost(op_cost_data, T(), d, W())
iszero(cost_term) && continue
for t in PSI.get_time_steps(container)
- PSI._add_proportional_term!(container, T(), d, cost_term * multiplier, t)
+ exp = PSI._add_proportional_term!(container, T(), d, cost_term * multiplier, t)
+ PSI.add_to_expression!(container, PSI.FixedCostExpression, exp, d, t)
end
end
return
@@ -153,8 +154,8 @@ function PSI.add_proportional_cost!(
# println("===============================================")
iszero(cost_term) && continue
for t in PSI.get_time_steps(container)
- PSI._add_proportional_term!(container, T(), d, cost_term * multiplier, t)
- #PSI._add_proportional_term!(container, T(), d, proportional_term * multiplier, t)
+ exp = PSI._add_proportional_term!(container, T(), d, cost_term * multiplier, t)
+ PSI.add_to_expression!(container, PSI.FixedCostExpression, exp, d, t)
end
end
return
@@ -213,7 +214,8 @@ function PSI.add_proportional_cost!(
cost_term = PSI.proportional_cost(op_cost_data, T(), d, W())
iszero(cost_term) && continue
for t in PSI.get_time_steps(container)
- PSI._add_proportional_term!(container, T(), d, cost_term * multiplier, t)
+ exp = PSI._add_proportional_term!(container, T(), d, cost_term * multiplier, t)
+ PSI.add_to_expression!(container, PSI.FixedCostExpression, exp, d, t)
end
end
return
diff --git a/test/Project.toml b/test/Project.toml
index 79ff5b1a..4d5971b5 100644
--- a/test/Project.toml
+++ b/test/Project.toml
@@ -19,4 +19,4 @@ Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
TimeSeries = "9e3dc215-6440-5c97-bce1-76c03772f85e"
[compat]
-julia = "^1.6"
+julia = "^1.10"
From 12ae686e0bb7d29d7733dc246a2d987cdb3ffd3e Mon Sep 17 00:00:00 2001
From: Jose Daniel Lara
Date: Fri, 12 Jun 2026 10:17:40 -0600
Subject: [PATCH 37/46] bump dependencies
---
Project.toml | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/Project.toml b/Project.toml
index 7d2634a5..9638cfaf 100644
--- a/Project.toml
+++ b/Project.toml
@@ -17,6 +17,6 @@ DataStructures = "~0.18, ^0.19"
DocStringExtensions = "0.8, 0.9.2"
JuMP = "^1.28"
MathOptInterface = "1"
-PowerSimulations = "^0.35"
-PowerSystems = "^5.10"
+PowerSimulations = "^0.35, ^0.36"
+PowerSystems = "^5.11"
julia = "^1.10"
From 5cc977fcc84e168c24291d801ee5d5b1eb740ed1 Mon Sep 17 00:00:00 2001
From: Jose Daniel Lara
Date: Fri, 12 Jun 2026 10:18:14 -0600
Subject: [PATCH 38/46] code correctness updates
---
src/add_constraints.jl | 87 +++++++++++++++++++++++++-----------------
src/add_parameters.jl | 9 ++++-
src/add_variables.jl | 9 ++---
3 files changed, 62 insertions(+), 43 deletions(-)
diff --git a/src/add_constraints.jl b/src/add_constraints.jl
index fe39769f..b1eee22a 100644
--- a/src/add_constraints.jl
+++ b/src/add_constraints.jl
@@ -12,13 +12,8 @@ function _has_reserve_slack_variables(
container::PSI.OptimizationContainer,
::Type{D},
) where {D <: PSY.HybridSystem}
- try
- PSI.get_variable(container, SlackReserveUp(), D)
- PSI.get_variable(container, SlackReserveDown(), D)
- return true
- catch
- return false
- end
+ return PSI.has_container_key(container, SlackReserveUp, D) &&
+ PSI.has_container_key(container, SlackReserveDown, D)
end
############ Total Power Constraints, HybridSystem ################
@@ -936,7 +931,8 @@ function _add_constraints_cyclingcharge!(
# param_value
# )
#else
- E_max = PSY.get_storage_level_limits(storage).max
+ E_max =
+ PSY.get_storage_level_limits(storage).max * PSY.get_storage_capacity(storage)
cycles_per_day = PSY.get_cycle_limits(storage)
cycles_in_horizon =
cycles_per_day * fraction_of_hour * length(time_steps) / HOURS_IN_DAY
@@ -971,7 +967,8 @@ function _add_constraints_cyclingcharge_withreserves!(
ci_name = PSY.get_name(device)
storage = PSY.get_storage(device)
efficiency = PSY.get_efficiency(storage)
- E_max = PSY.get_storage_level_limits(storage).max
+ E_max =
+ PSY.get_storage_level_limits(storage).max * PSY.get_storage_capacity(storage)
cycles_per_day = PSY.get_cycle_limits(storage)
cycles_in_horizon =
cycles_per_day * fraction_of_hour * length(time_steps) / HOURS_IN_DAY
@@ -998,7 +995,8 @@ function _add_constraints_cyclingcharge_withreserves!(
) <= param_value
)
else
- E_max = PSY.get_storage_level_limits(storage).max
+ E_max =
+ PSY.get_storage_level_limits(storage).max * PSY.get_storage_capacity(storage)
cycles_per_day = PSY.get_cycle_limits(storage)
cycles_in_horizon =
cycles_per_day * fraction_of_hour * length(time_steps) / HOURS_IN_DAY
@@ -1036,7 +1034,8 @@ function _add_constraints_cyclingcharge_decisionmodel!(
ci_name = PSY.get_name(device)
storage = PSY.get_storage(device)
efficiency = PSY.get_efficiency(storage)
- E_max = PSY.get_storage_level_limits(storage).max
+ E_max =
+ PSY.get_storage_level_limits(storage).max * PSY.get_storage_capacity(storage)
cycles_per_day = PSY.get_cycle_limits(storage)
cycles_in_horizon =
cycles_per_day * fraction_of_hour * length(time_steps) / HOURS_IN_DAY
@@ -1100,7 +1099,8 @@ function _add_constraints_cyclingdischarge!(
# sum(discharge_var[ci_name, :]) <= param_value
# )
#else
- E_max = PSY.get_storage_level_limits(storage).max
+ E_max =
+ PSY.get_storage_level_limits(storage).max * PSY.get_storage_capacity(storage)
cycles_per_day = PSY.get_cycle_limits(storage)
cycles_in_horizon =
cycles_per_day * fraction_of_hour * length(time_steps) / HOURS_IN_DAY
@@ -1136,7 +1136,8 @@ function _add_constraints_cyclingdischarge_withreserves!(
ci_name = PSY.get_name(device)
storage = PSY.get_storage(device)
efficiency = PSY.get_efficiency(storage)
- E_max = PSY.get_storage_level_limits(storage).max
+ E_max =
+ PSY.get_storage_level_limits(storage).max * PSY.get_storage_capacity(storage)
cycles_per_day = PSY.get_cycle_limits(storage)
cycles_in_horizon =
cycles_per_day * fraction_of_hour * length(time_steps) / HOURS_IN_DAY
@@ -1163,7 +1164,8 @@ function _add_constraints_cyclingdischarge_withreserves!(
) <= param_value
)
else
- E_max = PSY.get_storage_level_limits(storage).max
+ E_max =
+ PSY.get_storage_level_limits(storage).max * PSY.get_storage_capacity(storage)
cycles_per_day = PSY.get_cycle_limits(storage)
cycles_in_horizon =
cycles_per_day * fraction_of_hour * length(time_steps) / HOURS_IN_DAY
@@ -1202,7 +1204,8 @@ function _add_constraints_cyclingdischarge_decisionmodel!(
storage = PSY.get_storage(device)
efficiency = PSY.get_efficiency(storage)
- E_max = PSY.get_storage_level_limits(storage).max
+ E_max =
+ PSY.get_storage_level_limits(storage).max * PSY.get_storage_capacity(storage)
cycles_per_day = PSY.get_cycle_limits(storage)
cycles_in_horizon =
cycles_per_day * fraction_of_hour * length(time_steps) / HOURS_IN_DAY
@@ -1261,7 +1264,8 @@ function PSI.add_constraints!(
for d in devices
name = PSY.get_name(d)
storage = PSY.get_storage(d)
- target = PSY.get_storage_target(storage)
+ # storage_target is a ratio of storage capacity; EnergyVariable is in energy units.
+ target = PSY.get_storage_target(storage) * PSY.get_storage_capacity(storage)
constraint_container[name] = JuMP.@constraint(
PSI.get_jump_model(container),
energy_var[name, time_steps[end]] - surplus_var[name] + shortfall_var[name] == target
@@ -1660,7 +1664,8 @@ function _add_constraints_reservecoverage_withreserves!(
ci_name = PSY.get_name(device)
storage = PSY.get_storage(device)
efficiency = PSY.get_efficiency(storage).in
- E_max = PSY.get_storage_level_limits(storage).max
+ E_max =
+ PSY.get_storage_level_limits(storage).max * PSY.get_storage_capacity(storage)
sustained_param = efficiency * num_periods * fraction_of_hour
con[ci_name, 1] = JuMP.@constraint(
container.JuMPmodel,
@@ -1789,7 +1794,8 @@ function _add_constraints_reservecoverage_withreserves_endofperiod!(
for device in devices, t in time_steps
ci_name = PSY.get_name(device)
storage = PSY.get_storage(device)
- E_max = PSY.get_storage_level_limits(storage).max
+ E_max =
+ PSY.get_storage_level_limits(storage).max * PSY.get_storage_capacity(storage)
efficiency = PSY.get_efficiency(storage).in
sustained_param = efficiency * fraction_of_hour * num_periods
con[ci_name, t] = JuMP.@constraint(
@@ -1896,7 +1902,7 @@ function _add_constraints_discharging_reservelimit!(
for device in devices, t in time_steps
ci_name = PSY.get_name(device)
- max_limit = PSY.get_input_active_power_limits(PSY.get_storage(device)).max
+ max_limit = PSY.get_output_active_power_limits(PSY.get_storage(device)).max
con_ub[ci_name, t] = JuMP.@constraint(
PSI.get_jump_model(container),
p_ds[ci_name, t] + reg_ds_up[ci_name, t] <= max_limit * status_st[ci_name, t]
@@ -2778,7 +2784,9 @@ function add_constraints!(
for dev in devices
n = PSY.get_name(dev)
storage = PSY.get_storage(dev)
- VOM = storage.operation_cost.variable.cost
+ VOM = PSY.get_proportional_term(
+ PSY.get_vom_cost(PSY.get_charge_variable_cost(PSY.get_operation_cost(storage))),
+ )
η_ch = storage.efficiency.in * Δt_RT
for t in time_steps
con[n, t] = JuMP.@constraint(
@@ -2835,8 +2843,8 @@ function add_constraints!(
U <: Union{Vector{D}, IS.FlattenIteratorWrapper{D}},
W <: MerchantModelWithReserves,
} where {D <: PSY.HybridSystem}
- # Temp Fix
- Δt_RT = 1 / 12
+ resolution = PSI.get_resolution(container)
+ Δt_RT = Dates.value(Dates.Minute(resolution)) / PSI.MINUTES_IN_HOUR
time_steps = PSI.get_time_steps(container)
names = [PSY.get_name(d) for d in devices]
con = PSI.add_constraints_container!(container, T(), D, names, time_steps)
@@ -2852,7 +2860,11 @@ function add_constraints!(
for dev in devices
n = PSY.get_name(dev)
storage = PSY.get_storage(dev)
- VOM = storage.operation_cost.variable.cost
+ VOM = PSY.get_proportional_term(
+ PSY.get_vom_cost(
+ PSY.get_discharge_variable_cost(PSY.get_operation_cost(storage)),
+ ),
+ )
inv_η_ds = Δt_RT / storage.efficiency.out
# Written to match latex model
for t in time_steps
@@ -2890,7 +2902,8 @@ function add_constraints!(
for dev in devices
n = PSY.get_name(dev)
storage = PSY.get_storage(dev)
- e_max_ds = PSY.get_storage_level_limits(storage).max
+ e_max_ds =
+ PSY.get_storage_level_limits(storage).max * PSY.get_storage_capacity(storage)
for t in time_steps
assignment_constraint[n, t] =
JuMP.@constraint(jm, k_variable[n, t] == primal_var[n, t] - e_max_ds)
@@ -3145,7 +3158,7 @@ function add_constraints!(
} where {D <: PSY.HybridSystem}
time_steps = PSI.get_time_steps(container)
names = [PSY.get_name(d) for d in devices]
- dual_var = PSI.get_variable(container, μDsLb(), D)
+ dual_var = PSI.get_variable(container, μDsUb(), D)
primal_var = PSI.get_variable(container, BatteryDischarge(), D)
binary = PSI.get_variable(container, BatteryStatus(), D)
k_variable =
@@ -3209,7 +3222,7 @@ function add_constraints!(
} where {D <: PSY.HybridSystem}
time_steps = PSI.get_time_steps(container)
names = [PSY.get_name(d) for d in devices]
- dual_var = PSI.get_variable(container, μDsLb(), D)
+ dual_var = PSI.get_variable(container, μChUb(), D)
primal_var = PSI.get_variable(container, BatteryCharge(), D)
binary = PSI.get_variable(container, BatteryStatus(), D)
k_variable =
@@ -3226,7 +3239,7 @@ function add_constraints!(
for t in time_steps
assignment_constraint[n, t] = JuMP.@constraint(
jm,
- k_variable[n, t] == primal_var[n, t] - (1.0 - p_max_ch) * binary[n, t]
+ k_variable[n, t] == primal_var[n, t] - (1.0 - binary[n, t]) * p_max_ch
)
sos_constraint[n, t] =
JuMP.@constraint(jm, [k_variable[n, t], dual_var[n, t]] in JuMP.SOS1())
@@ -3306,9 +3319,9 @@ function add_constraints!(
)
for t in time_steps[2:end]
- assignment_constraint[ci_name, 1] = JuMP.@constraint(
+ assignment_constraint[ci_name, t] = JuMP.@constraint(
jm,
- k_variable[ci_name, 1] ==
+ k_variable[ci_name, t] ==
energy_var[ci_name, t - 1] +
fraction_of_hour * (
charge_var[ci_name, t] * efficiency.in -
@@ -3368,9 +3381,9 @@ function add_constraints!(
)
for t in time_steps[2:end]
- assignment_constraint[ci_name, 1] = JuMP.@constraint(
+ assignment_constraint[ci_name, t] = JuMP.@constraint(
jm,
- k_variable[ci_name, 1] ==
+ k_variable[ci_name, t] ==
energy_var[ci_name, t - 1] +
fraction_of_hour * (
charge_var[ci_name, t] * efficiency.in -
@@ -3410,7 +3423,8 @@ function add_constraints!(
for dev in devices
name = PSY.get_name(dev)
storage = PSY.get_storage(dev)
- _, E_max = PSY.get_storage_level_limits(storage)
+ E_max =
+ PSY.get_storage_level_limits(storage).max * PSY.get_storage_capacity(storage)
η_ch = storage.efficiency.in * Δt_RT
assignment_constraint[name] = JuMP.@constraint(
jm,
@@ -3435,7 +3449,7 @@ function add_constraints!(
time_steps = PSI.get_time_steps(container)
names = [PSY.get_name(d) for d in devices]
k_variable = PSI.get_variable(container, ComplementarySlackVarCyclingDischarge(), D)
- charge_var = PSI.get_variable(container, BatteryDischarge(), D)
+ discharge_var = PSI.get_variable(container, BatteryDischarge(), D)
dual_var = PSI.get_variable(container, κStDs(), D)
assignment_constraint =
PSI.add_constraints_container!(container, T(), D, names; meta = "eq")
@@ -3447,12 +3461,13 @@ function add_constraints!(
for dev in devices
name = PSY.get_name(dev)
storage = PSY.get_storage(dev)
- _, E_max = PSY.get_storage_level_limits(storage)
- η_ch = storage.efficiency.in * Δt_RT
+ E_max =
+ PSY.get_storage_level_limits(storage).max * PSY.get_storage_capacity(storage)
+ inv_η_ds = Δt_RT / storage.efficiency.out
assignment_constraint[name] = JuMP.@constraint(
jm,
k_variable[name] ==
- sum(charge_var[name, t] * η_ch for t in time_steps) - Cycles * E_max
+ sum(discharge_var[name, t] * inv_η_ds for t in time_steps) - Cycles * E_max
)
sos_constraint[name] =
JuMP.@constraint(jm, [k_variable[name], dual_var[name]] in JuMP.SOS1())
diff --git a/src/add_parameters.jl b/src/add_parameters.jl
index b12a186f..8c7be0a5 100644
--- a/src/add_parameters.jl
+++ b/src/add_parameters.jl
@@ -89,7 +89,10 @@ function _unwrap_hybrid_underlying_single_time_series(
return PSY.get_time_series(IS.SingleTimeSeries, device, ts_name)
end
return PSY.get_time_series(IS.SingleTimeSeries, device, ts_name; feat_kw...)
- catch
+ catch e
+ # IS throws ArgumentError when the series is missing or ambiguous at this
+ # type; only then fall back to the transformed DeterministicSingleTimeSeries.
+ e isa ArgumentError || rethrow()
if isempty(feat_kw)
dst = PSY.get_time_series(
IS.DeterministicSingleTimeSeries,
@@ -256,8 +259,12 @@ function PSI._update_parameter_values!(
template = PSI.get_template(model)
device_model = PSI.get_model(template, PSY.HybridSystem)
components = PSI.get_available_components(device_model, PSI.get_system(model))
+ # The parameter is only registered for the hybrids that own this profile
+ # (e.g. hybrids with a renewable unit); skip the rest like PSI's generic method does.
+ registered_names = PSI.get_component_names(attributes)
ts_uuids = Set{String}()
for component in components
+ PSY.get_name(component) in registered_names || continue
ts_uuid = PSI._get_ts_uuid(attributes, PSY.get_name(component))
if !(ts_uuid in ts_uuids)
ts_vector = _hybrid_profile_parameter_slice(
diff --git a/src/add_variables.jl b/src/add_variables.jl
index 434f35a1..db722dd6 100644
--- a/src/add_variables.jl
+++ b/src/add_variables.jl
@@ -6,12 +6,9 @@ function _get_day_ahead_time_steps(
container::PSI.OptimizationContainer,
devices::Vector{PSY.HybridSystem},
)
- da_key = get_day_ahead_time_series_key(container)
- metadata = first_matching_hybrid_scalar_metadata(
- first(devices),
- hybrid_energy_price_time_series_name(da_key),
- )
- return 1:time_series_metadata_horizon_steps(metadata)
+ # Must match the range used for DA constraints, parameters, and objective terms;
+ # otherwise trailing DA variables are created that no constraint or cost touches.
+ return merchant_da_time_step_range(container, first(devices))
end
# Energy Day-Ahead Bids
From bf1dcb9bf2ab159cc520b401ebc36017420fdb4f Mon Sep 17 00:00:00 2001
From: Jose Daniel Lara
Date: Fri, 12 Jun 2026 10:20:22 -0600
Subject: [PATCH 39/46] update the cycle ff
---
src/feedforwards.jl | 22 ++++------------------
1 file changed, 4 insertions(+), 18 deletions(-)
diff --git a/src/feedforwards.jl b/src/feedforwards.jl
index 54809759..18fd8dd0 100644
--- a/src/feedforwards.jl
+++ b/src/feedforwards.jl
@@ -261,10 +261,6 @@ function PSI.add_feedforward_constraints!(
) <= param_value
)
else
- E_max = PSY.get_storage_level_limits(storage).max
- cycles_per_day = PSY.get_cycle_limits(storage)
- cycles_in_horizon =
- cycles_per_day * fraction_of_hour * length(time_steps) / HOURS_IN_DAY
con_cycling_ch[ci_name] = JuMP.@constraint(
PSI.get_jump_model(container),
efficiency.in *
@@ -296,7 +292,8 @@ function PSI.add_feedforward_constraints!(
ci_name = PSY.get_name(device)
storage = PSY.get_storage(device)
efficiency = PSY.get_efficiency(storage)
- E_max = PSY.get_storage_level_limits(storage).max
+ E_max =
+ PSY.get_storage_level_limits(storage).max * PSY.get_storage_capacity(storage)
cycles_per_day = PSY.get_cycle_limits(storage)
cycles_in_horizon =
cycles_per_day * fraction_of_hour * length(time_steps) / HOURS_IN_DAY
@@ -312,10 +309,6 @@ function PSI.add_feedforward_constraints!(
param_value
)
else
- E_max = PSY.get_storage_level_limits(storage).max
- cycles_per_day = PSY.get_cycle_limits(storage)
- cycles_in_horizon =
- cycles_per_day * fraction_of_hour * length(time_steps) / HOURS_IN_DAY
con_cycling_ch[ci_name] = JuMP.@constraint(
PSI.get_jump_model(container),
efficiency.in * fraction_of_hour * sum(charge_var[ci_name, :]) <=
@@ -368,10 +361,6 @@ function PSI.add_feedforward_constraints!(
) <= param_value
)
else
- E_max = PSY.get_storage_level_limits(storage).max
- cycles_per_day = PSY.get_cycle_limits(storage)
- cycles_in_horizon =
- cycles_per_day * fraction_of_hour * length(time_steps) / HOURS_IN_DAY
con_cycling_ds[ci_name] = JuMP.@constraint(
PSI.get_jump_model(container),
(1.0 / efficiency.out) *
@@ -403,7 +392,8 @@ function PSI.add_feedforward_constraints!(
ci_name = PSY.get_name(device)
storage = PSY.get_storage(device)
efficiency = PSY.get_efficiency(storage)
- E_max = PSY.get_storage_level_limits(storage).max
+ E_max =
+ PSY.get_storage_level_limits(storage).max * PSY.get_storage_capacity(storage)
cycles_per_day = PSY.get_cycle_limits(storage)
cycles_in_horizon =
cycles_per_day * fraction_of_hour * length(time_steps) / HOURS_IN_DAY
@@ -420,10 +410,6 @@ function PSI.add_feedforward_constraints!(
sum(discharge_var[ci_name, :]) <= param_value
)
else
- E_max = PSY.get_storage_level_limits(storage).max
- cycles_per_day = PSY.get_cycle_limits(storage)
- cycles_in_horizon =
- cycles_per_day * fraction_of_hour * length(time_steps) / HOURS_IN_DAY
con_cycling_ds[ci_name] = JuMP.@constraint(
PSI.get_jump_model(container),
(1.0 / efficiency.out) *
From e31c3204a1093765050f9e2e66400803cab2a6e8 Mon Sep 17 00:00:00 2001
From: Jose Daniel Lara
Date: Fri, 12 Jun 2026 10:23:33 -0600
Subject: [PATCH 40/46] fix some docstrings
---
src/core/formulations.jl | 4 ++--
src/core/parameters.jl | 6 ++++++
2 files changed, 8 insertions(+), 2 deletions(-)
diff --git a/src/core/formulations.jl b/src/core/formulations.jl
index fd3e9a98..3a7be239 100644
--- a/src/core/formulations.jl
+++ b/src/core/formulations.jl
@@ -185,7 +185,7 @@ Regularization (if `"regularization" => true`): [`ChargeRegularizationConstraint
**Objective:**
Adds cost terms for thermal generation (variable and fixed costs), storage variable O&M,
-and penalties for energy target deviations and cycling violations (if enabled).
+renewable variable cost, and penalties for energy target deviations (if enabled).
"""
struct HybridDispatchWithReserves <: AbstractHybridFormulationWithReserves end
@@ -349,7 +349,7 @@ Regularization (if `"regularization" => true`): [`ChargeRegularizationConstraint
**Objective:**
Adds cost terms for thermal generation (variable and fixed costs), storage variable O&M,
-and penalties for energy target deviations and cycling violations (if enabled).
+renewable variable cost, and penalties for energy target deviations (if enabled).
"""
struct HybridEnergyOnlyDispatch <: AbstractHybridFormulation end
diff --git a/src/core/parameters.jl b/src/core/parameters.jl
index ddfc63b5..d60ab148 100644
--- a/src/core/parameters.jl
+++ b/src/core/parameters.jl
@@ -94,4 +94,10 @@ struct CyclingDischargeLimitParameter <: PSI.VariableValueParameter end
PSI.should_write_resulting_value(::Type{DayAheadEnergyPrice}) = true
PSI.should_write_resulting_value(::Type{RealTimeEnergyPrice}) = true
+# Required by the generic PSI.update_variable_cost! path used when updating
+# DayAheadEnergyPrice during simulation. RealTimeEnergyPrice deliberately has no
+# method: its update must go through the inlined RT path in add_parameters.jl,
+# which applies the RT-to-DA time mapping the generic path knows nothing about.
+PSI._constituent_cost_expression(::DayAheadEnergyPrice) = PSI.ProductionCostExpression
+
# convert_result_to_natural_units(::Type{EnergyTargetParameter}) = true
From aaf326f08defee03c04807ca878297f425f316f4 Mon Sep 17 00:00:00 2001
From: Jose Daniel Lara
Date: Fri, 12 Jun 2026 10:24:01 -0600
Subject: [PATCH 41/46] fix some modeling errors
---
src/hybrid_system_decision_models.jl | 10 +++++++---
src/hybrid_system_device_models.jl | 16 +++++++++++-----
2 files changed, 18 insertions(+), 8 deletions(-)
diff --git a/src/hybrid_system_decision_models.jl b/src/hybrid_system_decision_models.jl
index fae181d2..5abc18b4 100644
--- a/src/hybrid_system_decision_models.jl
+++ b/src/hybrid_system_decision_models.jl
@@ -193,9 +193,11 @@ function PSI.update_decision_state!(
) where {T <: Union{EnergyDABidOut, EnergyDABidIn}}
@debug "updating decision state $simulation_time"
state_data = PSI.get_decision_state_data(state, key)
- model_resolution = PSI.get_resolution(model_params)
state_resolution = PSI.get_data_resolution(state_data)
- resolution_ratio = model_resolution ÷ state_resolution
+ # DA bid variables are indexed by hourly DA slots (see merchant_da_time_step_range),
+ # not by the model's RT resolution: each stored value spans one hour of state rows.
+ resolution_ratio =
+ Dates.Millisecond(Dates.Hour(1)) ÷ Dates.Millisecond(state_resolution)
state_timestamps = state_data.timestamps
PSI.IS.@assert_op resolution_ratio >= 1
@@ -333,9 +335,11 @@ function PSI.update_decision_state!(
offset = resolution_ratio - 1
result_time_index = axes(store_data)[3]
+ max_state_index = PSI.get_num_rows(state_data)
PSI.set_update_timestamp!(state_data, simulation_time)
for t in result_time_index
- state_range = state_data_index:(state_data_index + offset)
+ state_data_index > max_state_index && break
+ state_range = state_data_index:min(max_state_index, state_data_index + offset)
for name in device_names, service in service_names, i in state_range
# TODO: We could also interpolate here
state_data.values[name, service, i] = store_data[name, service, t]
diff --git a/src/hybrid_system_device_models.jl b/src/hybrid_system_device_models.jl
index df9213cb..a17b9d5d 100644
--- a/src/hybrid_system_device_models.jl
+++ b/src/hybrid_system_device_models.jl
@@ -160,7 +160,9 @@ PSI.get_variable_upper_bound(
::PSI.EnergyVariable,
d::PSY.HybridSystem,
::AbstractHybridFormulation,
-) = PSY.get_storage_level_limits(PSY.get_storage(d)).max
+) =
+ PSY.get_storage_level_limits(PSY.get_storage(d)).max *
+ PSY.get_storage_capacity(PSY.get_storage(d))
PSI.get_variable_upper_bound(
::PSI.OnVariable,
@@ -212,7 +214,9 @@ PSI.get_variable_lower_bound(
::PSI.EnergyVariable,
d::PSY.HybridSystem,
::AbstractHybridFormulation,
-) = PSY.get_storage_level_limits(PSY.get_storage(d)).min
+) =
+ PSY.get_storage_level_limits(PSY.get_storage(d)).min *
+ PSY.get_storage_capacity(PSY.get_storage(d))
PSI.get_variable_lower_bound(
::PSI.OnVariable,
@@ -298,7 +302,9 @@ PSI.initial_condition_default(
::PSI.InitialEnergyLevel,
d::PSY.HybridSystem,
::AbstractHybridFormulation,
-) = PSY.get_initial_storage_capacity_level(PSY.get_storage(d))
+) =
+ PSY.get_initial_storage_capacity_level(PSY.get_storage(d)) *
+ PSY.get_storage_capacity(PSY.get_storage(d))
PSI.initial_condition_variable(
::PSI.InitialEnergyLevel,
@@ -414,8 +420,8 @@ PSI.get_initial_parameter_value(
::AbstractHybridFormulation,
) =
PSY.get_cycle_limits(PSY.get_storage(d)) *
- PSY.get_storage_level_limits(PSY.get_storage(d)).max
-#PSY.get_state_of_charge_limits(PSY.get_storage(d)).max
+ PSY.get_storage_level_limits(PSY.get_storage(d)).max *
+ PSY.get_storage_capacity(PSY.get_storage(d))
###################################################################
######################## Initial Conditions #######################
From f6046f86300da8d8778a078c78437d4c316bb6f7 Mon Sep 17 00:00:00 2001
From: Jose Daniel Lara
Date: Fri, 12 Jun 2026 10:24:18 -0600
Subject: [PATCH 42/46] fix objective function implementation
---
src/objective_function.jl | 27 ++++++++++++++++++++++++---
1 file changed, 24 insertions(+), 3 deletions(-)
diff --git a/src/objective_function.jl b/src/objective_function.jl
index 80e8a485..3435e893 100644
--- a/src/objective_function.jl
+++ b/src/objective_function.jl
@@ -5,7 +5,7 @@ PSI.objective_function_multiplier(
) = PSI.OBJECTIVE_FUNCTION_POSITIVE
PSI.objective_function_multiplier(
- ::Union{BatteryEnergySurplusVariable, BatteryEnergySurplusVariable},
+ ::Union{BatteryEnergySurplusVariable, BatteryEnergyShortageVariable},
::AbstractHybridFormulation,
) = PSI.OBJECTIVE_FUNCTION_POSITIVE
@@ -85,6 +85,11 @@ end
# end
# HSA 11-6-2024 ===
+_battery_variable_cost_curve(cost::PSY.OperationalCost, ::BatteryCharge) =
+ PSY.get_charge_variable_cost(cost)
+_battery_variable_cost_curve(cost::PSY.OperationalCost, ::BatteryDischarge) =
+ PSY.get_discharge_variable_cost(cost)
+
function PSI.add_proportional_cost!(
container::PSI.OptimizationContainer,
::T,
@@ -96,13 +101,29 @@ function PSI.add_proportional_cost!(
W <: AbstractHybridFormulation,
} where {D <: PSY.HybridSystem}
multiplier = PSI.objective_function_multiplier(T(), W())
+ resolution = PSI.get_resolution(container)
+ dt = Dates.value(Dates.Minute(resolution)) / PSI.MINUTES_IN_HOUR
+ base_power = PSI.get_base_power(container)
for d in devices
- op_cost_data = PSY.get_operation_cost(PSY.get_storage(d))
+ storage = PSY.get_storage(d)
+ op_cost_data = PSY.get_operation_cost(storage)
isnothing(op_cost_data) && continue
cost_term = PSI.proportional_cost(op_cost_data, T(), d, W())
iszero(cost_term) && continue
+ cost_per_system_unit = PSI.get_proportional_cost_per_system_unit(
+ cost_term,
+ PSY.get_power_units(_battery_variable_cost_curve(op_cost_data, T())),
+ base_power,
+ PSY.get_base_power(storage),
+ )
for t in PSI.get_time_steps(container)
- exp = PSI._add_proportional_term!(container, T(), d, cost_term * multiplier, t)
+ exp = PSI._add_proportional_term!(
+ container,
+ T(),
+ d,
+ cost_per_system_unit * dt * multiplier,
+ t,
+ )
PSI.add_to_expression!(container, PSI.FixedCostExpression, exp, d, t)
end
end
From 4cbe76998cb136f0757b208106ed5630afa48f1b Mon Sep 17 00:00:00 2001
From: Jose Daniel Lara
Date: Fri, 12 Jun 2026 10:53:25 -0600
Subject: [PATCH 43/46] update decision models
---
src/decision_models/bilevel_decision_model.jl | 28 ++++-----
.../cooptimizer_decision_model.jl | 43 ++++---------
.../only_energy_decision_model.jl | 61 +++++++++++--------
3 files changed, 58 insertions(+), 74 deletions(-)
diff --git a/src/decision_models/bilevel_decision_model.jl b/src/decision_models/bilevel_decision_model.jl
index cfafa57f..76d6f544 100644
--- a/src/decision_models/bilevel_decision_model.jl
+++ b/src/decision_models/bilevel_decision_model.jl
@@ -7,10 +7,14 @@ function PSI.build_impl!(decision_model::PSI.DecisionModel{MerchantHybridBilevel
model = container.JuMPmodel
sys = PSI.get_system(decision_model)
T = PSY.HybridSystem
- # Resolution
- RT_resolution = first(PSY.get_time_series_resolutions(sys))
+ hybrids = collect(PSY.get_components(PSY.HybridSystem, sys))
+ if isempty(hybrids)
+ error(
+ "MerchantHybridBilevelCase requires at least one HybridSystem in the " *
+ "System. Add a PSY.HybridSystem to the system or use a different decision model.",
+ )
+ end
Δt_DA = 1.0
- Δt_RT = Dates.value(Dates.Minute(RT_resolution)) / PSI.MINUTES_IN_HOUR
# Initialize Container
PSI.init_optimization_container!(
container,
@@ -19,25 +23,18 @@ function PSI.build_impl!(decision_model::PSI.DecisionModel{MerchantHybridBilevel
)
PSI.init_model_store_params!(decision_model)
set_time_series_keys!(container, decision_model)
+ # Resolution negotiated into settings by PSI.validate_time_series!
+ Δt_RT =
+ Dates.value(Dates.Minute(PSI.get_resolution(container))) / PSI.MINUTES_IN_HOUR
- da_key = get_day_ahead_time_series_key(decision_model)
rt_key = get_real_time_time_series_key(decision_model)
- hybrid_ref = first(collect(PSY.get_components(PSY.HybridSystem, sys)))
- da_metadata = first_matching_hybrid_scalar_metadata(
- hybrid_ref,
- hybrid_energy_price_time_series_name(da_key),
- )
+ hybrid_ref = first(hybrids)
rt_metadata = first_matching_hybrid_scalar_metadata(
hybrid_ref,
hybrid_energy_price_time_series_name(rt_key),
)
- len_DA_meta = time_series_metadata_horizon_steps(da_metadata)
len_RT_meta = time_series_metadata_horizon_steps(rt_metadata)
- settings = PSI.get_settings(container)
- h_ms = Dates.value(PSI.get_horizon(settings))
- da_slot_ms = Dates.value(Dates.Millisecond(Dates.Hour(1)))
- n_DA = max(1, div(h_ms, da_slot_ms))
- T_da = 1:min(n_DA, len_DA_meta)
+ T_da = merchant_da_time_step_range(container, hybrid_ref)
T_rt = PSI.get_time_steps(container)
len_RT = length(T_rt)
@@ -53,7 +50,6 @@ function PSI.build_impl!(decision_model::PSI.DecisionModel{MerchantHybridBilevel
######## Parameters ###########
###############################
- hybrids = collect(PSY.get_components(PSY.HybridSystem, sys))
h_names = PSY.get_name.(hybrids)
services = Set()
for h in hybrids
diff --git a/src/decision_models/cooptimizer_decision_model.jl b/src/decision_models/cooptimizer_decision_model.jl
index bc6395b0..0fc5b93d 100644
--- a/src/decision_models/cooptimizer_decision_model.jl
+++ b/src/decision_models/cooptimizer_decision_model.jl
@@ -8,10 +8,14 @@ function PSI.build_impl!(decision_model::PSI.DecisionModel{MerchantHybridCooptim
model = container.JuMPmodel
sys = PSI.get_system(decision_model)
T = PSY.HybridSystem
- # Resolution
- RT_resolution = first(PSY.get_time_series_resolutions(sys))
+ hybrids = collect(PSY.get_components(PSY.HybridSystem, sys))
+ if isempty(hybrids)
+ error(
+ "MerchantHybridCooptimizerCase requires at least one HybridSystem in the " *
+ "System. Add a PSY.HybridSystem to the system or use a different decision model.",
+ )
+ end
Δt_DA = 1.0
- Δt_RT = Dates.value(Dates.Minute(RT_resolution)) / PSI.MINUTES_IN_HOUR
# Initialize Container
PSI.init_optimization_container!(
container,
@@ -20,25 +24,18 @@ function PSI.build_impl!(decision_model::PSI.DecisionModel{MerchantHybridCooptim
)
PSI.init_model_store_params!(decision_model)
set_time_series_keys!(container, decision_model)
+ # Resolution negotiated into settings by PSI.validate_time_series!
+ Δt_RT =
+ Dates.value(Dates.Minute(PSI.get_resolution(container))) / PSI.MINUTES_IN_HOUR
- da_key = get_day_ahead_time_series_key(decision_model)
rt_key = get_real_time_time_series_key(decision_model)
- hybrid_ref = first(collect(PSY.get_components(PSY.HybridSystem, sys)))
- da_metadata = first_matching_hybrid_scalar_metadata(
- hybrid_ref,
- hybrid_energy_price_time_series_name(da_key),
- )
+ hybrid_ref = first(hybrids)
rt_metadata = first_matching_hybrid_scalar_metadata(
hybrid_ref,
hybrid_energy_price_time_series_name(rt_key),
)
- len_DA_meta = time_series_metadata_horizon_steps(da_metadata)
len_RT_meta = time_series_metadata_horizon_steps(rt_metadata)
- settings = PSI.get_settings(container)
- h_ms = Dates.value(PSI.get_horizon(settings))
- da_slot_ms = Dates.value(Dates.Millisecond(Dates.Hour(1)))
- n_DA = max(1, div(h_ms, da_slot_ms))
- T_da = 1:min(n_DA, len_DA_meta)
+ T_da = merchant_da_time_step_range(container, hybrid_ref)
T_rt = PSI.get_time_steps(container)
len_RT = length(T_rt)
@@ -54,7 +51,6 @@ function PSI.build_impl!(decision_model::PSI.DecisionModel{MerchantHybridCooptim
######## Parameters ###########
###############################
- hybrids = collect(PSY.get_components(PSY.HybridSystem, sys))
h_names = PSY.get_name.(hybrids)
services = Set()
for h in hybrids
@@ -812,8 +808,6 @@ function PSI.build_impl!(decision_model::PSI.DecisionModel{MerchantHybridCooptim
# Storage Variable Cost
if !isempty(_hybrids_with_storage)
- p_ch = PSI.get_variable(container, BatteryCharge(), PSY.HybridSystem)
- p_ds = PSI.get_variable(container, BatteryDischarge(), PSY.HybridSystem)
if PSI.get_attribute(device_model, "regularization")
PSI.add_proportional_cost!(
container,
@@ -851,19 +845,6 @@ function PSI.build_impl!(decision_model::PSI.DecisionModel{MerchantHybridCooptim
lin_cost_p_th = Δt_RT * C_th_var * p_th[name, t]
PSI.add_to_objective_invariant_expression!(container, lin_cost_p_th)
end
- if !isnothing(dev.storage)
- storage_cost = PSY.get_operation_cost(dev.storage)
- charge_vom = PSY.get_proportional_term(
- PSY.get_vom_cost(PSY.get_charge_variable_cost(storage_cost)),
- )
- discharge_vom = PSY.get_proportional_term(
- PSY.get_vom_cost(PSY.get_discharge_variable_cost(storage_cost)),
- )
- lin_cost_p_ch = 100.0 * Δt_RT * charge_vom * p_ch[name, t]
- lin_cost_p_ds = 100.0 * Δt_RT * discharge_vom * p_ds[name, t]
- PSI.add_to_objective_invariant_expression!(container, lin_cost_p_ch)
- PSI.add_to_objective_invariant_expression!(container, lin_cost_p_ds)
- end
if length(T_da) == 24 && !isempty(services)
dev_services = PSY.get_services(dev)
for service in dev_services
diff --git a/src/decision_models/only_energy_decision_model.jl b/src/decision_models/only_energy_decision_model.jl
index f2865d85..36f37fa1 100644
--- a/src/decision_models/only_energy_decision_model.jl
+++ b/src/decision_models/only_energy_decision_model.jl
@@ -5,11 +5,14 @@
function PSI.build_impl!(decision_model::PSI.DecisionModel{MerchantHybridEnergyCase})
container = PSI.get_optimization_container(decision_model)
sys = PSI.get_system(decision_model)
- # Resolution
+ hybrids = collect(PSY.get_components(PSY.HybridSystem, sys))
+ if isempty(hybrids)
+ error(
+ "MerchantHybridEnergyCase requires at least one HybridSystem in the " *
+ "System. Add a PSY.HybridSystem to the system or use a different decision model.",
+ )
+ end
Δt_DA = 1.0
- RT_resolution = first(PSY.get_time_series_resolutions(sys))
- sys = PSI.get_system(decision_model)
- Δt_RT = Dates.value(Dates.Minute(RT_resolution)) / PSI.MINUTES_IN_HOUR
# Initialize Container
PSI.init_optimization_container!(
container,
@@ -18,21 +21,17 @@ function PSI.build_impl!(decision_model::PSI.DecisionModel{MerchantHybridEnergyC
)
PSI.init_model_store_params!(decision_model)
set_time_series_keys!(container, decision_model)
+ # Resolution negotiated into settings by PSI.validate_time_series!
+ Δt_RT =
+ Dates.value(Dates.Minute(PSI.get_resolution(container))) / PSI.MINUTES_IN_HOUR
- da_key = get_day_ahead_time_series_key(decision_model)
rt_key = get_real_time_time_series_key(decision_model)
- hybrid_ref = first(collect(PSY.get_components(PSY.HybridSystem, sys)))
- da_metadata = first_matching_hybrid_scalar_metadata(
- hybrid_ref,
- hybrid_energy_price_time_series_name(da_key),
- )
+ hybrid_ref = first(hybrids)
rt_metadata = first_matching_hybrid_scalar_metadata(
hybrid_ref,
hybrid_energy_price_time_series_name(rt_key),
)
- len_DA_meta = time_series_metadata_horizon_steps(da_metadata)
len_RT_meta = time_series_metadata_horizon_steps(rt_metadata)
- settings = PSI.get_settings(container)
T_rt = PSI.get_time_steps(container)
len_RT = length(T_rt)
T_da = merchant_da_time_step_range(container, hybrid_ref)
@@ -47,7 +46,6 @@ function PSI.build_impl!(decision_model::PSI.DecisionModel{MerchantHybridEnergyC
######## Parameters ###########
###############################
- hybrids = collect(PSY.get_components(PSY.HybridSystem, sys))
h_names = PSY.get_name.(hybrids)
services = Set()
for d in hybrids
@@ -55,6 +53,14 @@ function PSI.build_impl!(decision_model::PSI.DecisionModel{MerchantHybridEnergyC
end
device_model = PSI.get_model(PSI.get_template(decision_model), PSY.HybridSystem)
+ if device_model === nothing
+ error(
+ "MerchantHybridEnergyCase requires a DeviceModel for HybridSystem in the " *
+ "ProblemTemplate. Call set_device_model!(template, DeviceModel(PSY.HybridSystem, " *
+ "HybridEnergyOnlyDispatch)) or another appropriate hybrid formulation before " *
+ "constructing the DecisionModel.",
+ )
+ end
###############################
######## Variables ############
@@ -261,6 +267,18 @@ function PSI.build_impl!(decision_model::PSI.DecisionModel{MerchantHybridEnergyC
p_ds = PSI.get_variable(container, BatteryDischarge(), PSY.HybridSystem)
e_st = PSI.get_variable(container, PSI.EnergyVariable(), PSY.HybridSystem)
status_st = PSI.get_variable(container, BatteryStatus(), PSY.HybridSystem)
+ PSI.add_proportional_cost!(
+ container,
+ BatteryCharge(),
+ _hybrids_with_storage,
+ MerchantModelEnergyOnly(),
+ )
+ PSI.add_proportional_cost!(
+ container,
+ BatteryDischarge(),
+ _hybrids_with_storage,
+ MerchantModelEnergyOnly(),
+ )
if PSI.get_attribute(device_model, "regularization")
PSI.add_proportional_cost!(
container,
@@ -295,19 +313,6 @@ function PSI.build_impl!(decision_model::PSI.DecisionModel{MerchantHybridEnergyC
lin_cost_p_th = Δt_RT * C_th_var * p_th[name, t]
PSI.add_to_objective_invariant_expression!(container, lin_cost_p_th)
end
- if !isnothing(dev.storage)
- storage_cost = PSY.get_operation_cost(dev.storage)
- charge_vom = PSY.get_proportional_term(
- PSY.get_vom_cost(PSY.get_charge_variable_cost(storage_cost)),
- )
- discharge_vom = PSY.get_proportional_term(
- PSY.get_vom_cost(PSY.get_discharge_variable_cost(storage_cost)),
- )
- lin_cost_p_ch = Δt_RT * charge_vom * p_ch[name, t]
- lin_cost_p_ds = Δt_RT * discharge_vom * p_ds[name, t]
- PSI.add_to_objective_invariant_expression!(container, lin_cost_p_ch)
- PSI.add_to_objective_invariant_expression!(container, lin_cost_p_ds)
- end
end
end
@@ -527,7 +532,9 @@ function PSI.build_impl!(decision_model::PSI.DecisionModel{MerchantHybridEnergyC
η_ch = storage.efficiency.in
η_ds = storage.efficiency.out
inv_η_ds = 1.0 / η_ds
- E_max = PSY.get_storage_level_limits(storage).max
+ E_max =
+ PSY.get_storage_level_limits(storage).max *
+ PSY.get_storage_capacity(storage)
constraint_cycling_charge[name] = JuMP.@constraint(
model,
inv_η_ds * Δt_RT * sum(p_ds[name, t] for t in T_rt) <= Cycles * E_max
From 130a8833eeb70724e59f2db79cc4231daad5484a Mon Sep 17 00:00:00 2001
From: Jose Daniel Lara
Date: Fri, 12 Jun 2026 10:53:31 -0600
Subject: [PATCH 44/46] improve testing
---
src/utils.jl | 67 -------------------------------
test/test_merchant_cooptimizer.jl | 10 +++--
test/test_merchant_only_energy.jl | 22 +++++++---
test/test_merchant_sequence.jl | 18 +++++++++
test/test_utils/function_utils.jl | 10 ++++-
5 files changed, 48 insertions(+), 79 deletions(-)
delete mode 100644 src/utils.jl
diff --git a/src/utils.jl b/src/utils.jl
deleted file mode 100644
index 4bd8f08b..00000000
--- a/src/utils.jl
+++ /dev/null
@@ -1,67 +0,0 @@
-function _get_time_series(
- container::OptimizationContainer,
- component::PSY.HybridSystem,
- subcomponent::S,
- attributes::TimeSeriesAttributes{T},
-) where {S <: PSY.Component, T <: PSY.TimeSeriesData}
- return get_time_series_initial_values!(
- container,
- T,
- component,
- PSY.make_subsystem_time_series_name(subcomponent, get_time_series_name(attributes)),
- )
-end
-
-function get_time_series(
- container::OptimizationContainer,
- component::S,
- subcomponent_type::Type{T},
- parameter::TimeSeriesParameter,
- # HSA - 10.02.2024 ---------------
- meta = ISOPT.CONTAINER_KEY_EMPTY_META,
-) where {S <: PSY.HybridSystem, T <: PSY.Component}
- parameter_container = get_parameter(container, parameter, S, meta)
- subcomponent = get_subcomponent(component, subcomponent_type)
- return _get_time_series(
- container,
- component,
- subcomponent,
- parameter_container.attributes,
- )
-end
-
-function _update_parameter_values!(
- param_array::SparseAxisArray,
- attributes::TimeSeriesAttributes{U},
- ::Type{V},
- model::DecisionModel,
- ::DatasetContainer{InMemoryDataset},
-) where {U <: PSY.AbstractDeterministic, V <: PSY.HybridSystem}
- initial_forecast_time = get_current_time(model) # Function not well defined for DecisionModels
- horizon = get_time_steps(get_optimization_container(model))[end]
- multiplier_id = get_time_series_multiplier_id(attributes)
- ts_name = get_time_series_name(attributes)
- components = get_available_components(V, get_system(model))
- ts_uuids = Set{String}()
- for component in components, subcomp_type in [PSY.RenewableGen, PSY.ElectricLoad]
- subcomponent = get_subcomponent(component, subcomp_type)
- !does_subcomponent_exist(component, subcomp_type) && continue
- ss_ts_name = PSY.make_subsystem_time_series_name(subcomponent, ts_name)
- ts_uuid = get_time_series_uuid(U, subcomponent, ss_ts_name)
- if !(ts_uuid in ts_uuids)
- ts_vector = get_time_series_values!(
- U,
- model,
- component,
- ss_ts_name,
- multiplier_id,
- initial_forecast_time,
- horizon,
- )
- for (t, value) in enumerate(ts_vector)
- _set_param_value_hss!(param_array, value, ts_uuid, string(subcomp_type), t)
- end
- push!(ts_uuids, ts_uuid)
- end
- end
-end
diff --git a/test/test_merchant_cooptimizer.jl b/test/test_merchant_cooptimizer.jl
index d744fb4d..db0dcf37 100644
--- a/test/test_merchant_cooptimizer.jl
+++ b/test/test_merchant_cooptimizer.jl
@@ -60,14 +60,16 @@ function _run_cooptimizer_case(with_services::Bool)
name = "MerchantHybridCooptimizerCase_DA",
)
- build!(decision_optimizer_DA; output_dir = mktempdir())
- solve!(decision_optimizer_DA)
+ @test build!(decision_optimizer_DA; output_dir = mktempdir()) ==
+ PSI.ModelBuildStatus.BUILT
+ @test solve!(decision_optimizer_DA) == PSI.RunStatus.SUCCESSFULLY_FINALIZED
results = PSI.OptimizationProblemResults(decision_optimizer_DA)
var_results = results.variable_values
rt_bid_out = read_variable(results, "EnergyRTBidOut__HybridSystem")
da_bid_out = var_results[PSI.VariableKey{HSS.EnergyDABidOut, HybridSystem}("")]
- @test length(da_bid_out[!, 1]) == horizon_merchant_rt
+ # DA bid and reserve bid variables span hourly DA slots; RT bids span RT steps.
+ @test length(da_bid_out[!, 1]) == horizon_merchant_da
@test length(rt_bid_out[!, 1]) == 288
if with_services
regup_bid_out =
@@ -77,7 +79,7 @@ function _run_cooptimizer_case(with_services::Bool)
}(
"Reg_Up",
)]
- @test length(regup_bid_out[!, 1]) == horizon_merchant_rt
+ @test length(regup_bid_out[!, 1]) == horizon_merchant_da
end
end
diff --git a/test/test_merchant_only_energy.jl b/test/test_merchant_only_energy.jl
index f7405d9d..8b3541f5 100644
--- a/test/test_merchant_only_energy.jl
+++ b/test/test_merchant_only_energy.jl
@@ -1,4 +1,8 @@
-function _run_only_energy_case(horizon_merchant_rt::Int, horizon_merchant_da::Int)
+function _run_only_energy_case(
+ horizon_merchant_rt::Int,
+ horizon_merchant_da::Int;
+ use_rt_resolution_for_da::Bool = true,
+)
injection_steps = max(horizon_merchant_rt, 300)
sys = PSB.build_RTS_GMLC_RT_sys(;
raw_data = PSB.RTS_DIR,
@@ -15,9 +19,9 @@ function _run_only_energy_case(horizon_merchant_rt::Int, horizon_merchant_da::In
bus_name = "chuhsi",
attach_services = false,
rt_steps = horizon_merchant_rt,
- da_steps = horizon_merchant_rt,
+ da_steps = use_rt_resolution_for_da ? horizon_merchant_rt : horizon_merchant_da,
injection_rt_steps = injection_steps,
- use_rt_resolution_for_da = true,
+ use_rt_resolution_for_da = use_rt_resolution_for_da,
)
strip_non_hybrid_single_time_series!(sys)
ts_rt = PSY.get_time_series(
@@ -44,14 +48,16 @@ function _run_only_energy_case(horizon_merchant_rt::Int, horizon_merchant_da::In
name = "MerchantHybridEnergyCase_DA",
)
- build!(decision_optimizer_DA; output_dir = mktempdir())
- solve!(decision_optimizer_DA)
+ @test build!(decision_optimizer_DA; output_dir = mktempdir()) ==
+ PSI.ModelBuildStatus.BUILT
+ @test solve!(decision_optimizer_DA) == PSI.RunStatus.SUCCESSFULLY_FINALIZED
results = PSI.OptimizationProblemResults(decision_optimizer_DA)
var_results = results.variable_values
rt_bid_out = read_variable(results, "EnergyRTBidOut__HybridSystem")
da_bid_out = var_results[PSI.VariableKey{HSS.EnergyDABidOut, HybridSystem}("")]
- @test length(da_bid_out[!, 1]) == horizon_merchant_rt
+ # DA bid variables span hourly DA slots over the model horizon; RT bids span RT steps.
+ @test length(da_bid_out[!, 1]) == horizon_merchant_da
@test length(rt_bid_out[!, 1]) == horizon_merchant_rt
end
@@ -59,6 +65,10 @@ end
_run_only_energy_case(288, 24)
end
+@testset "Test HybridSystem Merchant Decision Model Only Energy Hourly DA Prices" begin
+ _run_only_energy_case(288, 24; use_rt_resolution_for_da = false)
+end
+
@testset "Test HybridSystem Merchant Decision Model Only Energy Extended Horizon" begin
_run_only_energy_case(864, 72)
end
diff --git a/test/test_merchant_sequence.jl b/test/test_merchant_sequence.jl
index 078b1434..d4b9cb47 100644
--- a/test/test_merchant_sequence.jl
+++ b/test/test_merchant_sequence.jl
@@ -60,5 +60,23 @@
else
@test execute!(sim_optimizer; enable_progress_bar = false) ==
PSI.RunStatus.SUCCESSFULLY_FINALIZED
+
+ # Verify the merchant stage wrote its DA bids to the store: one finite value per
+ # hourly DA slot. (The UC system carries no hybrid, so the FixValueFeedforwards
+ # have no target variables there — pinning UC PCC variables to merchant DA bids
+ # is structurally infeasible while merchant DA buy/sell positions can overlap.)
+ sim_results = SimulationResults(sim_optimizer)
+ merchant_results = get_decision_problem_results(
+ sim_results,
+ "MerchantHybridEnergyCase_Sequence",
+ )
+ for merchant_var in
+ ("EnergyDABidOut__HybridSystem", "EnergyDABidIn__HybridSystem")
+ bid_df = first(values(read_variable(merchant_results, merchant_var)))
+ @test nrow(bid_df) == 24
+ value_cols = [c for c in names(bid_df) if eltype(bid_df[!, c]) <: Real]
+ @test !isempty(value_cols)
+ @test all(isfinite, Matrix(bid_df[!, value_cols]))
+ end
end
end
diff --git a/test/test_utils/function_utils.jl b/test/test_utils/function_utils.jl
index 67880ae0..95a6cf96 100644
--- a/test/test_utils/function_utils.jl
+++ b/test/test_utils/function_utils.jl
@@ -357,7 +357,10 @@ function _read_hybrid_profile_underlying_values(hybrid::PSY.HybridSystem, ts_nam
vm = getfield(ta, :values)
vals = ndims(vm) == 1 ? Vector(vm) : vec(vm[:, 1])
return vals, first(ts)
- catch
+ catch e
+ # IS throws ArgumentError when the SingleTimeSeries is missing; anything else
+ # (API drift, data corruption) should fail the test loudly.
+ e isa ArgumentError || rethrow()
for ts in collect(
PSY.get_time_series_multiple(
hybrid;
@@ -388,7 +391,10 @@ function _strip_single_time_series_from_owner!(sys::PSY.System, owner)
res = IS.get_resolution(ts)
try
PSY.remove_time_series!(sys, IS.SingleTimeSeries, owner, nm; resolution = res)
- catch
+ catch e
+ # Already-removed series (shared references) surface as ArgumentError; rethrow
+ # anything else so removal failures don't silently leave conflicting series.
+ e isa ArgumentError || rethrow()
end
end
return
From 860a051828e08c2cdf2cd21ba9b88af4144a33fe Mon Sep 17 00:00:00 2001
From: Jose Daniel Lara
Date: Fri, 12 Jun 2026 12:46:55 -0600
Subject: [PATCH 45/46] fix deps
---
Project.toml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Project.toml b/Project.toml
index 9638cfaf..ebedf07d 100644
--- a/Project.toml
+++ b/Project.toml
@@ -17,6 +17,6 @@ DataStructures = "~0.18, ^0.19"
DocStringExtensions = "0.8, 0.9.2"
JuMP = "^1.28"
MathOptInterface = "1"
-PowerSimulations = "^0.35, ^0.36"
+PowerSimulations = "~0.36.2"
PowerSystems = "^5.11"
julia = "^1.10"
From 732cf1aa23079d8bc7322e5006902c782d798fe4 Mon Sep 17 00:00:00 2001
From: Jose Daniel Lara
Date: Fri, 12 Jun 2026 13:06:56 -0600
Subject: [PATCH 46/46] add the claude.md file
---
.claude/claude.md | 90 +++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 90 insertions(+)
create mode 100644 .claude/claude.md
diff --git a/.claude/claude.md b/.claude/claude.md
new file mode 100644
index 00000000..8bfe0a7c
--- /dev/null
+++ b/.claude/claude.md
@@ -0,0 +1,90 @@
+# HybridSystemsSimulations.jl Repository Guide
+
+> **Development Guidelines:** Always load [Sienna.md](./Sienna.md) development preferences, style conventions, and best practices for projects using Sienna. Before running tests confirm that the [Sienna.md](./Sienna.md) file has been read.
+
+## Overview
+
+HybridSystemsSimulations.jl is an extension of PowerSimulations.jl (PSI) that provides optimization models for `PowerSystems.HybridSystem` devices: a thermal unit, renewable unit, storage, and/or electric load co-located behind a single point of common coupling (PCC). It is part of the Sienna ecosystem.
+
+The package supports two distinct usage modes:
+
+1. **Device formulations** — model a `HybridSystem` as one device inside a standard PSI `ProblemTemplate` (e.g., a UC or ED problem), dispatching the hybrid's internal assets subject to PCC limits.
+2. **Merchant decision models** — custom `PSI.DecisionModel`s (with their own `build_impl!`) that optimize a price-taker hybrid's energy and ancillary-service bids against day-ahead (DA) and real-time (RT) market prices, including a bilevel/KKT formulation.
+
+## Device Formulations
+
+| Formulation | Description |
+|---|---|
+| `HybridEnergyOnlyDispatch` | Energy-only dispatch of the hybrid's internal assets behind the PCC |
+| `HybridDispatchWithReserves` | Dispatch with ancillary-service participation, reserve assignment, and reserve coverage constraints |
+| `HybridFixedDA` | Hybrid with fixed day-ahead positions (used downstream of a merchant stage) |
+
+Optional `DeviceModel` attributes: `"cycling"`, `"energy_target"`, `"regularization"`, `"reservation"`.
+
+## Merchant Decision Models
+
+All subtype `HybridDecisionProblem <: PSI.DecisionProblem` and implement custom `PSI.build_impl!`:
+
+| Decision model | Description |
+|---|---|
+| `MerchantHybridEnergyCase` | DA + RT energy-only bid co-optimization |
+| `MerchantHybridEnergyFixedDA` | RT subproblem with locked DA bids |
+| `MerchantHybridCooptimizerCase` | DA + RT energy and ancillary-service bid co-optimization |
+| `MerchantHybridBilevelCase` | Bilevel formulation (KKT conditions + complementary slackness via SOS1, strong duality) |
+
+A `HybridSystem` must be present in the `System`; the builds error early otherwise.
+
+## Time and Data Conventions (important — easy to get wrong)
+
+- **DA bids are hourly slots; RT variables follow the model resolution.** The DA axis is `merchant_da_time_step_range(container, hybrid)` = `1:min(horizon_hours, DA-series length)`; the variable, parameter, constraint, and objective axes must all use it. RT-to-DA index mapping goes through `merchant_rt_to_da_tmap(rt_len, da_len)` — never hand-roll `div`-based maps.
+- **Storage energy quantities are in energy units**, computed as `get_storage_level_limits(storage).{min,max} * get_storage_capacity(storage)`. The same convention applies to initial conditions (`get_initial_storage_capacity_level * capacity`), cycling limits, and `storage_target`. Do not use the bare level fractions.
+- **Market prices are hybrid-attached scalar `SingleTimeSeries`**, keyed by name (defaults `"DA"`/`"RT"`, override via `model.ext["day_ahead_time_series_key"]` / `"real_time_time_series_key"`):
+ - Energy: `hybrid_energy_price_time_series_name(key)` → `"HybridSystem__energy_price__"`
+ - Ancillary: `hybrid_ancillary_service_price_time_series_name(service, key)`
+ - Profiles: `"RenewableDispatch__max_active_power"`, `"PowerLoad__max_active_power"`
+- `Δt_RT` must come from the container/settings resolution (`PSI.get_resolution`), not from `first(PSY.get_time_series_resolutions(sys))` — systems may carry multiple resolutions and `PSI.validate_time_series!` negotiates the selected one into settings.
+
+## How It Extends PowerSimulations.jl
+
+- **Custom variables**: market bids (`EnergyDABidOut/In`, `EnergyRTBidOut/In`, `BidReserveVariableOut/In`), internal asset variables (`ThermalPower`, `RenewablePower`, `BatteryCharge/Discharge`, `BatteryStatus`), reserves (`TotalReserve`, slacks), and bilevel dual/complementarity variables (`λ`, `μ`, `γ`, `κ`, `ν` families).
+- **Custom parameters**: `DayAheadEnergyPrice`, `RealTimeEnergyPrice`, `AncillaryServicePrice`, `CyclingCharge/DischargeLimitParameter`.
+- **Feedforwards**: `CyclingChargeLimitFeedforward`, `CyclingDischargeLimitFeedforward` for DA→RT cycling budget coupling.
+- **PSI internal overrides** (sensitive to PSI version changes — re-verify on every PSI bump):
+ - `PSI.update_decision_state!` for DA bid and reserve variable state (DA bids span one hour of state rows each; all methods clamp to `max_state_index`).
+ - `PSI._update_parameter_values!` for hybrid profile/price updates during simulation (guarded by `PSI.get_component_names(attributes)`).
+ - `PSI._constituent_cost_expression(::DayAheadEnergyPrice)` — required by PSI's generic `update_variable_cost!`; without it, merchant simulations fail at the first parameter update (not at build).
+ - `PSI.validate_time_series!` for `HybridDecisionProblem` (multi-resolution and interval negotiation).
+- **Catch discipline**: time-series lookups only swallow `ArgumentError` (`e isa ArgumentError || rethrow()`); container probes use `PSI.has_container_key`, never try/catch.
+
+## Source Layout
+
+```
+src/
+ core/ # Type definitions: decision models + time-series keys
+ # (decision_models.jl), formulations, variables,
+ # aux_variables, constraints, expressions, parameters
+ hybrid_system_decision_models.jl # Decision-state updates, simulation-stage parameter glue
+ hybrid_system_device_models.jl # Variable bounds, initial conditions, device-model attributes
+ add_variables.jl # Variable constructors (DA axis via merchant_da_time_step_range)
+ add_aux_variables.jl # Cycling usage aux variables
+ add_parameters.jl # Time-series + price parameters, simulation update overrides
+ add_constraints.jl # All constraints incl. bilevel KKT/complementary slackness
+ objective_function.jl # Cost terms (Δt- and system-unit-scaled) and PSY5 cost helpers
+ feedforwards.jl # Cycling limit feedforwards
+ decision_models/ # build_impl! for only_energy, cooptimizer, bilevel cases
+ hybrid_system_constructor.jl # PSI device constructor entry points
+```
+
+Respect the include order in `src/HybridSystemsSimulations.jl`: `core/` files are included first; new types/constants go there.
+
+## Dependencies and Compatibility
+
+- Requires PowerSystems 5.x and PowerSimulations 0.36.2+ (`~0.36.2` compat deliberately excludes PSI 0.36.0/0.36.1, which are broken for PowerModels-translated networks with parallel branches — `get_equivalent_physical_branch_parameters` MethodError against PNM 0.23).
+- Storage cost access must use PSY5 accessors: `get_charge_variable_cost` / `get_discharge_variable_cost` → `get_vom_cost` → `get_proportional_term`. `StorageCost` has no `variable` field.
+
+## Testing
+
+- Run with `julia --project=test test/runtests.jl` (single testset: append the test file basename, e.g. `test_merchant_sequence`). See Sienna.md for the full test-environment conventions.
+- Test systems come from PowerSystemCaseBuilder (RTS GMLC); market price fixtures are the `test/inputs/chuhsi_*` CSVs (hourly, 5-min, and 300/864-step variants). `attach_hybrid_market_time_series!` in `test/test_utils/function_utils.jl` is the single entry point for attaching them — `use_rt_resolution_for_da` switches between hourly-DA and RT-resolution-DA price setups, and both paths must stay covered.
+- Merchant tests assert hourly DA axes (e.g., 24 DA bids vs 288 RT bids for a 24 h / 5 min model). Always assert `build!`/`solve!`/`execute!` return statuses, not just result shapes.
+- Known modeling caveat: pinning a downstream UC's PCC variables to merchant DA bids via `FixValueFeedforward` is structurally infeasible while merchant DA buy/sell positions can overlap in the same hour (UC's reservation constraint forbids simultaneous in/out). See the note in `test/test_merchant_sequence.jl`.