diff --git a/CHANGELOG.md b/CHANGELOG.md index e9e838ce6..02cb528e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,28 @@ Classify the change according to the following categories: ##### Removed ### Patches +## gridRE +### Major Updates +### Added +- Added the following inputs to account for the clean or renewable energy fraction of grid-purchased electricity: + - ElectricUtility **cambium_cef_metric** to utilize clean energy data from NREL's Cambium database + - ElectricUtility **renewable_energy_fraction_series** to supply a custom grid clean or renewable energy scalar or series + - Site **include_grid_renewable_fraction_in_RE_constraints** - to allow user to choose whether to include grid RE in min max constraints +- Added the following outputs: + - ElectricUtility **annual_renewable_electricity_supplied_kwh** + - Site **onsite_and_grid_renewable_electricity_fraction_of_elec_load** + - Site **onsite_and_grid_renewable_energy_fraction_of_total_load** +- Added input option optimize_soc_init_fraction (defaults to false) to ElectricStorage, which makes the optimization choose the inital SOC (equal to final SOC) instead of using soc_init_fraction. The initial SOC is also constrained to equal the final SOC, which eliminates the "free energy" issue. We currently do not fix SOC when soc_init_fraction is used because this has caused infeasibility. +### Changed +- Changed name of the following inputs: + - ElectricUtility input **cambium_metric_col** changed to **cambium_co2_metric**, to distinguish between the CO2 and clean energy fraction metrics +- Changed name of the following outputs: + - ElectricUtility **cambium_emissions_region** changed to **cambium_region** + - Site **annual_renewable_electricity_kwh** changed to **annual_onsite_renewable_electricity_kwh** + - Site **renewable_electricity_fraction** changed to **onsite_renewable_electricity_fraction_of_elec_load** + - Site **total_renewable_energy_fraction** changed to **onsite_renewable_energy_fraction_of_total_load** +- Changed v3 endpoint "cambium_emissions_profile" to "cambium_profile" +- In REopt.jl: Updated Cambium API call to Cambium 2023 dataset, Updated AVERT emissions data to v4.3, which uses Regional Data Files for year 2023 for CONUS. For Alaska and Hawaii (regions AKGD, HIMS, HIOA), updated eGRID data to eGRID2022 datafile, adjusted to CO2e values. ## v3.11.0 ### Minor Updates ##### Changed diff --git a/julia_src/Manifest.toml b/julia_src/Manifest.toml index 1803c1732..55fd3376d 100644 --- a/julia_src/Manifest.toml +++ b/julia_src/Manifest.toml @@ -929,9 +929,11 @@ uuid = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb" [[deps.REopt]] deps = ["ArchGDAL", "CSV", "CoolProp", "DataFrames", "Dates", "DelimitedFiles", "HTTP", "JLD", "JSON", "JuMP", "LinDistFlow", "LinearAlgebra", "Logging", "MathOptInterface", "Requires", "Roots", "Statistics", "TestEnv"] -git-tree-sha1 = "324394f21cb7e2db3d9e7ebde19c4e83c5a64e0f" +git-tree-sha1 = "b58ddeb73296aadfb077c2bb41229fec4091834f" +repo-rev = "gridRE-dev" +repo-url = "https://github.com/NREL/REopt.jl.git" uuid = "d36ad4e8-d74a-4f7a-ace1-eaea049febf6" -version = "0.50.0" +version = "0.49.1" [[deps.Random]] deps = ["SHA"] diff --git a/julia_src/http.jl b/julia_src/http.jl index 90a3df764..5e2a75d94 100644 --- a/julia_src/http.jl +++ b/julia_src/http.jl @@ -72,7 +72,8 @@ function reopt(req::HTTP.Request) ] inputs_with_defaults_from_avert_or_cambium = [ :emissions_factor_series_lb_CO2_per_kwh, :emissions_factor_series_lb_NOx_per_kwh, - :emissions_factor_series_lb_SO2_per_kwh, :emissions_factor_series_lb_PM25_per_kwh + :emissions_factor_series_lb_SO2_per_kwh, :emissions_factor_series_lb_PM25_per_kwh, + :renewable_energy_fraction_series ] if haskey(d, "CHP") inputs_with_defaults_from_chp = [ @@ -348,9 +349,9 @@ function avert_emissions_profile(req::HTTP.Request) return HTTP.Response(200, JSON.json(data)) end -function cambium_emissions_profile(req::HTTP.Request) +function cambium_profile(req::HTTP.Request) d = JSON.parse(String(req.body)) - @info "Getting Cambium CO2 emissions profile..." + @info "Getting emissions or clean energy data from Cambium..." data = Dict() error_response = Dict() try @@ -360,7 +361,7 @@ function cambium_emissions_profile(req::HTTP.Request) lifetime = typeof(d["lifetime"]) == String ? parse(Int, d["lifetime"]) : d["lifetime"] load_year = typeof(d["load_year"]) == String ? parse(Int, d["load_year"]) : d["load_year"] - data = reoptjl.cambium_emissions_profile(;scenario= d["scenario"], + data = reoptjl.cambium_profile(;scenario= d["scenario"], location_type = d["location_type"], latitude=latitude, longitude=longitude, @@ -597,7 +598,7 @@ HTTP.register!(ROUTER, "POST", "/erp", erp) HTTP.register!(ROUTER, "POST", "/ghpghx", ghpghx) HTTP.register!(ROUTER, "GET", "/chp_defaults", chp_defaults) HTTP.register!(ROUTER, "GET", "/avert_emissions_profile", avert_emissions_profile) -HTTP.register!(ROUTER, "GET", "/cambium_emissions_profile", cambium_emissions_profile) +HTTP.register!(ROUTER, "GET", "/cambium_profile", cambium_profile) HTTP.register!(ROUTER, "GET", "/easiur_costs", easiur_costs) HTTP.register!(ROUTER, "GET", "/simulated_load", simulated_load) HTTP.register!(ROUTER, "GET", "/absorption_chiller_defaults", absorption_chiller_defaults) diff --git a/reoptjl/custom_table_config.py b/reoptjl/custom_table_config.py index bfafcaea5..c363e6c43 100644 --- a/reoptjl/custom_table_config.py +++ b/reoptjl/custom_table_config.py @@ -453,8 +453,8 @@ { "label" : "Annual % Renewable Electricity (%)", "key" : "annual_renewable_electricity", - "bau_value" : lambda df: safe_get(df, "outputs.Site.renewable_electricity_fraction_bau"), - "scenario_value": lambda df: safe_get(df, "outputs.Site.renewable_electricity_fraction") + "bau_value" : lambda df: safe_get(df, "outputs.Site.onsite_renewable_electricity_fraction_of_elec_load_bau"), + "scenario_value": lambda df: safe_get(df, "outputs.Site.onsite_renewable_electricity_fraction_of_elec_load") }, { "label" : "Annual CO2 Emissions (tonnes)", diff --git a/reoptjl/migrations/0077_rename_cambium_metric_col_electricutilityinputs_cambium_co2_metric_and_more.py b/reoptjl/migrations/0077_rename_cambium_metric_col_electricutilityinputs_cambium_co2_metric_and_more.py new file mode 100644 index 000000000..1ff0eca46 --- /dev/null +++ b/reoptjl/migrations/0077_rename_cambium_metric_col_electricutilityinputs_cambium_co2_metric_and_more.py @@ -0,0 +1,133 @@ +# Generated by Django 4.0.7 on 2025-01-24 22:02 + +import django.contrib.postgres.fields +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('reoptjl', '0076_ashpspaceheaterinputs_force_dispatch_and_more'), + ] + + operations = [ + migrations.RenameField( + model_name='electricutilityinputs', + old_name='cambium_metric_col', + new_name='cambium_co2_metric', + ), + migrations.RenameField( + model_name='electricutilityoutputs', + old_name='cambium_emissions_region', + new_name='cambium_region', + ), + migrations.RenameField( + model_name='siteoutputs', + old_name='annual_renewable_electricity_kwh', + new_name='annual_onsite_renewable_electricity_kwh', + ), + migrations.RenameField( + model_name='siteoutputs', + old_name='annual_renewable_electricity_kwh_bau', + new_name='annual_onsite_renewable_electricity_kwh_bau', + ), + migrations.RenameField( + model_name='siteoutputs', + old_name='renewable_electricity_fraction', + new_name='onsite_renewable_electricity_fraction_of_elec_load', + ), + migrations.RenameField( + model_name='siteoutputs', + old_name='renewable_electricity_fraction_bau', + new_name='onsite_renewable_electricity_fraction_of_elec_load_bau', + ), + migrations.RemoveField( + model_name='siteoutputs', + name='total_renewable_energy_fraction', + ), + migrations.RemoveField( + model_name='siteoutputs', + name='total_renewable_energy_fraction_bau', + ), + migrations.AddField( + model_name='electricstorageinputs', + name='optimize_soc_init_fraction', + field=models.BooleanField(blank=True, default=False, help_text='If true, soc_init_fraction will not apply. Model will optimize initial SOC and constrain initial SOC = final SOC.'), + ), + migrations.AddField( + model_name='electricutilityinputs', + name='cambium_cef_metric', + field=models.TextField(blank=True, default='cef_load', help_text="Options = ['cef_load', 'cef_gen']. cef_load is the fraction of generation that is clean, for the generation that is allocated to a region’s end-use load; cef_gen is the fraction of generation that is clean within a region."), + ), + migrations.AddField( + model_name='electricutilityinputs', + name='renewable_energy_fraction_series', + field=django.contrib.postgres.fields.ArrayField(base_field=models.FloatField(blank=True), blank=True, default=list, help_text='Fraction of energy supplied by the grid that is renewable. Can be scalar or timeseries (aligned with time_steps_per_hour).', size=None), + ), + migrations.AddField( + model_name='electricutilityoutputs', + name='annual_renewable_electricity_supplied_kwh', + field=models.FloatField(blank=True, help_text='Total renewable electricity supplied from the grid in an average year.', null=True), + ), + migrations.AddField( + model_name='electricutilityoutputs', + name='annual_renewable_electricity_supplied_kwh_bau', + field=models.FloatField(blank=True, help_text='Total renewable electricity supplied from the grid in an average year.', null=True), + ), + migrations.AddField( + model_name='siteinputs', + name='include_grid_renewable_fraction_in_RE_constraints', + field=models.BooleanField(blank=True, default=True, help_text='If True, then the renewable energy content of energy from the grid is included in any min or max renewable energy requirements.'), + ), + migrations.AddField( + model_name='siteoutputs', + name='onsite_and_grid_renewable_electricity_fraction_of_elec_load', + field=models.FloatField(blank=True, help_text='Calculation is the same as onsite_renewable_electricity_fraction_of_elec_load, but additionally includes the renewable energycontent of grid-purchased electricity, accounting for any battery efficiency losses.', null=True), + ), + migrations.AddField( + model_name='siteoutputs', + name='onsite_and_grid_renewable_electricity_fraction_of_elec_load_bau', + field=models.FloatField(blank=True, help_text='Calculation is the same as onsite_renewable_electricity_fraction_of_elec_load, but additionally includes the renewable energycontent of grid-purchased electricity, accounting for any battery efficiency losses.', null=True), + ), + migrations.AddField( + model_name='siteoutputs', + name='onsite_and_grid_renewable_energy_fraction_of_total_load', + field=models.FloatField(blank=True, help_text='Calculation is the same as onsite_renewable_energy_fraction_of_total_load, but additionally includes the renewable energycontent of grid-purchased electricity, accounting for any battery efficiency losses.', null=True), + ), + migrations.AddField( + model_name='siteoutputs', + name='onsite_and_grid_renewable_energy_fraction_of_total_load_bau', + field=models.FloatField(blank=True, help_text='Calculation is the same as onsite_renewable_energy_fraction_of_total_load, but additionally includes the renewable energycontent of grid-purchased electricity, accounting for any battery efficiency losses.', null=True), + ), + migrations.AddField( + model_name='siteoutputs', + name='onsite_renewable_energy_fraction_of_total_load', + field=models.FloatField(blank=True, help_text='Portion of annual total energy consumption that is derived from on-site renewable resource generation.The numerator is calculated as total annual RE electricity consumption (calculation described for annual_onsite_renewable_electricity_kwh output),plus total annual thermal energy content of steam/hot water generated from renewable fuels (non-electrified heat loads).The thermal energy content is calculated as total energy content of steam/hot water generation from renewable fuels,minus waste heat generated by renewable fuels, minus any applicable hot water thermal energy storage efficiency losses.The denominator is calculated as total annual electricity consumption plus total annual thermal steam/hot water load.', null=True), + ), + migrations.AddField( + model_name='siteoutputs', + name='onsite_renewable_energy_fraction_of_total_load_bau', + field=models.FloatField(blank=True, help_text='Portion of annual total energy consumption that is derived from on-site renewable resource generation in the BAU case.The numerator is calculated as total annual RE electricity consumption (calculation described for annual_onsite_renewable_electricity_kwh_bau output),plus total annual thermal energy content of steam/hot water generated from renewable fuels (non-electrified heat loads).The thermal energy content is calculated as total energy content of steam/hot water generation from renewable fuels,minus waste heat generated by renewable fuels, minus any applicable hot water thermal energy storage efficiency losses.The denominator is calculated as total annual electricity consumption plus total annual thermal steam/hot water load.', null=True), + ), + migrations.AlterField( + model_name='electricutilityinputs', + name='cambium_levelization_years', + field=models.IntegerField(blank=True, help_text='Expected lifetime or analysis period of the intervention being studied. Emissions and clean energy fraction will be averaged over this period. Default: analysis_years (from Financial struct)', null=True, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(100)]), + ), + migrations.AlterField( + model_name='electricutilityinputs', + name='cambium_location_type', + field=models.TextField(blank=True, default='GEA Regions 2023', help_text="Geographic boundary at which emissions and clean energy fraction are calculated. Options: ['Nations', 'GEA Regions 2023']."), + ), + migrations.AlterField( + model_name='electricutilityinputs', + name='cambium_scenario', + field=models.TextField(blank=True, default='Mid-case', help_text="Cambium Scenario for evolution of electricity sector (see Cambium documentation for descriptions).Options: ['Mid-case', 'Low renewable energy cost', 'High renewable energy cost', 'High demand growth', 'Low natural gas prices', 'High natural gas prices', 'Mid-case with 95% decarbonization by 2050', 'Mid-case with 100% decarbonization by 2035']"), + ), + migrations.AlterField( + model_name='electricutilityinputs', + name='cambium_start_year', + field=models.IntegerField(blank=True, default=2025, help_text='First year of operation of system. Emissions will be levelized starting in this year for the duration of cambium_levelization_years.', validators=[django.core.validators.MinValueValidator(2025), django.core.validators.MaxValueValidator(2050)]), + ), + ] diff --git a/reoptjl/migrations/0078_merge_20250128_1541.py b/reoptjl/migrations/0078_merge_20250128_1541.py new file mode 100644 index 000000000..c25b30900 --- /dev/null +++ b/reoptjl/migrations/0078_merge_20250128_1541.py @@ -0,0 +1,14 @@ +# Generated by Django 4.0.7 on 2025-01-28 15:41 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('reoptjl', '0077_electricstorageinputs_max_duration_hours_and_more'), + ('reoptjl', '0077_rename_cambium_metric_col_electricutilityinputs_cambium_co2_metric_and_more'), + ] + + operations = [ + ] diff --git a/reoptjl/migrations/0079_alter_siteinputs_include_grid_renewable_fraction_in_re_constraints_and_more.py b/reoptjl/migrations/0079_alter_siteinputs_include_grid_renewable_fraction_in_re_constraints_and_more.py new file mode 100644 index 000000000..0bd74bc3e --- /dev/null +++ b/reoptjl/migrations/0079_alter_siteinputs_include_grid_renewable_fraction_in_re_constraints_and_more.py @@ -0,0 +1,28 @@ +# Generated by Django 4.0.7 on 2025-02-05 18:26 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('reoptjl', '0078_merge_20250128_1541'), + ] + + operations = [ + migrations.AlterField( + model_name='siteinputs', + name='include_grid_renewable_fraction_in_RE_constraints', + field=models.BooleanField(blank=True, default=False, help_text='If True, then the renewable energy content of energy from the grid is included in any min or max renewable energy requirements.'), + ), + migrations.AlterField( + model_name='siteoutputs', + name='onsite_and_grid_renewable_electricity_fraction_of_elec_load_bau', + field=models.FloatField(blank=True, help_text='Calculation is the same as onsite_renewable_electricity_fraction_of_elec_load_bau, but additionally includes the renewable energycontent of grid-purchased electricity, accounting for any battery efficiency losses.', null=True), + ), + migrations.AlterField( + model_name='siteoutputs', + name='onsite_and_grid_renewable_energy_fraction_of_total_load_bau', + field=models.FloatField(blank=True, help_text='Calculation is the same as onsite_renewable_energy_fraction_of_total_load_bau, but additionally includes the renewable energycontent of grid-purchased electricity, accounting for any battery efficiency losses.', null=True), + ), + ] diff --git a/reoptjl/models.py b/reoptjl/models.py index 7f375aa83..b931f830a 100644 --- a/reoptjl/models.py +++ b/reoptjl/models.py @@ -69,10 +69,10 @@ class MACRS_YEARS_CHOICES(models.IntegerChoices): } EMISSIONS_DECREASE_DEFAULTS = { # year over year decrease in grid emissions rate - "CO2e" : 0.02163, - "NOx" : 0.02163, - "SO2" : 0.02163, - "PM25" : 0.02163 + "CO2e" : 0.0459, + "NOx" : 0.0459, + "SO2" : 0.0459, + "PM25" : 0.0459 } WIND_COST_DEFAULTS = { # size_class_to_installed_cost @@ -406,6 +406,13 @@ class SiteInputs(BaseModel, models.Model): null=True, blank=True, help_text="Maximum allowed percentage of site electric consumption met by renewable energy on an annual basis." ) + + include_grid_renewable_fraction_in_RE_constraints = models.BooleanField( + default=False, + blank=True, + help_text=("If True, then the renewable energy content of energy from the grid is included in any min or max renewable energy requirements.") + ) + include_exported_elec_emissions_in_total = models.BooleanField( default=True, blank=True, @@ -437,14 +444,14 @@ class SiteOutputs(BaseModel, models.Model): primary_key=True ) - annual_renewable_electricity_kwh = models.FloatField( + annual_onsite_renewable_electricity_kwh = models.FloatField( null=True, blank=True, help_text=( "Electricity consumption (incl. electric heating/cooling loads) that is derived from on-site renewable resource generation." "Calculated as total annual RE electric generation, minus storage losses and curtailment, with the user selecting whether exported renewable generation is included). " ) ) - renewable_electricity_fraction = models.FloatField( + onsite_renewable_electricity_fraction_of_elec_load = models.FloatField( null=True, blank=True, help_text=( "Portion of electricity consumption (incl. electric heating/cooling loads) that is derived from on-site renewable resource generation." @@ -452,17 +459,31 @@ class SiteOutputs(BaseModel, models.Model): "divided by total annual electric consumption." ) ) - total_renewable_energy_fraction = models.FloatField( + onsite_renewable_energy_fraction_of_total_load = models.FloatField( null=True, blank=True, help_text=( "Portion of annual total energy consumption that is derived from on-site renewable resource generation." - "The numerator is calculated as total annual RE electricity consumption (calculation described for annual_renewable_electricity_kwh output)," + "The numerator is calculated as total annual RE electricity consumption (calculation described for annual_onsite_renewable_electricity_kwh output)," "plus total annual thermal energy content of steam/hot water generated from renewable fuels (non-electrified heat loads)." "The thermal energy content is calculated as total energy content of steam/hot water generation from renewable fuels," "minus waste heat generated by renewable fuels, minus any applicable hot water thermal energy storage efficiency losses." "The denominator is calculated as total annual electricity consumption plus total annual thermal steam/hot water load." ) ) + onsite_and_grid_renewable_electricity_fraction_of_elec_load = models.FloatField( + null=True, blank=True, + help_text=( + "Calculation is the same as onsite_renewable_electricity_fraction_of_elec_load, but additionally includes the renewable energy" + "content of grid-purchased electricity, accounting for any battery efficiency losses." + ) + ) + onsite_and_grid_renewable_energy_fraction_of_total_load = models.FloatField( + null=True, blank=True, + help_text=( + "Calculation is the same as onsite_renewable_energy_fraction_of_total_load, but additionally includes the renewable energy" + "content of grid-purchased electricity, accounting for any battery efficiency losses." + ) + ) annual_emissions_tonnes_CO2 = models.FloatField( null=True, blank=True, help_text="Average annual total tons of emissions associated with the site's grid-purchased electricity and on-site fuel consumption." @@ -527,14 +548,14 @@ class SiteOutputs(BaseModel, models.Model): null=True, blank=True, help_text="Total tons of PM2.5 emissions associated with the site's onsite fuel burn over the analysis period." ) - annual_renewable_electricity_kwh_bau = models.FloatField( + annual_onsite_renewable_electricity_kwh_bau = models.FloatField( null=True, blank=True, help_text=( "Electricity consumption (incl. electric heating/cooling loads) that is derived from on-site renewable resource generation in the BAU case." "Calculated as total RE electric generation in the BAU case, minus storage losses and curtailment, with the user selecting whether exported renewable generation is included). " ) ) - renewable_electricity_fraction_bau = models.FloatField( + onsite_renewable_electricity_fraction_of_elec_load_bau = models.FloatField( null=True, blank=True, help_text=( "Electricity consumption (incl. electric heating/cooling loads) that is derived from on-site renewable resource generation in the BAU case." @@ -542,17 +563,31 @@ class SiteOutputs(BaseModel, models.Model): "divided by total annual electric consumption." ) ) - total_renewable_energy_fraction_bau = models.FloatField( + onsite_renewable_energy_fraction_of_total_load_bau = models.FloatField( null=True, blank=True, help_text=( "Portion of annual total energy consumption that is derived from on-site renewable resource generation in the BAU case." - "The numerator is calculated as total annual RE electricity consumption (calculation described for annual_renewable_electricity_kwh_bau output)," + "The numerator is calculated as total annual RE electricity consumption (calculation described for annual_onsite_renewable_electricity_kwh_bau output)," "plus total annual thermal energy content of steam/hot water generated from renewable fuels (non-electrified heat loads)." "The thermal energy content is calculated as total energy content of steam/hot water generation from renewable fuels," "minus waste heat generated by renewable fuels, minus any applicable hot water thermal energy storage efficiency losses." "The denominator is calculated as total annual electricity consumption plus total annual thermal steam/hot water load." ) ) + onsite_and_grid_renewable_electricity_fraction_of_elec_load_bau = models.FloatField( + null=True, blank=True, + help_text=( + "Calculation is the same as onsite_renewable_electricity_fraction_of_elec_load_bau, but additionally includes the renewable energy" + "content of grid-purchased electricity, accounting for any battery efficiency losses." + ) + ) + onsite_and_grid_renewable_energy_fraction_of_total_load_bau = models.FloatField( + null=True, blank=True, + help_text=( + "Calculation is the same as onsite_renewable_energy_fraction_of_total_load_bau, but additionally includes the renewable energy" + "content of grid-purchased electricity, accounting for any battery efficiency losses." + ) + ) annual_emissions_tonnes_CO2_bau = models.FloatField( null=True, blank=True, help_text="Total tons of CO2e emissions associated with the site's energy consumption in an average year in the BAU case." @@ -1740,22 +1775,22 @@ class ElectricUtilityInputs(BaseModel, models.Model): blank=True, default = "Mid-case", help_text=("Cambium Scenario for evolution of electricity sector (see Cambium documentation for descriptions)." - "Options: ['Mid-case', 'Mid-case with tax credit expiration', 'Low renewable energy cost', 'Low renewable energy cost with tax credit expiration', 'High renewable energy cost', 'High electrification', 'Low natrual gas prices', 'High natrual gas prices', 'Mid-case with 95% decarbonization by 2050', 'Mid-case with 100% decarbonization by 2035']") + "Options: ['Mid-case', 'Low renewable energy cost', 'High renewable energy cost', 'High demand growth', 'Low natural gas prices', 'High natural gas prices', 'Mid-case with 95% decarbonization by 2050', 'Mid-case with 100% decarbonization by 2035']") ) cambium_location_type = models.TextField( blank=True, - default = "GEA Regions", - help_text=("Geographic boundary at which emissions are calculated. Options: ['Nations', 'GEA Regions', 'States'].") + default = "GEA Regions 2023", + help_text=("Geographic boundary at which emissions and clean energy fraction are calculated. Options: ['Nations', 'GEA Regions 2023'].") ) - cambium_metric_col = models.TextField( + cambium_co2_metric = models.TextField( blank=True, default = "lrmer_co2e", help_text=("Emissions metric used. Default is Long-run marginal emissions rate for CO2-equivalant, combined combustion and pre-combustion emissions rates. Options: See metric definitions and names in the Cambium documentation.") ) cambium_start_year = models.IntegerField( - default=2024, + default=2025, validators=[ - MinValueValidator(2023), + MinValueValidator(2025), MaxValueValidator(2050) ], blank=True, @@ -1769,7 +1804,7 @@ class ElectricUtilityInputs(BaseModel, models.Model): blank=True, null=True, help_text=("Expected lifetime or analysis period of the intervention being studied. " - "Emissions will be averaged over this period. Default: analysis_years (from Financial struct)") + "Emissions and clean energy fraction will be averaged over this period. Default: analysis_years (from Financial struct)") ) cambium_grid_level = models.TextField( blank=True, @@ -1852,6 +1887,20 @@ class ElectricUtilityInputs(BaseModel, models.Model): help_text="Annual percent decrease in the total annual PM2.5 marginal emissions rate of the grid. A negative value indicates an annual increase." ) + cambium_cef_metric = models.TextField( + blank=True, + default = "cef_load", + help_text=("Options = ['cef_load', 'cef_gen']. cef_load is the fraction of generation that is clean, for the generation that is allocated to a region’s end-use load; cef_gen is the fraction of generation that is clean within a region.") + ) + + renewable_energy_fraction_series = ArrayField( + models.FloatField( + blank=True, + ), + default=list, blank=True, + help_text=("Fraction of energy supplied by the grid that is renewable. Can be scalar or timeseries (aligned with time_steps_per_hour).") + ) + def clean(self): error_messages = {} @@ -1932,6 +1981,14 @@ class ElectricUtilityOutputs(BaseModel, models.Model): null=True, blank=True, help_text=("Average annual energy supplied from grid to load") ) + annual_renewable_electricity_supplied_kwh = models.FloatField( + null=True, blank=True, + help_text=("Total renewable electricity supplied from the grid in an average year.") + ) + annual_renewable_electricity_supplied_kwh_bau = models.FloatField( + null=True, blank=True, + help_text=("Total renewable electricity supplied from the grid in an average year.") + ) annual_emissions_tonnes_CO2 = models.FloatField( null=True, blank=True, help_text=("Total tons of CO2 emissions associated with the site's grid-purchased electricity in an average year. " @@ -2042,7 +2099,7 @@ class ElectricUtilityOutputs(BaseModel, models.Model): null=True, blank=True, help_text=("Distance in meters from the site to the nearest AVERT emissions region.") ) - cambium_emissions_region = models.TextField( + cambium_region = models.TextField( blank=True, help_text=("Name of the Cambium emissions region used for climate emissions for grid electricity. " "Determined from site longitude and latitude and the cambium_location_type if " @@ -3468,6 +3525,12 @@ class ElectricStorageInputs(BaseModel, models.Model): help_text="Maximum amount of time storage can discharge at its rated power capacity" ) + optimize_soc_init_fraction = models.BooleanField( + default=False, + blank=True, + help_text="If true, soc_init_fraction will not apply. Model will optimize initial SOC and constrain initial SOC = final SOC." + ) + class ElectricStorageOutputs(BaseModel, models.Model): key = "ElectricStorageOutputs" diff --git a/reoptjl/test/posts/all_inputs_test.json b/reoptjl/test/posts/all_inputs_test.json index fa2e13683..6811af43c 100644 --- a/reoptjl/test/posts/all_inputs_test.json +++ b/reoptjl/test/posts/all_inputs_test.json @@ -55,7 +55,8 @@ "renewable_electricity_max_fraction": null, "include_exported_elec_emissions_in_total": true, "include_exported_renewable_electricity_in_total": true, - "min_resil_time_steps": 100 + "min_resil_time_steps": 100, + "include_grid_renewable_fraction_in_RE_constraints": false }, "Settings": { "timeout_seconds": 420, @@ -145,7 +146,10 @@ "emissions_factor_CO2_decrease_fraction": 0.01174, "emissions_factor_NOx_decrease_fraction": 0.01174, "emissions_factor_SO2_decrease_fraction": 0.01174, - "emissions_factor_PM25_decrease_fraction": 0.01174 + "emissions_factor_PM25_decrease_fraction": 0.01174, + "cambium_co2_metric": "lrmer_co2e", + "cambium_cef_metric": "cef_load", + "renewable_energy_fraction_series": [] }, "ElectricStorage": { "min_kw": 100.0, @@ -172,7 +176,8 @@ "macrs_itc_reduction": 0.5, "total_itc_fraction": 0.0, "total_rebate_per_kw": 0.0, - "total_rebate_per_kwh": 0.0 + "total_rebate_per_kwh": 0.0, + "optimize_soc_init_fraction": false }, "Generator": { "existing_kw": 0.0, diff --git a/reoptjl/test/posts/ashp_defaults_update.json b/reoptjl/test/posts/ashp_defaults_update.json index d3302c1d6..0a937aa55 100644 --- a/reoptjl/test/posts/ashp_defaults_update.json +++ b/reoptjl/test/posts/ashp_defaults_update.json @@ -27,8 +27,8 @@ "blended_annual_demand_rate": 0.0 }, "ElectricUtility": { - "cambium_location_type": "GEA Regions", - "cambium_metric_col": "lrmer_co2e", + "cambium_location_type": "GEA Regions 2023", + "cambium_co2_metric": "lrmer_co2e", "cambium_scenario": "Mid-case", "cambium_grid_level": "enduse" }, diff --git a/reoptjl/test/posts/central_plant_ghp.json b/reoptjl/test/posts/central_plant_ghp.json index 942942a22..4aa266c7a 100644 --- a/reoptjl/test/posts/central_plant_ghp.json +++ b/reoptjl/test/posts/central_plant_ghp.json @@ -53,7 +53,12 @@ "tess_ghx_minimum_timesteps_per_hour": 1, "max_sizing_iterations": 10, "init_sizing_factor_ft_per_peak_ton": 300.0, - "heat_pump_configuration": "WWHP" + "heat_pump_configuration": "WWHP", + "ghx_header_depth_ft": 4.0, + "borehole_diameter_inch": 5.0, + "ghx_pipe_thermal_conductivity_btu_per_hr_ft_f": 0.25, + "ghx_shank_space_inch": 2.5, + "grout_thermal_conductivity_btu_per_hr_ft_f": 1.0 }] }, "PV": { diff --git a/reoptjl/test/posts/hybrid_ghp.json b/reoptjl/test/posts/hybrid_ghp.json index 5a5b45595..69114e2c5 100644 --- a/reoptjl/test/posts/hybrid_ghp.json +++ b/reoptjl/test/posts/hybrid_ghp.json @@ -45,7 +45,12 @@ "tess_ghx_minimum_timesteps_per_hour": 1, "max_sizing_iterations": 10, "init_sizing_factor_ft_per_peak_ton": 300.0, - "heat_pump_configuration": "WSHP" + "heat_pump_configuration": "WSHP", + "ghx_header_depth_ft": 4.0, + "borehole_diameter_inch": 5.0, + "ghx_pipe_thermal_conductivity_btu_per_hr_ft_f": 0.25, + "ghx_shank_space_inch": 2.5, + "grout_thermal_conductivity_btu_per_hr_ft_f": 1.0 }] }, "PV": { diff --git a/reoptjl/test/test_http_endpoints.py b/reoptjl/test/test_http_endpoints.py index d844fd513..8b0e25ad6 100644 --- a/reoptjl/test/test_http_endpoints.py +++ b/reoptjl/test/test_http_endpoints.py @@ -231,13 +231,13 @@ def test_avert_emissions_profile_endpoint(self): self.assertTrue("error" in view_response) - def test_cambium_emissions_profile_endpoint(self): - # Call to the django view endpoint v3/cambium_emissions_profile which calls the http.jl endpoint + def test_cambium_profile_endpoint(self): + # Call to the django view endpoint v3/cambium_profile which calls the http.jl endpoint #case 1: location in CONUS (Seattle, WA) inputs = { "load_year": 2021, "scenario": "Mid-case", - "location_type": "States", + "location_type": "GEA Regions 2023", "latitude": 47.606211, # Seattle "longitude": -122.336052, # Seattle "start_year": 2024, @@ -245,30 +245,30 @@ def test_cambium_emissions_profile_endpoint(self): "metric_col": "lrmer_co2e", "grid_level": "enduse" } - resp = self.api_client.get(f'/v3/cambium_emissions_profile', data=inputs) + resp = self.api_client.get(f'/v3/cambium_profile', data=inputs) self.assertHttpOK(resp) view_response = json.loads(resp.content) self.assertEquals(view_response["metric_col"], "lrmer_co2e") - self.assertEquals(view_response["location"], "Washington") - self.assertEquals(len(view_response["emissions_factor_series_lb_CO2_per_kwh"]), 8760) + self.assertEquals(view_response["location"], "Northern Grid West") + self.assertEquals(len(view_response["data_series"]), 8760) #case 2: location off shore of NJ (works for AVERT, not Cambium) inputs["latitude"] = 39.034417 inputs["longitude"] = -74.759292 - resp = self.api_client.get(f'/v3/cambium_emissions_profile', data=inputs) + resp = self.api_client.get(f'/v3/cambium_profile', data=inputs) self.assertHttpBadRequest(resp) view_response = json.loads(resp.content) self.assertTrue("error" in view_response) #case 3: Honolulu, HI (works for AVERT but not Cambium) inputs["latitude"] = 21.3099 inputs["longitude"] = -157.8581 - resp = self.api_client.get(f'/v3/cambium_emissions_profile', data=inputs) + resp = self.api_client.get(f'/v3/cambium_profile', data=inputs) self.assertHttpBadRequest(resp) view_response = json.loads(resp.content) self.assertTrue("error" in view_response) #case 4: location well outside of US (does not work) inputs["latitude"] = 0.0 inputs["longitude"] = 0.0 - resp = self.api_client.get(f'/v3/cambium_emissions_profile', data=inputs) + resp = self.api_client.get(f'/v3/cambium_profile', data=inputs) self.assertHttpBadRequest(resp) view_response = json.loads(resp.content) self.assertTrue("error" in view_response) diff --git a/reoptjl/test/test_job_endpoint.py b/reoptjl/test/test_job_endpoint.py index 579e16806..5dfe67391 100644 --- a/reoptjl/test/test_job_endpoint.py +++ b/reoptjl/test/test_job_endpoint.py @@ -59,7 +59,7 @@ def test_pv_battery_and_emissions_defaults_from_julia(self): self.assertAlmostEqual(results["ElectricStorage"]["size_kw"], 49.05, places=1) self.assertAlmostEqual(results["ElectricStorage"]["size_kwh"], 83.32, places=1) - self.assertIsNotNone(results["Site"]["total_renewable_energy_fraction"]) + self.assertIsNotNone(results["Site"]["onsite_renewable_energy_fraction_of_total_load"]) self.assertIsNotNone(results["Site"]["annual_emissions_tonnes_CO2"]) self.assertIsNotNone(results["Site"]["lifecycle_emissions_tonnes_NOx"]) diff --git a/reoptjl/urls.py b/reoptjl/urls.py index a0ffe3536..89617a7cf 100644 --- a/reoptjl/urls.py +++ b/reoptjl/urls.py @@ -11,7 +11,7 @@ re_path(r'^chp_defaults/?$', views.chp_defaults), re_path(r'^absorption_chiller_defaults/?$', views.absorption_chiller_defaults), re_path(r'^avert_emissions_profile/?$', views.avert_emissions_profile), - re_path(r'^cambium_emissions_profile/?$', views.cambium_emissions_profile), + re_path(r'^cambium_profile/?$', views.cambium_profile), re_path(r'^easiur_costs/?$', views.easiur_costs), re_path(r'^simulated_load/?$', views.simulated_load), re_path(r'^user/(?P[0-9a-f-]+)/summary/?$', views.summary), diff --git a/reoptjl/views.py b/reoptjl/views.py index 3dc915bc1..566733f5e 100644 --- a/reoptjl/views.py +++ b/reoptjl/views.py @@ -1493,7 +1493,7 @@ def avert_emissions_profile(request): log.error(debug_msg) return JsonResponse({"Error": "Unexpected Error. Please check your input parameters and contact reopt@nrel.gov if problems persist."}, status=500) -def cambium_emissions_profile(request): +def cambium_profile(request): try: inputs = { "scenario": request.GET['scenario'], @@ -1512,7 +1512,7 @@ def cambium_emissions_profile(request): "julia" ) http_jl_response = requests.get( - "http://" + julia_host + ":8081/cambium_emissions_profile/", + "http://" + julia_host + ":8081/cambium_profile/", json=inputs ) response = JsonResponse(