diff --git a/julia_src/Manifest.toml b/julia_src/Manifest.toml index dc825f89c..f87793e8b 100644 --- a/julia_src/Manifest.toml +++ b/julia_src/Manifest.toml @@ -948,7 +948,7 @@ version = "1.11.0" [[deps.REopt]] deps = ["ArchGDAL", "CSV", "CoolProp", "DataFrames", "Dates", "DelimitedFiles", "HTTP", "JLD", "JSON", "JuMP", "LinDistFlow", "LinearAlgebra", "Logging", "MathOptInterface", "Requires", "Roots", "Statistics", "TestEnv"] -git-tree-sha1 = "e807b62ab249fca7b59ac6ab7bb71725f504cc5f" +git-tree-sha1 = "decb34bd1299777d95d9e450e713db69189162d7" uuid = "d36ad4e8-d74a-4f7a-ace1-eaea049febf6" version = "0.59.2" diff --git a/julia_src/http.jl b/julia_src/http.jl index 9eab6396d..5a0bee7d1 100644 --- a/julia_src/http.jl +++ b/julia_src/http.jl @@ -120,13 +120,18 @@ function reopt(req::HTTP.Request) ] if haskey(d, "CHP") inputs_with_defaults_from_julia_chp = [ + :name, :installed_cost_per_kw, :tech_sizes_for_cost_curve, :om_cost_per_kwh, :electric_efficiency_full_load, :thermal_efficiency_full_load, :min_allowable_kw, :cooling_thermal_factor, :min_turn_down_fraction, :unavailability_periods, :max_kw, :size_class, :electric_efficiency_half_load, :thermal_efficiency_half_load, :macrs_option_years, :macrs_bonus_fraction, :federal_itc_fraction ] - chp_dict = Dict(key=>getfield(model_inputs.s.chp, key) for key in inputs_with_defaults_from_julia_chp) + if length(model_inputs.s.chps) == 1 + chp_dict = Dict(key=>getfield(model_inputs.s.chps[1], key) for key in inputs_with_defaults_from_julia_chp) + else + chp_dict = [Dict(key=>getfield(chp, key) for key in inputs_with_defaults_from_julia_chp) for chp in model_inputs.s.chps] + end else chp_dict = Dict() end @@ -192,9 +197,13 @@ function reopt(req::HTTP.Request) end if haskey(d, "PV") inputs_with_defaults_from_julia_pv = [ - :size_class, :installed_cost_per_kw, :om_cost_per_kw, :macrs_option_years, :macrs_bonus_fraction, :federal_itc_fraction + :name, :size_class, :installed_cost_per_kw, :om_cost_per_kw, :macrs_option_years, :macrs_bonus_fraction, :federal_itc_fraction ] - pv_dict = Dict(key=>getfield(model_inputs.s.pvs[1], key) for key in inputs_with_defaults_from_julia_pv) + if length(model_inputs.s.pvs) == 1 + pv_dict = Dict(key=>getfield(model_inputs.s.pvs[1], key) for key in inputs_with_defaults_from_julia_pv) + else + pv_dict = [Dict(key=>getfield(pv, key) for key in inputs_with_defaults_from_julia_pv) for pv in model_inputs.s.pvs] + end else pv_dict = Dict() end diff --git a/reoptjl/migrations/0117_chp_multiple_and_new_fields.py b/reoptjl/migrations/0117_chp_multiple_and_new_fields.py new file mode 100644 index 000000000..6890e880c --- /dev/null +++ b/reoptjl/migrations/0117_chp_multiple_and_new_fields.py @@ -0,0 +1,108 @@ +from django.db import migrations, models +import django.contrib.postgres.fields +import django.core.validators + + +class Migration(migrations.Migration): + + dependencies = [ + ('reoptjl', '0116_alter_apimeta_api_key_alter_apimeta_job_type'), + ] + + operations = [ + migrations.AlterField( + model_name='chpinputs', + name='meta', + field=models.ForeignKey(on_delete=models.deletion.CASCADE, related_name='CHPInputs', to='reoptjl.apimeta'), + ), + migrations.AddField( + model_name='chpinputs', + name='existing_kw', + field=models.FloatField(blank=True, default=0, help_text='Existing CHP electric capacity (based on rated electric power)', validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(100000.0)]), + ), + migrations.AddField( + model_name='chpinputs', + name='fuel_cost_escalation_rate_fraction', + field=models.FloatField(blank=True, help_text='Annual nominal chp fuel cost escalation rate, as a decimal.', null=True, validators=[django.core.validators.MinValueValidator(-1.0), django.core.validators.MaxValueValidator(1.0)]), + ), + migrations.AddField( + model_name='chpinputs', + name='name', + field=models.TextField(blank=True, default='CHP', help_text='CHP description for distinguishing between multiple CHP models'), + ), + migrations.AddField( + model_name='chpinputs', + name='operating_reserve_required_fraction', + field=models.FloatField(blank=True, help_text='Only applicable when off_grid_flag=True. Required operating reserves applied to each timestep as a fraction of CHP generation serving load in that timestep.', null=True, validators=[django.core.validators.MinValueValidator(0.0), django.core.validators.MaxValueValidator(1.0)]), + ), + migrations.AddField( + model_name='chpinputs', + name='production_factor_series', + field=django.contrib.postgres.fields.ArrayField(base_field=models.FloatField(blank=True), blank=True, default=list, help_text='Optional user-defined production factors. Must be normalized to units of kW-AC/kW-AC nameplate, representing the AC power (kW) output per 1 kW-AC of CHP capacity in each time step. The series must be one year (January through December) of hourly, 30-minute, or 15-minute data.', size=None), + ), + migrations.AddField( + model_name='chpinputs', + name='ramp_rate_fraction_per_hour', + field=models.FloatField(blank=True, default=1.0, help_text='Maximum rate of change in electric production per hour as a fraction of size_kw [kW/size_kw/hour].', validators=[django.core.validators.MinValueValidator(0.0), django.core.validators.MaxValueValidator(1000.0)]), + ), + migrations.AlterField( + model_name='chpoutputs', + name='meta', + field=models.ForeignKey(on_delete=models.deletion.CASCADE, related_name='CHPOutputs', to='reoptjl.apimeta'), + ), + migrations.AddField( + model_name='chpoutputs', + name='annual_electric_production_kwh_bau', + field=models.FloatField(blank=True, help_text='Electric energy produced in a year by the existing CHP system in BAU [kWh]', null=True), + ), + migrations.AddField( + model_name='chpoutputs', + name='annual_fuel_consumption_mmbtu_bau', + field=models.FloatField(blank=True, help_text='Fuel consumed in a year by the existing CHP system in BAU [MMBtu]', null=True), + ), + migrations.AddField( + model_name='chpoutputs', + name='annual_thermal_production_mmbtu_bau', + field=models.FloatField(blank=True, help_text='Thermal energy produced in a year by the existing CHP system in BAU [MMBtu]', null=True), + ), + migrations.AddField( + model_name='chpoutputs', + name='lifecycle_fuel_cost_after_tax_bau', + field=models.FloatField(blank=True, help_text='Present value of cost of fuel consumed by the existing CHP system in BAU, after tax [$]', null=True), + ), + migrations.AddField( + model_name='chpoutputs', + name='lifecycle_standby_cost_after_tax_bau', + field=models.FloatField(blank=True, help_text='Present value of all CHP standby charges for the existing CHP system in BAU, after tax.', null=True), + ), + migrations.AddField( + model_name='chpoutputs', + name='name', + field=models.TextField(blank=True, default='CHP', help_text='CHP description for distinguishing between multiple CHP models'), + ), + migrations.AddField( + model_name='chpoutputs', + name='size_kw_bau', + field=models.FloatField(blank=True, help_text='Power capacity size of the existing CHP system in BAU [kW]', null=True), + ), + migrations.AddField( + model_name='chpoutputs', + name='year_one_fuel_cost_after_tax_bau', + field=models.FloatField(blank=True, help_text='Cost of fuel consumed by the existing CHP system in year one in BAU, after tax [$]', null=True), + ), + migrations.AddField( + model_name='chpoutputs', + name='year_one_fuel_cost_before_tax_bau', + field=models.FloatField(blank=True, help_text='Cost of fuel consumed by the existing CHP system in year one in BAU [$]', null=True), + ), + migrations.AddField( + model_name='chpoutputs', + name='year_one_standby_cost_after_tax_bau', + field=models.FloatField(blank=True, help_text='CHP standby charges in year one for the existing CHP system in BAU, after tax [$]', null=True), + ), + migrations.AddField( + model_name='chpoutputs', + name='year_one_standby_cost_before_tax_bau', + field=models.FloatField(blank=True, help_text='CHP standby charges in year one for the existing CHP system in BAU [$]', null=True), + ), + ] diff --git a/reoptjl/models.py b/reoptjl/models.py index d7df3b936..dcad7db39 100644 --- a/reoptjl/models.py +++ b/reoptjl/models.py @@ -4361,11 +4361,16 @@ class GeneratorOutputs(BaseModel, models.Model): class CHPInputs(BaseModel, models.Model): key = "CHP" - meta = models.OneToOneField( + meta = models.ForeignKey( to=APIMeta, on_delete=models.CASCADE, related_name="CHPInputs", - unique=True + unique=False + ) + name = models.TextField( + blank=True, + default="CHP", + help_text="CHP description for distinguishing between multiple CHP models" ) # Prime mover @@ -4491,7 +4496,7 @@ class CHPInputs(BaseModel, models.Model): ], null=True, blank=True, - help_text="Maximum CHP size (in kWe) constraint for optimization. Set to zero to disable CHP" + help_text="Maximum new CHP size (in kWe) constraint for optimization (upper bound on additional capacity beyond existing_kw). Set to zero to disable CHP" ) cooling_thermal_factor = models.FloatField( validators=[ @@ -4526,6 +4531,15 @@ class CHPInputs(BaseModel, models.Model): blank=True, help_text="CHP size class. Must be an integer value between 0 and 7" ) + existing_kw = models.FloatField( + default=0, + validators=[ + MinValueValidator(0), + MaxValueValidator(1.0e5) + ], + blank=True, + help_text="Existing CHP electric capacity (based on rated electric power)" + ) min_kw = models.FloatField( default=0, validators=[ @@ -4533,7 +4547,7 @@ class CHPInputs(BaseModel, models.Model): MaxValueValidator(1.0e9) ], blank=True, - help_text="Minimum CHP size constraint for optimization" + help_text="Minimum new CHP size constraint for optimization (lower bound on additional capacity beyond existing_kw)." ) fuel_type = models.TextField( null=False, @@ -4561,6 +4575,15 @@ class CHPInputs(BaseModel, models.Model): blank=True, help_text="CHP system per-operating-hour (variable) operations and maintenance costs in $/hr-kW" ) + ramp_rate_fraction_per_hour = models.FloatField( + default=1.0, + validators=[ + MinValueValidator(0.0), + MaxValueValidator(1.0e3) + ], + blank=True, + help_text="Maximum rate of change in electric production per hour as a fraction of size_kw [kW/size_kw/hour]." + ) supplementary_firing_capital_cost_per_kw = models.FloatField( default=150, validators=[ @@ -4663,6 +4686,26 @@ class CHPInputs(BaseModel, models.Model): blank=True, help_text="Boolean indicator if CHP can serve process heat load" ) + operating_reserve_required_fraction = models.FloatField( + validators=[ + MinValueValidator(0.0), + MaxValueValidator(1.0) + ], + blank=True, + null=True, + help_text=("Only applicable when off_grid_flag=True. Required operating reserves applied to each timestep " + "as a fraction of CHP generation serving load in that timestep.") + ) + production_factor_series = ArrayField( + models.FloatField( + blank=True + ), + default=list, + blank=True, + help_text=("Optional user-defined production factors. Must be normalized to units of kW-AC/kW-AC nameplate, " + "representing the AC power (kW) output per 1 kW-AC of CHP capacity in each time step. " + "The series must be one year (January through December) of hourly, 30-minute, or 15-minute data.") + ) #Financial and emissions @@ -4884,6 +4927,15 @@ class CHPInputs(BaseModel, models.Model): null=True, help_text="Pounds of CO2 emitted per MMBTU of CHP fuel burned." ) + fuel_cost_escalation_rate_fraction = models.FloatField( + validators=[ + MinValueValidator(-1.0), + MaxValueValidator(1.0) + ], + null=True, + blank=True, + help_text="Annual nominal chp fuel cost escalation rate, as a decimal." + ) def clean(self): error_messages = {} @@ -4913,17 +4965,26 @@ def clean(self): class CHPOutputs(BaseModel, models.Model): key = "CHPOutputs" - meta = models.OneToOneField( + meta = models.ForeignKey( to=APIMeta, on_delete=models.CASCADE, related_name="CHPOutputs", - unique=True + unique=False + ) + name = models.TextField( + blank=True, + default="CHP", + help_text="CHP description for distinguishing between multiple CHP models" ) size_kw = models.FloatField( null=True, blank=True, help_text="Power capacity size of the CHP system [kW]" ) + size_kw_bau = models.FloatField( + null=True, blank=True, + help_text="Power capacity size of the existing CHP system in BAU [kW]" + ) size_supplemental_firing_kw = models.FloatField( null=True, blank=True, help_text="Power capacity of CHP supplementary firing system [kW]" @@ -4932,14 +4993,26 @@ class CHPOutputs(BaseModel, models.Model): null=True, blank=True, help_text="Fuel consumed in a year [MMBtu]" ) + annual_fuel_consumption_mmbtu_bau = models.FloatField( + null=True, blank=True, + help_text="Fuel consumed in a year by the existing CHP system in BAU [MMBtu]" + ) annual_electric_production_kwh = models.FloatField( null=True, blank=True, help_text="Electric energy produced in a year [kWh]" ) + annual_electric_production_kwh_bau = models.FloatField( + null=True, blank=True, + help_text="Electric energy produced in a year by the existing CHP system in BAU [kWh]" + ) annual_thermal_production_mmbtu = models.FloatField( null=True, blank=True, help_text="Thermal energy produced in a year [MMBtu]" ) + annual_thermal_production_mmbtu_bau = models.FloatField( + null=True, blank=True, + help_text="Thermal energy produced in a year by the existing CHP system in BAU [MMBtu]" + ) electric_production_series_kw = ArrayField( models.FloatField( null=True, blank=True @@ -5007,26 +5080,50 @@ class CHPOutputs(BaseModel, models.Model): null=True, blank=True, help_text="Cost of fuel consumed by the CHP system in year one [$]" ) + year_one_fuel_cost_before_tax_bau = models.FloatField( + null=True, blank=True, + help_text="Cost of fuel consumed by the existing CHP system in year one in BAU [$]" + ) year_one_fuel_cost_after_tax = models.FloatField( null=True, blank=True, help_text="Cost of fuel consumed by the CHP system in year one, after tax [$]" - ) + ) + year_one_fuel_cost_after_tax_bau = models.FloatField( + null=True, blank=True, + help_text="Cost of fuel consumed by the existing CHP system in year one in BAU, after tax [$]" + ) lifecycle_fuel_cost_after_tax = models.FloatField( null=True, blank=True, help_text="Present value of cost of fuel consumed by the CHP system, after tax [$]" ) + lifecycle_fuel_cost_after_tax_bau = models.FloatField( + null=True, blank=True, + help_text="Present value of cost of fuel consumed by the existing CHP system in BAU, after tax [$]" + ) year_one_standby_cost_before_tax = models.FloatField( null=True, blank=True, help_text="CHP standby charges in year one [$]" ) + year_one_standby_cost_before_tax_bau = models.FloatField( + null=True, blank=True, + help_text="CHP standby charges in year one for the existing CHP system in BAU [$]" + ) year_one_standby_cost_after_tax = models.FloatField( null=True, blank=True, help_text="CHP standby charges in year one, after tax [$]" - ) + ) + year_one_standby_cost_after_tax_bau = models.FloatField( + null=True, blank=True, + help_text="CHP standby charges in year one for the existing CHP system in BAU, after tax [$]" + ) lifecycle_standby_cost_after_tax = models.FloatField( null=True, blank=True, help_text="Present value of all CHP standby charges, after tax." ) + lifecycle_standby_cost_after_tax_bau = models.FloatField( + null=True, blank=True, + help_text="Present value of all CHP standby charges for the existing CHP system in BAU, after tax." + ) thermal_production_series_mmbtu_per_hour = ArrayField( models.FloatField(null=True, blank=True), default = list, @@ -9422,8 +9519,16 @@ def filter_none_and_empty_array(d:dict): try: d["ColdThermalStorage"] = filter_none_and_empty_array(meta.ColdThermalStorageInputs.dict) except: pass - try: d["CHP"] = filter_none_and_empty_array(meta.CHPInputs.dict) - except: pass + try: + chps = meta.CHPInputs.all() + if len(chps) == 1: + d["CHP"] = filter_none_and_empty_array(chps[0].dict) + elif len(chps) > 1: + d["CHP"] = [] + for chp in chps: + d["CHP"].append(filter_none_and_empty_array(chp.dict)) + except: + pass try: d["SteamTurbine"] = filter_none_and_empty_array(meta.SteamTurbineInputs.dict) except: pass diff --git a/reoptjl/src/process_results.py b/reoptjl/src/process_results.py index 38036f6fc..cef2c71b4 100644 --- a/reoptjl/src/process_results.py +++ b/reoptjl/src/process_results.py @@ -64,7 +64,11 @@ def process_results(results: dict, run_uuid: str) -> None: if "CoolingLoad" in results.keys(): CoolingLoadOutputs.create(meta=meta, **results["CoolingLoad"]).save() if "CHP" in results.keys(): - CHPOutputs.create(meta=meta, **results["CHP"]).save() + if isinstance(results["CHP"], dict): + CHPOutputs.create(meta=meta, **results["CHP"]).save() + elif isinstance(results["CHP"], list): + for chpdict in results["CHP"]: + CHPOutputs.create(meta=meta, **chpdict).save() if "AbsorptionChiller" in results.keys(): AbsorptionChillerOutputs.create(meta=meta, **results["AbsorptionChiller"]).save() if "Outages" in results.keys(): @@ -134,10 +138,21 @@ def update_inputs_in_database(inputs_to_update: dict, run_uuid: str) -> None: 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 - if inputs_to_update["CHP"].get("installed_cost_per_kw") and type(inputs_to_update["CHP"].get("installed_cost_per_kw")) == float: - inputs_to_update["CHP"]["installed_cost_per_kw"] = [inputs_to_update["CHP"]["installed_cost_per_kw"]] - CHPInputs.objects.filter(meta__run_uuid=run_uuid).update(**inputs_to_update["CHP"]) + if inputs_to_update["CHP"]: # Will be an empty dictionary/list if CHP is not considered + if isinstance(inputs_to_update["CHP"], dict): + if inputs_to_update["CHP"].get("installed_cost_per_kw") and type(inputs_to_update["CHP"].get("installed_cost_per_kw")) == float: + inputs_to_update["CHP"]["installed_cost_per_kw"] = [inputs_to_update["CHP"]["installed_cost_per_kw"]] + prune_update_fields(CHPInputs, inputs_to_update["CHP"]) + CHPInputs.objects.filter(meta__run_uuid=run_uuid).update(**inputs_to_update["CHP"]) + elif isinstance(inputs_to_update["CHP"], list): + for chp_input in inputs_to_update["CHP"]: + if chp_input.get("installed_cost_per_kw") and type(chp_input.get("installed_cost_per_kw")) == float: + chp_input["installed_cost_per_kw"] = [chp_input["installed_cost_per_kw"]] + prune_update_fields(CHPInputs, chp_input) + name = chp_input.get("name") + if not name: + raise ValueError(f"CHP list item missing required 'name' field for update. Cannot update all CHP rows without identifying specific CHP.") + CHPInputs.objects.filter(meta__run_uuid=run_uuid, name=name).update(**chp_input) if inputs_to_update["SteamTurbine"]: # Will be an empty dictionary if SteamTurbine is not considered SteamTurbineInputs.objects.filter(meta__run_uuid=run_uuid).update(**inputs_to_update["SteamTurbine"]) if inputs_to_update["GHP"]: @@ -156,7 +171,15 @@ def update_inputs_in_database(inputs_to_update: dict, run_uuid: str) -> None: ASHPWaterHeaterInputs.objects.filter(meta__run_uuid=run_uuid).update(**inputs_to_update["ASHPWaterHeater"]) if inputs_to_update["PV"]: prune_update_fields(PVInputs, inputs_to_update["PV"]) - PVInputs.objects.filter(meta__run_uuid=run_uuid).update(**inputs_to_update["PV"]) + if isinstance(inputs_to_update["PV"], dict): + PVInputs.objects.filter(meta__run_uuid=run_uuid).update(**inputs_to_update["PV"]) + elif isinstance(inputs_to_update["PV"], list): + for pv_input in inputs_to_update["PV"]: + prune_update_fields(PVInputs, pv_input) + name = pv_input.get("name") + if not name: + raise ValueError(f"PV list item missing required 'name' field for update. Cannot update all PV rows without identifying specific PV.") + PVInputs.objects.filter(meta__run_uuid=run_uuid, name=name).update(**pv_input) if inputs_to_update["Wind"]: prune_update_fields(WindInputs, inputs_to_update["Wind"]) WindInputs.objects.filter(meta__run_uuid=run_uuid).update(**inputs_to_update["Wind"]) diff --git a/reoptjl/validators.py b/reoptjl/validators.py index 21f65cc76..a4b8d96fb 100644 --- a/reoptjl/validators.py +++ b/reoptjl/validators.py @@ -89,6 +89,7 @@ def __init__(self, raw_inputs: dict, ghpghx_inputs_validation_errors=None): CSTInputs ) self.pvnames = [] + self.chpnames = [] on_grid_required_object_names = [ "Site", "ElectricLoad", "ElectricTariff" ] @@ -105,12 +106,27 @@ def __init__(self, raw_inputs: dict, ghpghx_inputs_validation_errors=None): for obj in self.objects: if obj == APIMeta: continue # already created and saved if obj.key in raw_inputs.keys(): - if isinstance(raw_inputs[obj.key], list) and obj.key == "PV": # only handle array of PV - for (i, user_pv) in enumerate(raw_inputs["PV"]): - name = user_pv.get("name", "") - self.pvnames.append(name if not name == "" else "PV" + str(i)) - filtered_user_post[self.pvnames[-1]] = scrub_fields(obj, user_pv) - self.models[self.pvnames[-1]] = obj.create(meta=meta, **filtered_user_post[self.pvnames[-1]]) + if isinstance(raw_inputs[obj.key], list) and obj.key in ["PV", "CHP"]: + # only PV and CHP can be arrays of technology objects + seen_names = set() + for (i, user_tech) in enumerate(raw_inputs[obj.key]): + name = user_tech.get("name", "") + tech_name = name if not name == "" else obj.key + str(i) + + # Check for duplicate names within the list + if tech_name in seen_names: + self.validation_errors[obj.key] = f"Duplicate {obj.key} name: '{tech_name}'. All {obj.key} names must be unique." + continue + seen_names.add(tech_name) + + if obj.key == "PV": + self.pvnames.append(tech_name) + else: + self.chpnames.append(tech_name) + if name == "": + user_tech["name"] = tech_name + filtered_user_post[tech_name] = scrub_fields(obj, user_tech) + self.models[tech_name] = obj.create(meta=meta, **filtered_user_post[tech_name]) else: filtered_user_post[obj.key] = scrub_fields(obj, raw_inputs[obj.key]) self.models[obj.key] = obj.create(meta=meta, **filtered_user_post[obj.key]) @@ -151,7 +167,7 @@ def messages(self): if "ElectricUtility" in self.models.keys(): msg_dict["ignored inputs"] = ("ElectricUtility inputs are not applicable when off_grid_flag is true, and will be ignored. " "Provided ElectricUtility can be removed from inputs") - msg_dict["info"] = ("When off_grid_flag is true, only PV, Wind, ElectricStorage, Generator technologies can be modeled.") + msg_dict["info"] = ("When off_grid_flag is true, only PV, Wind, ElectricStorage, Generator, CHP technologies can be modeled.") return msg_dict @property @@ -164,11 +180,11 @@ def validated_input_dict(self): """ d = dict() for model in self.models.values(): - if model.key == "PV" and self.pvnames: - if "PV" not in d.keys(): - d["PV"] = [{k: v for (k, v) in model.dict.items() if v not in [None, []]}] + if model.key in ["PV", "CHP"] and (self.pvnames if model.key == "PV" else self.chpnames): + if model.key not in d.keys(): + d[model.key] = [{k: v for (k, v) in model.dict.items() if v not in [None, []]}] else: - d["PV"].append({k: v for (k, v) in model.dict.items() if v not in [None, []]}) + d[model.key].append({k: v for (k, v) in model.dict.items() if v not in [None, []]}) else: d[model.key] = {k: v for (k, v) in model.dict.items() if v not in [None, []]} # cleaning out model attribute @@ -272,6 +288,27 @@ def update_pv_defaults_offgrid(self, pvmodel): cross_clean_pv(self.models[pvname]) update_pv_defaults_offgrid(self, self.models[pvname]) + """ + CHP validation + """ + def cross_clean_chp(chpmodel_key): + if len(self.models[chpmodel_key].production_factor_series) > 0: + self.clean_time_series(chpmodel_key, "production_factor_series") + + if self.models["Settings"].off_grid_flag: + if self.models[chpmodel_key].operating_reserve_required_fraction is None: + self.models[chpmodel_key].operating_reserve_required_fraction = 0.0 + else: + # operating reserve requirement for CHP only applies to off-grid + self.models[chpmodel_key].operating_reserve_required_fraction = 0.0 + + if "CHP" in self.models.keys(): # single CHP + cross_clean_chp("CHP") + + if len(self.chpnames) > 0: # multiple CHP + for chpname in self.chpnames: + cross_clean_chp(chpname) + """ Time series values are up or down sampled to align with Settings.time_steps_per_hour """ @@ -540,9 +577,17 @@ def assign_ref_buildings_from_electric_load(self, load_to_assign): def validate_offgrid_keys(self): # From https://github.com/NatLabRockies/REopt.jl/blob/4b0fb7f6556b2b6e9a9a7e8fa65398096fb6610f/src/core/scenario.jl#L88 - valid_input_keys_offgrid = ["PV", "Wind", "ElectricStorage", "Generator", "Settings", "Site", "Financial", "ElectricLoad", "ElectricTariff", "ElectricUtility", "Meta"] + valid_input_keys_offgrid = ["PV", "Wind", "ElectricStorage", "Generator", "CHP", "Settings", "Site", "Financial", "ElectricLoad", "ElectricTariff", "ElectricUtility", "Meta"] + + normalized_model_keys = set(self.models.keys()) + if len(self.pvnames) > 0: + normalized_model_keys -= set(self.pvnames) + normalized_model_keys.add("PV") + if len(self.chpnames) > 0: + normalized_model_keys -= set(self.chpnames) + normalized_model_keys.add("CHP") - invalid_input_keys_offgrid = list(set(list(self.models.keys()))-set(valid_input_keys_offgrid)) + invalid_input_keys_offgrid = list(normalized_model_keys - set(valid_input_keys_offgrid)) if 'APIMeta' in invalid_input_keys_offgrid: invalid_input_keys_offgrid.remove('APIMeta') diff --git a/reoptjl/views.py b/reoptjl/views.py index 08b1e21e8..5720729f3 100644 --- a/reoptjl/views.py +++ b/reoptjl/views.py @@ -254,8 +254,16 @@ def results(request, run_uuid): try: r["inputs"]["ProcessHeatLoad"] = meta.ProcessHeatLoadInputs.dict except: pass - try: r["inputs"]["CHP"] = meta.CHPInputs.dict - except: pass + try: + chps = meta.CHPInputs.all() + if len(chps) == 1: + r["inputs"]["CHP"] = chps[0].dict + elif len(chps) > 1: + r["inputs"]["CHP"] = [] + for chp in chps: + r["inputs"]["CHP"].append(chp.dict) + except: + pass try: r["inputs"]["AbsorptionChiller"] = meta.AbsorptionChillerInputs.dict except: pass @@ -340,8 +348,16 @@ def results(request, run_uuid): except: pass try: r["outputs"]["ColdThermalStorage"] = meta.ColdThermalStorageOutputs.dict except: pass - try: r["outputs"]["CHP"] = meta.CHPOutputs.dict - except: pass + try: + chps = meta.CHPOutputs.all() + if len(chps) == 1: + r["outputs"]["CHP"] = chps[0].dict + elif len(chps) > 1: + r["outputs"]["CHP"] = [] + for chp in chps: + r["outputs"]["CHP"].append(chp.dict) + except: + pass try: r["outputs"]["AbsorptionChiller"] = meta.AbsorptionChillerOutputs.dict except: pass try: r["outputs"]["HeatingLoad"] = meta.HeatingLoadOutputs.dict @@ -1366,23 +1382,26 @@ def queryset_for_summary(api_metas,summary_dict:dict): # assumes run_uuids exist in both CHPInputs and CHPOutputs chpInputs = CHPInputs.objects.filter(meta__run_uuid__in=run_uuids).only( 'meta__run_uuid', + 'name', 'thermal_efficiency_full_load' ) thermal_efficiency_full_load = dict() if len(chpInputs) > 0: for m in chpInputs: - thermal_efficiency_full_load[str(m.meta.run_uuid)] = m.thermal_efficiency_full_load + thermal_efficiency_full_load[(str(m.meta.run_uuid), m.name)] = m.thermal_efficiency_full_load chpOutputs = CHPOutputs.objects.filter(meta__run_uuid__in=run_uuids).only( 'meta__run_uuid', + 'name', 'size_kw' ) if len(chpOutputs) > 0: for m in chpOutputs: - if thermal_efficiency_full_load[str(m.meta.run_uuid)] == 0: - summary_dict[str(m.meta.run_uuid)]['prime_gen_kw'] = m.size_kw + rte = thermal_efficiency_full_load.get((str(m.meta.run_uuid), m.name)) + if rte == 0: + summary_dict[str(m.meta.run_uuid)]['prime_gen_kw'] = (summary_dict[str(m.meta.run_uuid)].get('prime_gen_kw') or 0) + (m.size_kw or 0) else: - summary_dict[str(m.meta.run_uuid)]['chp_kw'] = m.size_kw + summary_dict[str(m.meta.run_uuid)]['chp_kw'] = (summary_dict[str(m.meta.run_uuid)].get('chp_kw') or 0) + (m.size_kw or 0) ghpOutputs = GHPOutputs.objects.filter(meta__run_uuid__in=run_uuids).only( 'meta__run_uuid',