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 text](url) + # !\[…\] — alt in brackets; \(…\) — path in parens markdown_image_pattern = r"!\[[^\]]*\]\([^\)]*\)" + # A bare not already covered by the

case above. + # ]*? — attributes; /?> — self-closing or > standalone_img_pattern = r"]*?/?>" + # Union of the four cases: (?: A | B | C | D ) image_fragment_pattern = Regex( "(?:" * p_with_img_pattern.pattern * "|" * @@ -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`.