diff --git a/.helm/values.production.yaml b/.helm/values.production.yaml index 7c028a0e3..34f49d5e3 100644 --- a/.helm/values.production.yaml +++ b/.helm/values.production.yaml @@ -3,11 +3,11 @@ djangoSettingsModule: reopt_api.production_settings djangoReplicas: 10 djangoMemoryRequest: "2800Mi" djangoMemoryLimit: "2800Mi" -celeryReplicas: 20 +celeryReplicas: 10 celeryMemoryRequest: "900Mi" celeryMemoryLimit: "900Mi" -juliaReplicas: 20 -juliaCpuRequest: "300m" +juliaReplicas: 15 +juliaCpuRequest: "1000m" juliaCpuLimit: "4000m" -juliaMemoryRequest: "8000Mi" -juliaMemoryLimit: "8000Mi" +juliaMemoryRequest: "12000Mi" +juliaMemoryLimit: "12000Mi" diff --git a/.helm/values.yaml b/.helm/values.yaml index 5b3421e42..83c14452b 100644 --- a/.helm/values.yaml +++ b/.helm/values.yaml @@ -15,7 +15,7 @@ djangoMemoryRequest: "1600Mi" djangoMemoryLimit: "1600Mi" celeryReplicas: 2 celeryCpuRequest: "100m" -celeryCpuLimit: "800m" +celeryCpuLimit: "2000m" celeryMemoryRequest: "700Mi" celeryMemoryLimit: "700Mi" juliaReplicas: 2 diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b6a4e942..8c483b235 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,13 @@ Classify the change according to the following categories: ##### Removed ### Patches +## Develop 2023/09/06 +### Minor Updates +##### Added +- /v3 endpoints which use the reoptjl app and the REopt.jl Julia package, but /stable still points to /v2 so this is not a breaking change +##### Fixed +- Fixed a bug in the `get_existing_chiller_default_cop` endpoint not accepting blank/null inputs that are optional + ## v2.15.0 ### Minor Updates ##### Added diff --git a/reopt_api/urls.py b/reopt_api/urls.py index 2443b9ab5..35eb80943 100644 --- a/reopt_api/urls.py +++ b/reopt_api/urls.py @@ -35,7 +35,7 @@ from resilience_stats.api import ERPJob from tastypie.api import Api from reo import views -from reoptjl.api import Job as DevJob +from reoptjl.api import Job as REoptJLJob from futurecosts.api import FutureCostsAPI from ghpghx.resources import GHPGHXJob from reo.api import Job2 @@ -50,13 +50,18 @@ v2_api.register(OutageSimJob()) v2_api.register(GHPGHXJob()) +v3_api = Api(api_name='v3') +v3_api.register(REoptJLJob()) +v3_api.register(GHPGHXJob()) +v3_api.register(ERPJob()) + stable_api = Api(api_name='stable') stable_api.register(Job2()) stable_api.register(OutageSimJob()) stable_api.register(GHPGHXJob()) dev_api = Api(api_name='dev') -dev_api.register(DevJob()) +dev_api.register(REoptJLJob()) dev_api.register(FutureCostsAPI()) dev_api.register(GHPGHXJob()) dev_api.register(ERPJob()) @@ -92,6 +97,14 @@ def page_not_found(request, url): path('v2/', include('ghpghx.urls')), re_path(r'', include(v2_api.urls)), + path('v3/', include('reoptjl.urls')), + path('v3/', include('resilience_stats.urls_v3plus')), + path('v3/', include('ghpghx.urls')), + path('v3/', include('load_builder.urls')), + # TODO proforma for v3 + # (summary is within reoptjl.urls) + re_path(r'', include(v3_api.urls)), + path('stable/', include('reo.urls_v2')), path('stable/', include('resilience_stats.urls_v1_v2')), path('stable/', include('proforma.urls')), diff --git a/reoptjl/api.py b/reoptjl/api.py index 64b24d122..c7d362ab9 100644 --- a/reoptjl/api.py +++ b/reoptjl/api.py @@ -124,7 +124,7 @@ def obj_create(self, bundle, **kwargs): meta = { "run_uuid": run_uuid, "api_version": 3, - "reopt_version": "0.30.0", + "reopt_version": "0.32.7", "status": "Validating..." } bundle.data.update({"APIMeta": meta}) diff --git a/reoptjl/migrations/0042_alter_boilerinputs_efficiency_and_more.py b/reoptjl/migrations/0042_alter_boilerinputs_efficiency_and_more.py new file mode 100644 index 000000000..158cc1071 --- /dev/null +++ b/reoptjl/migrations/0042_alter_boilerinputs_efficiency_and_more.py @@ -0,0 +1,94 @@ +# Generated by Django 4.0.7 on 2023-09-12 22:25 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('reoptjl', '0041_merge_20230901_1601'), + ] + + operations = [ + migrations.AlterField( + model_name='boilerinputs', + name='efficiency', + field=models.FloatField(blank=True, default=0.8, help_text='New boiler system efficiency - conversion of fuel to usable heating thermal energy.', null=True, validators=[django.core.validators.MinValueValidator(0.0), django.core.validators.MaxValueValidator(1.0)]), + ), + migrations.AlterField( + model_name='boilerinputs', + name='max_mmbtu_per_hour', + field=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)]), + ), + migrations.AlterField( + model_name='boilerinputs', + name='min_mmbtu_per_hour', + field=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)]), + ), + migrations.AlterField( + model_name='ghpinputs', + name='building_sqft', + field=models.FloatField(help_text='Building square footage for GHP/HVAC cost calculations', null=True, validators=[django.core.validators.MinValueValidator(0.0), django.core.validators.MaxValueValidator(100000000.0)]), + ), + migrations.AlterField( + model_name='steamturbineinputs', + name='can_curtail', + field=models.BooleanField(blank=True, default=False, help_text='True/False for if technology has the ability to curtail energy production.', null=True), + ), + migrations.AlterField( + model_name='steamturbineinputs', + name='can_export_beyond_nem_limit', + field=models.BooleanField(blank=True, default=False, help_text='True/False for if technology can export energy beyond the annual site load (and be compensated for that energy at the export_rate_beyond_net_metering_limit).Note that if off-grid is true, can_export_beyond_nem_limit is always set to False.', null=True), + ), + migrations.AlterField( + model_name='steamturbineinputs', + name='can_net_meter', + field=models.BooleanField(blank=True, default=False, help_text='True/False for if technology has option to participate in net metering agreement with utility. Note that a technology can only participate in either net metering or wholesale rates (not both).Note that if off-grid is true, net metering is always set to False.', null=True), + ), + migrations.AlterField( + model_name='steamturbineinputs', + name='can_wholesale', + field=models.BooleanField(blank=True, default=False, help_text='True/False for if technology has option to export energy that is compensated at the wholesale_rate. Note that a technology can only participate in either net metering or wholesale rates (not both).Note that if off-grid is true, can_wholesale is always set to False.', null=True), + ), + migrations.AlterField( + model_name='steamturbineinputs', + name='inlet_steam_superheat_degF', + field=models.FloatField(blank=True, default=0.0, help_text='Alternative input to inlet steam temperature, this is the superheat amount (delta from T_saturation) to the steam turbine', null=True, validators=[django.core.validators.MinValueValidator(0.0), django.core.validators.MaxValueValidator(700.0)]), + ), + migrations.AlterField( + model_name='steamturbineinputs', + name='is_condensing', + field=models.BooleanField(blank=True, default=False, help_text='Steam turbine type, if it is a condensing turbine which produces no useful thermal (max electric output)', null=True), + ), + migrations.AlterField( + model_name='steamturbineinputs', + name='macrs_bonus_fraction', + field=models.FloatField(blank=True, default=1.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)]), + ), + migrations.AlterField( + model_name='steamturbineinputs', + name='macrs_option_years', + field=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), + ), + migrations.AlterField( + model_name='steamturbineinputs', + name='max_kw', + field=models.FloatField(blank=True, default=100000000.0, help_text='Maximum steam turbine size constraint for optimization', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1000000000.0)]), + ), + migrations.AlterField( + model_name='steamturbineinputs', + name='min_kw', + field=models.FloatField(blank=True, default=0.0, help_text='Minimum steam turbine size constraint for optimization', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1000000000.0)]), + ), + migrations.AlterField( + model_name='steamturbineinputs', + name='om_cost_per_kw', + field=models.FloatField(blank=True, default=0.0, help_text='Annual steam turbine fixed operations and maintenance costs in $/kW', null=True, validators=[django.core.validators.MinValueValidator(0.0), django.core.validators.MaxValueValidator(5000.0)]), + ), + migrations.AlterField( + model_name='steamturbineinputs', + name='outlet_steam_min_vapor_fraction', + field=models.FloatField(blank=True, default=0.8, help_text='Minimum practical vapor fraction of steam at the exit of the steam turbine', null=True, validators=[django.core.validators.MinValueValidator(0.0), django.core.validators.MaxValueValidator(1.0)]), + ), + ] diff --git a/reoptjl/models.py b/reoptjl/models.py index aa6cda4ef..dfd4edffa 100644 --- a/reoptjl/models.py +++ b/reoptjl/models.py @@ -4819,6 +4819,8 @@ class BoilerInputs(BaseModel, models.Model): )) min_mmbtu_per_hour = models.FloatField( + null=True, + blank=True, validators=[ MinValueValidator(0.0), MaxValueValidator(MAX_BIG_NUMBER) @@ -4828,11 +4830,13 @@ class BoilerInputs(BaseModel, models.Model): ) max_mmbtu_per_hour = models.FloatField( + null=True, + blank=True, validators=[ MinValueValidator(0.0), MaxValueValidator(MAX_BIG_NUMBER) ], - default=0.0, + default=1.0E7, help_text="Maximum thermal power size" ) @@ -4842,6 +4846,7 @@ class BoilerInputs(BaseModel, models.Model): MaxValueValidator(1.0) ], null=True, + blank=True, default=0.8, help_text="New boiler system efficiency - conversion of fuel to usable heating thermal energy." ) @@ -5009,6 +5014,7 @@ class SIZE_CLASS_LIST(models.IntegerChoices): FOUR = 4 min_kw = models.FloatField( + null=True, default=0.0, validators=[ MinValueValidator(0), @@ -5018,7 +5024,8 @@ class SIZE_CLASS_LIST(models.IntegerChoices): help_text="Minimum steam turbine size constraint for optimization" ) max_kw = models.FloatField( - default=0.0, + null=True, + default=MAX_BIG_NUMBER, validators=[ MinValueValidator(0), MaxValueValidator(1.0e9) @@ -5137,6 +5144,7 @@ class SIZE_CLASS_LIST(models.IntegerChoices): ) is_condensing = models.BooleanField( + null=True, blank=True, default = False, help_text="Steam turbine type, if it is a condensing turbine which produces no useful thermal (max electric output)" @@ -5144,6 +5152,7 @@ class SIZE_CLASS_LIST(models.IntegerChoices): inlet_steam_superheat_degF = models.FloatField( null=True, + blank=True, validators=[ MinValueValidator(0.0), MaxValueValidator(700.0) @@ -5153,6 +5162,7 @@ class SIZE_CLASS_LIST(models.IntegerChoices): ) outlet_steam_min_vapor_fraction = models.FloatField( + null=True, default=0.8, validators=[ MinValueValidator(0.0), @@ -5179,10 +5189,12 @@ class SIZE_CLASS_LIST(models.IntegerChoices): ], default=0.0, null=True, + blank=True, help_text="Annual steam turbine fixed operations and maintenance costs in $/kW" ) can_net_meter = models.BooleanField( + null=True, blank=True, default = False, help_text=("True/False for if technology has option to participate in net metering agreement with utility. " @@ -5191,6 +5203,7 @@ class SIZE_CLASS_LIST(models.IntegerChoices): ) can_wholesale = models.BooleanField( + null=True, blank=True, default = False, help_text=("True/False for if technology has option to export energy that is compensated at the wholesale_rate. " @@ -5198,6 +5211,7 @@ class SIZE_CLASS_LIST(models.IntegerChoices): "Note that if off-grid is true, can_wholesale is always set to False.") ) can_export_beyond_nem_limit = models.BooleanField( + null=True, blank=True, default = False, help_text=("True/False for if technology can export energy beyond the annual site load (and be compensated for " @@ -5207,6 +5221,7 @@ class SIZE_CLASS_LIST(models.IntegerChoices): can_curtail = models.BooleanField( default=False, + null=True, blank=True, help_text="True/False for if technology has the ability to curtail energy production." ) @@ -5214,6 +5229,7 @@ class SIZE_CLASS_LIST(models.IntegerChoices): macrs_option_years = models.IntegerField( default=MACRS_YEARS_CHOICES.ZERO, choices=MACRS_YEARS_CHOICES.choices, + null=True, blank=True, help_text="Duration over which accelerated depreciation will occur. Set to zero to disable" ) @@ -5224,6 +5240,7 @@ class SIZE_CLASS_LIST(models.IntegerChoices): MinValueValidator(0), MaxValueValidator(1) ], + null=True, blank=True, help_text="Percent of upfront project costs to depreciate in year one in addition to scheduled depreciation" ) @@ -6431,6 +6448,7 @@ class GHPInputs(BaseModel, models.Model): # REQUIRED FOR GHP building_sqft = models.FloatField( + null=True, validators=[ MinValueValidator(0.0), MaxValueValidator(MAX_BIG_NUMBER) diff --git a/reoptjl/test/test_http_endpoints.py b/reoptjl/test/test_http_endpoints.py index 973d10db9..0e36bbe1e 100644 --- a/reoptjl/test/test_http_endpoints.py +++ b/reoptjl/test/test_http_endpoints.py @@ -218,9 +218,21 @@ def test_default_existing_chiller_cop(self): "max_load_kw_thermal":100 } - # Call to the django view endpoint /ghp_efficiency_thermal_factors which calls the http.jl endpoint + # Call to the django view endpoint /get_existing_chiller_default_cop which calls the http.jl endpoint + resp = self.api_client.get(f'/dev/get_existing_chiller_default_cop', data=inputs_dict) + view_response = json.loads(resp.content) + print(view_response) + + self.assertEqual(view_response["existing_chiller_cop"], 4.4) + + # Test empty dictionary, which should return unknown value + inputs_dict = {} + + # Call to the django view endpoint /get_existing_chiller_default_cop which calls the http.jl endpoint resp = self.api_client.get(f'/dev/get_existing_chiller_default_cop', data=inputs_dict) view_response = json.loads(resp.content) print(view_response) - self.assertEqual(view_response["existing_chiller_cop"], 4.4) \ No newline at end of file + self.assertEqual(view_response["existing_chiller_cop"], 4.545) + + \ No newline at end of file diff --git a/reoptjl/validators.py b/reoptjl/validators.py index 6b6a8855c..253ca9442 100644 --- a/reoptjl/validators.py +++ b/reoptjl/validators.py @@ -542,7 +542,7 @@ def assign_ref_buildings_from_electric_load(self, load_to_assign): def validate_offgrid_keys(self): # From https://github.com/NREL/REopt.jl/blob/4b0fb7f6556b2b6e9a9a7e8fa65398096fb6610f/src/core/scenario.jl#L88 - valid_input_keys_offgrid = ["PV", "Wind", "ElectricStorage", "Generator", "Settings", "Site", "Financial", "ElectricLoad", "ElectricTariff", "ElectricUtility"] + valid_input_keys_offgrid = ["PV", "Wind", "ElectricStorage", "Generator", "Settings", "Site", "Financial", "ElectricLoad", "ElectricTariff", "ElectricUtility", "Meta"] invalid_input_keys_offgrid = list(set(list(self.models.keys()))-set(valid_input_keys_offgrid)) if 'APIMeta' in invalid_input_keys_offgrid: diff --git a/reoptjl/views.py b/reoptjl/views.py index dcc82f3d8..7f88dad67 100644 --- a/reoptjl/views.py +++ b/reoptjl/views.py @@ -593,13 +593,27 @@ def get_existing_chiller_default_cop(request): return: existing_chiller_cop: default COP of existing chiller [fraction] """ try: - existing_chiller_max_thermal_factor_on_peak_load = float(request.GET['existing_chiller_max_thermal_factor_on_peak_load']) # need float to convert unicode - max_load_kw = float(request.GET['max_load_kw']) - max_load_kw_thermal = float(request.GET['max_load_kw_thermal']) - - inputs_dict = {"existing_chiller_max_thermal_factor_on_peak_load": existing_chiller_max_thermal_factor_on_peak_load, - "max_load_kw": max_load_kw, - "max_load_kw_thermal": max_load_kw_thermal} + existing_chiller_max_thermal_factor_on_peak_load = request.GET.get('existing_chiller_max_thermal_factor_on_peak_load') + if existing_chiller_max_thermal_factor_on_peak_load is not None: + existing_chiller_max_thermal_factor_on_peak_load = float(existing_chiller_max_thermal_factor_on_peak_load) + else: + existing_chiller_max_thermal_factor_on_peak_load = 1.25 # default from REopt.jl + + max_load_kw = request.GET.get('max_load_kw') + if max_load_kw is not None: + max_load_kw = float(max_load_kw) + + max_load_ton = request.GET.get('max_load_ton') + if max_load_ton is not None: + max_load_kw_thermal = float(max_load_ton) * 3.51685 # kWh thermal per ton-hour + else: + max_load_kw_thermal = None + + inputs_dict = { + "existing_chiller_max_thermal_factor_on_peak_load": existing_chiller_max_thermal_factor_on_peak_load, + "max_load_kw": max_load_kw, + "max_load_kw_thermal": max_load_kw_thermal + } julia_host = os.environ.get('JULIA_HOST', "julia") http_jl_response = requests.get("http://" + julia_host + ":8081/get_existing_chiller_default_cop/", json=inputs_dict)