diff --git a/CHANGELOG.md b/CHANGELOG.md index 50a1318dd..cf855d1f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,16 @@ Classify the change according to the following categories: ##### Removed ### Patches +## v3.10.0 +### Minor Updates +#### Added +- Added new model **ElectricHeaterInputs** +- Added new model **ElectricHeaterOutputs** +- Added new model **ASHPWaterHeaterInputs** +- Added new model **ASHPWaterHeaterOutputs** +- Added new model **ASHPSpaceHeaterInputs** +- Added new model **ASHPSpaceHeaterOutputs** + ## v3.9.4 ### Minor Updates #### Added diff --git a/julia_src/Dockerfile b/julia_src/Dockerfile index 60bb2034c..e75b58da6 100644 --- a/julia_src/Dockerfile +++ b/julia_src/Dockerfile @@ -5,6 +5,10 @@ ARG NREL_ROOT_CERT_URL_ROOT="" RUN set -x && if [ -n "$NREL_ROOT_CERT_URL_ROOT" ]; then curl -fsSLk -o /usr/local/share/ca-certificates/nrel_root.crt "${NREL_ROOT_CERT_URL_ROOT}/nrel_root.pem" && curl -fsSLk -o /usr/local/share/ca-certificates/nrel_xca1.crt "${NREL_ROOT_CERT_URL_ROOT}/nrel_xca1.pem" && update-ca-certificates; fi ENV REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt +# Use git cli instead of Julia's libgit2, since we're facing SSL issues with it. +RUN apt-get update && apt-get -y install git && rm -rf /var/lib/apt/lists/* /var/lib/dpkg/*-old /var/cache/* /var/log/* +ENV JULIA_PKG_USE_CLI_GIT=true + # Install Julia packages ENV JULIA_NUM_THREADS=2 ENV XPRESS_JL_SKIP_LIB_CHECK=True diff --git a/julia_src/Manifest.toml b/julia_src/Manifest.toml index ea4f14952..feddf4486 100644 --- a/julia_src/Manifest.toml +++ b/julia_src/Manifest.toml @@ -917,9 +917,9 @@ 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 = "c02ff7c0b60352164b89f39789424422083ae4eb" +git-tree-sha1 = "32499f329265d270e9f77c8831892772b5fbf28f" uuid = "d36ad4e8-d74a-4f7a-ace1-eaea049febf6" -version = "0.47.2" +version = "0.48.0" [[deps.Random]] deps = ["SHA"] diff --git a/julia_src/http.jl b/julia_src/http.jl index 6e8bbd544..fc91cabc9 100644 --- a/julia_src/http.jl +++ b/julia_src/http.jl @@ -105,6 +105,31 @@ function reopt(req::HTTP.Request) else ghp_dict = Dict() end + if haskey(d, "ASHPSpaceHeater") + inputs_with_defaults_from_ashp = [ + :max_ton, :installed_cost_per_ton, :om_cost_per_ton, + :macrs_option_years, :macrs_bonus_fraction, :can_supply_steam_turbine, + :can_serve_process_heat, :can_serve_dhw, :can_serve_space_heating, :can_serve_cooling, + :back_up_temp_threshold_degF, :sizing_factor, :heating_cop_reference, :heating_cf_reference, + :heating_reference_temps_degF, :cooling_cop_reference, :cooling_cf_reference, + :cooling_reference_temps_degF + ] + ashp_dict = Dict(key=>getfield(model_inputs.s.ashp, key) for key in inputs_with_defaults_from_ashp) + else + ashp_dict = Dict() + end + if haskey(d, "ASHPWaterHeater") + inputs_with_defaults_from_ashp_wh = [ + :max_ton, :installed_cost_per_ton, :om_cost_per_ton, + :macrs_option_years, :macrs_bonus_fraction, :can_supply_steam_turbine, + :can_serve_process_heat, :can_serve_dhw, :can_serve_space_heating, :can_serve_cooling, + :back_up_temp_threshold_degF, :sizing_factor, :heating_cop_reference, :heating_cf_reference, + :heating_reference_temps_degF + ] + ashp_wh_dict = Dict(key=>getfield(model_inputs.s.ashp_wh, key) for key in inputs_with_defaults_from_ashp_wh) + else + ashp_wh_dict = Dict() + end if haskey(d, "CoolingLoad") inputs_with_defaults_from_chiller = [ :cop @@ -112,15 +137,23 @@ function reopt(req::HTTP.Request) chiller_dict = Dict(key=>getfield(model_inputs.s.existing_chiller, key) for key in inputs_with_defaults_from_chiller) else chiller_dict = Dict() - end + end + if !isnothing(model_inputs.s.site.outdoor_air_temperature_degF) + site_dict = Dict(:outdoor_air_temperature_degF => model_inputs.s.site.outdoor_air_temperature_degF) + else + site_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_easiur), "ElectricUtility" => Dict(key=>getfield(model_inputs.s.electric_utility, key) for key in inputs_with_defaults_from_avert_or_cambium), + "Site" => site_dict, "CHP" => chp_dict, "SteamTurbine" => steamturbine_dict, "GHP" => ghp_dict, - "ExistingChiller" => chiller_dict - ) + "ExistingChiller" => chiller_dict, + "ASHPSpaceHeater" => ashp_dict, + "ASHPWaterHeater" => ashp_wh_dict + ) catch e @error "Something went wrong in REopt optimization!" exception=(e, catch_backtrace()) error_response["error"] = sprint(showerror, e) # append instead of rewrite? @@ -511,6 +544,41 @@ function get_existing_chiller_default_cop(req::HTTP.Request) end end +function get_ashp_defaults(req::HTTP.Request) + d = JSON.parse(String(req.body)) + defaults = nothing + + if !("load_served" in keys(d)) + @info("ASHP load served not provided. Using default of SpaceHeating.") + d["load_served"] = "SpaceHeating" + end + if !("force_into_system" in keys(d)) + @info("ASHP force_into_system not provided. Using default of false.") + d["force_into_system"] = false + elseif typeof(d["force_into_system"]) == String + d["force_into_system"] = parse(Bool, d["force_into_system"]) + end + + @info "Getting default ASHP attributes..." + error_response = Dict() + try + # Have to specify "REopt.get_existing..." because http function has the same name + defaults = reoptjl.get_ashp_defaults(d["load_served"],d["force_into_system"]) + catch e + @error "Something went wrong in the get_ashp_defaults endpoint" exception=(e, catch_backtrace()) + error_response["error"] = sprint(showerror, e) + end + if isempty(error_response) + @info("ASHP defaults obtained.") + response = defaults + return HTTP.Response(200, JSON.json(response)) + else + @info "An error occured in the get_ashp_defaults endpoint" + return HTTP.Response(500, JSON.json(error_response)) + end +end + + function job_no_xpress(req::HTTP.Request) error_response = Dict("error" => "V1 and V2 not available without Xpress installation.") return HTTP.Response(500, JSON.json(error_response)) @@ -537,4 +605,5 @@ HTTP.register!(ROUTER, "GET", "/ghp_efficiency_thermal_factors", ghp_efficiency_ HTTP.register!(ROUTER, "GET", "/ground_conductivity", ground_conductivity) HTTP.register!(ROUTER, "GET", "/health", health) HTTP.register!(ROUTER, "GET", "/get_existing_chiller_default_cop", get_existing_chiller_default_cop) +HTTP.register!(ROUTER, "GET", "/get_ashp_defaults", get_ashp_defaults) HTTP.serve(ROUTER, "0.0.0.0", 8081, reuseaddr=true) diff --git a/reoptjl/migrations/0061_ashpspaceheaterinputs_ashpspaceheateroutputs_and_more.py b/reoptjl/migrations/0061_ashpspaceheaterinputs_ashpspaceheateroutputs_and_more.py new file mode 100644 index 000000000..17fdbbfcd --- /dev/null +++ b/reoptjl/migrations/0061_ashpspaceheaterinputs_ashpspaceheateroutputs_and_more.py @@ -0,0 +1,150 @@ +# Generated by Django 4.0.7 on 2024-08-31 03:20 + +import django.contrib.postgres.fields +import django.core.validators +from django.db import migrations, models +import django.db.models.deletion +import reoptjl.models + + +class Migration(migrations.Migration): + + dependencies = [ + ('reoptjl', '0060_processheatloadinputs_addressable_load_fraction_and_more'), + ] + + operations = [ + migrations.CreateModel( + name='ASHPSpaceHeaterInputs', + fields=[ + ('meta', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, related_name='ASHPSpaceHeaterInputs', serialize=False, to='reoptjl.apimeta')), + ('min_ton', models.FloatField(blank=True, default=0.0, help_text='Minimum thermal power size constraint for optimization [ton]', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(100000000.0)])), + ('max_ton', models.FloatField(blank=True, default=100000000.0, help_text='Maximum thermal power size constraint for optimization [ton]', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(100000000.0)])), + ('min_allowable_ton', models.FloatField(blank=True, default=0.0, help_text='Minimum nonzero thermal power size constraint for optimization [ton]', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(100000000.0)])), + ('min_allowable_peak_capacity_fraction', models.FloatField(blank=True, default=0.5, help_text='Minimum nonzero thermal power as a fucniton of coincident peak load - constraint for optimization [ton]', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(100000000.0)])), + ('sizing_factor', models.FloatField(blank=True, default=1.0, help_text='Size of system relative to max dispatch output [fraction]', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(100000000.0)])), + ('installed_cost_per_ton', models.FloatField(blank=True, default=2250.0, help_text='Thermal power-based cost of ASHP space heater [$/ton] (3.5 ton to 1 kWt)', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(100000000.0)])), + ('om_cost_per_ton', models.FloatField(blank=True, default=0.0, help_text='Thermal power-based cost of ASHP space heater [$/ton] (3.5 ton to 1 kWt)', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(100000000.0)])), + ('macrs_option_years', models.IntegerField(blank=True, choices=[(0, 'Zero'), (5, 'Five'), (7, 'Seven')], default=0, help_text='Duration over which accelerated depreciation will occur. Set to zero to disable', null=True)), + ('macrs_bonus_fraction', models.FloatField(blank=True, default=0.0, help_text='Percent of upfront project costs to depreciate in year one in addition to scheduled depreciation', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1)])), + ('heating_cop_reference', django.contrib.postgres.fields.ArrayField(base_field=models.FloatField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(20.0)]), blank=True, default=list, help_text='Reference points for ASHP space heating system heating coefficient of performance (COP) (ratio of usable heating thermal energy produced per unit electric energy consumed)', size=None)), + ('heating_cf_reference', django.contrib.postgres.fields.ArrayField(base_field=models.FloatField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(20.0)]), blank=True, default=list, help_text='Reference points for ASHP space heating system heating capac)ity factor(ratio of heating thermal power to rated capacity)', size=None)), + ('heating_reference_temps_degF', django.contrib.postgres.fields.ArrayField(base_field=models.FloatField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(-275), django.core.validators.MaxValueValidator(200.0)]), blank=True, default=list, help_text="Reference temperatures for ASHP space heating system's heating COP and CF", size=None)), + ('cooling_cop_reference', django.contrib.postgres.fields.ArrayField(base_field=models.FloatField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(20.0)]), blank=True, default=list, help_text='Reference points for ASHP space heating system cooling coefficient of performance (COP) (ratio of usable heating thermal energy produced per unit electric energy consumed)', size=None)), + ('cooling_cf_reference', django.contrib.postgres.fields.ArrayField(base_field=models.FloatField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(20.0)]), blank=True, default=list, help_text='Reference points for ASHP space heating system cooling capac)ity factor(ratio of heating thermal power to rated capacity)', size=None)), + ('cooling_reference_temps_degF', django.contrib.postgres.fields.ArrayField(base_field=models.FloatField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(-275), django.core.validators.MaxValueValidator(200.0)]), blank=True, default=list, help_text="Reference temperatures for ASHP space heating system's cooling COP and CF", size=None)), + ('can_serve_cooling', models.BooleanField(blank=True, default=True, help_text='Boolean indicator if ASHP space heater can serve cooling load', null=True)), + ('force_into_system', models.BooleanField(blank=True, default=False, help_text='Boolean indicator if ASHP space heater serves compatible thermal loads exclusively in optimized scenario', null=True)), + ('avoided_capex_by_ashp_present_value', models.FloatField(blank=True, default=0.0, help_text='net present value of avoided capital expenditures due to ASHP system being present [$]', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(100000000.0)])), + ('back_up_temp_threshold_degF', models.FloatField(blank=True, default=-10.0, help_text='Temperature threshold below which resistive back-up heater turns on [Fahrenheit]', null=True, validators=[django.core.validators.MinValueValidator(-275.0), django.core.validators.MaxValueValidator(200.0)])), + ], + bases=(reoptjl.models.BaseModel, models.Model), + ), + migrations.CreateModel( + name='ASHPSpaceHeaterOutputs', + fields=[ + ('meta', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, related_name='ASHPSpaceHeaterOutputs', serialize=False, to='reoptjl.apimeta')), + ('size_ton', models.FloatField(blank=True, null=True)), + ('annual_electric_consumption_kwh', models.FloatField(blank=True, null=True)), + ('electric_consumption_series_kw', django.contrib.postgres.fields.ArrayField(base_field=models.FloatField(blank=True, null=True), default=list, size=None)), + ('annual_thermal_production_mmbtu', models.FloatField(blank=True, null=True)), + ('annual_thermal_production_tonhour', models.FloatField(blank=True, null=True)), + ('thermal_to_storage_series_mmbtu_per_hour', django.contrib.postgres.fields.ArrayField(base_field=models.FloatField(blank=True, null=True), default=list, size=None)), + ('thermal_production_series_mmbtu_per_hour', django.contrib.postgres.fields.ArrayField(base_field=models.FloatField(blank=True, null=True), default=list, size=None)), + ('thermal_to_load_series_mmbtu_per_hour', django.contrib.postgres.fields.ArrayField(base_field=models.FloatField(blank=True, null=True), default=list, size=None)), + ('thermal_to_space_heating_load_series_mmbtu_per_hour', django.contrib.postgres.fields.ArrayField(base_field=models.FloatField(blank=True, null=True), default=list, size=None)), + ('thermal_to_storage_series_ton', django.contrib.postgres.fields.ArrayField(base_field=models.FloatField(blank=True, null=True), default=list, size=None)), + ('thermal_to_load_series_ton', django.contrib.postgres.fields.ArrayField(base_field=models.FloatField(blank=True, null=True), default=list, size=None)), + ('heating_cop', django.contrib.postgres.fields.ArrayField(base_field=models.FloatField(blank=True, null=True), default=list, size=None)), + ('heating_cf', django.contrib.postgres.fields.ArrayField(base_field=models.FloatField(blank=True, null=True), default=list, size=None)), + ('cooling_cop', django.contrib.postgres.fields.ArrayField(base_field=models.FloatField(blank=True, null=True), default=list, size=None)), + ('cooling_cf', django.contrib.postgres.fields.ArrayField(base_field=models.FloatField(blank=True, null=True), default=list, size=None)), + ], + bases=(reoptjl.models.BaseModel, models.Model), + ), + migrations.CreateModel( + name='ASHPWaterHeaterInputs', + fields=[ + ('meta', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, related_name='ASHPWaterHeaterInputs', serialize=False, to='reoptjl.apimeta')), + ('min_ton', models.FloatField(blank=True, default=0.0, help_text='Minimum thermal power size constraint for optimization [ton]', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(100000000.0)])), + ('max_ton', models.FloatField(blank=True, default=100000000.0, help_text='Maximum thermal power size constraint for optimization [ton]', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(100000000.0)])), + ('min_allowable_ton', models.FloatField(blank=True, default=0.0, help_text='Minimum nonzero thermal power size constraint for optimization [ton]', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(100000000.0)])), + ('min_allowable_peak_capacity_fraction', models.FloatField(blank=True, default=0.0, help_text='Minimum nonzero thermal power as a function of coincident peak load / CF - constraint for optimization [ton]', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(100000000.0)])), + ('sizing_factor', models.FloatField(blank=True, default=0.0, help_text='Size of system relative to max dispatch output [fraction]', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(100000000.0)])), + ('installed_cost_per_ton', models.FloatField(blank=True, default=2250.0, help_text='Thermal power-based cost of ASHP water heater [$/ton] (3.5 ton to 1 kWt)', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(100000000.0)])), + ('om_cost_per_ton', models.FloatField(blank=True, default=0.0, help_text='Thermal power-based cost of ASHP water heater [$/ton] (3.5 ton to 1 kWt)', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(100000000.0)])), + ('macrs_option_years', models.IntegerField(blank=True, choices=[(0, 'Zero'), (5, 'Five'), (7, 'Seven')], default=0, help_text='Duration over which accelerated depreciation will occur. Set to zero to disable', null=True)), + ('macrs_bonus_fraction', models.FloatField(blank=True, default=0.0, help_text='Percent of upfront project costs to depreciate in year one in addition to scheduled depreciation', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1)])), + ('heating_cop_reference', django.contrib.postgres.fields.ArrayField(base_field=models.FloatField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(20.0)]), blank=True, default=list, help_text='Reference points for ASHP space heating system heating coefficient of performance (COP) (ratio of usable heating thermal energy produced per unit electric energy consumed)', size=None)), + ('heating_cf_reference', django.contrib.postgres.fields.ArrayField(base_field=models.FloatField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(20.0)]), blank=True, default=list, help_text='Reference points for ASHP space heating system heating capac)ity factor(ratio of heating thermal power to rated capacity)', size=None)), + ('heating_reference_temps_degF', django.contrib.postgres.fields.ArrayField(base_field=models.FloatField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(-275.0), django.core.validators.MaxValueValidator(200.0)]), blank=True, default=list, help_text="Reference temperatures for ASHP space heating system's heating COP and CF [Fahrenheit]", size=None)), + ('avoided_capex_by_ashp_present_value', models.FloatField(blank=True, default=0.0, help_text='Net present value of avoided capital expenditures due to ASHP system being present [$]', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(100000000.0)])), + ('back_up_temp_threshold_degF', models.FloatField(blank=True, default=-10.0, help_text='Temperature threshold below which resistive back-up heater turns on [Fahrenheit]', null=True, validators=[django.core.validators.MinValueValidator(-275.0), django.core.validators.MaxValueValidator(200.0)])), + ('force_into_system', models.BooleanField(blank=True, default=False, help_text='Boolean indicator if ASHP space heater serves compatible thermal loads exclusively in optimized scenario', null=True)), + ], + bases=(reoptjl.models.BaseModel, models.Model), + ), + migrations.CreateModel( + name='ASHPWaterHeaterOutputs', + fields=[ + ('meta', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, related_name='ASHPWaterHeaterOutputs', serialize=False, to='reoptjl.apimeta')), + ('size_ton', models.FloatField(blank=True, null=True)), + ('annual_electric_consumption_kwh', models.FloatField(blank=True, null=True)), + ('electric_consumption_series_kw', django.contrib.postgres.fields.ArrayField(base_field=models.FloatField(blank=True, null=True), default=list, size=None)), + ('annual_thermal_production_mmbtu', models.FloatField(blank=True, null=True)), + ('thermal_to_storage_series_mmbtu_per_hour', django.contrib.postgres.fields.ArrayField(base_field=models.FloatField(blank=True, null=True), default=list, size=None)), + ('thermal_production_series_mmbtu_per_hour', django.contrib.postgres.fields.ArrayField(base_field=models.FloatField(blank=True, null=True), default=list, size=None)), + ('thermal_to_load_series_mmbtu_per_hour', django.contrib.postgres.fields.ArrayField(base_field=models.FloatField(blank=True, null=True), default=list, size=None)), + ('thermal_to_dhw_load_series_mmbtu_per_hour', django.contrib.postgres.fields.ArrayField(base_field=models.FloatField(blank=True, null=True), default=list, size=None)), + ('heating_cop', django.contrib.postgres.fields.ArrayField(base_field=models.FloatField(blank=True, null=True), default=list, size=None)), + ('heating_cf', django.contrib.postgres.fields.ArrayField(base_field=models.FloatField(blank=True, null=True), default=list, size=None)), + ], + bases=(reoptjl.models.BaseModel, models.Model), + ), + migrations.CreateModel( + name='ElectricHeaterInputs', + fields=[ + ('meta', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, related_name='ElectricHeaterInputs', serialize=False, to='reoptjl.apimeta')), + ('min_mmbtu_per_hour', models.FloatField(blank=True, default=0.0, help_text='Minimum thermal power size', null=True, validators=[django.core.validators.MinValueValidator(0.0), django.core.validators.MaxValueValidator(100000000.0)])), + ('max_mmbtu_per_hour', models.FloatField(blank=True, default=10000000.0, help_text='Maximum thermal power size', null=True, validators=[django.core.validators.MinValueValidator(0.0), django.core.validators.MaxValueValidator(100000000.0)])), + ('installed_cost_per_mmbtu_per_hour', models.FloatField(blank=True, default=154902.0, help_text='Thermal power-based cost', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(100000000.0)])), + ('om_cost_per_mmbtu_per_hour', models.FloatField(blank=True, default=0.0, help_text='Thermal power-based fixed O&M cost', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(100000000.0)])), + ('macrs_option_years', models.IntegerField(blank=True, choices=[(0, 'Zero'), (5, 'Five'), (7, 'Seven')], default=0, help_text='Duration over which accelerated depreciation will occur. Set to zero to disable', null=True)), + ('macrs_bonus_fraction', models.FloatField(blank=True, default=0.0, help_text='Percent of upfront project costs to depreciate in year one in addition to scheduled depreciation', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1)])), + ('cop', models.FloatField(blank=True, default=1.0, help_text='Electric heater system coefficient of performance (COP) (ratio of usable thermal energy produced per unit electric energy consumed)', null=True, validators=[django.core.validators.MinValueValidator(0.01), django.core.validators.MaxValueValidator(20)])), + ('can_supply_steam_turbine', models.BooleanField(blank=True, default=True, help_text='If the boiler can supply steam to the steam turbine for electric production', null=True)), + ('can_serve_dhw', models.BooleanField(blank=True, default=True, help_text='Boolean indicator if the electric heater can serve domestic hot water load', null=True)), + ('can_serve_space_heating', models.BooleanField(blank=True, default=True, help_text='Boolean indicator if the electric heater can serve space heating load', null=True)), + ('can_serve_process_heat', models.BooleanField(blank=True, default=True, help_text='Boolean indicator if the electric heater can serve process heat load', null=True)), + ], + bases=(reoptjl.models.BaseModel, models.Model), + ), + migrations.CreateModel( + name='ElectricHeaterOutputs', + fields=[ + ('meta', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, related_name='ElectricHeaterOutputs', serialize=False, to='reoptjl.apimeta')), + ('size_mmbtu_per_hour', models.FloatField(blank=True, null=True)), + ('annual_electric_consumption_kwh', models.FloatField(blank=True, null=True)), + ('electric_consumption_series_kw', django.contrib.postgres.fields.ArrayField(base_field=models.FloatField(blank=True, null=True), default=list, size=None)), + ('annual_thermal_production_mmbtu', models.FloatField(blank=True, null=True)), + ('thermal_to_storage_series_mmbtu_per_hour', django.contrib.postgres.fields.ArrayField(base_field=models.FloatField(blank=True, null=True), default=list, size=None)), + ('thermal_to_steamturbine_series_mmbtu_per_hour', django.contrib.postgres.fields.ArrayField(base_field=models.FloatField(blank=True, null=True), default=list, size=None)), + ('thermal_production_series_mmbtu_per_hour', django.contrib.postgres.fields.ArrayField(base_field=models.FloatField(blank=True, null=True), default=list, size=None)), + ('thermal_to_load_series_mmbtu_per_hour', django.contrib.postgres.fields.ArrayField(base_field=models.FloatField(blank=True, null=True), default=list, size=None)), + ('thermal_to_dhw_load_series_mmbtu_per_hour', django.contrib.postgres.fields.ArrayField(base_field=models.FloatField(blank=True, null=True), default=list, size=None)), + ('thermal_to_space_heating_load_series_mmbtu_per_hour', django.contrib.postgres.fields.ArrayField(base_field=models.FloatField(blank=True, null=True), default=list, size=None)), + ('thermal_to_process_heat_load_series_mmbtu_per_hour', django.contrib.postgres.fields.ArrayField(base_field=models.FloatField(blank=True, null=True), default=list, size=None)), + ], + bases=(reoptjl.models.BaseModel, models.Model), + ), + migrations.AddField( + model_name='existingchillerinputs', + name='retire_in_optimal', + field=models.BooleanField(blank=True, default=False, help_text='Boolean indicator if the existing chiller is unavailable in the optimal case (still used in BAU)', null=True), + ), + migrations.AlterField( + model_name='apimeta', + name='reopt_version', + field=models.TextField(blank=True, default='', help_text='Version number of the Julia package for REopt that is used to solve the problem.', null=True), + ), + ] diff --git a/reoptjl/migrations/0062_ashpspaceheateroutputs_annual_electric_consumption_for_cooling_kwh_and_more.py b/reoptjl/migrations/0062_ashpspaceheateroutputs_annual_electric_consumption_for_cooling_kwh_and_more.py new file mode 100644 index 000000000..060213ebc --- /dev/null +++ b/reoptjl/migrations/0062_ashpspaceheateroutputs_annual_electric_consumption_for_cooling_kwh_and_more.py @@ -0,0 +1,34 @@ +# Generated by Django 4.0.7 on 2024-09-04 21:37 + +import django.contrib.postgres.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('reoptjl', '0061_ashpspaceheaterinputs_ashpspaceheateroutputs_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='ashpspaceheateroutputs', + name='annual_electric_consumption_for_cooling_kwh', + field=models.FloatField(blank=True, null=True), + ), + migrations.AddField( + model_name='ashpspaceheateroutputs', + name='annual_electric_consumption_for_heating_kwh', + field=models.FloatField(blank=True, null=True), + ), + migrations.AddField( + model_name='ashpspaceheateroutputs', + name='electric_consumption_for_cooling_series_kw', + field=django.contrib.postgres.fields.ArrayField(base_field=models.FloatField(blank=True, null=True), default=list, size=None), + ), + migrations.AddField( + model_name='ashpspaceheateroutputs', + name='electric_consumption_for_heating_series_kw', + field=django.contrib.postgres.fields.ArrayField(base_field=models.FloatField(blank=True, null=True), default=list, size=None), + ), + ] diff --git a/reoptjl/migrations/0063_alter_ashpwaterheaterinputs_min_allowable_peak_capacity_fraction_and_more.py b/reoptjl/migrations/0063_alter_ashpwaterheaterinputs_min_allowable_peak_capacity_fraction_and_more.py new file mode 100644 index 000000000..1296f9df6 --- /dev/null +++ b/reoptjl/migrations/0063_alter_ashpwaterheaterinputs_min_allowable_peak_capacity_fraction_and_more.py @@ -0,0 +1,24 @@ +# Generated by Django 4.0.7 on 2024-09-05 20:48 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('reoptjl', '0062_ashpspaceheateroutputs_annual_electric_consumption_for_cooling_kwh_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='ashpwaterheaterinputs', + name='min_allowable_peak_capacity_fraction', + field=models.FloatField(blank=True, help_text='Minimum nonzero thermal power as a function of coincident peak load / CF - constraint for optimization [ton]', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(100000000.0)]), + ), + migrations.AlterField( + model_name='ashpwaterheaterinputs', + name='min_allowable_ton', + field=models.FloatField(blank=True, help_text='Minimum nonzero thermal power size constraint for optimization [ton]', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(100000000.0)]), + ), + ] diff --git a/reoptjl/migrations/0063_ghpoutputs_thermal_to_dhw_load_series_mmbtu_per_hour_and_more.py b/reoptjl/migrations/0063_ghpoutputs_thermal_to_dhw_load_series_mmbtu_per_hour_and_more.py new file mode 100644 index 000000000..ca068d693 --- /dev/null +++ b/reoptjl/migrations/0063_ghpoutputs_thermal_to_dhw_load_series_mmbtu_per_hour_and_more.py @@ -0,0 +1,29 @@ +# Generated by Django 4.0.7 on 2024-09-09 15:18 + +import django.contrib.postgres.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('reoptjl', '0062_ashpspaceheateroutputs_annual_electric_consumption_for_cooling_kwh_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='ghpoutputs', + name='thermal_to_dhw_load_series_mmbtu_per_hour', + field=django.contrib.postgres.fields.ArrayField(base_field=models.FloatField(blank=True, null=True), blank=True, default=list, null=True, size=None), + ), + migrations.AddField( + model_name='ghpoutputs', + name='thermal_to_load_series_ton', + field=django.contrib.postgres.fields.ArrayField(base_field=models.FloatField(blank=True, null=True), blank=True, default=list, null=True, size=None), + ), + migrations.AddField( + model_name='ghpoutputs', + name='thermal_to_space_heating_load_series_mmbtu_per_hour', + field=django.contrib.postgres.fields.ArrayField(base_field=models.FloatField(blank=True, null=True), blank=True, default=list, null=True, size=None), + ), + ] diff --git a/reoptjl/migrations/0064_alter_ashpspaceheaterinputs_min_allowable_peak_capacity_fraction_and_more.py b/reoptjl/migrations/0064_alter_ashpspaceheaterinputs_min_allowable_peak_capacity_fraction_and_more.py new file mode 100644 index 000000000..a3ce7fe8f --- /dev/null +++ b/reoptjl/migrations/0064_alter_ashpspaceheaterinputs_min_allowable_peak_capacity_fraction_and_more.py @@ -0,0 +1,24 @@ +# Generated by Django 4.0.7 on 2024-09-05 20:52 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('reoptjl', '0063_alter_ashpwaterheaterinputs_min_allowable_peak_capacity_fraction_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='ashpspaceheaterinputs', + name='min_allowable_peak_capacity_fraction', + field=models.FloatField(blank=True, help_text='Minimum nonzero thermal power as a fucniton of coincident peak load - constraint for optimization [ton]', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(100000000.0)]), + ), + migrations.AlterField( + model_name='ashpspaceheaterinputs', + name='min_allowable_ton', + field=models.FloatField(blank=True, help_text='Minimum nonzero thermal power size constraint for optimization [ton]', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(100000000.0)]), + ), + ] diff --git a/reoptjl/migrations/0064_siteinputs_outdoor_air_temperature_degf.py b/reoptjl/migrations/0064_siteinputs_outdoor_air_temperature_degf.py new file mode 100644 index 000000000..93c48874e --- /dev/null +++ b/reoptjl/migrations/0064_siteinputs_outdoor_air_temperature_degf.py @@ -0,0 +1,19 @@ +# Generated by Django 4.0.7 on 2024-09-10 01:25 + +import django.contrib.postgres.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('reoptjl', '0063_ghpoutputs_thermal_to_dhw_load_series_mmbtu_per_hour_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='siteinputs', + name='outdoor_air_temperature_degF', + field=django.contrib.postgres.fields.ArrayField(base_field=models.FloatField(blank=True), blank=True, default=list, help_text="The outdoor air (dry-bulb) temperature in degrees Fahrenheit as determined by the site's location TMY3 data from the PVWatts call or user input. This is used for GHP COP and ASHP COP and CF values based on the default or custom mapping of those.", size=None), + ), + ] diff --git a/reoptjl/migrations/0065_merge_20240910_0128.py b/reoptjl/migrations/0065_merge_20240910_0128.py new file mode 100644 index 000000000..b2b319d73 --- /dev/null +++ b/reoptjl/migrations/0065_merge_20240910_0128.py @@ -0,0 +1,14 @@ +# Generated by Django 4.0.7 on 2024-09-10 01:28 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('reoptjl', '0064_alter_ashpspaceheaterinputs_min_allowable_peak_capacity_fraction_and_more'), + ('reoptjl', '0064_siteinputs_outdoor_air_temperature_degf'), + ] + + operations = [ + ] diff --git a/reoptjl/migrations/0066_alter_siteinputs_outdoor_air_temperature_degf.py b/reoptjl/migrations/0066_alter_siteinputs_outdoor_air_temperature_degf.py new file mode 100644 index 000000000..0d8173fa6 --- /dev/null +++ b/reoptjl/migrations/0066_alter_siteinputs_outdoor_air_temperature_degf.py @@ -0,0 +1,19 @@ +# Generated by Django 4.0.7 on 2024-09-10 03:31 + +import django.contrib.postgres.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('reoptjl', '0065_merge_20240910_0128'), + ] + + operations = [ + migrations.AlterField( + model_name='siteinputs', + name='outdoor_air_temperature_degF', + field=django.contrib.postgres.fields.ArrayField(base_field=models.FloatField(blank=True, null=True), blank=True, default=list, help_text="The outdoor air (dry-bulb) temperature in degrees Fahrenheit as determined by the site's location TMY3 data from the PVWatts call or user input. This is used for GHP COP and ASHP COP and CF values based on the default or custom mapping of those.", null=True, size=None), + ), + ] diff --git a/reoptjl/migrations/0067_alter_ashpwaterheaterinputs_sizing_factor.py b/reoptjl/migrations/0067_alter_ashpwaterheaterinputs_sizing_factor.py new file mode 100644 index 000000000..f6c735fe7 --- /dev/null +++ b/reoptjl/migrations/0067_alter_ashpwaterheaterinputs_sizing_factor.py @@ -0,0 +1,19 @@ +# Generated by Django 4.0.7 on 2024-09-15 21:23 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('reoptjl', '0066_alter_siteinputs_outdoor_air_temperature_degf'), + ] + + operations = [ + migrations.AlterField( + model_name='ashpwaterheaterinputs', + name='sizing_factor', + field=models.FloatField(blank=True, default=1.0, help_text='Size of system relative to max dispatch output [fraction]', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(100000000.0)]), + ), + ] diff --git a/reoptjl/migrations/0068_existingboileroutputs_size_mmbtu_per_hour.py b/reoptjl/migrations/0068_existingboileroutputs_size_mmbtu_per_hour.py new file mode 100644 index 000000000..09c41d489 --- /dev/null +++ b/reoptjl/migrations/0068_existingboileroutputs_size_mmbtu_per_hour.py @@ -0,0 +1,18 @@ +# Generated by Django 4.0.7 on 2024-09-16 03:39 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('reoptjl', '0067_alter_ashpwaterheaterinputs_sizing_factor'), + ] + + operations = [ + migrations.AddField( + model_name='existingboileroutputs', + name='size_mmbtu_per_hour', + field=models.FloatField(blank=True, null=True), + ), + ] diff --git a/reoptjl/migrations/0069_financialoutputs_initial_capital_costs_after_incentives_without_macrs_and_more.py b/reoptjl/migrations/0069_financialoutputs_initial_capital_costs_after_incentives_without_macrs_and_more.py new file mode 100644 index 000000000..43b36b0be --- /dev/null +++ b/reoptjl/migrations/0069_financialoutputs_initial_capital_costs_after_incentives_without_macrs_and_more.py @@ -0,0 +1,114 @@ +# Generated by Django 4.0.7 on 2024-09-23 21:04 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('reoptjl', '0068_existingboileroutputs_size_mmbtu_per_hour'), + ] + + operations = [ + migrations.AddField( + model_name='financialoutputs', + name='initial_capital_costs_after_incentives_without_macrs', + field=models.FloatField(blank=True, help_text='Up-front capital costs for all technologies, in present value, excluding replacement costs, including incentives except for MACRS.', null=True), + ), + migrations.AddField( + model_name='heatingloadoutputs', + name='annual_emissions_from_unaddressable_heating_load_tonnes_CO2', + field=models.FloatField(blank=True, default=0, help_text='Annual site total unaddressable heating fuel climate CO2 emissions [tonnes]', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(100000000.0)]), + ), + migrations.AddField( + model_name='heatingloadoutputs', + name='annual_total_unaddressable_heating_load_mmbtu', + field=models.FloatField(blank=True, default=0, help_text='Annual site total unaddressable heating fuel [MMBTU]', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(100000000.0)]), + ), + migrations.AlterField( + model_name='ashpspaceheaterinputs', + name='back_up_temp_threshold_degF', + field=models.FloatField(blank=True, help_text='Temperature threshold below which resistive back-up heater turns on [Fahrenheit]', null=True, validators=[django.core.validators.MinValueValidator(-275.0), django.core.validators.MaxValueValidator(200.0)]), + ), + migrations.AlterField( + model_name='ashpspaceheaterinputs', + name='can_serve_cooling', + field=models.BooleanField(blank=True, help_text='Boolean indicator if ASHP space heater can serve cooling load', null=True), + ), + migrations.AlterField( + model_name='ashpspaceheaterinputs', + name='force_into_system', + field=models.BooleanField(blank=True, help_text='Boolean indicator if ASHP space heater serves compatible thermal loads exclusively in optimized scenario', null=True), + ), + migrations.AlterField( + model_name='ashpspaceheaterinputs', + name='installed_cost_per_ton', + field=models.FloatField(blank=True, help_text='Thermal power-based cost of ASHP space heater [$/ton] (3.5 ton to 1 kWt)', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(100000000.0)]), + ), + migrations.AlterField( + model_name='ashpspaceheaterinputs', + name='macrs_bonus_fraction', + field=models.FloatField(blank=True, help_text='Percent of upfront project costs to depreciate in year one in addition to scheduled depreciation', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1)]), + ), + migrations.AlterField( + model_name='ashpspaceheaterinputs', + name='macrs_option_years', + field=models.IntegerField(blank=True, choices=[(0, 'Zero'), (5, 'Five'), (7, 'Seven')], help_text='Duration over which accelerated depreciation will occur. Set to zero to disable', null=True), + ), + migrations.AlterField( + model_name='ashpspaceheaterinputs', + name='max_ton', + field=models.FloatField(blank=True, help_text='Maximum thermal power size constraint for optimization [ton]', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(100000000.0)]), + ), + migrations.AlterField( + model_name='ashpspaceheaterinputs', + name='om_cost_per_ton', + field=models.FloatField(blank=True, help_text='Thermal power-based cost of ASHP space heater [$/ton] (3.5 ton to 1 kWt)', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(100000000.0)]), + ), + migrations.AlterField( + model_name='ashpspaceheaterinputs', + name='sizing_factor', + field=models.FloatField(blank=True, help_text='Size of system relative to max dispatch output [fraction]', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(100000000.0)]), + ), + migrations.AlterField( + model_name='ashpwaterheaterinputs', + name='back_up_temp_threshold_degF', + field=models.FloatField(blank=True, help_text='Temperature threshold below which resistive back-up heater turns on [Fahrenheit]', null=True, validators=[django.core.validators.MinValueValidator(-275.0), django.core.validators.MaxValueValidator(200.0)]), + ), + migrations.AlterField( + model_name='ashpwaterheaterinputs', + name='force_into_system', + field=models.BooleanField(blank=True, help_text='Boolean indicator if ASHP space heater serves compatible thermal loads exclusively in optimized scenario', null=True), + ), + migrations.AlterField( + model_name='ashpwaterheaterinputs', + name='installed_cost_per_ton', + field=models.FloatField(blank=True, help_text='Thermal power-based cost of ASHP water heater [$/ton] (3.5 ton to 1 kWt)', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(100000000.0)]), + ), + migrations.AlterField( + model_name='ashpwaterheaterinputs', + name='macrs_bonus_fraction', + field=models.FloatField(blank=True, help_text='Percent of upfront project costs to depreciate in year one in addition to scheduled depreciation', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1)]), + ), + migrations.AlterField( + model_name='ashpwaterheaterinputs', + name='macrs_option_years', + field=models.IntegerField(blank=True, choices=[(0, 'Zero'), (5, 'Five'), (7, 'Seven')], help_text='Duration over which accelerated depreciation will occur. Set to zero to disable', null=True), + ), + migrations.AlterField( + model_name='ashpwaterheaterinputs', + name='max_ton', + field=models.FloatField(blank=True, help_text='Maximum thermal power size constraint for optimization [ton]', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(100000000.0)]), + ), + migrations.AlterField( + model_name='ashpwaterheaterinputs', + name='om_cost_per_ton', + field=models.FloatField(blank=True, help_text='Thermal power-based cost of ASHP water heater [$/ton] (3.5 ton to 1 kWt)', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(100000000.0)]), + ), + migrations.AlterField( + model_name='ashpwaterheaterinputs', + name='sizing_factor', + field=models.FloatField(blank=True, help_text='Size of system relative to max dispatch output [fraction]', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(100000000.0)]), + ), + ] diff --git a/reoptjl/migrations/0070_merge_20240925_0600.py b/reoptjl/migrations/0070_merge_20240925_0600.py new file mode 100644 index 000000000..4d42e6343 --- /dev/null +++ b/reoptjl/migrations/0070_merge_20240925_0600.py @@ -0,0 +1,14 @@ +# Generated by Django 4.0.7 on 2024-09-25 06:00 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('reoptjl', '0061_portfoliounlinkedruns_apimeta_portfolio_uuid_and_more'), + ('reoptjl', '0069_financialoutputs_initial_capital_costs_after_incentives_without_macrs_and_more'), + ] + + operations = [ + ] diff --git a/reoptjl/models.py b/reoptjl/models.py index 83f5b87c3..35c3d96d0 100644 --- a/reoptjl/models.py +++ b/reoptjl/models.py @@ -416,6 +416,16 @@ class SiteInputs(BaseModel, models.Model): blank=True, help_text=("If True, then renewable energy exported to the grid is counted in renewable electricity percent calculation.") ) + outdoor_air_temperature_degF = ArrayField( + models.FloatField( + null=True, + blank=True + ), + default=list, + null=True, + blank=True, + help_text=("The outdoor air (dry-bulb) temperature in degrees Fahrenheit as determined by the site's location TMY3 data from the PVWatts call or user input. This is used for GHP COP and ASHP COP and CF values based on the default or custom mapping of those.") + ) class SiteOutputs(BaseModel, models.Model): key = "SiteOutputs" @@ -933,6 +943,10 @@ class FinancialOutputs(BaseModel, models.Model): null=True, blank=True, help_text="Up-front capital costs for all technologies, in present value, excluding replacement costs, including incentives." ) + initial_capital_costs_after_incentives_without_macrs = models.FloatField( + null=True, blank=True, + help_text="Up-front capital costs for all technologies, in present value, excluding replacement costs, including incentives except for MACRS." + ) om_and_replacement_present_cost_after_tax = models.FloatField( null=True, blank=True, help_text="Net O&M and replacement costs in present value, after-tax." @@ -4750,6 +4764,13 @@ class ExistingChillerInputs(BaseModel, models.Model): "This factor limits the max production which could otherwise be exploited with ColdThermalStorage") ) + retire_in_optimal = models.BooleanField( + default=False, + null=True, + blank=True, + help_text="Boolean indicator if the existing chiller is unavailable in the optimal case (still used in BAU)" + ) + def clean(self): pass @@ -5030,6 +5051,7 @@ class ExistingBoilerOutputs(BaseModel, models.Model): primary_key=True ) + size_mmbtu_per_hour = models.FloatField(null=True, blank=True) annual_fuel_consumption_mmbtu = models.FloatField(null=True, blank=True) annual_fuel_consumption_mmbtu_bau = models.FloatField(null=True, blank=True) @@ -5084,54 +5106,15 @@ def clean(self): # perform custom validation here. pass -class REoptjlMessageOutputs(BaseModel, models.Model): - - key = "Messages" - meta = models.OneToOneField( - APIMeta, - on_delete=models.CASCADE, - related_name="REoptjlMessageOutputs", - primary_key=True - ) - - errors = ArrayField( - models.TextField(null=True, blank=True), - default = list, - ) - - warnings = ArrayField( - models.TextField(null=True, blank=True), - default = list, - ) - - has_stacktrace = models.BooleanField( - blank=True, - default=False, - help_text=("REopt.jl can return a handled error with corrective instructions or an unhandled error with a stacktrace of what went wrong for further insepction." - "This field is True if the error message has a stacktrace, otherwise False.") - ) - - def clean(self): - pass - -class BoilerInputs(BaseModel, models.Model): - key = "Boiler" - +class ElectricHeaterInputs(BaseModel, models.Model): + key = "ElectricHeater" meta = models.OneToOneField( - APIMeta, + to=APIMeta, on_delete=models.CASCADE, - related_name="BoilerInputs", + related_name="ElectricHeaterInputs", primary_key=True ) - - FUEL_TYPE_LIST = models.TextChoices('FUEL_TYPE_LIST', ( - "natural_gas", - "landfill_bio_gas", - "propane", - "diesel_oil", - "uranium" - )) - + min_mmbtu_per_hour = models.FloatField( null=True, blank=True, @@ -5154,27 +5137,26 @@ class BoilerInputs(BaseModel, models.Model): help_text="Maximum thermal power size" ) - efficiency = models.FloatField( - validators=[ - MinValueValidator(0.0), - MaxValueValidator(1.0) - ], + installed_cost_per_mmbtu_per_hour = models.FloatField( + default=154902.0, null=True, blank=True, - default=0.8, - help_text="New boiler system efficiency - conversion of fuel to usable heating thermal energy." + validators=[ + MinValueValidator(0), + MaxValueValidator(MAX_BIG_NUMBER) + ], + help_text="Thermal power-based cost" ) - fuel_cost_per_mmbtu = ArrayField( - models.FloatField( - blank=True, - validators=[ - MinValueValidator(0) - ] - ), - default=list, + om_cost_per_mmbtu_per_hour = models.FloatField( + default=0.0, null=True, - help_text="Fuel cost in [$/MMBtu]" + blank=True, + validators=[ + MinValueValidator(0), + MaxValueValidator(MAX_BIG_NUMBER) + ], + help_text="Thermal power-based fixed O&M cost" ) macrs_option_years = models.IntegerField( @@ -5196,45 +5178,16 @@ class BoilerInputs(BaseModel, models.Model): help_text="Percent of upfront project costs to depreciate in year one in addition to scheduled depreciation" ) - installed_cost_per_mmbtu_per_hour = models.FloatField( - default=293000.0, - validators=[ - MinValueValidator(0), - MaxValueValidator(MAX_BIG_NUMBER) - ], - blank=True, - null=True, - help_text="Thermal power-based cost" - ) - - om_cost_per_mmbtu_per_hour = models.FloatField( - default=2930.0, - validators=[ - MinValueValidator(0), - MaxValueValidator(MAX_BIG_NUMBER) - ], - blank=True, - null=True, - help_text="Thermal power-based fixed O&M cost" - ) - - om_cost_per_mmbtu = models.FloatField( - default=0.0, + cop = models.FloatField( validators=[ - MinValueValidator(0), - MaxValueValidator(MAX_BIG_NUMBER) + MinValueValidator(0.01), + MaxValueValidator(20) ], - blank=True, + default=1.0, null=True, - help_text="Thermal energy-based variable O&M cost" - ) - - fuel_type = models.TextField( - default=FUEL_TYPE_LIST.natural_gas, - choices=FUEL_TYPE_LIST.choices, blank=True, - null=True, - help_text="Existing boiler fuel type, one of natural_gas, landfill_bio_gas, propane, diesel_oil, uranium" + help_text=("Electric heater system coefficient of performance (COP) " + "(ratio of usable thermal energy produced per unit electric energy consumed)") ) can_supply_steam_turbine = models.BooleanField( @@ -5248,60 +5201,48 @@ class BoilerInputs(BaseModel, models.Model): default=True, null=True, blank=True, - help_text="Boolean indicator if boiler can serve domestic hot water load" + help_text="Boolean indicator if the electric heater can serve domestic hot water load" ) can_serve_space_heating = models.BooleanField( default=True, null=True, blank=True, - help_text="Boolean indicator if boiler can serve space heating load" + help_text="Boolean indicator if the electric heater can serve space heating load" ) can_serve_process_heat = models.BooleanField( default=True, null=True, blank=True, - help_text="Boolean indicator if boiler can serve process heat load" + help_text="Boolean indicator if the electric heater can serve process heat load" ) - - - # For custom validations within model. - def clean(self): - error_messages = {} - if not self.dict.get("fuel_cost_per_mmbtu"): - error_messages["required inputs"] = "Must provide fuel_cost_per_mmbtu to model {}".format(self.key) - - if error_messages: - raise ValidationError(error_messages) - - self.fuel_cost_per_mmbtu = scalar_or_monthly_to_8760(self.fuel_cost_per_mmbtu) - -class BoilerOutputs(BaseModel, models.Model): - key = "Boiler" +class ElectricHeaterOutputs(BaseModel, models.Model): + key = "ElectricHeaterOutputs" meta = models.OneToOneField( APIMeta, on_delete=models.CASCADE, - related_name="BoilerOutputs", + related_name="ElectricHeaterOutputs", primary_key=True ) - fuel_consumption_series_mmbtu_per_hour = ArrayField( + size_mmbtu_per_hour = models.FloatField(null=True, blank=True) + annual_electric_consumption_kwh = models.FloatField(null=True, blank=True) + + electric_consumption_series_kw = ArrayField( models.FloatField(null=True, blank=True), - default = list, + default=list, ) - thermal_to_load_series_mmbtu_per_hour = ArrayField( + annual_thermal_production_mmbtu = models.FloatField(null=True, blank=True) + + thermal_to_storage_series_mmbtu_per_hour = ArrayField( models.FloatField(null=True, blank=True), default = list, ) - year_one_fuel_cost_before_tax = models.FloatField( - null=True, blank=True - ) - thermal_to_steamturbine_series_mmbtu_per_hour = ArrayField( models.FloatField(null=True, blank=True), default = list, @@ -5312,25 +5253,11 @@ class BoilerOutputs(BaseModel, models.Model): default = list, ) - thermal_to_storage_series_mmbtu_per_hour = ArrayField( + thermal_to_load_series_mmbtu_per_hour = ArrayField( models.FloatField(null=True, blank=True), default = list, ) - size_mmbtu_per_hour = models.FloatField(null=True, blank=True) - - annual_fuel_consumption_mmbtu = models.FloatField(null=True, blank=True) - - lifecycle_per_unit_prod_om_costs = models.FloatField( - null=True, blank=True - ) - - lifecycle_fuel_cost_after_tax = models.FloatField( - null=True, blank=True - ) - - annual_thermal_production_mmbtu = models.FloatField(null=True, blank=True) - thermal_to_dhw_load_series_mmbtu_per_hour = ArrayField( models.FloatField(null=True, blank=True), default = list @@ -5346,85 +5273,894 @@ class BoilerOutputs(BaseModel, models.Model): default = list ) - -class SteamTurbineInputs(BaseModel, models.Model): - - key = "SteamTurbine" - +class ASHPSpaceHeaterInputs(BaseModel, models.Model): + key = "ASHPSpaceHeater" meta = models.OneToOneField( - APIMeta, + to=APIMeta, on_delete=models.CASCADE, - related_name="SteamTurbineInputs", + related_name="ASHPSpaceHeaterInputs", primary_key=True ) - - class SIZE_CLASS_LIST(models.IntegerChoices): - ZERO = 0, - ONE = 1 - TWO = 2 - THREE = 3 - - min_kw = models.FloatField( - null=True, - default=0.0, + + min_ton = models.FloatField( validators=[ MinValueValidator(0), - MaxValueValidator(1.0e9) + MaxValueValidator(MAX_BIG_NUMBER) ], + null=True, blank=True, - help_text="Minimum steam turbine size constraint for optimization" + default = 0.0, + help_text=("Minimum thermal power size constraint for optimization [ton]") ) - max_kw = models.FloatField( - null=True, - default=MAX_BIG_NUMBER, + + max_ton = models.FloatField( validators=[ MinValueValidator(0), - MaxValueValidator(1.0e9) + MaxValueValidator(MAX_BIG_NUMBER) ], + null=True, blank=True, - help_text="Maximum steam turbine size constraint for optimization" + help_text=("Maximum thermal power size constraint for optimization [ton]") ) - ## default values for these fields are returned from REoptInputs() call in http.jl - size_class = models.IntegerField( + min_allowable_ton = models.FloatField( + validators=[ + MinValueValidator(0), + MaxValueValidator(MAX_BIG_NUMBER) + ], null=True, - choices=SIZE_CLASS_LIST.choices, blank=True, - help_text="Steam turbine size class for using appropriate default inputs" + help_text=("Minimum nonzero thermal power size constraint for optimization [ton]") ) - gearbox_generator_efficiency = models.FloatField( - null=True, + min_allowable_peak_capacity_fraction = models.FloatField( validators=[ - MinValueValidator(0.0), - MaxValueValidator(1.0) + MinValueValidator(0), + MaxValueValidator(MAX_BIG_NUMBER) ], + null=True, blank=True, - help_text="Combined gearbox (if applicable) and electric motor/generator efficiency" + help_text=("Minimum nonzero thermal power as a fucniton of coincident peak load - constraint for optimization [ton]") ) - isentropic_efficiency = models.FloatField( - null=True, + sizing_factor = models.FloatField( validators=[ - MinValueValidator(0.0), - MaxValueValidator(1.0) + MinValueValidator(0), + MaxValueValidator(MAX_BIG_NUMBER) ], + null=True, blank=True, - help_text="Combined gearbox (if applicable) and electric motor/generator efficiency" + help_text=("Size of system relative to max dispatch output [fraction]") ) - - inlet_steam_pressure_psig = models.FloatField( - null=True, + + + installed_cost_per_ton = models.FloatField( validators=[ - MinValueValidator(0.0), - MaxValueValidator(5.0e3) + MinValueValidator(0), + MaxValueValidator(MAX_BIG_NUMBER) ], + null=True, blank=True, - help_text="Inlet steam pressure to the steam turbine" + help_text=("Thermal power-based cost of ASHP space heater [$/ton] (3.5 ton to 1 kWt)") ) - inlet_steam_temperature_degF = models.FloatField( - null=True, + om_cost_per_ton = models.FloatField( + validators=[ + MinValueValidator(0), + MaxValueValidator(MAX_BIG_NUMBER) + ], + null=True, + blank=True, + help_text=("Thermal power-based cost of ASHP space heater [$/ton] (3.5 ton to 1 kWt)") + ) + + macrs_option_years = models.IntegerField( + choices=MACRS_YEARS_CHOICES.choices, + blank=True, + null=True, + help_text="Duration over which accelerated depreciation will occur. Set to zero to disable" + ) + + macrs_bonus_fraction = models.FloatField( + validators=[ + MinValueValidator(0), + MaxValueValidator(1) + ], + blank=True, + null=True, + help_text="Percent of upfront project costs to depreciate in year one in addition to scheduled depreciation" + ) + + heating_cop_reference = ArrayField( + models.FloatField( + null=True, blank=True, + validators=[ + MinValueValidator(0), + MaxValueValidator(20.0) + ], + ), + default=list, + blank=True, + help_text=(("Reference points for ASHP space heating system heating coefficient of performance (COP) " + "(ratio of usable heating thermal energy produced per unit electric energy consumed)")) + ) + + heating_cf_reference = ArrayField( + models.FloatField( + null=True, blank=True, + validators=[ + MinValueValidator(0), + MaxValueValidator(20.0) + ], + ), + default=list, + blank=True, + help_text=(("Reference points for ASHP space heating system heating capac)ity factor" + "(ratio of heating thermal power to rated capacity)")) + ) + + heating_reference_temps_degF = ArrayField( + models.FloatField( + null=True, blank=True, + validators=[ + MinValueValidator(-275), + MaxValueValidator(200.0) + ], + ), + default=list, + blank=True, + help_text=(("Reference temperatures for ASHP space heating system's heating COP and CF")) + ) + + cooling_cop_reference = ArrayField( + models.FloatField( + null=True, blank=True, + validators=[ + MinValueValidator(0), + MaxValueValidator(20.0) + ], + ), + default=list, + blank=True, + help_text=(("Reference points for ASHP space heating system cooling coefficient of performance (COP) " + "(ratio of usable heating thermal energy produced per unit electric energy consumed)")) + ) + + cooling_cf_reference = ArrayField( + models.FloatField( + null=True, blank=True, + validators=[ + MinValueValidator(0), + MaxValueValidator(20.0) + ], + ), + default=list, + blank=True, + help_text=(("Reference points for ASHP space heating system cooling capac)ity factor" + "(ratio of heating thermal power to rated capacity)")) + ) + + cooling_reference_temps_degF = ArrayField( + models.FloatField( + null=True, blank=True, + validators=[ + MinValueValidator(-275), + MaxValueValidator(200.0) + ], + ), + default=list, + blank=True, + help_text=(("Reference temperatures for ASHP space heating system's cooling COP and CF")) + ) + + can_serve_cooling = models.BooleanField( + null=True, + blank=True, + help_text="Boolean indicator if ASHP space heater can serve cooling load" + ) + + force_into_system = models.BooleanField( + null=True, + blank=True, + help_text="Boolean indicator if ASHP space heater serves compatible thermal loads exclusively in optimized scenario" + ) + + avoided_capex_by_ashp_present_value = models.FloatField( + validators=[ + MinValueValidator(0), + MaxValueValidator(MAX_BIG_NUMBER) + ], + null=True, + blank=True, + default = 0.0, + help_text=("net present value of avoided capital expenditures due to ASHP system being present [$]") + ) + + back_up_temp_threshold_degF = models.FloatField( + validators=[ + MinValueValidator(-275.0), + MaxValueValidator(200.0) + ], + null=True, + blank=True, + help_text=("Temperature threshold below which resistive back-up heater turns on [Fahrenheit]") + ) + + def clean(self): + error_messages = {} + if self.dict.get("min_allowable_ton") in [None, "", []] and self.dict.get("min_allowable_peak_capacity_fraction") in [None, "", []]: + self.min_allowable_peak_capacity_fraction = 0.5 + + if self.dict.get("min_allowable_ton") not in [None, "", []] and self.dict.get("min_allowable_peak_capacity_fraction") not in [None, "", []]: + error_messages["bad inputs"] = "At most one of min_allowable_ton and min_allowable_peak_capacity_fraction may be input to model {}".format(self.key) + + if len(self.dict.get("heating_cop_reference")) != len(self.dict.get("heating_cf_reference")) or len(self.dict.get("heating_cop_reference")) != len(self.dict.get("heating_reference_temps_degF")): + error_messages["mismatched length"] = "Model {} inputs heating_cop_reference, heating_cf_reference, and heating_reference_temps_degF must all have the same length.".format(self.key) + + if len(self.dict.get("cooling_cop_reference")) != len(self.dict.get("cooling_cf_reference")) or len(self.dict.get("cooling_cop_reference")) != len(self.dict.get("cooling_reference_temps_degF")): + error_messages["mismatched length"] = "Model {} inputs cooling_cop_reference, cooling_cf_reference, and cooling_reference_temps_degF must all have the same length.".format(self.key) + + if error_messages: + raise ValidationError(error_messages) + + + +class ASHPSpaceHeaterOutputs(BaseModel, models.Model): + key = "ASHPSpaceHeaterOutputs" + + meta = models.OneToOneField( + APIMeta, + on_delete=models.CASCADE, + related_name="ASHPSpaceHeaterOutputs", + primary_key=True + ) + + size_ton = models.FloatField(null=True, blank=True) + annual_electric_consumption_kwh = models.FloatField(null=True, blank=True) + + electric_consumption_series_kw = ArrayField( + models.FloatField(null=True, blank=True), + default=list + ) + + annual_thermal_production_mmbtu = models.FloatField(null=True, blank=True) + annual_thermal_production_tonhour = models.FloatField(null=True, blank=True) + + thermal_to_storage_series_mmbtu_per_hour = ArrayField( + models.FloatField(null=True, blank=True), + default = list + ) + + thermal_production_series_mmbtu_per_hour = ArrayField( + models.FloatField(null=True, blank=True), + default = list + ) + + thermal_to_load_series_mmbtu_per_hour = ArrayField( + models.FloatField(null=True, blank=True), + default = list + ) + + thermal_to_space_heating_load_series_mmbtu_per_hour = ArrayField( + models.FloatField(null=True, blank=True), + default = list + ) + + thermal_to_storage_series_ton = ArrayField( + models.FloatField(null=True, blank=True), + default = list + ) + + thermal_to_load_series_ton = ArrayField( + models.FloatField(null=True, blank=True), + default = list + ) + + heating_cop = ArrayField( + models.FloatField(null=True, blank=True), + default = list + ) + + heating_cf = ArrayField( + models.FloatField(null=True, blank=True), + default = list + ) + + cooling_cop = ArrayField( + models.FloatField(null=True, blank=True), + default = list + ) + + cooling_cf = ArrayField( + models.FloatField(null=True, blank=True), + default = list + ) + + electric_consumption_for_cooling_series_kw = ArrayField( + models.FloatField(null=True, blank=True), + default = list + ) + + electric_consumption_for_heating_series_kw = ArrayField( + models.FloatField(null=True, blank=True), + default = list + ) + + annual_electric_consumption_for_cooling_kwh = models.FloatField(null=True, blank=True) + annual_electric_consumption_for_heating_kwh = models.FloatField(null=True, blank=True) + +class ASHPWaterHeaterInputs(BaseModel, models.Model): + key = "ASHPWaterHeater" + meta = models.OneToOneField( + to=APIMeta, + on_delete=models.CASCADE, + related_name="ASHPWaterHeaterInputs", + primary_key=True + ) + + min_ton = models.FloatField( + validators=[ + MinValueValidator(0), + MaxValueValidator(MAX_BIG_NUMBER) + ], + null=True, + blank=True, + default = 0.0, + help_text=("Minimum thermal power size constraint for optimization [ton]") + ) + + max_ton = models.FloatField( + validators=[ + MinValueValidator(0), + MaxValueValidator(MAX_BIG_NUMBER) + ], + null=True, + blank=True, + help_text=("Maximum thermal power size constraint for optimization [ton]") + ) + + min_allowable_ton = models.FloatField( + validators=[ + MinValueValidator(0), + MaxValueValidator(MAX_BIG_NUMBER) + ], + null=True, + blank=True, + help_text=("Minimum nonzero thermal power size constraint for optimization [ton]") + ) + + min_allowable_peak_capacity_fraction = models.FloatField( + validators=[ + MinValueValidator(0), + MaxValueValidator(MAX_BIG_NUMBER) + ], + null=True, + blank=True, + help_text=("Minimum nonzero thermal power as a function of coincident peak load / CF - constraint for optimization [ton]") + ) + + sizing_factor = models.FloatField( + validators=[ + MinValueValidator(0), + MaxValueValidator(MAX_BIG_NUMBER) + ], + null=True, + blank=True, + help_text=("Size of system relative to max dispatch output [fraction]") + ) + + installed_cost_per_ton = models.FloatField( + validators=[ + MinValueValidator(0), + MaxValueValidator(MAX_BIG_NUMBER) + ], + null=True, + blank=True, + help_text=("Thermal power-based cost of ASHP water heater [$/ton] (3.5 ton to 1 kWt)") + ) + + om_cost_per_ton = models.FloatField( + validators=[ + MinValueValidator(0), + MaxValueValidator(MAX_BIG_NUMBER) + ], + null=True, + blank=True, + help_text=("Thermal power-based cost of ASHP water heater [$/ton] (3.5 ton to 1 kWt)") + ) + + macrs_option_years = models.IntegerField( + choices=MACRS_YEARS_CHOICES.choices, + blank=True, + null=True, + help_text="Duration over which accelerated depreciation will occur. Set to zero to disable" + ) + + macrs_bonus_fraction = models.FloatField( + validators=[ + MinValueValidator(0), + MaxValueValidator(1) + ], + blank=True, + null=True, + help_text="Percent of upfront project costs to depreciate in year one in addition to scheduled depreciation" + ) + + heating_cop_reference = ArrayField( + models.FloatField( + null=True, blank=True, + validators=[ + MinValueValidator(0), + MaxValueValidator(20.0) + ], + ), + default=list, + blank=True, + help_text=(("Reference points for ASHP space heating system heating coefficient of performance (COP) " + "(ratio of usable heating thermal energy produced per unit electric energy consumed)")) + ) + + heating_cf_reference = ArrayField( + models.FloatField( + null=True, blank=True, + validators=[ + MinValueValidator(0), + MaxValueValidator(20.0) + ], + ), + default=list, + blank=True, + help_text=(("Reference points for ASHP space heating system heating capac)ity factor" + "(ratio of heating thermal power to rated capacity)")) + ) + + heating_reference_temps_degF = ArrayField( + models.FloatField( + null=True, blank=True, + validators=[ + MinValueValidator(-275.0), + MaxValueValidator(200.0) + ], + ), + default=list, + blank=True, + help_text=(("Reference temperatures for ASHP space heating system's heating COP and CF [Fahrenheit]")) + ) + + avoided_capex_by_ashp_present_value = models.FloatField( + validators=[ + MinValueValidator(0), + MaxValueValidator(MAX_BIG_NUMBER) + ], + null=True, + blank=True, + default = 0.0, + help_text=("Net present value of avoided capital expenditures due to ASHP system being present [$]") + ) + + back_up_temp_threshold_degF = models.FloatField( + validators=[ + MinValueValidator(-275.0), + MaxValueValidator(200.0) + ], + null=True, + blank=True, + help_text=("Temperature threshold below which resistive back-up heater turns on [Fahrenheit]") + ) + + force_into_system = models.BooleanField( + null=True, + blank=True, + help_text="Boolean indicator if ASHP space heater serves compatible thermal loads exclusively in optimized scenario" + ) + + def clean(self): + error_messages = {} + if self.dict.get("min_allowable_ton") in [None, "", []] and self.dict.get("min_allowable_peak_capacity_fraction") in [None, "", []]: + self.min_allowable_peak_capacity_fraction = 0.5 + + if self.dict.get("min_allowable_ton") not in [None, "", []] and self.dict.get("min_allowable_peak_capacity_fraction") not in [None, "", []]: + error_messages["bad inputs"] = "At most one of min_allowable_ton and min_allowable_peak_capacity_fraction may be input to model {}".format(self.key) + + if len(self.dict.get("heating_cop_reference")) != len(self.dict.get("heating_cf_reference")) or len(self.dict.get("heating_cop_reference")) != len(self.dict.get("heating_reference_temps_degF")): + error_messages["mismatched length"] = "Model {} inputs heating_cop_reference, heating_cf_reference, and heating_reference_temps_degF must all have the same length.".format(self.key) + + if error_messages: + raise ValidationError(error_messages) + + +class ASHPWaterHeaterOutputs(BaseModel, models.Model): + key = "ASHPWaterHeaterOutputs" + + meta = models.OneToOneField( + APIMeta, + on_delete=models.CASCADE, + related_name="ASHPWaterHeaterOutputs", + primary_key=True + ) + + size_ton = models.FloatField(null=True, blank=True) + annual_electric_consumption_kwh = models.FloatField(null=True, blank=True) + + electric_consumption_series_kw = ArrayField( + models.FloatField(null=True, blank=True), + default=list + ) + + annual_thermal_production_mmbtu = models.FloatField(null=True, blank=True) + + thermal_to_storage_series_mmbtu_per_hour = ArrayField( + models.FloatField(null=True, blank=True), + default = list + ) + + thermal_production_series_mmbtu_per_hour = ArrayField( + models.FloatField(null=True, blank=True), + default = list + ) + + thermal_to_load_series_mmbtu_per_hour = ArrayField( + models.FloatField(null=True, blank=True), + default = list + ) + + thermal_to_dhw_load_series_mmbtu_per_hour = ArrayField( + models.FloatField(null=True, blank=True), + default = list + ) + + heating_cop = ArrayField( + models.FloatField(null=True, blank=True), + default = list + ) + + heating_cf = ArrayField( + models.FloatField(null=True, blank=True), + default = list + ) + + +class REoptjlMessageOutputs(BaseModel, models.Model): + + key = "Messages" + meta = models.OneToOneField( + APIMeta, + on_delete=models.CASCADE, + related_name="REoptjlMessageOutputs", + primary_key=True + ) + + errors = ArrayField( + models.TextField(null=True, blank=True), + default = list, + ) + + warnings = ArrayField( + models.TextField(null=True, blank=True), + default = list, + ) + + has_stacktrace = models.BooleanField( + blank=True, + default=False, + help_text=("REopt.jl can return a handled error with corrective instructions or an unhandled error with a stacktrace of what went wrong for further insepction." + "This field is True if the error message has a stacktrace, otherwise False.") + ) + + def clean(self): + pass + +class BoilerInputs(BaseModel, models.Model): + key = "Boiler" + + meta = models.OneToOneField( + APIMeta, + on_delete=models.CASCADE, + related_name="BoilerInputs", + primary_key=True + ) + + FUEL_TYPE_LIST = models.TextChoices('FUEL_TYPE_LIST', ( + "natural_gas", + "landfill_bio_gas", + "propane", + "diesel_oil", + "uranium" + )) + + min_mmbtu_per_hour = models.FloatField( + null=True, + blank=True, + validators=[ + MinValueValidator(0.0), + MaxValueValidator(MAX_BIG_NUMBER) + ], + default=0.0, + help_text="Minimum thermal power size" + ) + + max_mmbtu_per_hour = models.FloatField( + null=True, + blank=True, + validators=[ + MinValueValidator(0.0), + MaxValueValidator(MAX_BIG_NUMBER) + ], + default=1.0E7, + help_text="Maximum thermal power size" + ) + + efficiency = models.FloatField( + validators=[ + MinValueValidator(0.0), + MaxValueValidator(1.0) + ], + null=True, + blank=True, + default=0.8, + help_text="New boiler system efficiency - conversion of fuel to usable heating thermal energy." + ) + + fuel_cost_per_mmbtu = ArrayField( + models.FloatField( + blank=True, + validators=[ + MinValueValidator(0) + ] + ), + default=list, + null=True, + help_text="Fuel cost in [$/MMBtu]" + ) + + macrs_option_years = models.IntegerField( + default=MACRS_YEARS_CHOICES.ZERO, + choices=MACRS_YEARS_CHOICES.choices, + blank=True, + null=True, + help_text="Duration over which accelerated depreciation will occur. Set to zero to disable" + ) + + macrs_bonus_fraction = models.FloatField( + default=0.0, + validators=[ + MinValueValidator(0), + MaxValueValidator(1) + ], + blank=True, + null=True, + help_text="Percent of upfront project costs to depreciate in year one in addition to scheduled depreciation" + ) + + installed_cost_per_mmbtu_per_hour = models.FloatField( + default=293000.0, + validators=[ + MinValueValidator(0), + MaxValueValidator(MAX_BIG_NUMBER) + ], + blank=True, + null=True, + help_text="Thermal power-based cost" + ) + + om_cost_per_mmbtu_per_hour = models.FloatField( + default=2930.0, + validators=[ + MinValueValidator(0), + MaxValueValidator(MAX_BIG_NUMBER) + ], + blank=True, + null=True, + help_text="Thermal power-based fixed O&M cost" + ) + + om_cost_per_mmbtu = models.FloatField( + default=0.0, + validators=[ + MinValueValidator(0), + MaxValueValidator(MAX_BIG_NUMBER) + ], + blank=True, + null=True, + help_text="Thermal energy-based variable O&M cost" + ) + + fuel_type = models.TextField( + default=FUEL_TYPE_LIST.natural_gas, + choices=FUEL_TYPE_LIST.choices, + blank=True, + null=True, + help_text="Existing boiler fuel type, one of natural_gas, landfill_bio_gas, propane, diesel_oil, uranium" + ) + + can_supply_steam_turbine = models.BooleanField( + default=True, + blank=True, + null=True, + help_text="If the boiler can supply steam to the steam turbine for electric production" + ) + + can_serve_dhw = models.BooleanField( + default=True, + null=True, + blank=True, + help_text="Boolean indicator if boiler can serve domestic hot water load" + ) + + can_serve_space_heating = models.BooleanField( + default=True, + null=True, + blank=True, + help_text="Boolean indicator if boiler can serve space heating load" + ) + + can_serve_process_heat = models.BooleanField( + default=True, + null=True, + blank=True, + help_text="Boolean indicator if boiler can serve process heat load" + ) + + + # For custom validations within model. + def clean(self): + error_messages = {} + if not self.dict.get("fuel_cost_per_mmbtu"): + error_messages["required inputs"] = "Must provide fuel_cost_per_mmbtu to model {}".format(self.key) + + if error_messages: + raise ValidationError(error_messages) + + self.fuel_cost_per_mmbtu = scalar_or_monthly_to_8760(self.fuel_cost_per_mmbtu) + +class BoilerOutputs(BaseModel, models.Model): + + key = "Boiler" + + meta = models.OneToOneField( + APIMeta, + on_delete=models.CASCADE, + related_name="BoilerOutputs", + primary_key=True + ) + + fuel_consumption_series_mmbtu_per_hour = ArrayField( + models.FloatField(null=True, blank=True), + default = list, + ) + + thermal_to_load_series_mmbtu_per_hour = ArrayField( + models.FloatField(null=True, blank=True), + default = list, + ) + + year_one_fuel_cost_before_tax = models.FloatField( + null=True, blank=True + ) + + thermal_to_steamturbine_series_mmbtu_per_hour = ArrayField( + models.FloatField(null=True, blank=True), + default = list, + ) + + thermal_production_series_mmbtu_per_hour = ArrayField( + models.FloatField(null=True, blank=True), + default = list, + ) + + thermal_to_storage_series_mmbtu_per_hour = ArrayField( + models.FloatField(null=True, blank=True), + default = list, + ) + + size_mmbtu_per_hour = models.FloatField(null=True, blank=True) + + annual_fuel_consumption_mmbtu = models.FloatField(null=True, blank=True) + + lifecycle_per_unit_prod_om_costs = models.FloatField( + null=True, blank=True + ) + + lifecycle_fuel_cost_after_tax = models.FloatField( + null=True, blank=True + ) + + annual_thermal_production_mmbtu = models.FloatField(null=True, blank=True) + + thermal_to_dhw_load_series_mmbtu_per_hour = ArrayField( + models.FloatField(null=True, blank=True), + default = list + ) + + thermal_to_space_heating_load_series_mmbtu_per_hour = ArrayField( + models.FloatField(null=True, blank=True), + default = list + ) + + thermal_to_process_heat_load_series_mmbtu_per_hour = ArrayField( + models.FloatField(null=True, blank=True), + default = list + ) + + +class SteamTurbineInputs(BaseModel, models.Model): + + key = "SteamTurbine" + + meta = models.OneToOneField( + APIMeta, + on_delete=models.CASCADE, + related_name="SteamTurbineInputs", + primary_key=True + ) + + class SIZE_CLASS_LIST(models.IntegerChoices): + ZERO = 0, + ONE = 1 + TWO = 2 + THREE = 3 + + min_kw = models.FloatField( + null=True, + default=0.0, + validators=[ + MinValueValidator(0), + MaxValueValidator(1.0e9) + ], + blank=True, + help_text="Minimum steam turbine size constraint for optimization" + ) + max_kw = models.FloatField( + null=True, + default=MAX_BIG_NUMBER, + validators=[ + MinValueValidator(0), + MaxValueValidator(1.0e9) + ], + blank=True, + help_text="Maximum steam turbine size constraint for optimization" + ) + + ## default values for these fields are returned from REoptInputs() call in http.jl + size_class = models.IntegerField( + null=True, + choices=SIZE_CLASS_LIST.choices, + blank=True, + help_text="Steam turbine size class for using appropriate default inputs" + ) + + gearbox_generator_efficiency = models.FloatField( + null=True, + validators=[ + MinValueValidator(0.0), + MaxValueValidator(1.0) + ], + blank=True, + help_text="Combined gearbox (if applicable) and electric motor/generator efficiency" + ) + + isentropic_efficiency = models.FloatField( + null=True, + validators=[ + MinValueValidator(0.0), + MaxValueValidator(1.0) + ], + blank=True, + help_text="Combined gearbox (if applicable) and electric motor/generator efficiency" + ) + + inlet_steam_pressure_psig = models.FloatField( + null=True, + validators=[ + MinValueValidator(0.0), + MaxValueValidator(5.0e3) + ], + blank=True, + help_text="Inlet steam pressure to the steam turbine" + ) + + inlet_steam_temperature_degF = models.FloatField( + null=True, validators=[ MinValueValidator(0.0), MaxValueValidator(1300.0) @@ -6725,6 +7461,28 @@ class HeatingLoadOutputs(BaseModel, models.Model): help_text=("Annual site total heating boiler fuel load [MMBTU]") ) + annual_total_unaddressable_heating_load_mmbtu = models.FloatField( + validators=[ + MinValueValidator(0), + MaxValueValidator(MAX_BIG_NUMBER) + ], + null=True, + blank=True, + default=0, + help_text=("Annual site total unaddressable heating fuel [MMBTU]") + ) + + annual_emissions_from_unaddressable_heating_load_tonnes_CO2 = models.FloatField( + validators=[ + MinValueValidator(0), + MaxValueValidator(MAX_BIG_NUMBER) + ], + null=True, + blank=True, + default=0, + help_text=("Annual site total unaddressable heating fuel climate CO2 emissions [tonnes]") + ) + def clean(self): pass @@ -7392,6 +8150,9 @@ class GHPOutputs(BaseModel, models.Model): cooling_thermal_load_reduction_with_ghp_ton = ArrayField( models.FloatField(null=True, blank=True), default=list, null=True, blank=True) ghx_residual_value_present_value = models.FloatField(null=True, blank=True) + thermal_to_space_heating_load_series_mmbtu_per_hour = ArrayField(models.FloatField(null=True, blank=True), default=list, null=True, blank=True) + thermal_to_dhw_load_series_mmbtu_per_hour = ArrayField(models.FloatField(null=True, blank=True), default=list, null=True, blank=True) + thermal_to_load_series_ton = ArrayField(models.FloatField(null=True, blank=True), default=list, null=True, blank=True) def get_input_dict_from_run_uuid(run_uuid:str): @@ -7483,6 +8244,15 @@ def filter_none_and_empty_array(d:dict): try: d["GHP"] = filter_none_and_empty_array(meta.GHPInputs.dict) except: pass + try: d["ElectricHeater"] = filter_none_and_empty_array(meta.ElectricHeaterInputs.dict) + except: pass + + try: d["ASHPSpaceHeater"] = filter_none_and_empty_array(meta.ASHPSpaceHeaterInputs.dict) + except: pass + + try: d["ASHPWaterHeater"] = filter_none_and_empty_array(meta.ASHPWaterHeaterInputs.dict) + except: pass + return d ''' diff --git a/reoptjl/src/process_results.py b/reoptjl/src/process_results.py index b29008b05..87023850b 100644 --- a/reoptjl/src/process_results.py +++ b/reoptjl/src/process_results.py @@ -6,7 +6,9 @@ ExistingChillerOutputs, CoolingLoadOutputs, HeatingLoadOutputs,\ HotThermalStorageOutputs, ColdThermalStorageOutputs, OutageOutputs,\ REoptjlMessageOutputs, AbsorptionChillerOutputs, BoilerOutputs, SteamTurbineInputs, \ - SteamTurbineOutputs, GHPInputs, GHPOutputs, ExistingChillerInputs + SteamTurbineOutputs, GHPInputs, GHPOutputs, ExistingChillerInputs, \ + ElectricHeaterOutputs, ASHPSpaceHeaterOutputs, ASHPWaterHeaterOutputs, \ + SiteInputs, ASHPSpaceHeaterInputs, ASHPWaterHeaterInputs import numpy as np import sys import traceback as tb @@ -82,6 +84,12 @@ def process_results(results: dict, run_uuid: str) -> None: SteamTurbineOutputs.create(meta=meta, **results["SteamTurbine"]).save() if "GHP" in results.keys(): GHPOutputs.create(meta=meta, **results["GHP"]).save() + if "ElectricHeater" in results.keys(): + ElectricHeaterOutputs.create(meta=meta, **results["ElectricHeater"]).save() + if "ASHPSpaceHeater" in results.keys(): + ASHPSpaceHeaterOutputs.create(meta=meta, **results["ASHPSpaceHeater"]).save() + if "ASHPWaterHeater" in results.keys(): + ASHPWaterHeaterOutputs.create(meta=meta, **results["ASHPWaterHeater"]).save() # TODO process rest of results except Exception as e: exc_type, exc_value, exc_traceback = sys.exc_info() @@ -113,6 +121,9 @@ def update_inputs_in_database(inputs_to_update: dict, run_uuid: str) -> None: # get input models that need updating FinancialInputs.objects.filter(meta__run_uuid=run_uuid).update(**inputs_to_update["Financial"]) ElectricUtilityInputs.objects.filter(meta__run_uuid=run_uuid).update(**inputs_to_update["ElectricUtility"]) + + if inputs_to_update["Site"]: + SiteInputs.objects.filter(meta__run_uuid=run_uuid).update(**inputs_to_update["Site"]) if inputs_to_update["CHP"]: # Will be an empty dictionary if CHP is not considered CHPInputs.objects.filter(meta__run_uuid=run_uuid).update(**inputs_to_update["CHP"]) @@ -127,6 +138,10 @@ def update_inputs_in_database(inputs_to_update: dict, run_uuid: str) -> None: ExistingChillerInputs.create(meta=meta, **inputs_to_update["ExistingChiller"]).save() else: ExistingChillerInputs.objects.filter(meta__run_uuid=run_uuid).update(**inputs_to_update["ExistingChiller"]) + if inputs_to_update["ASHPSpaceHeater"]: + ASHPSpaceHeaterInputs.objects.filter(meta__run_uuid=run_uuid).update(**inputs_to_update["ASHPSpaceHeater"]) + if inputs_to_update["ASHPWaterHeater"]: + ASHPWaterHeaterInputs.objects.filter(meta__run_uuid=run_uuid).update(**inputs_to_update["ASHPWaterHeater"]) 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/all_inputs_test.json b/reoptjl/test/posts/all_inputs_test.json index 0a7fd1f53..5611d439a 100644 --- a/reoptjl/test/posts/all_inputs_test.json +++ b/reoptjl/test/posts/all_inputs_test.json @@ -65,7 +65,7 @@ "include_climate_in_objective": false, "include_health_in_objective": false, "off_grid_flag": false, - "solver_name": "SCIP" + "solver_name": "HiGHS" }, "PV": { "name": "PV", @@ -258,7 +258,10 @@ "emissions_factor_lb_SO2_per_mmbtu": 0.000578592, "emissions_factor_lb_PM25_per_mmbtu": 0.007328833, "fuel_cost_per_mmbtu": [0.0], - "fuel_type": "natural_gas" + "fuel_type": "natural_gas", + "can_serve_dhw": true, + "can_serve_space_heating": true, + "can_serve_process_heat": true }, "SpaceHeatingLoad": { "annual_mmbtu": null, @@ -277,6 +280,10 @@ "blended_doe_reference_names": [], "blended_doe_reference_percents": [] }, + "ProcessHeatLoad": { + "annual_mmbtu": 8760.0, + "industry_reference_name": "Warehouse" + }, "Boiler": { "min_mmbtu_per_hour": 5.0, "max_mmbtu_per_hour": 5.0, @@ -288,7 +295,10 @@ "om_cost_per_mmbtu_per_hour": 0.0, "om_cost_per_mmbtu": 0.0, "fuel_type": "natural_gas", - "can_supply_steam_turbine": true + "can_supply_steam_turbine": true, + "can_serve_dhw": true, + "can_serve_space_heating": true, + "can_serve_process_heat": true }, "SteamTurbine": { "size_class": null, @@ -311,6 +321,60 @@ "can_export_beyond_nem_limit": false, "can_curtail": false, "macrs_option_years": 0, - "macrs_bonus_fraction": 1.0 + "macrs_bonus_fraction": 1.0, + "can_serve_dhw": true, + "can_serve_space_heating": true, + "can_serve_process_heat": true + }, + "ElectricHeater": { + "min_mmbtu_per_hour": 10.0, + "max_mmbtu_per_hour": 10.0, + "installed_cost_per_mmbtu_per_hour": 2250.0, + "om_cost_per_mmbtu_per_hour": 0.0, + "macrs_option_years": 0, + "macrs_bonus_fraction": 1.0, + "cop": 1.0, + "can_supply_steam_turbine": true, + "can_serve_dhw": true, + "can_serve_space_heating": true, + "can_serve_process_heat": true + }, + "ASHPSpaceHeater": { + "min_ton": 10.0, + "max_ton": 10.0, + "min_allowable_ton": 0.0, + "min_allowable_peak_capacity_fraction": null, + "sizing_factor": 1.0, + "installed_cost_per_ton": 2250.0, + "om_cost_per_ton": 0.0, + "macrs_option_years": 0, + "macrs_bonus_fraction": 1.0, + "heating_cop_reference": [1.1,2.1,3.5], + "heating_cf_reference": [0.4, 0.65, 1.0], + "heating_reference_temps_degF": [-5, 17, 47], + "cooling_cop_reference": [3.2, 2.6], + "cooling_cf_reference": [1.01, 0.94], + "cooling_reference_temps_degF": [82, 95], + "can_serve_cooling": true, + "force_into_system": false, + "avoided_capex_by_ashp_present_value": 0, + "back_up_temp_threshold_degF": -10.0 + }, + "ASHPWaterHeater": { + "min_ton": 10.0, + "max_ton": 10.0, + "min_allowable_ton": null, + "min_allowable_peak_capacity_fraction": 0.0, + "sizing_factor": 1.0, + "installed_cost_per_ton": 2250.0, + "om_cost_per_ton": 0.0, + "macrs_option_years": 0, + "macrs_bonus_fraction": 1.0, + "heating_cop_reference": [1.1,2.1,3.5], + "heating_cf_reference": [0.4, 0.65, 1.0], + "heating_reference_temps_degF": [-5, 17, 47], + "force_into_system": false, + "avoided_capex_by_ashp_present_value": 0, + "back_up_temp_threshold_degF": -10.0 } } \ No newline at end of file diff --git a/reoptjl/test/test_http_endpoints.py b/reoptjl/test/test_http_endpoints.py index 760c10775..d844fd513 100644 --- a/reoptjl/test/test_http_endpoints.py +++ b/reoptjl/test/test_http_endpoints.py @@ -378,4 +378,30 @@ def test_default_existing_chiller_cop(self): self.assertEqual(view_response["existing_chiller_cop"], 4.4) + def test_get_ashp_defaults(self): + inputs_dict = { + "load_served": "SpaceHeating", + "force_into_system": "true" + } + + # Call to the django view endpoint /get_existing_chiller_default_cop which calls the http.jl endpoint + resp = self.api_client.get(f'/v3/get_ashp_defaults', data=inputs_dict) + view_response = json.loads(resp.content) + + self.assertEqual(view_response["installed_cost_per_ton"], 2250) + self.assertEqual(view_response["om_cost_per_ton"], 0.0) + self.assertEqual(view_response["sizing_factor"], 1.1) + + inputs_dict = { + "load_served": "DomesticHotWater", + "force_into_system": "false" + } + + # Call to the django view endpoint /get_existing_chiller_default_cop which calls the http.jl endpoint + resp = self.api_client.get(f'/v3/get_ashp_defaults', data=inputs_dict) + view_response = json.loads(resp.content) + + self.assertNotIn("cooling_cf_reference", view_response.keys()) + self.assertEqual(view_response["sizing_factor"], 1.0) + diff --git a/reoptjl/test/test_job_endpoint.py b/reoptjl/test/test_job_endpoint.py index 8a41b24ce..aebd2bf7a 100644 --- a/reoptjl/test/test_job_endpoint.py +++ b/reoptjl/test/test_job_endpoint.py @@ -235,8 +235,12 @@ def test_superset_input_fields(self): resp = self.api_client.get(f'/v3/job/{run_uuid}/results') r = json.loads(resp.content) results = r["outputs"] - self.assertAlmostEqual(results["Financial"]["npv"], -258533.19, delta=0.01*results["Financial"]["lcc"]) + self.assertAlmostEqual(results["Financial"]["npv"], -326156.69, delta=0.01*results["Financial"]["lcc"]) assert(resp.status_code==200) + self.assertIn("ElectricHeater", list(results.keys())) + self.assertIn("ASHPSpaceHeater", list(results.keys())) + self.assertIn("ASHPWaterHeater", list(results.keys())) + def test_steamturbine_defaults_from_julia(self): # Test that the inputs_with_defaults_set_in_julia feature worked for SteamTurbine, consistent with /chp_defaults diff --git a/reoptjl/test/test_validator.py b/reoptjl/test/test_validator.py index 7f73fdc06..8e0c1c6d3 100644 --- a/reoptjl/test/test_validator.py +++ b/reoptjl/test/test_validator.py @@ -344,3 +344,30 @@ def boiler_validation(self): # self.assertAlmostEqual(len(validator.models["ExistingBoiler"].fuel_cost_per_mmbtu), 8760) # self.assertAlmostEqual(sum(validator.models["ExistingBoiler"].fuel_cost_per_mmbtu), 8760*0.5) + def ashp_validation(self): + """ + Ensure that bad inputs are caught by clean() for ASHP systems. + """ + post_file = os.path.join('job', 'test', 'posts', 'all_inputs_test.json') + post = json.load(open(post_file, 'r')) + + post["APIMeta"]["run_uuid"] = uuid.uuid4() + post["ASHPSpaceHeater"]["cooling_cf_reference"] = [] + + validator = InputValidator(post) + validator.clean_fields() + validator.clean() + validator.cross_clean() + assert("mismatched length" in validator.validation_errors["ASHPSpaceHeater"].keys()) + + post = json.load(open(post_file, 'r')) + post["APIMeta"]["run_uuid"] = uuid.uuid4() + post["ASHPWaterHeater"]["min_allowable_ton"] = 1000 + post["ASHPWaterHeater"]["min_allowable_peak_capacity_fraction"] = 0.9 + + validator = InputValidator(post) + validator.clean_fields() + validator.clean() + validator.cross_clean() + assert("bad inputs" in validator.validation_errors["ASHPWaterHeater"].keys()) + \ No newline at end of file diff --git a/reoptjl/urls.py b/reoptjl/urls.py index f41bbb5b8..59d6d9ad2 100644 --- a/reoptjl/urls.py +++ b/reoptjl/urls.py @@ -23,6 +23,7 @@ re_path(r'^invalid_urdb/?$', reoviews.invalid_urdb), re_path(r'^schedule_stats/?$', reoviews.schedule_stats), re_path(r'^get_existing_chiller_default_cop/?$', views.get_existing_chiller_default_cop), + re_path(r'^get_ashp_defaults/?$', views.get_ashp_defaults), re_path(r'^summary_by_runuuids/?$', views.summary_by_runuuids), re_path(r'^link_run_to_portfolios/?$', views.link_run_uuids_to_portfolio_uuid) ] diff --git a/reoptjl/validators.py b/reoptjl/validators.py index 4e7238ea4..a65a60b02 100644 --- a/reoptjl/validators.py +++ b/reoptjl/validators.py @@ -4,7 +4,8 @@ from reoptjl.models import MAX_BIG_NUMBER, APIMeta, ExistingBoilerInputs, UserProvidedMeta, SiteInputs, Settings, ElectricLoadInputs, ElectricTariffInputs, \ FinancialInputs, BaseModel, Message, ElectricUtilityInputs, PVInputs, ElectricStorageInputs, GeneratorInputs, WindInputs, SpaceHeatingLoadInputs, \ DomesticHotWaterLoadInputs, CHPInputs, CoolingLoadInputs, ExistingChillerInputs, HotThermalStorageInputs, ColdThermalStorageInputs, \ - AbsorptionChillerInputs, BoilerInputs, SteamTurbineInputs, GHPInputs, ProcessHeatLoadInputs + AbsorptionChillerInputs, BoilerInputs, SteamTurbineInputs, GHPInputs, ProcessHeatLoadInputs, ElectricHeaterInputs, ASHPSpaceHeaterInputs, \ + ASHPWaterHeaterInputs from django.core.exceptions import ValidationError from pyproj import Proj from typing import Tuple @@ -80,7 +81,10 @@ def __init__(self, raw_inputs: dict, ghpghx_inputs_validation_errors=None): AbsorptionChillerInputs, SteamTurbineInputs, GHPInputs, - ProcessHeatLoadInputs + ProcessHeatLoadInputs, + ElectricHeaterInputs, + ASHPSpaceHeaterInputs, + ASHPWaterHeaterInputs ) self.pvnames = [] on_grid_required_object_names = [ diff --git a/reoptjl/views.py b/reoptjl/views.py index 5587b7658..fe9300fd2 100644 --- a/reoptjl/views.py +++ b/reoptjl/views.py @@ -15,7 +15,9 @@ CoolingLoadOutputs, HeatingLoadOutputs, REoptjlMessageOutputs, HotThermalStorageInputs, HotThermalStorageOutputs,\ ColdThermalStorageInputs, ColdThermalStorageOutputs, AbsorptionChillerInputs, AbsorptionChillerOutputs,\ FinancialInputs, FinancialOutputs, UserUnlinkedRuns, BoilerInputs, BoilerOutputs, SteamTurbineInputs, \ - SteamTurbineOutputs, GHPInputs, GHPOutputs, PortfolioUnlinkedRuns, ProcessHeatLoadInputs + SteamTurbineOutputs, GHPInputs, GHPOutputs, ProcessHeatLoadInputs, ElectricHeaterInputs, ElectricHeaterOutputs, \ + ASHPSpaceHeaterInputs, ASHPSpaceHeaterOutputs, ASHPWaterHeaterInputs, ASHPWaterHeaterOutputs, PortfolioUnlinkedRuns + import os import requests import numpy as np @@ -60,6 +62,10 @@ def help(request): d["AbsorptionChiller"] = AbsorptionChillerInputs.info_dict(AbsorptionChillerInputs) d["SteamTurbine"] = SteamTurbineInputs.info_dict(SteamTurbineInputs) d["GHP"] = GHPInputs.info_dict(GHPInputs) + d["ElectricHeater"] = ElectricHeaterInputs.info_dict(ElectricHeaterInputs) + d["ASHPSpaceHeater"] = ASHPSpaceHeaterInputs.info_dict(ASHPSpaceHeaterInputs) + d["ASHPWaterHeater"] = ASHPWaterHeaterInputs.info_dict(ASHPWaterHeaterInputs) + return JsonResponse(d) except Exception as e: @@ -105,6 +111,9 @@ def outputs(request): d["CHP"] = CHPOutputs.info_dict(CHPOutputs) d["AbsorptionChiller"] = AbsorptionChillerOutputs.info_dict(AbsorptionChillerOutputs) d["GHP"] = GHPOutputs.info_dict(GHPOutputs) + d["ElectricHeater"] = ElectricHeaterOutputs.info_dict(ElectricHeaterOutputs) + d["ASHPSpaceHeater"] = ASHPSpaceHeaterOutputs.info_dict(ASHPSpaceHeaterOutputs) + d["ASHPWaterHeater"] = ASHPWaterHeaterOutputs.info_dict(ASHPWaterHeaterOutputs) d["Messages"] = REoptjlMessageOutputs.info_dict(REoptjlMessageOutputs) d["SteamTurbine"] = SteamTurbineOutputs.info_dict(SteamTurbineOutputs) return JsonResponse(d) @@ -227,9 +236,19 @@ def results(request, run_uuid): try: r["inputs"]["SteamTurbine"] = meta.SteamTurbineInputs.dict except: pass + try: r["inputs"]["GHP"] = meta.GHPInputs.dict except: pass + try: r["inputs"]["ElectricHeater"] = meta.ElectricHeaterInputs.dict + except: pass + + try: r["inputs"]["ASHPSpaceHeater"] = meta.ASHPSpaceHeaterInputs.dict + except: pass + + try: r["inputs"]["ASHPWaterHeater"] = meta.ASHPWaterHeaterInputs.dict + except: pass + try: r["outputs"] = dict() r["messages"] = dict() @@ -302,6 +321,12 @@ def results(request, run_uuid): except: pass try: r["outputs"]["GHP"] = meta.GHPOutputs.dict except: pass + try: r["outputs"]["ElectricHeater"] = meta.ElectricHeaterOutputs.dict + except: pass + try: r["outputs"]["ASHPSpaceHeater"] = meta.ASHPSpaceHeaterOutputs.dict + except: pass + try: r["outputs"]["ASHPWaterHeater"] = meta.ASHPWaterHeaterOutputs.dict + except: pass for d in r["outputs"].values(): if isinstance(d, dict): @@ -441,6 +466,38 @@ def absorption_chiller_defaults(request): log.debug(debug_msg) return JsonResponse({"Error": "Unexpected error in absorption_chiller_defaults endpoint. Check log for more."}, status=500) +def get_ashp_defaults(request): + inputs = {} + if request.GET.get("load_served") not in [None, "", []]: + inputs["load_served"] = request.GET.get("load_served") + else: + return JsonResponse({"Error: Missing input load_served in get_ashp_defualts endpoint."}, status=400) + if request.GET.get("force_into_system") not in [None, "", []]: + inputs["force_into_system"] = request.GET.get("force_into_system") + else: + return JsonResponse({"Error: Missing input force_into_system in get_ashp_defualts endpoint."}, status=400) + try: + julia_host = os.environ.get('JULIA_HOST', "julia") + http_jl_response = requests.get("http://" + julia_host + ":8081/get_ashp_defaults/", json=inputs) + response = JsonResponse( + http_jl_response.json(), + status=http_jl_response.status_code + ) + return response + + except ValueError as e: + return JsonResponse({"Error": str(e.args[0])}, status=400) + + except KeyError as e: + return JsonResponse({"Error. Missing": str(e.args[0])}, status=400) + + except Exception: + exc_type, exc_value, exc_traceback = sys.exc_info() + debug_msg = "exc_type: {}; exc_value: {}; exc_traceback: {}".format(exc_type, exc_value.args[0], + tb.format_tb(exc_traceback)) + log.debug(debug_msg) + return JsonResponse({"Error": "Unexpected error in get_ashp_defaults endpoint. Check log for more."}, status=500) + def simulated_load(request): try: @@ -734,12 +791,26 @@ def link_run_uuids_to_portfolio_uuid(request): for s in scenario: s.portfolio_uuid = p_uuid s.save() + + # Existing portfolio runs could have been "unlinked" from portfolio + # so they are independent and show up in summary endpoint. If these runs + # are re-linked with a portfolio, their portfolio_id is updated above. + # BUT these runs could still show up under `Summary` results (niche case) + # because they are present in PortfolioUnlinkedRuns. + # Below, these runs are removed from PortfolioUnlinkedRuns + # so they are "linked" to a portfolio and do not show up under `Summary` + if PortfolioUnlinkedRuns.objects.filter(run_uuid=r_uuid).exists(): + obj = PortfolioUnlinkedRuns.objects.get(run_uuid=r_uuid) + obj.delete() + resp_str = ' and deleted run entry from PortfolioUnlinkedRuns' + else: + resp_str = '' else: # Stop processing on first bad run_uuid response = JsonResponse({"Error": "No scenarios found for run_uuid '{}'".format(r_uuid)}, content_type='application/json', status=500) return response - response = JsonResponse({"Success": "All runs associated with given prortfolios"}, status=200, safe=False) + response = JsonResponse({"Success": "All runs associated with given portfolios'{}'".format(resp_str)}, status=200, safe=False) return response except Exception as e: @@ -810,6 +881,8 @@ def summary(request, user_uuid): run_uuid__in=[i.run_uuid for i in UserUnlinkedRuns.objects.filter(user_uuid=user_uuid)] ).only( 'run_uuid', + 'user_uuid', + 'portfolio_uuid', 'status', 'created' ).order_by("-created") @@ -950,6 +1023,8 @@ def queryset_for_summary(api_metas,summary_dict:dict): summary_dict[str(m.run_uuid)] = dict() summary_dict[str(m.run_uuid)]['status'] = m.status summary_dict[str(m.run_uuid)]['run_uuid'] = str(m.run_uuid) + summary_dict[str(m.run_uuid)]['user_uuid'] = str(m.user_uuid) + summary_dict[str(m.run_uuid)]['portfolio_uuid'] = str(m.portfolio_uuid) summary_dict[str(m.run_uuid)]['created'] = str(m.created) run_uuids = summary_dict.keys() @@ -1171,6 +1246,29 @@ def queryset_for_summary(api_metas,summary_dict:dict): summary_dict[str(m.meta.run_uuid)]['ghp_heating_ton'] = m.size_wwhp_heating_pump_ton summary_dict[str(m.meta.run_uuid)]['ghp_n_bores'] = m.ghpghx_chosen_outputs['number_of_boreholes'] + elecHeater = ElectricHeaterOutputs.objects.filter(meta__run_uuid__in=run_uuids).only( + 'meta__run_uuid', + 'size_mmbtu_per_hour' + ) + if len(elecHeater) > 0: + for m in elecHeater: + summary_dict[str(m.meta.run_uuid)]['electric_heater_mmbtu_per_hour'] = m.size_mmbtu_per_hour + + ashpSpaceHeater = ASHPSpaceHeaterOutputs.objects.filter(meta__run_uuid__in=run_uuids).only( + 'meta__run_uuid', + 'size_ton' + ) + if len(ashpSpaceHeater) > 0: + for m in ashpSpaceHeater: + summary_dict[str(m.meta.run_uuid)]['ASHPSpace_heater_ton'] = m.size_ton + + ashpWaterHeater = ASHPWaterHeaterOutputs.objects.filter(meta__run_uuid__in=run_uuids).only( + 'meta__run_uuid', + 'size_ton' + ) + if len(ashpWaterHeater) > 0: + for m in ashpWaterHeater: + summary_dict[str(m.meta.run_uuid)]['ASHPWater_heater_ton'] = m.size_ton hottes = HotThermalStorageOutputs.objects.filter(meta__run_uuid__in=run_uuids).only( 'meta__run_uuid', 'size_gal' @@ -1287,7 +1385,7 @@ def unlink_from_portfolio(request, user_uuid, portfolio_uuid, run_uuid): elif runs[0].user_uuid != user_uuid: return JsonResponse({"Error": "Run {} is not associated with user {}".format(run_uuid, user_uuid)}, status=400) else: - return JsonResponse({"Error": "Error in unlinking run {} from portfolio {}".format(run_uuid, portfolio_uuid)}, status=400) + pass # Run exists and is tied to porfolio provided in request, hence unlink now. if not PortfolioUnlinkedRuns.objects.filter(run_uuid=run_uuid).exists():