diff --git a/julia_src/Manifest.toml b/julia_src/Manifest.toml index 196aae6bd..16e510202 100644 --- a/julia_src/Manifest.toml +++ b/julia_src/Manifest.toml @@ -948,7 +948,9 @@ version = "1.11.0" [[deps.REopt]] deps = ["ArchGDAL", "CSV", "CoolProp", "DataFrames", "Dates", "DelimitedFiles", "HTTP", "JLD", "JSON", "JuMP", "LinDistFlow", "LinearAlgebra", "Logging", "MathOptInterface", "Requires", "Roots", "Statistics", "TestEnv"] -git-tree-sha1 = "103761fa0f7447377726347af656cde6ab1160cc" +git-tree-sha1 = "277df4545cd30a5fc227bf982897ef65d5ea3520" +repo-rev = "elec_util_usage" +repo-url = "https://github.com/NREL/REopt.jl.git" uuid = "d36ad4e8-d74a-4f7a-ace1-eaea049febf6" version = "0.55.1" diff --git a/julia_src/http.jl b/julia_src/http.jl index 1c9e36d99..b59e8f418 100644 --- a/julia_src/http.jl +++ b/julia_src/http.jl @@ -91,6 +91,7 @@ function reopt(req::HTTP.Request) # Catch handled/unhandled exceptions in data pre-processing, JuMP setup try model_inputs = reoptjl.REoptInputs(d) + @info "Successfully processed REopt inputs." catch e @error "Something went wrong during REopt inputs processing!" exception=(e, catch_backtrace()) error_response["error"] = sprint(showerror, e) @@ -102,6 +103,7 @@ function reopt(req::HTTP.Request) # Catch handled/unhandled exceptions in optimization try results = reoptjl.run_reopt(ms, model_inputs) + @info "Successfully ran REopt optimization." inputs_with_defaults_from_julia_financial = [ :NOx_grid_cost_per_tonne, :SO2_grid_cost_per_tonne, :PM25_grid_cost_per_tonne, :NOx_onsite_fuelburn_cost_per_tonne, :SO2_onsite_fuelburn_cost_per_tonne, :PM25_onsite_fuelburn_cost_per_tonne, @@ -235,6 +237,14 @@ function reopt(req::HTTP.Request) high_temp_storage_dict = Dict(key=>getfield(model_inputs.s.storage.attr["HighTempThermalStorage"], key) for key in inputs_with_defaults_from_julia_high_temp_storage) else high_temp_storage_dict = Dict() + end + if haskey(d, "ElectricTariff") && !isempty(model_inputs.s.electric_tariff.urdb_metadata) + inputs_from_julia_electric_tariff = [ + :urdb_metadata + ] + electric_tariff_dict = Dict(key=>getfield(model_inputs.s.electric_tariff, key) for key in inputs_from_julia_electric_tariff) + else + electric_tariff_dict = Dict() end inputs_with_defaults_set_in_julia = Dict( "Financial" => Dict(key=>getfield(model_inputs.s.financial, key) for key in inputs_with_defaults_from_julia_financial), @@ -251,7 +261,8 @@ function reopt(req::HTTP.Request) "ElectricStorage" => electric_storage_dict, "ColdThermalStorage" => cold_storage_dict, "HotThermalStorage" => hot_storage_dict, - "HighTempThermalStorage" => high_temp_storage_dict + "HighTempThermalStorage" => high_temp_storage_dict, + "ElectricTariff" => electric_tariff_dict ) catch e @error "Something went wrong in REopt optimization!" exception=(e, catch_backtrace()) @@ -272,6 +283,11 @@ function reopt(req::HTTP.Request) if isempty(error_response) @info "REopt model solved with status $(results["status"])." + # These are matrices that need to be vector. + if haskey(results, "ElectricTariff") + results["ElectricTariff"]["year_one_electric_to_load_energy_cost_series_before_tax"] = results["ElectricTariff"]["year_one_electric_to_load_energy_cost_series_before_tax"][:,1] + results["ElectricTariff"]["monthly_facility_demand_cost_series_before_tax"] = results["ElectricTariff"]["monthly_facility_demand_cost_series_before_tax"][:,1] + end response = Dict( "results" => results, "reopt_version" => string(pkgversion(reoptjl)) diff --git a/reoptjl/migrations/0110_electricloadoutputs_annual_peak_kw_and_more.py b/reoptjl/migrations/0110_electricloadoutputs_annual_peak_kw_and_more.py new file mode 100644 index 000000000..54bd6b56f --- /dev/null +++ b/reoptjl/migrations/0110_electricloadoutputs_annual_peak_kw_and_more.py @@ -0,0 +1,139 @@ +# Generated by Django 4.2.25 on 2025-10-28 18:05 + +import django.contrib.postgres.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('reoptjl', '0109_remove_ghpoutputs_iterations_auto_guess_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='electricloadoutputs', + name='annual_peak_kw', + field=models.FloatField(blank=True, help_text='Annual peak energy demand determined from load_series_kw. Does not include electric load for any new heating or cooling techs.', null=True), + ), + migrations.AddField( + model_name='electricloadoutputs', + name='monthly_calculated_kwh', + field=django.contrib.postgres.fields.ArrayField(base_field=models.FloatField(blank=True, null=True), default=list, help_text='Monthly energy consumption calculated by summing up load_series_kw. Does not include electric load for any new heating or cooling techs.', size=None), + ), + migrations.AddField( + model_name='electricloadoutputs', + name='monthly_peaks_kw', + field=django.contrib.postgres.fields.ArrayField(base_field=models.FloatField(blank=True, null=True), default=list, help_text='Monthly peak energy demand determined from load_series_kw. Does not include electric load for any new heating or cooling techs.', size=None), + ), + migrations.AddField( + model_name='electrictariffinputs', + name='urdb_metadata', + field=models.JSONField(blank=True, help_text='Utility rate meta data from Utility Rate Database API', null=True), + ), + migrations.AddField( + model_name='electrictariffoutputs', + name='demand_rate_average_series', + field=django.contrib.postgres.fields.ArrayField(base_field=models.FloatField(blank=True, null=True), default=list, help_text='Series of average (across tiers) demand rates for each timestep in year one.', size=None), + ), + migrations.AddField( + model_name='electrictariffoutputs', + name='energy_cost_series_before_tax', + field=models.JSONField(blank=True, help_text='Series of cost of power purchased from grid to serve load in each timestep, by Tier_i.', null=True), + ), + migrations.AddField( + model_name='electrictariffoutputs', + name='energy_cost_series_before_tax_bau', + field=models.JSONField(blank=True, help_text='Business as usual series of cost of power purchased from grid to serve load in each timestep, by Tier_i.', null=True), + ), + migrations.AddField( + model_name='electrictariffoutputs', + name='energy_rate_average_series', + field=django.contrib.postgres.fields.ArrayField(base_field=models.FloatField(blank=True, null=True), default=list, help_text='Series of average (across tiers) energy rates for each timestep in year one.', size=None), + ), + migrations.AddField( + model_name='electrictariffoutputs', + name='energy_rate_series', + field=models.JSONField(blank=True, help_text='Series of billed energy rates for each timestep in year one.', null=True), + ), + migrations.AddField( + model_name='electrictariffoutputs', + name='energy_rate_tier_limits', + field=models.JSONField(blank=True, help_text='Energy rate tier limits', null=True), + ), + migrations.AddField( + model_name='electrictariffoutputs', + name='facility_demand_monthly_rate_series', + field=models.JSONField(blank=True, help_text='Facility (not dependent on TOU) demand charge rates by Tier_i', null=True), + ), + migrations.AddField( + model_name='electrictariffoutputs', + name='facility_demand_monthly_rate_tier_limits', + field=models.JSONField(blank=True, help_text='Facility (not dependent on TOU) demand charge tier limits', null=True), + ), + migrations.AddField( + model_name='electrictariffoutputs', + name='monthly_demand_cost_series_before_tax', + field=django.contrib.postgres.fields.ArrayField(base_field=models.FloatField(blank=True, null=True), default=list, help_text='Series of total (facility and TOU for all tiers) monthly demand charges for each month.', size=None), + ), + migrations.AddField( + model_name='electrictariffoutputs', + name='monthly_demand_cost_series_before_tax_bau', + field=django.contrib.postgres.fields.ArrayField(base_field=models.FloatField(blank=True, null=True), default=list, help_text='Business as usual series of total (facility and TOU for all tiers) monthly demand charges for each month.', size=None), + ), + migrations.AddField( + model_name='electrictariffoutputs', + name='monthly_energy_cost_series_before_tax', + field=django.contrib.postgres.fields.ArrayField(base_field=models.FloatField(blank=True, null=True), default=list, help_text='Series of monthly cost of power purchased from grid to serve loads.', size=None), + ), + migrations.AddField( + model_name='electrictariffoutputs', + name='monthly_energy_cost_series_before_tax_bau', + field=django.contrib.postgres.fields.ArrayField(base_field=models.FloatField(blank=True, null=True), default=list, help_text='Business as usual series of monthly cost of power purchased from grid to serve loads.', size=None), + ), + migrations.AddField( + model_name='electrictariffoutputs', + name='monthly_facility_demand_cost_series_before_tax', + field=django.contrib.postgres.fields.ArrayField(base_field=models.FloatField(blank=True, null=True), default=list, help_text='Series of total (all tiers) monthly facility demand charges by month.', size=None), + ), + migrations.AddField( + model_name='electrictariffoutputs', + name='monthly_facility_demand_cost_series_before_tax_bau', + field=django.contrib.postgres.fields.ArrayField(base_field=models.FloatField(blank=True, null=True), default=list, help_text='Business as usual series of total (all tiers) monthly facility demand charges by month.', size=None), + ), + migrations.AddField( + model_name='electrictariffoutputs', + name='monthly_fixed_cost', + field=django.contrib.postgres.fields.ArrayField(base_field=models.FloatField(blank=True, null=True), default=list, help_text='Year one fixed utility costs for each month.', size=None), + ), + migrations.AddField( + model_name='electrictariffoutputs', + name='monthly_fixed_cost_bau', + field=django.contrib.postgres.fields.ArrayField(base_field=models.FloatField(blank=True, null=True), default=list, help_text='Business as usual year one fixed utility costs for each month.', size=None), + ), + migrations.AddField( + model_name='electrictariffoutputs', + name='monthly_tou_demand_cost_series_before_tax', + field=django.contrib.postgres.fields.ArrayField(base_field=models.FloatField(blank=True, null=True), default=list, help_text='Series of total time-of-use demand charges for each month.', size=None), + ), + migrations.AddField( + model_name='electrictariffoutputs', + name='monthly_tou_demand_cost_series_before_tax_bau', + field=django.contrib.postgres.fields.ArrayField(base_field=models.FloatField(blank=True, null=True), default=list, help_text='Business as usual series of total time-of-use demand charges for each month.', size=None), + ), + migrations.AddField( + model_name='electrictariffoutputs', + name='tou_demand_metrics', + field=models.JSONField(blank=True, help_text='Dictionary of TOU demand metrics, including month, tier, demand_rate, measured_tou_peak_demand, and demand_charge_before_tax', null=True), + ), + migrations.AddField( + model_name='electrictariffoutputs', + name='tou_demand_rate_series', + field=models.JSONField(blank=True, help_text='Series of demand rates by Tier_i for each timestep.', null=True), + ), + migrations.AddField( + model_name='electrictariffoutputs', + name='tou_demand_rate_tier_limits', + field=models.JSONField(blank=True, help_text='TOU demand rate tier limits', null=True), + ), + ] diff --git a/reoptjl/models.py b/reoptjl/models.py index d535fe252..a56a34318 100644 --- a/reoptjl/models.py +++ b/reoptjl/models.py @@ -1546,6 +1546,24 @@ class ElectricLoadOutputs(BaseModel, models.Model): null=True, blank=True, help_text="Annual energy consumption calculated by summing up load_series_kw. Does not include electric load for any new heating or cooling techs." ) + monthly_calculated_kwh = ArrayField( + models.FloatField( + null=True, blank=True + ), + default=list, + help_text="Monthly energy consumption calculated by summing up load_series_kw. Does not include electric load for any new heating or cooling techs." + ) + monthly_peaks_kw = ArrayField( + models.FloatField( + null=True, blank=True + ), + default=list, + help_text="Monthly peak energy demand determined from load_series_kw. Does not include electric load for any new heating or cooling techs." + ) + annual_peak_kw = models.FloatField( + null=True, blank=True, + help_text="Annual peak energy demand determined from load_series_kw. Does not include electric load for any new heating or cooling techs." + ) annual_electric_load_with_thermal_conversions_kwh = models.FloatField( null=True, blank=True, help_text="Total end-use electrical load, including electrified heating and cooling end-use load." @@ -1720,6 +1738,10 @@ class ElectricTariffInputs(BaseModel, models.Model): help_text=("Optional coincident peak demand charge that is applied to the max load during the time_steps " "specified in coincident_peak_load_active_time_steps") ) + urdb_metadata = models.JSONField( + null=True, blank=True, + help_text=("Utility rate meta data from Utility Rate Database API") + ) def clean(self): error_messages = {} @@ -2603,6 +2625,127 @@ class ElectricTariffOutputs(BaseModel, models.Model): related_name="ElectricTariffOutputs", primary_key=True ) + + monthly_fixed_cost = ArrayField( + models.FloatField( + null=True, blank=True + ), + default=list, + help_text="Year one fixed utility costs for each month." + ) + monthly_fixed_cost_bau = ArrayField( + models.FloatField( + null=True, blank=True + ), + default=list, + help_text="Business as usual year one fixed utility costs for each month." + ) + energy_cost_series_before_tax = models.JSONField( + null=True, blank=True, + help_text="Series of cost of power purchased from grid to serve load in each timestep, by Tier_i." + ) + energy_cost_series_before_tax_bau = models.JSONField( + null=True, blank=True, + help_text="Business as usual series of cost of power purchased from grid to serve load in each timestep, by Tier_i." + ) + monthly_energy_cost_series_before_tax = ArrayField( + models.FloatField( + null=True, blank=True + ), + default=list, + help_text="Series of monthly cost of power purchased from grid to serve loads." + ) + monthly_energy_cost_series_before_tax_bau = ArrayField( + models.FloatField( + null=True, blank=True + ), + default=list, + help_text="Business as usual series of monthly cost of power purchased from grid to serve loads." + ) + monthly_facility_demand_cost_series_before_tax = ArrayField( + models.FloatField( + null=True, blank=True + ), + default=list, + help_text="Series of total (all tiers) monthly facility demand charges by month." + ) + monthly_facility_demand_cost_series_before_tax_bau = ArrayField( + models.FloatField( + null=True, blank=True + ), + default=list, + help_text="Business as usual series of total (all tiers) monthly facility demand charges by month." + ) + energy_rate_series = models.JSONField( + null=True, blank=True, + help_text="Series of billed energy rates for each timestep in year one." + ) + energy_rate_average_series = ArrayField( + models.FloatField( + null=True, blank=True + ), + default=list, + help_text="Series of average (across tiers) energy rates for each timestep in year one." + ) + monthly_tou_demand_cost_series_before_tax = ArrayField( + models.FloatField( + null=True, blank=True + ), + default=list, + help_text="Series of total time-of-use demand charges for each month." + ) + monthly_tou_demand_cost_series_before_tax_bau = ArrayField( + models.FloatField( + null=True, blank=True + ), + default=list, + help_text="Business as usual series of total time-of-use demand charges for each month." + ) + monthly_demand_cost_series_before_tax = ArrayField( + models.FloatField( + null=True, blank=True + ), + default=list, + help_text="Series of total (facility and TOU for all tiers) monthly demand charges for each month." + ) + monthly_demand_cost_series_before_tax_bau = ArrayField( + models.FloatField( + null=True, blank=True + ), + default=list, + help_text="Business as usual series of total (facility and TOU for all tiers) monthly demand charges for each month." + ) + tou_demand_metrics = models.JSONField( + null=True, blank=True, + help_text="Dictionary of TOU demand metrics, including month, tier, demand_rate, measured_tou_peak_demand, and demand_charge_before_tax" + ) + facility_demand_monthly_rate_tier_limits = models.JSONField( + null=True, blank=True, + help_text="Facility (not dependent on TOU) demand charge tier limits" + ) + facility_demand_monthly_rate_series = models.JSONField( + null=True, blank=True, + help_text="Facility (not dependent on TOU) demand charge rates by Tier_i" + ) + tou_demand_rate_tier_limits = models.JSONField( + null=True, blank=True, + help_text="TOU demand rate tier limits" + ) + energy_rate_tier_limits = models.JSONField( + null=True, blank=True, + help_text="Energy rate tier limits" + ) + tou_demand_rate_series = models.JSONField( + null=True, blank=True, + help_text="Series of demand rates by Tier_i for each timestep." + ) + demand_rate_average_series = ArrayField( + models.FloatField( + null=True, blank=True + ), + default=list, + help_text="Series of average (across tiers) demand rates for each timestep in year one." + ) year_one_energy_cost_before_tax = models.FloatField( null=True, blank=True, help_text="Optimal year one utility energy cost" diff --git a/reoptjl/src/process_results.py b/reoptjl/src/process_results.py index 20b26e209..22bf0a292 100644 --- a/reoptjl/src/process_results.py +++ b/reoptjl/src/process_results.py @@ -9,7 +9,7 @@ SteamTurbineOutputs, GHPInputs, GHPOutputs, ExistingChillerInputs, \ ElectricHeaterOutputs, ASHPSpaceHeaterOutputs, ASHPWaterHeaterOutputs, \ SiteInputs, ASHPSpaceHeaterInputs, ASHPWaterHeaterInputs, CSTInputs, CSTOutputs, PVInputs, \ - HighTempThermalStorageInputs, HighTempThermalStorageOutputs + HighTempThermalStorageInputs, HighTempThermalStorageOutputs, ElectricTariffInputs import numpy as np import sys import traceback as tb @@ -179,6 +179,9 @@ def update_inputs_in_database(inputs_to_update: dict, run_uuid: str) -> None: if inputs_to_update.get("HighTempThermalStorage") is not None: prune_update_fields(HighTempThermalStorageInputs, inputs_to_update["HighTempThermalStorage"]) HighTempThermalStorageInputs.objects.filter(meta__run_uuid=run_uuid).update(**inputs_to_update["HighTempThermalStorage"]) + if inputs_to_update.get("ElectricTariff"): + prune_update_fields(ElectricTariffInputs, inputs_to_update["ElectricTariff"]) + ElectricTariffInputs.objects.filter(meta__run_uuid=run_uuid).update(**inputs_to_update["ElectricTariff"]) except Exception as e: exc_type, exc_value, exc_traceback = sys.exc_info() debug_msg = "exc_type: {}; exc_value: {}; exc_traceback: {}".format( diff --git a/reoptjl/test/posts/sector_defaults_post.json b/reoptjl/test/posts/sector_defaults_post.json index 1df5abbae..ca174468f 100644 --- a/reoptjl/test/posts/sector_defaults_post.json +++ b/reoptjl/test/posts/sector_defaults_post.json @@ -23,44 +23,24 @@ "ExistingBoiler": { "fuel_cost_per_mmbtu": 10.0 }, - "CHP": { - "prime_mover": "recip_engine", - "max_kw": 100, - "fuel_cost_per_mmbtu": 10.0, - "can_supply_steam_turbine": true - }, "PV": { "min_kw": 1000.0, "max_kw": 1000.0, "federal_itc_fraction": 0.2 }, - "SteamTurbine":{ - "min_kw": 100, - "max_kw": 100 - }, - "HighTempThermalStorage": { - "min_kwh": 10, - "max_kwh": 10, - "thermal_decay_rate_fraction": 0.0 - }, "Wind": { "min_kw": 100, "max_kw": 100 }, - "GHP": { - "require_ghp_purchase": true, - "building_sqft": 50000.0, - "can_serve_dhw": false, - "space_heating_efficiency_thermal_factor": 0.85, - "cooling_efficiency_thermal_factor": 0.6, - "ghpghx_inputs": [{ - "borehole_depth_ft": 400.0, - "simulation_years": 20, - "solver_eft_tolerance_f": 2.0, - "ghx_model": "TESS", - "tess_ghx_minimum_timesteps_per_hour": 1, - "max_sizing_iterations": 10, - "init_sizing_factor_ft_per_peak_ton": 300.0 - }] + "ElectricStorage": { + "min_kw": 50, + "max_kw": 50, + "min_kwh": 100, + "max_kwh": 100 + }, + "Boiler": { + "fuel_cost_per_mmbtu": 10.0, + "min_mmbtu_per_hour": 0.5, + "max_mmbtu_per_hour": 0.5 } } \ No newline at end of file diff --git a/reoptjl/test/test_job_endpoint.py b/reoptjl/test/test_job_endpoint.py index 6f74663aa..f880ba921 100644 --- a/reoptjl/test/test_job_endpoint.py +++ b/reoptjl/test/test_job_endpoint.py @@ -178,8 +178,6 @@ def test_sector_defaults_from_julia(self): self.assertEqual(saved_model_inputs.get(input_key), post[model_name][input_key]) else: # Check that default got assigned consistent with /sector_defaults - if model_name == "SteamTurbine" and input_key == "federal_itc_fraction": - continue #ST doesn't have federal_itc_fraction input self.assertEqual(saved_model_inputs.get(input_key), default_input_val) def test_chp_defaults_from_julia(self):