From d027e9675b3f7f2775777ddf0b1803cc17407d6d Mon Sep 17 00:00:00 2001 From: Jacob Schwartz Date: Wed, 4 Oct 2023 17:34:34 -0400 Subject: [PATCH] Maintenance formulation (for thermal generators) (#556) Add an optional formulation for scheduled maintenance. Plants with this formulation active (so far, limited to thermal-commit plants, THERM=1) need to undergo a certain number of contiguous hours of maintenance every y >= 1 years. During this time they produce no power. This may be particularly useful in modeling fission plants, which need roughly 4 weeks of maintenance every 18 or 24 months. (Here, 18 would need to be rounded up to 24, as only maintenance cycles which are an integer number of years work with this formulation.) --- CHANGELOG.md | 1 + docs/make.jl | 1 + docs/src/data_documentation.md | 11 + docs/src/maintenance.md | 101 ++++++++ src/model/resources/maintenance.jl | 217 ++++++++++++++++++ src/model/resources/resources.jl | 38 +++ src/model/resources/thermal/thermal.jl | 11 +- src/model/resources/thermal/thermal_commit.jl | 69 +++++- src/write_outputs/write_maintenance.jl | 28 +++ src/write_outputs/write_outputs.jl | 4 + 10 files changed, 479 insertions(+), 2 deletions(-) create mode 100644 docs/src/maintenance.md create mode 100644 src/model/resources/maintenance.jl create mode 100644 src/write_outputs/write_maintenance.jl diff --git a/CHANGELOG.md b/CHANGELOG.md index 4159ae7dfb..76509c86c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add output for dual of capacity constraint (#473) - Add PR template (#516) - Validation ensures that resource flags (THERM, HYDRO, LDS etc) are self-consistent (#513). +- Maintenance formulation for thermal-commit plants (#556). ### Fixed - Set MUST_RUN=1 for RealSystemExample/small_hydro plants (#517). diff --git a/docs/make.jl b/docs/make.jl index 9cf0d9b0b7..4c76763ebe 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -50,6 +50,7 @@ pages = OrderedDict( "Thermal No Commit" => "thermal_no_commit.md" ], "Hydrogen Electrolyzers" => "electrolyzers.md", + "Scheduled maintenance for various resources" => "maintenance.md", ], "Multi_stage" => [ "Configure multi-stage inputs" => "configure_multi_stage_inputs.md", diff --git a/docs/src/data_documentation.md b/docs/src/data_documentation.md index 068b584302..a987a4b1dc 100644 --- a/docs/src/data_documentation.md +++ b/docs/src/data_documentation.md @@ -355,6 +355,7 @@ This file contains cost and performance parameters for various generators and ot |Min\_Retired\_Cap\_MW |Minimum required discharge capacity retirements in the current model period. This field can be used to enforce lifetime retirements of existing capacity. Note that for co-located VRE-STOR resources, this value pertains to the grid connection (other minimum required discharge capacity retirements for different components of the resource can be found in the VRE-STOR dataframe). | |Min\_Retired\_Energy\_Cap\_MW |Minimum required energy capacity retirements in the current model period. This field can be used to enforce lifetime retirements of existing energy capacity. Note that for co-located VRE-STOR resources, this value pertains to the storage component (other minimum required capacity retirements for different components of the resource can be found in the VRE-STOR dataframe).| |Min\_Retired\_Charge\_Cap\_MW |Minimum required energy capacity retirements in the current model period. This field can be used to enforce lifetime retirements of existing charge capacity. | + ###### Table 6: Settings-specific columns in the Generators\_data.csv file --- |**Column Name** | **Description**| @@ -388,6 +389,11 @@ This file contains cost and performance parameters for various generators and ot |PWFU\_Fuel\_Usage\_Zero\_Load\_MMBTU\_per\_h|The fuel usage (MMBTU/h) for the first PWFU segemnt (y-intercept) at zero load.| |PWFU\_Heat\_Rate\_MMBTU\_per\_MWh\_*i| The slope of fuel usage function of the segment i.| |PWFU\_Load\_Point\_MW\_*i| The end of segment i (MW).| +|**Maintenance data**| +|MAINT|[0,1], toggles scheduled maintenance formulation.| +|Maintenance\_Duration| (Positive integer, less than total length of simulation.) Duration of the maintenance period, in number of timesteps. Only used if `MAINT=1`.| +|Maintenance\_Cycle\_Length\_Years| Length of scheduled maintenance cycle, in years. `1` is maintenance every year, `3` is every three years, etc. (Positive integer. Only used if `MAINT=1`.)| +|Maintenance\_Begin\_Cadence| Cadence of timesteps in which scheduled maintenance can begin. `1` means that a maintenance period can start in any timestep, `24` means it can start only in timesteps 1, 25, 49, etc. A larger number can decrease the simulation computational cost as it limits the optimizer's choices. (Positive integer, less than total length of simulation. Only used if `MAINT=1`.)| |**Electrolyzer related parameters required if the set ELECTROLYZER is not empty**| |Hydrogen_MWh_Per_Tonne| Electrolyzer efficiency in megawatt-hours (MWh) of electricity per metric tonne of hydrogen produced (MWh/t)| |Electrolyzer_Min_kt| Minimum annual quantity of hydrogen that must be produced by electrolyzer in kilotonnes (kt)| @@ -1011,3 +1017,8 @@ Reports solar PV generation in AC terms by each co-located VRE and storage resou #### 3.2.15 vre_stor_wind_power.csv Reports wind generation in AC terms by each co-located VRE and storage resource in each model time step. + +#### 3.2.16 maint_down.csv + +Only written if at least one plant has the scheduled maintenance formulation enabled. +Reports the number of resource-components which are under maintenance during each model time step. diff --git a/docs/src/maintenance.md b/docs/src/maintenance.md new file mode 100644 index 0000000000..6e02c28bc5 --- /dev/null +++ b/docs/src/maintenance.md @@ -0,0 +1,101 @@ +# Optimized Scheduled Maintenance +_Added in v0.4_ + +In the real world, some types of resources (notably, fission) require regular scheduled maintenance, which often takes several weeks. +During this time, the plant produces no power. +This module allows GenX to find the best time of year for plants to undergo maintenance. + +Scheduled maintenance is implemented **only** for thermal plants with unit commitment (THERM=1). + +## Description of the maintenance model +A plant requires a single contiguous period of $h \ge 1$ hours of maintenance, every $y \ge 1$ years. +For each plant, the best time to start the maintenance period is determined by the optimizer. + +During maintenance, the plant cannot be "commited", and therefore + +* uses no fuel, +* produces no power, +* and does not contribute to reserves. + +Additionally, + +* the plant does not contribute to any Capacity Reserve Margin. + +### Treatment of plants that require maintenance only every few years +GenX models a long-term equilibrium, +and each problem generally represents a single full year. +If a plant requires maintenance every $y$ years, we take the simplification that at least $1/y$ of the plants must undergo maintenance in the modeled year. + +See also "Interaction with integer unit commitment" below. + +### Reduction of number of possible start dates +This module creates constraints which work across long periods, and consequently can be very expensive to solve. +In order to reduce the expense, the set of possible maintenance start dates can be limited. +Rather than have maintenance potentially start every hour, one can have possible start dates which are once per day, once per week, etc. +(In reality, maintenance is likely scheduled months in advance, so optimizing down to the hour may not be realistic anyway.) + +## How to use +There are four columns which need to be added to the plant data, i.e. in `Generators_data.csv`: + +1. `MAINT` should be `1` for plants that require maintenance and `0` otherwise. +2. `Maintenance_Duration` is the number of hours the maintenance period lasts. +3. `Maintenance_Cycle_Length_Years`. If `1`, maintenance every year, if `3` maintenance every 3 years, etc. +4. `Maintenance_Begin_Cadence`. Spacing between hours in which maintenance can start. + +The last three fields must be integers which are greater than 0. +They are ignored for any plants which do not require maintenance. + +`Maintenance_Duration` must be less than the total number of hours in the year. + +If `Maintenance_Begin_Cadence` is `1` then the maintenance can begin in any hour. +If it is `168` then it can begin in hours 1, 169, 337, etc. + +## Restrictions on use +The maintenance module has these restrictions: + +- More than a single maintenance period per year (i.e. every three months) is not possible in the current formulation. +- Only full-year cases can be run; there must be only one "representative period". +It would not make sense to model a *month*-long maintenance period when the year is modeled as a series of representative *weeks*, for example. + +### Interaction with integer unit commitment +If integer unit commitment is on (`UCommit=1`) this module may not produce correct results; there may be more maintenance than the user wants. +This is because the formulation specifies that the number of plants that go down for maintenance in the simulated year must be at least (the number of plants in the zone)/(the maintenance cycle length in years). +As a reminder, the number of plants is `eTotalCap / Cap_Size`. + +If there were three 500 MW plants (total 1500 MW) in a zone, and they require maintenance every three years (`Maintenance_Cycle_Length_Years=3`), +the formulation will work properly: one of the three plants will go under maintenance. + +But if there was only one 500 MW plant, and it requires maintenance every 3 years, the constraint will still make it do maintenance **every year**, because `ceil(1/3)` is `1`. The whole 500 MW plant will do maintenance. This is the unexpected behavior. + +However, if integer unit commitment was relaxed to "linearized" unit commitment (`UCommit=2`), the model will have only 500 MW / 3 = 166.6 MW worth of this plant do maintenance. + +## Hint: pre-scheduling maintenance +If you want to pre-schedule when maintenance occurs, you might not need this module. +Instead, you could set the maximum power output of the plant to zero for a certain period, or make its fuel extremely expensive during that time. +However, the plant would still be able to contribute to the Capacity Reserve Margin. + +## Outputs produced +If at least one plant has `MAINT=1`, a file `maint_down.csv` will be written listing how many plants are down for maintenance in each timestep. + +## Notes on mathematical formulation +The formulation of the maintenance state is very similar to the formulation of unit commitment. + +There is a variable called something like `vMSHUT` which is analogous to `vSTART` and controls the start of the maintenance period. +There is another variable called something like `vMDOWN` analogous to `vCOMMIT` which controls the maintenance status in any hour. + +A constraint ensures that the value of `vMDOWN` in any hour is always more than the number of `vMSHUT`s in the previous `Maintenance_Duration` hours. + +Another constraint ensures that the number of plants committed (`vCOMMIT`) at any one time plus the number of plants under maintenance (`vMDOWN`) is less than the total number of plants. + +## Developer note: adding maintenance to a resource +The maintenance formulation is applied on a per-resource basis, by calling the function `maintenance_formulation!`. + +```@docs +GenX.maintenance_formulation! +``` + +See `maintenance_formulation_thermal_commit!` for an example of how to apply it to a new resource. + +* The resource must have a `vCOMMIT`-like variable which is proportional to maximum the power output, etc at any given timestep. +* The resource must have a `eTotalCap`-like quantity and a `Cap_Size`-like parameter; only the ratio of the two is used. + diff --git a/src/model/resources/maintenance.jl b/src/model/resources/maintenance.jl new file mode 100644 index 0000000000..1499fa09c8 --- /dev/null +++ b/src/model/resources/maintenance.jl @@ -0,0 +1,217 @@ +const MAINTENANCE_DOWN_VARS = "MaintenanceDownVariables" +const MAINTENANCE_SHUT_VARS = "MaintenanceShutVariables" + +@doc raw""" + resources_with_maintenance(df::DataFrame)::Vector{Int} + + Get a vector of the R_ID's of all resources listed in a dataframe + that have maintenance requirements. If there are none, return an empty vector. + + This method takes a specific dataframe because compound resources may have their + data in multiple dataframes. +""" +function resources_with_maintenance(df::DataFrame)::Vector{Int} + if "MAINT" in names(df) + df[df.MAINT.>0, :R_ID] + else + Vector{Int}[] + end +end + +@doc raw""" + maintenance_down_name(resource_component::AbstractString)::String + + JuMP variable name to control whether a resource-component is down for maintenance. + Here resource-component could be a whole resource or a component (for complex resources). +""" +function maintenance_down_name(resource_component::AbstractString)::String + "vMDOWN_" * resource_component +end + +@doc raw""" + maintenance_shut_name(resource_component::AbstractString)::String + + JuMP variable name to control when a resource-components begins maintenance. + Here resource-component could be a whole resource or a component (for complex resources). +""" +function maintenance_shut_name(resource_component::AbstractString)::String + "vMSHUT_" * resource_component +end + +function sanity_check_maintenance(MAINT::Vector{Int}, inputs::Dict) + rep_periods = inputs["REP_PERIOD"] + + is_maint_reqs = !isempty(MAINT) + if rep_periods > 1 && is_maint_reqs + @error """Resources with R_ID $MAINT have MAINT > 0, + but the number of representative periods ($rep_periods) is greater than 1. + These are incompatible with a Maintenance requirement.""" + error("Incompatible GenX settings and maintenance requirements.") + end +end + +@doc raw""" + controlling_maintenance_start_hours(p::Int, t::Int, maintenance_duration::Int, maintenance_begin_hours::UnitRange{Int64}) + + p: hours_per_subperiod + t: the current hour + maintenance_duration: length of a maintenance period + maintenance_begin_hours: collection of hours in which maintenance is allowed to start +""" +function controlling_maintenance_start_hours( + p::Int, + t::Int, + maintenance_duration::Int, + maintenance_begin_hours, +) + controlled_hours = hoursbefore(p, t, 0:(maintenance_duration-1)) + return intersect(controlled_hours, maintenance_begin_hours) +end + +@doc raw""" + maintenance_formulation!(EP::Model, + inputs::Dict, + resource_component::AbstractString, + r_id::Int, + maint_begin_cadence::Int, + maint_dur::Int, + maint_freq_years::Int, + cap::Float64, + vcommit::Symbol, + ecap::Symbol, + integer_operational_unit_commitment::Bool) + + EP: the JuMP model + inputs: main data storage + resource_component: unique resource name with optional component name + If the plant has more than one component, this could identify a specific part which + is undergoing maintenance. + r_id: Resource ID (unique resource integer) + maint_begin_cadence: + It may be too expensive (from an optimization perspective) to allow maintenance + to begin at any time step during the simulation. Instead this integer describes + the cadence of timesteps in which maintenance can begin. Must be at least 1. + maint_dur: Number of timesteps that maintenance takes. Must be at least 1. + maint_freq_years: 1 is maintenannce every year, + 2 is maintenance every other year, etc. Must be at least 1. + cap: Plant electrical capacity. + vcommit: Symbol of vCOMMIT-like variable. + ecap: Symbol of eTotalCap-like variable. + integer_operational_unit_commitment: whether this plant has integer unit + commitment for operational variables. + + Creates maintenance-tracking variables and adds their Symbols to two Sets in `inputs`. + Adds constraints which act on the vCOMMIT-like variable. +""" +function maintenance_formulation!( + EP::Model, + inputs::Dict, + resource_component::AbstractString, + r_id::Int, + maint_begin_cadence::Int, + maint_dur::Int, + maint_freq_years::Int, + cap::Float64, + vcommit::Symbol, + ecap::Symbol, + integer_operational_unit_commitment::Bool, +) + + T = 1:inputs["T"] + hours_per_subperiod = inputs["hours_per_subperiod"] + + y = r_id + down_name = maintenance_down_name(resource_component) + shut_name = maintenance_shut_name(resource_component) + down = Symbol(down_name) + shut = Symbol(shut_name) + + union!(inputs[MAINTENANCE_DOWN_VARS], (down,)) + union!(inputs[MAINTENANCE_SHUT_VARS], (shut,)) + + maintenance_begin_hours = 1:maint_begin_cadence:T[end] + + # create variables + vMDOWN = EP[down] = @variable(EP, [t in T], base_name = down_name, lower_bound = 0) + vMSHUT = + EP[shut] = @variable( + EP, + [t in maintenance_begin_hours], + base_name = shut_name, + lower_bound = 0 + ) + + if integer_operational_unit_commitment + set_integer.(vMDOWN) + set_integer.(vMSHUT) + end + + vcommit = EP[vcommit] + ecap = EP[ecap] + + # Maintenance variables are measured in # of plants + @constraints(EP, begin + [t in maintenance_begin_hours], vMSHUT[t] <= ecap[y] / cap + end) + + # Plant is non-committed during maintenance + @constraint(EP, [t in T], vMDOWN[t] + vcommit[y, t] <= ecap[y] / cap) + + controlling_hours(t) = controlling_maintenance_start_hours( + hours_per_subperiod, + t, + maint_dur, + maintenance_begin_hours, + ) + # Plant is down for the required number of hours + @constraint(EP, [t in T], vMDOWN[t] == sum(vMSHUT[controlling_hours(t)])) + + # Plant requires maintenance every (certain number of) year(s) + @constraint( + EP, + sum(vMSHUT[t] for t in maintenance_begin_hours) >= ecap[y] / cap / maint_freq_years + ) + + return +end + +@doc raw""" + ensure_maintenance_variable_records!(dict::Dict) + + dict: a dictionary of model data + + This should be called by each method that adds maintenance formulations, + to ensure that certain entries in the model data dict exist. +""" +function ensure_maintenance_variable_records!(dict::Dict) + for var in (MAINTENANCE_DOWN_VARS, MAINTENANCE_SHUT_VARS) + if var ∉ keys(dict) + dict[var] = Set{Symbol}() + end + end +end + +@doc raw""" + has_maintenance(dict::Dict) + + dict: a dictionary of model data + + Checks whether the dictionary contains listings of maintenance-related variable names. + This is true only after `maintenance_formulation!` has been called. +""" +function has_maintenance(dict::Dict)::Bool + rep_periods = dict["REP_PERIOD"] + MAINTENANCE_DOWN_VARS in keys(dict) && rep_periods == 1 +end + +@doc raw""" + maintenance_down_variables(dict::Dict) + + dict: a dictionary of model data + + get listings of maintenance-related variable names. + This is available only after `maintenance_formulation!` has been called. +""" +function maintenance_down_variables(dict::Dict)::Set{Symbol} + dict[MAINTENANCE_DOWN_VARS] +end diff --git a/src/model/resources/resources.jl b/src/model/resources/resources.jl index af88ac1ab8..99e142dfe8 100644 --- a/src/model/resources/resources.jl +++ b/src/model/resources/resources.jl @@ -62,6 +62,43 @@ function check_longdurationstorage_applicability(r::GenXResource) return error_strings end +@doc raw""" + check_maintenance_applicability(r::GenXResource) + +Check whether the MAINT flag is set appropriately +""" +function check_maintenance_applicability(r::GenXResource) + applicable_resources = [:THERM] + + not_set = resource_attribute_not_set() + value = get(r, :MAINT, not_set) + + error_strings = String[] + + if value == not_set + # not MAINT so the rest is not applicable + return error_strings + end + + check_for_flag_set(el) = get(r, el, not_set) > 0 + statuses = check_for_flag_set.(applicable_resources) + + if count(statuses) == 0 + e = string("Resource ", resource_name(r), " has :MAINT = ", value, ".\n", + "This setting is valid only for resources where the type is \n", + "one of $applicable_resources. \n", + ) + push!(error_strings, e) + end + if get(r, :THERM, not_set) == 2 + e = string("Resource ", resource_name(r), " has :MAINT = ", value, ".\n", + "This is valid only for resources with unit commitment (:THERM = 1);\n", + "this has :THERM = 2.") + push!(error_strings, e) + end + return error_strings +end + @doc raw""" check_resource(r::GenXResource)::Vector{String} @@ -72,6 +109,7 @@ function check_resource(r::GenXResource)::Vector{String} e = String[] e = [e; check_resource_type_flags(r)] e = [e; check_longdurationstorage_applicability(r)] + e = [e; check_maintenance_applicability(r)] return e end diff --git a/src/model/resources/thermal/thermal.jl b/src/model/resources/thermal/thermal.jl index 856313673c..a849a16d46 100644 --- a/src/model/resources/thermal/thermal.jl +++ b/src/model/resources/thermal/thermal.jl @@ -30,8 +30,16 @@ function thermal!(EP::Model, inputs::Dict, setup::Dict) # Capacity Reserves Margin policy if setup["CapacityReserveMargin"] > 0 - @expression(EP, eCapResMarBalanceThermal[res=1:inputs["NCapacityReserveMargin"], t=1:T], sum(dfGen[y,Symbol("CapRes_$res")] * EP[:eTotalCap][y] for y in THERM_ALL)) + ncapres = inputs["NCapacityReserveMargin"] + capresfactor(y, capres) = dfGen[y, Symbol("CapRes_$capres")] + @expression(EP, eCapResMarBalanceThermal[capres in 1:ncapres, t in 1:T], + sum(capresfactor(y, capres) * EP[:eTotalCap][y] for y in THERM_ALL)) add_similar_to_expression!(EP[:eCapResMarBalance], eCapResMarBalanceThermal) + + MAINT = resources_with_maintenance(dfGen) + if !isempty(intersect(MAINT, THERM_COMMIT)) + thermal_maintenance_capacity_reserve_margin_adjustment!(EP, inputs) + end end #= ##CO2 Polcy Module Thermal Generation by zone @@ -41,3 +49,4 @@ function thermal!(EP::Model, inputs::Dict, setup::Dict) EP[:eGenerationByZone] += eGenerationByThermAll =# ##From main end + diff --git a/src/model/resources/thermal/thermal_commit.jl b/src/model/resources/thermal/thermal_commit.jl index 5e1b3e8b0f..dce4c29aa2 100644 --- a/src/model/resources/thermal/thermal_commit.jl +++ b/src/model/resources/thermal/thermal_commit.jl @@ -218,7 +218,9 @@ function thermal_commit!(EP::Model, inputs::Dict, setup::Dict) ) ## END Constraints for thermal units subject to integer (discrete) unit commitment decisions - + if !isempty(resources_with_maintenance(dfGen)) + maintenance_formulation_thermal_commit!(EP, inputs, setup) + end end @doc raw""" @@ -336,3 +338,68 @@ function thermal_commit_reserves!(EP::Model, inputs::Dict) end +@doc raw""" + maintenance_formulation_thermal_commit!(EP::Model, inputs::Dict, setup::Dict) + + Creates maintenance variables and constraints for thermal-commit plants. +""" +function maintenance_formulation_thermal_commit!(EP::Model, inputs::Dict, setup::Dict) + + @info "Maintenance Module for Thermal plants" + + ensure_maintenance_variable_records!(inputs) + dfGen = inputs["dfGen"] + by_rid(rid, sym) = by_rid_df(rid, sym, dfGen) + + MAINT = resources_with_maintenance(dfGen) + resource_component(y) = by_rid(y, :Resource) + cap(y) = by_rid(y, :Cap_Size) + maint_dur(y) = Int(floor(by_rid(y, :Maintenance_Duration))) + maint_freq(y) = Int(floor(by_rid(y, :Maintenance_Cycle_Length_Years))) + maint_begin_cadence(y) = Int(floor(by_rid(y, :Maintenance_Begin_Cadence))) + + integer_operational_unit_committment = setup["UCommit"] == 1 + + vcommit = :vCOMMIT + ecap = :eTotalCap + + sanity_check_maintenance(MAINT, inputs) + + for y in MAINT + maintenance_formulation!(EP, + inputs, + resource_component(y), + y, + maint_begin_cadence(y), + maint_dur(y), + maint_freq(y), + cap(y), + vcommit, + ecap, + integer_operational_unit_committment) + end +end + +@doc raw""" + thermal_maintenance_capacity_reserve_margin_adjustment!(EP::Model, inputs::Dict) + + Eliminates the contribution of a plant to the capacity reserve margin while it is down + for maintenance. +""" +function thermal_maintenance_capacity_reserve_margin_adjustment!(EP::Model, + inputs::Dict) + dfGen = inputs["dfGen"] + T = inputs["T"] # Number of time steps (hours) + ncapres = inputs["NCapacityReserveMargin"] + THERM_COMMIT = inputs["THERM_COMMIT"] + MAINT = resources_with_maintenance(dfGen) + applicable_resources = intersect(MAINT, THERM_COMMIT) + + resource_component(y) = dfGen[y, :Resource] + capresfactor(y, capres) = dfGen[y, Symbol("CapRes_$capres")] + cap_size(y) = dfGen[y, :Cap_Size] + down_var(y) = EP[Symbol(maintenance_down_name(resource_component(y)))] + maint_adj = @expression(EP, [capres in 1:ncapres, t in 1:T], + -sum(capresfactor(y, capres) * down_var(y)[t] * cap_size(y) for y in applicable_resources)) + add_similar_to_expression!(EP[:eCapResMarBalance], maint_adj) +end diff --git a/src/write_outputs/write_maintenance.jl b/src/write_outputs/write_maintenance.jl new file mode 100644 index 0000000000..741abdcfa8 --- /dev/null +++ b/src/write_outputs/write_maintenance.jl @@ -0,0 +1,28 @@ +function write_simple_csv(filename::AbstractString, df::DataFrame) + CSV.write(filename, df) +end + +function write_simple_csv(filename::AbstractString, header::Vector, matrix) + df = DataFrame(matrix, header) + write_simple_csv(filename, df) +end + +function prepare_timeseries_variables(EP::Model, set::Set{Symbol}) + # function to extract data from DenseAxisArray + data(var) = value.(EP[var]).data + + return DataFrame(set .=> data.(set)) +end + +function write_timeseries_variables(EP, set::Set{Symbol}, filename::AbstractString) + df = prepare_timeseries_variables(EP, set) + write_simple_csv(filename, df) +end + +@doc raw""" + write_maintenance(path::AbstractString, inputs::Dict, EP::Model) +""" +function write_maintenance(path::AbstractString, inputs::Dict, EP::Model) + downvars = maintenance_down_variables(inputs) + write_timeseries_variables(EP, downvars, joinpath(path, "maint_down.csv")) +end diff --git a/src/write_outputs/write_outputs.jl b/src/write_outputs/write_outputs.jl index 13e7df2f57..32abd1b6f0 100644 --- a/src/write_outputs/write_outputs.jl +++ b/src/write_outputs/write_outputs.jl @@ -136,6 +136,10 @@ function write_outputs(EP::Model, path::AbstractString, setup::Dict, inputs::Dic println("Time elapsed for writing co2 is") println(elapsed_time_emissions) + if has_maintenance(inputs) + write_maintenance(path, inputs, EP) + end + # Temporary! Suppress these outputs until we know that they are compatable with multi-stage modeling if setup["MultiStage"] == 0 dfPrice = DataFrame()