Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,14 @@ developer_nrel_key.rb
lib/measures/.rubocop.yml
lib/measures/test_results/*
lib/measures/*/tests/output/*

# TEMPORARY
balanced_scenario_5_3
unbalanced_scenario_5_3
test_reopt_1.rb
test_reopt_balanced_load_ny_basecomparison.rb
test_reopt_balanced_load_ny.rb
test_reopt_balanced_load.rb
test_reopt_demo.rb
test_reopt_unbalanced_load_ny.rb
reopt_ghp_result_summary.json
3 changes: 2 additions & 1 deletion lib/urbanopt/reopt.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@
require 'urbanopt/reopt/reopt_post_processor'
require 'urbanopt/reopt/version'
require 'urbanopt/reopt/reopt_ghp_post_processor'
require 'urbanopt/reopt/reopt_ghp_adapter'
require 'urbanopt/reopt/reopt_ghp_adapter_ghp'
require 'urbanopt/reopt/reopt_ghp_api'
require 'urbanopt/reopt/reopt_ghp_result'

Large diffs are not rendered by default.

15 changes: 12 additions & 3 deletions lib/urbanopt/reopt/reopt_ghp_post_processor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
require 'bundler/setup'
require 'urbanopt/reopt/reopt_logger'
require 'urbanopt/reopt/reopt_ghp_api'
require 'urbanopt/reopt/reopt_ghp_result'
require 'csv'
require 'json'
require 'fileutils'
Expand Down Expand Up @@ -115,12 +116,14 @@ def run_reopt_lcca(system_parameter_hash: nil, reopt_ghp_assumptions_hash: nil,
end

building_ids.each do |building_id|
# create REopt building input file for all buildings in loop order list
reopt_input_building = adapter.create_reopt_input_building(@run_dir, @system_parameter_input_hash, @reopt_ghp_assumptions_input_hash, building_id, @modelica_result_input)
# create REopt building input file for all buildings in loop order list in GHP scenario
reopt_input_building = adapter.create_reopt_input_building_ghp(@run_dir, @system_parameter_input_hash, @reopt_ghp_assumptions_input_hash, building_id, @modelica_result_input)
#create REopt building input file for all buildings in loop order list in BAU scenario
reopt_input_building_bau = adapter.create_reopt_input_building_bau(@run_dir, @system_parameter_input_hash, @reopt_ghp_assumptions_input_hash, building_id, @modelica_result_input)
end
ghp_ids.each do |ghp_id|
# create REopt district input file
reopt_input_district = adapter.create_reopt_input_district(@run_dir, @system_parameter_input_hash, @reopt_ghp_assumptions_input_hash, ghp_id, @modelica_result_input)
reopt_input_district = adapter.create_reopt_input_district_ghp(@run_dir, @system_parameter_input_hash, @reopt_ghp_assumptions_input_hash, ghp_id, @modelica_result_input)
end

Dir.foreach(reopt_ghp_input) do |input_file|
Expand All @@ -142,8 +145,14 @@ def run_reopt_lcca(system_parameter_hash: nil, reopt_ghp_assumptions_hash: nil,
# call the REopt API
api = URBANopt::REopt::REoptLiteGHPAPI.new(reopt_input_data, DEVELOPER_NREL_KEY, reopt_output_file, @localhost)
api.get_api_results

end

## POST PROCESS RESULTS
ghp_results = URBANopt::REopt::REoptGHPResult.new
results = ghp_results.result_calculate(reopt_ghp_dir)
end

end # REoptGHPPostProcessor
end # REopt
end # URBANopt
145 changes: 145 additions & 0 deletions lib/urbanopt/reopt/reopt_ghp_result.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
require 'json'
require 'fileutils'

module URBANopt # :nodoc:
module REopt # :nodoc:
class REoptGHPResult

def initialize
@@logger ||= URBANopt::REopt.reopt_logger
end

def result_calculate(reopt_ghp_dir)
reopt_output = File.join(reopt_ghp_dir, 'reopt_ghp_outputs')
bau_outputs = []
ghp_outputs = []

# Collect all valid output files with building ID
Dir.glob(File.join(reopt_output, '*_output.json')) do |file|
filename = File.basename(file)
parts = filename.split('_') # ["BAU", "building", "1", "output.json"]
next unless parts.length >= 4

scenario = parts[0] # "BAU", "GHP", etc.
building_id = parts[1..2].join('_') # "building_1"

if scenario == 'BAU'
bau_outputs << [file, building_id]
elsif ['GHP', 'GHX'].include?(scenario)
ghp_outputs << [file, building_id]
end
end

# Initialize grouped results
lcc = {}
lifecycle_capital_costs = {}
initial_capital_costs = {}
initial_capital_costs_after_incentives = {}
lifecycle_elecbill_after_tax = {}
npv = {}

# Totals
lcc_total_bau = 0
lcc_total_ghp = 0
lifecycle_capital_costs_total_bau = 0
lifecycle_capital_costs_total_ghp = 0
initial_capital_costs_total_bau = 0
initial_capital_costs_total_ghp = 0
initial_capital_costs_after_incentives_total_bau = 0
initial_capital_costs_after_incentives_total_ghp = 0
lifecycle_elecbill_after_tax_total_bau = 0
lifecycle_elecbill_after_tax_total_ghp = 0
npv_total_bau = 0
npv_total_ghp = 0

# Process BAU files
bau_outputs.each do |file, building_id|
data = JSON.parse(File.read(file), symbolize_names: true)
financial = data.dig(:outputs, :Financial) || {}

lcc["lcc_bau_#{building_id}"] = financial[:lcc] || 0
lcc_total_bau += lcc["lcc_bau_#{building_id}"]

lifecycle_capital_costs["bau_#{building_id}"] = financial[:lifecycle_capital_costs] || 0
lifecycle_capital_costs_total_bau += lifecycle_capital_costs["bau_#{building_id}"]

initial_capital_costs["bau_#{building_id}"] = financial[:initial_capital_costs] || 0
initial_capital_costs_total_bau += initial_capital_costs["bau_#{building_id}"]

initial_capital_costs_after_incentives["bau_#{building_id}"] = financial[:initial_capital_costs_after_incentives] || 0
initial_capital_costs_after_incentives_total_bau += initial_capital_costs_after_incentives["bau_#{building_id}"]

lifecycle_elecbill_after_tax["bau_#{building_id}"] = financial[:lifecycle_elecbill_after_tax_bau] || 0
lifecycle_elecbill_after_tax_total_bau += lifecycle_elecbill_after_tax["bau_#{building_id}"]

npv["bau_#{building_id}"] = financial[:npv] || 0
npv_total_bau += npv["bau_#{building_id}"]
end

# Process GHP files
ghp_outputs.each do |file, building_id|
data = JSON.parse(File.read(file), symbolize_names: true)
financial = data.dig(:outputs, :Financial) || {}

lcc["lcc_ghp_#{building_id}"] = financial[:lcc] || 0
lcc_total_ghp += lcc["lcc_ghp_#{building_id}"]

lifecycle_capital_costs["ghp_#{building_id}"] = financial[:lifecycle_capital_costs] || 0
lifecycle_capital_costs_total_ghp += lifecycle_capital_costs["ghp_#{building_id}"]

initial_capital_costs["ghp_#{building_id}"] = financial[:initial_capital_costs] || 0
initial_capital_costs_total_ghp += initial_capital_costs["ghp_#{building_id}"]

initial_capital_costs_after_incentives["ghp_#{building_id}"] = financial[:initial_capital_costs_after_incentives] || 0
initial_capital_costs_after_incentives_total_ghp += initial_capital_costs_after_incentives["ghp_#{building_id}"]

lifecycle_elecbill_after_tax["ghp_#{building_id}"] = financial[:lifecycle_elecbill_after_tax] || 0
lifecycle_elecbill_after_tax_total_ghp += lifecycle_elecbill_after_tax["ghp_#{building_id}"]

npv["ghp_#{building_id}"] = financial[:npv] || 0
npv_total_ghp += npv["ghp_#{building_id}"]
end

# Add totals and net values
lcc["lcc_bau_total"] = lcc_total_bau
lcc["lcc_ghp_total"] = lcc_total_ghp
lcc["lcc_net"] = lcc_total_ghp - lcc_total_bau

lifecycle_capital_costs["bau_total"] = lifecycle_capital_costs_total_bau
lifecycle_capital_costs["ghp_total"] = lifecycle_capital_costs_total_ghp
lifecycle_capital_costs["net"] = lifecycle_capital_costs_total_ghp - lifecycle_capital_costs_total_bau

initial_capital_costs["bau_total"] = initial_capital_costs_total_bau
initial_capital_costs["ghp_total"] = initial_capital_costs_total_ghp
initial_capital_costs["net"] = initial_capital_costs_total_ghp - initial_capital_costs_total_bau

initial_capital_costs_after_incentives["bau_total"] = initial_capital_costs_after_incentives_total_bau
initial_capital_costs_after_incentives["ghp_total"] = initial_capital_costs_after_incentives_total_ghp
initial_capital_costs_after_incentives["net"] = initial_capital_costs_after_incentives_total_ghp - initial_capital_costs_after_incentives_total_bau

lifecycle_elecbill_after_tax["bau_total"] = lifecycle_elecbill_after_tax_total_bau
lifecycle_elecbill_after_tax["ghp_total"] = lifecycle_elecbill_after_tax_total_ghp
lifecycle_elecbill_after_tax["net"] = lifecycle_elecbill_after_tax_total_ghp - lifecycle_elecbill_after_tax_total_bau

npv["bau_total"] = npv_total_bau
npv["ghp_total"] = npv_total_ghp
npv["net"] = npv_total_ghp - npv_total_bau

# Final result structure
result_data = {
lcc: lcc,
lifecycle_capital_cost: lifecycle_capital_costs,
initial_capital_costs: initial_capital_costs,
initial_capital_costs_after_incentives: initial_capital_costs_after_incentives,
lifecycle_elecbill_after_tax: lifecycle_elecbill_after_tax,
npv: npv
}

output_path = File.join(reopt_ghp_dir, "reopt_ghp_result_summary.json")
File.open(output_path, "w") { |file| file.write(JSON.pretty_generate(result_data)) }
puts "Wrote summary to #{output_path}"
end

end
end
end
174 changes: 174 additions & 0 deletions lib/urbanopt/reopt/reopt_schema/REopt-BAU-input.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
{
"title": "REopt GHP BAU Inputs (Minimal Valid Schema Wrapper)",
"type": "object",
"properties": {
"Site": {
"type": "object",
"properties": {
"latitude": {
"type": "number",
"minimum": -90,
"maximum": 90,
"description": "The approximate latitude of the site in decimal degrees."
},
"longitude": {
"type": "number",
"minimum": -180,
"maximum": 180,
"description": "The approximate longitude of the site in decimal degrees."
}
},
"required": ["latitude", "longitude"]
},

"SpaceHeatingLoad": {
"type": "object",
"properties": {
"fuel_loads_mmbtu_per_hour": {
"type": "array",
"description": "This is the fuel energy consumption for space heating at the individual building level. This is used in the business as usual (BAU) LCCA analysis in the GHP module to calculate BAU bills. These are 8760 hourly timeseries values.",
"note": "This parameter is required to run REopt's BAU for GHP module. If there is no fuel based heating in BAU, add values close to 0."
}
},
"required": ["fuel_loads_mmbtu_per_hour"]
},

"DomesticHotWaterLoad": {
"type": "object",
"properties": {
"fuel_loads_mmbtu_per_hour": {
"type": "array",
"description": "This is the fuel enegry consumption for providing domestic hot water at the individual building level. This is used in the business as usual (BAU) LCCA analysis in the GHP module. These are 8760 hourly timeseries values.",
"note": "Only required if BAU system has DHW load and uses fuel for this load. If BAU system has no DHW load, or does not use fuel for DHW heating, set to values close to zeros. If BAU system uses electricity for DHW, please add this electric DHW load to ElectricLoad"
}
}
},

"CoolingLoad": {
"type": "object",
"properties": {
"fuel_loads_mmbtu_per_hour": {
"type": "array",
"description": "This is the fuel energy consumption for space cooling at the individual building level. This is used in the business as usual (BAU) LCCA analysis in the GHP module to calculate BAU bills. These are 8760 hourly timeseries values.",
"note": "Only required if BAU system uses fuel for cooling. If BAU system uses electricity for cooling (AC), please add this load to ElectricLoad defined below. If BAU system has no cooling load, set to values close to zeros."
}
},
"required": ["fuel_loads_mmbtu_per_hour"]
},

"ElectricLoad": {
"type": "object",
"properties": {
"loads_kw": {
"type": "array",
"description": "8760 timeseries",
"note": "This is the electric load profile for heating and cooling in the BAU scenario (kW). It is an hourly timeseries load profile with 8760 values. If there is no electricity based heating or cooling in BAU, set these values close to 0."
}
},
"required": ["loads_kw"]
},

"ElectricTarriff": {
"type": "object",
"description": "Label attribute of utility rate structure from https://openei.org/services/doc/rest/util_rates/?version=3.",
"properties": {
"urdb_label": { "type": "string" }
},
"required": ["urdb_label"]
},

"ExistingBoiler": {
"type": "object",
"properties": {
"fuel_cost_per_mmbtu": {
"type": "number",
"description": "Only required if BAU system use fuel for heating. In this case, fuel cost needs to be specified"
},
"installed_cost_per_mmbtu_per_hour": {
"type": "number",
"description": "Capital cost of ExistingBoiler. Include if counting the capital cost of upgrading ExistingBoiler in the BAU system in NPV calculation. Default is $56,000/MMBtu (EIA). The peak heating load is considered to be the size of the system and is multiplied by this field to get total capital cost. Note: this option can only be called from the API after July 2, 2025"
}
}
},

"ElectricHeater": {
"type": "object",
"properties": {
"installed_cost_per_mmbtu_per_hour": {
"type": "number",
"description": "Capital cost of ElectricHeater/Electric furnace. Include if BAU system use electric heater/electric furnace for heating and if counting capital cost of upgrading electric heater/furnace in the BAU system in NPV calculation. Default value is $59,412/MMBtu (EIA)"
},
"min_mmbtu_per_hour": {
"type": "number",
"description": "User defined input for the size of electric heater system. This value should be the same in min_mmbtu_per_hour and max_mmbtu_per_hour."
},
"max_mmbtu_per_hour": {
"type": "number",
"description": "User defined input for the size of electric heater system. This value should be the same in min_mmbtu_per_hour and max_mmbtu_per_hour."
},
"can_serve_space_heating": {
"type": "boolean",
"description": "true if electric heater can serve space heating load in the BAU system"
},
"can_serve_dhw": {
"type": "boolean",
"description": "true if electric heater can serve water heating load in the BAU system"
}
}
},

"Financial": {
"type": "object",
"properties": {
"elec_cost_escalation_rate_fraction": {
"default": 0.026,
"minimum": -1,
"maximum": 1,
"type": "number",
"description": "Annual nominal utility electricity cost escalation rate",
"required": false
},
"owner_discount_rate_fraction": {
"default": 0.081,
"minimum": 0,
"maximum": 1,
"type": "number",
"description": "Nominal host discount rate. This is used when host site owns the investment",
"required": false
},
"offtaker_discount_rate_fraction": {
"default": 0.081,
"minimum": 0,
"maximum": 1,
"type": "number",
"description": "Nominal discount rate. This is used when site finances the investment through a third party",
"required": false
},
"owner_tax_rate_fraction": {
"default": 0.26,
"minimum": 0,
"maximum": 1,
"type": "number",
"description": "Tax rate site when host site owns the investment",
"required": false
},
"offtaker_tax_rate_fraction": {
"default": 0.26,
"minimum": 0,
"maximum": 1,
"type": "number",
"description": "Tax rate when site finances the investment through a third party",
"required": false
},
"om_cost_escalation_rate_fraction": {
"default": 0.025,
"minimum": -1,
"maximum": 1,
"type": "number",
"description": "Annual nominal O&M cost escalation rate",
"required": false
}
}
}
}
}
Loading