Skip to content

Commit b7470e8

Browse files
authored
Merge pull request #593 from NatLabRockies/hrly-gen-costs
Add Generator O&M Costs by Hour
2 parents 140a5a9 + d2cc9d0 commit b7470e8

8 files changed

Lines changed: 61 additions & 11 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ Classify the change according to the following categories:
2525
### Deprecated
2626
### Removed
2727

28+
## hrly-gen-costs
29+
### Added
30+
- **Generator** **om_cost_per_hr_per_kw_rated**: Generator non-fuel variable operations and maintenance costs in \$/hr/kw_rated (default of 0.0)
31+
2832
## v0.58.1
2933
### Fixed
3034
- Calculation of offgrid_microgrid_lcoe_dollars_per_kwh for sub-hourly runs.

src/constraints/generator_constraints.jl

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ function add_binGenIsOnInTS_constraints(m,p)
3636
end
3737
end
3838

39-
4039
function add_gen_can_run_constraints(m,p)
4140
if p.s.generator.only_runs_during_grid_outage
4241
for ts in p.time_steps_with_grid, t in p.techs.gen
@@ -58,6 +57,38 @@ function add_gen_rated_prod_constraint(m, p)
5857
)
5958
end
6059

60+
"""
61+
add_generator_hourly_om_charges(m, p)
62+
63+
- add decision variable "dvOMByHourBySizeGen" for the hourly Generator operations and maintenance costs
64+
- add the cost to TotalPerUnitHourOMCosts
65+
"""
66+
function add_generator_hourly_om_charges(m, p)
67+
dv = "dvOMByHourBySizeGen"
68+
m[Symbol(dv)] = @variable(m, [p.techs.gen, p.time_steps], base_name=dv, lower_bound=0)
69+
70+
#Constraint Generator-hourly-om-a: om per hour, per time step >= per_unit_size_cost * size for when on, >= zero when off
71+
@constraint(m, GeneratorHourlyOMBySizeA[t in p.techs.gen, ts in p.time_steps],
72+
p.s.generator.om_cost_per_hr_per_kw_rated * m[Symbol("dvSize")][t] -
73+
(p.s.generator.existing_kw + p.s.generator.max_kw) * p.s.generator.om_cost_per_hr_per_kw_rated * (1-m[Symbol("binGenIsOnInTS")][t,ts])
74+
<= m[Symbol("dvOMByHourBySizeGen")][t, ts]
75+
)
76+
#Constraint Generator-hourly-om-b: om per hour, per time step <= per_unit_size_cost * size for each hour
77+
@constraint(m, GeneratorHourlyOMBySizeB[t in p.techs.gen, ts in p.time_steps],
78+
p.s.generator.om_cost_per_hr_per_kw_rated * m[Symbol("dvSize")][t]
79+
>= m[Symbol("dvOMByHourBySizeGen")][t, ts]
80+
)
81+
#Constraint Generator-hourly-om-c: om per hour, per time step <= zero when off, <= per_unit_size_cost*max_size
82+
@constraint(m, GeneratorHourlyOMBySizeC[t in p.techs.gen, ts in p.time_steps],
83+
(p.s.generator.existing_kw + p.s.generator.max_kw) * p.s.generator.om_cost_per_hr_per_kw_rated * m[Symbol("binGenIsOnInTS")][t,ts]
84+
>= m[Symbol("dvOMByHourBySizeGen")][t, ts]
85+
)
86+
87+
m[:TotalHourlyGenOMCosts] = @expression(m, p.third_party_factor * p.pwf_om *
88+
sum(m[Symbol(dv)][t, ts] * p.hours_per_time_step for t in p.techs.gen, ts in p.time_steps))
89+
nothing
90+
end
91+
6192

6293
"""
6394
add_gen_constraints(m, p)
@@ -70,6 +101,11 @@ function add_gen_constraints(m, p)
70101
add_gen_can_run_constraints(m,p)
71102
add_gen_rated_prod_constraint(m,p)
72103

104+
m[:TotalHourlyGenOMCosts] = 0
105+
if p.s.generator.om_cost_per_hr_per_kw_rated > 1.0E-7
106+
add_generator_hourly_om_charges(m, p)
107+
end
108+
73109
m[:TotalGenPerUnitProdOMCosts] = @expression(m, p.third_party_factor * p.pwf_om *
74110
sum(p.s.generator.om_cost_per_kwh * p.hours_per_time_step *
75111
m[:dvRatedProduction][t, ts] for t in p.techs.gen, ts in p.time_steps)

src/core/generator.jl

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
installed_cost_per_kw::Real = off_grid_flag ? 880 : only_runs_during_grid_outage ? 650.0 : 800.0,
1010
om_cost_per_kw::Real = off_grid_flag ? 10.0 : 20.0,
1111
om_cost_per_kwh::Real = 0.0,
12+
om_cost_per_hr_per_kw_rated::Float64 = 0.0, # Generator non-fuel variable operations and maintenance costs in \$/hr/kw_rated
1213
fuel_cost_per_gallon::Real = 2.25,
1314
electric_efficiency_full_load::Real = 0.322,
1415
electric_efficiency_half_load::Real = electric_efficiency_full_load,
@@ -57,6 +58,7 @@ struct Generator <: AbstractGenerator
5758
installed_cost_per_kw
5859
om_cost_per_kw
5960
om_cost_per_kwh
61+
om_cost_per_hr_per_kw_rated
6062
fuel_cost_per_gallon
6163
electric_efficiency_full_load
6264
electric_efficiency_half_load
@@ -104,6 +106,7 @@ struct Generator <: AbstractGenerator
104106
installed_cost_per_kw::Real = off_grid_flag ? 880 : only_runs_during_grid_outage ? 650.0 : 800.0,
105107
om_cost_per_kw::Real= off_grid_flag ? 10.0 : 20.0,
106108
om_cost_per_kwh::Real = 0.0,
109+
om_cost_per_hr_per_kw_rated::Float64 = 0.0, # Generator non-fuel variable operations and maintenance costs in \$/hr/kw_rated
107110
fuel_cost_per_gallon::Real = 2.25,
108111
electric_efficiency_full_load::Real = 0.322,
109112
electric_efficiency_half_load::Real = electric_efficiency_full_load,
@@ -152,6 +155,7 @@ struct Generator <: AbstractGenerator
152155
installed_cost_per_kw,
153156
om_cost_per_kw,
154157
om_cost_per_kwh,
158+
om_cost_per_hr_per_kw_rated,
155159
fuel_cost_per_gallon,
156160
electric_efficiency_full_load,
157161
electric_efficiency_half_load,

src/core/reopt.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,7 @@ function build_reopt!(m::JuMP.AbstractModel, p::REoptInputs)
274274
add_gen_constraints(m, p)
275275
m[:TotalPerUnitProdOMCosts] += m[:TotalGenPerUnitProdOMCosts]
276276
m[:TotalFuelCosts] += m[:TotalGenFuelCosts]
277+
m[:TotalPerUnitHourOMCosts] += m[:TotalHourlyGenOMCosts]
277278
end
278279

279280
if !isempty(p.techs.chp)

src/mpc/model.jl

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@ function build_mpc!(m::JuMP.AbstractModel, p::MPCInputs)
153153

154154
m[:TotalFuelCosts] = 0.0
155155
m[:TotalPerUnitProdOMCosts] = 0.0
156+
m[:TotalPerUnitHourOMCosts] = 0.0
156157

157158
if !isempty(p.techs.gen)
158159
add_gen_constraints(m, p)
@@ -164,6 +165,7 @@ function build_mpc!(m::JuMP.AbstractModel, p::MPCInputs)
164165
sum(m[:dvFuelUsage][t,ts] * p.s.generator.fuel_cost_per_gallon for t in p.techs.gen, ts in p.time_steps)
165166
)
166167
m[:TotalFuelCosts] += m[:TotalGenFuelCosts]
168+
m[:TotalPerUnitHourOMCosts] += m[:TotalHourlyGenOMCosts]
167169
end
168170

169171
add_elec_utility_expressions(m, p)
@@ -203,7 +205,7 @@ function build_mpc!(m::JuMP.AbstractModel, p::MPCInputs)
203205
@expression(m, Costs,
204206

205207
# Variable O&M
206-
m[:TotalPerUnitProdOMCosts] +
208+
m[:TotalPerUnitProdOMCosts] + m[:TotalPerUnitHourOMCosts] +
207209

208210
# Total Generator Fuel Costs
209211
m[:TotalFuelCosts] +

src/mpc/structs.jl

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,7 @@ function MPCGenerator(;
261261
only_runs_during_grid_outage::Bool = true,
262262
sells_energy_back_to_grid::Bool = false,
263263
om_cost_per_kwh::Real=0.0,
264+
om_cost_per_hr_per_kw_rated::Real=0.0,
264265
)
265266
```
266267
"""
@@ -276,6 +277,7 @@ struct MPCGenerator <: AbstractGenerator
276277
only_runs_during_grid_outage
277278
sells_energy_back_to_grid
278279
om_cost_per_kwh
280+
om_cost_per_hr_per_kw_rated
279281

280282
function MPCGenerator(;
281283
size_kw::Real,
@@ -288,6 +290,7 @@ struct MPCGenerator <: AbstractGenerator
288290
only_runs_during_grid_outage::Bool = true,
289291
sells_energy_back_to_grid::Bool = false,
290292
om_cost_per_kwh::Real=0.0,
293+
om_cost_per_hr_per_kw_rated::Real=0.0,
291294
)
292295

293296
max_kw = size_kw
@@ -304,6 +307,7 @@ struct MPCGenerator <: AbstractGenerator
304307
only_runs_during_grid_outage,
305308
sells_energy_back_to_grid,
306309
om_cost_per_kwh,
310+
om_cost_per_hr_per_kw_rated
307311
)
308312
end
309313
end

src/results/generator.jl

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44
- `size_kw` Optimal generator capacity
55
- `lifecycle_fixed_om_cost_after_tax` Lifecycle fixed operations and maintenance cost in present value, after tax
66
- `year_one_fixed_om_cost_before_tax` fixed operations and maintenance cost over the first year, before considering tax benefits
7-
- `lifecycle_variable_om_cost_after_tax` Lifecycle variable operations and maintenance cost in present value, after tax
8-
- `year_one_variable_om_cost_before_tax` variable operations and maintenance cost over the first year, before considering tax benefits
7+
- `lifecycle_variable_om_cost_after_tax` Lifecycle variable operations and maintenance cost in present value, after tax. Includes om_cost_per_kwh and om_cost_per_hr_per_kw_rated
8+
- `year_one_variable_om_cost_before_tax` variable operations and maintenance cost over the first year, before considering tax benefits. Includes om_cost_per_kwh and om_cost_per_hr_per_kw_rated
99
- `lifecycle_fuel_cost_after_tax` Lifecycle fuel cost in present value, after tax
1010
- `year_one_fuel_cost_before_tax` Fuel cost over the first year, before considering tax benefits. Does not include fuel use during outages if using multiple outage modeling.
1111
- `year_one_fuel_cost_after_tax` Fuel cost over the first year, after considering tax benefits. Does not include fuel use during outages if using multiple outage modeling.
@@ -28,17 +28,13 @@ function add_generator_results(m::JuMP.AbstractModel, p::REoptInputs, d::Dict; _
2828

2929
GenPerUnitSizeOMCosts = @expression(m, p.third_party_factor * p.pwf_om * sum(m[:dvSize][t] * p.om_cost_per_kw[t] for t in p.techs.gen))
3030

31-
GenPerUnitProdOMCosts = @expression(m, p.third_party_factor * p.pwf_om * p.hours_per_time_step *
32-
sum(m[:dvRatedProduction][t, ts] * p.production_factor[t, ts] * p.s.generator.om_cost_per_kwh
33-
for t in p.techs.gen, ts in p.time_steps)
34-
)
3531
r["size_kw"] = round(value(sum(m[:dvSize][t] for t in p.techs.gen)), digits=2)
3632
r["lifecycle_fixed_om_cost_after_tax"] = round(value(GenPerUnitSizeOMCosts) * (1 - p.s.financial.owner_tax_rate_fraction), digits=0)
37-
r["lifecycle_variable_om_cost_after_tax"] = round(value(m[:TotalPerUnitProdOMCosts]) * (1 - p.s.financial.owner_tax_rate_fraction), digits=0)
33+
r["lifecycle_variable_om_cost_after_tax"] = round((value(m[:TotalGenPerUnitProdOMCosts]) + value(m[:TotalHourlyGenOMCosts])) * (1 - p.s.financial.owner_tax_rate_fraction), digits=0)
3834
r["lifecycle_fuel_cost_after_tax"] = round(value(m[:TotalGenFuelCosts]) * (1 - p.s.financial.offtaker_tax_rate_fraction), digits=2)
3935
r["year_one_fuel_cost_before_tax"] = round(value(m[:TotalGenFuelCosts]) / p.pwf_fuel["Generator"], digits=2)
4036
r["year_one_fuel_cost_after_tax"] = r["year_one_fuel_cost_before_tax"] * (1 - p.s.financial.offtaker_tax_rate_fraction)
41-
r["year_one_variable_om_cost_before_tax"] = round(value(GenPerUnitProdOMCosts) / (p.pwf_om * p.third_party_factor), digits=0)
37+
r["year_one_variable_om_cost_before_tax"] = round(value(m[:TotalGenPerUnitProdOMCosts] + m[:TotalHourlyGenOMCosts]) / (p.pwf_om * p.third_party_factor), digits=0)
4238
r["year_one_fixed_om_cost_before_tax"] = round(value(GenPerUnitSizeOMCosts) / (p.pwf_om * p.third_party_factor), digits=0)
4339

4440
if !isempty(p.s.storage.types.elec)

test/runtests.jl

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2500,6 +2500,7 @@ else # run HiGHS tests
25002500
post["PV"]["max_kw"] = 0.0
25012501
post["ElectricStorage"]["max_kw"] = 0.0
25022502
post["Generator"]["min_turn_down_fraction"] = 0.0
2503+
post["Generator"]["om_cost_per_hr_per_kw_rated"] = 2.0
25032504
finalize(backend(m))
25042505
empty!(m)
25052506
GC.gc()
@@ -2518,7 +2519,9 @@ else # run HiGHS tests
25182519
@test r["Financial"]["lifecycle_capital_costs"] 100*(700+324.235442*(1-0.26)) + other_offgrid_capex_after_tax atol=0.1 # replacement in yr 10 is considered tax deductible
25192520
@test r["Financial"]["initial_capital_costs_after_incentives"] 700*100 + other_offgrid_capex_after_tax atol=0.1
25202521
@test r["Financial"]["replacements_future_cost_after_tax"] 700*100
2521-
@test r["Financial"]["replacements_present_cost_after_tax"] 100*(324.235442*(1-0.26)) atol=0.1
2522+
@test r["Financial"]["replacements_present_cost_after_tax"] 100*(324.235442*(1-0.26)) atol=0.1
2523+
generator_hours_runtime = sum(x -> x > 0, r["Generator"]["electric_to_load_series_kw"]) + sum(x -> x > 0, r["Generator"]["electric_to_storage_series_kw"])
2524+
@test r["Generator"]["year_one_variable_om_cost_before_tax"] generator_hours_runtime * r["Generator"]["size_kw"] * post["Generator"]["om_cost_per_hr_per_kw_rated"] atol=0.1
25222525

25232526
## Scenario 3: Fixed Generator that can meet load, but cannot meet load operating reserve requirement
25242527
## This test ensures the load operating reserve requirement is being enforced

0 commit comments

Comments
 (0)