diff --git a/.github/workflows/pull_request_tests.yml b/.github/workflows/pull_request_tests.yml index 2c98750af..ebfeb643e 100644 --- a/.github/workflows/pull_request_tests.yml +++ b/.github/workflows/pull_request_tests.yml @@ -3,6 +3,10 @@ name: Built-in Tests for Pull Requests (Xpress in Ubuntu 18.04) on: pull_request: + types: + - opened + - reopened + - ready_for_review branches: - master - develop diff --git a/.github/workflows/push_tests.yml b/.github/workflows/push_tests.yml index 19ff67b19..23ce8132c 100644 --- a/.github/workflows/push_tests.yml +++ b/.github/workflows/push_tests.yml @@ -3,6 +3,9 @@ name: Built-in Tests for Push (Xpress in Ubuntu 18.04) on: push: + paths-ignore: + - README.md + - CHANGELOG.md jobs: diff --git a/CHANGELOG.md b/CHANGELOG.md index bddb95a43..b803ffb7a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,58 @@ Classify the change according to the following categories: ##### Removed ### Patches +# v2.1.0 +### Minor Updates +##### Changed +- The `/stable` URL now correctly calls the `v2` version of the REopt model (`/job` endpoint) +- Don't trigger Built-in Tests workflow on a push that only changes README.md and/or CHANGELOG.md +- Avoid triggering duplicate GitHub workflows. When pushing to a branch that's in a PR, only trigger tests on the push not on the PR sync also. +`job/models.py` +- **Settings** + - Added **off_grid_flag** + - Changed **run_bau** to be nullable +- **FinancialInputs** + - Added **offgrid_other_capital_costs** + - Added **offgrid_other_annual_costs** +- **FinancialOutputs** + - Added **lifecycle_generation_tech_capital_costs** + - Added **lifecycle_storage_capital_costs** + - Added **lifecycle_om_costs_after_tax** + - Added **lifecycle_fuel_costs_after_tax** + - Added **lifecycle_chp_standby_cost_after_tax** + - Added **lifecycle_elecbill_after_tax** + - Added **lifecycle_production_incentive_after_tax** + - Added **lifecycle_offgrid_other_annual_costs_after_tax** + - Added **lifecycle_offgrid_other_capital_costs** + - Added **lifecycle_outage_cost** + - Added **lifecycle_MG_upgrade_and_fuel_cost** + - Added **replacements_future_cost_after_tax** + - Added **replacements_present_cost_after_tax** + - Added **offgrid_microgrid_lcoe_dollars_per_kwh** + - Changed **lifecycle_capital_costs_plus_om** and **lifecycle_om_costs_bau** field names to include before/after tax +- **ElectricLoadOutputs** + - Added **offgrid_load_met_pct** + - Added **offgrid_annual_oper_res_required_series_kwh** + - Added **offgrid_annual_oper_res_provided_series_kwh** + - Added **offgrid_load_met_series_kw** +- **ElectricTariffInputs** + - Changed all instances of `coincident_peak_load_active_timesteps` to `coincident_peak_load_active_time_steps` +- **ElectricTariffOutputs** + - Changed field names to add suffixes denoting `before_tax` or `after_tax` values +- **ElectricTariffInputs** + - Changed validation of this model to be conditional on **Settings.off_grid_flag** being False +`job/run_jump_model.py` - Remove `run_uuid` key from input dictionary before running REopt to avoid downstream errors from REopt.jl +`job/validators.py` +- Changed **ElectricTariffInputs** to validate if **ElectricTariff** key exists in inputs +- Added message to `messages()` to alert user if valid ElectricTariff input is provided when **Settings.off_grid_flag** is true. +- Added message to `messages()` to alert user of technologies which can be modeled when **Settings.off_grid_flag** is true. +- Added validation error to alert user of input keys which can't be modeled when **Settings.off_grid_flag** is true. +`job/models.py` +- Changed **ElectricTariffInputs** `required inputs` error message to alert user that ElectricTariff inputs are not required if **Settings.off_grid_flag** is true. +`job/views.py` - Changed validation code to try to save **ElectricTariffInputs** +`job/test_job_endpoint.py` - Added test to validate API off-grid functionality +- Added migration file `0005_remove_...` which contains the data model for all Added and Changed fields + # v2.0.3 ### Minor Updates ##### Fixed diff --git a/job/api.py b/job/api.py index bd6306269..08fc0c34f 100644 --- a/job/api.py +++ b/job/api.py @@ -100,8 +100,8 @@ def obj_create(self, bundle, **kwargs): run_uuid = str(uuid.uuid4()) meta = { "run_uuid": run_uuid, - "api_version": 2, - "reopt_version": "0.11.0", + "api_version": 3, + "reopt_version": "0.16.2", "status": "validating..." } bundle.data.update({"APIMeta": meta}) diff --git a/job/migrations/0005_remove_electrictariffinputs_coincident_peak_load_active_timesteps_and_more.py b/job/migrations/0005_remove_electrictariffinputs_coincident_peak_load_active_timesteps_and_more.py new file mode 100644 index 000000000..18e32e658 --- /dev/null +++ b/job/migrations/0005_remove_electrictariffinputs_coincident_peak_load_active_timesteps_and_more.py @@ -0,0 +1,569 @@ +# Generated by Django 4.0.4 on 2022-07-13 14:41 + +import django.contrib.postgres.fields +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('job', '0004_electricstorageoutputs_initial_capital_cost_and_more'), + ] + + operations = [ + migrations.RemoveField( + model_name='electrictariffinputs', + name='coincident_peak_load_active_timesteps', + ), + migrations.RemoveField( + model_name='electrictariffoutputs', + name='lifecycle_chp_standby_cost', + ), + migrations.RemoveField( + model_name='electrictariffoutputs', + name='lifecycle_coincident_peak_cost', + ), + migrations.RemoveField( + model_name='electrictariffoutputs', + name='lifecycle_coincident_peak_cost_bau', + ), + migrations.RemoveField( + model_name='electrictariffoutputs', + name='lifecycle_demand_cost', + ), + migrations.RemoveField( + model_name='electrictariffoutputs', + name='lifecycle_demand_cost_bau', + ), + migrations.RemoveField( + model_name='electrictariffoutputs', + name='lifecycle_energy_cost', + ), + migrations.RemoveField( + model_name='electrictariffoutputs', + name='lifecycle_energy_cost_bau', + ), + migrations.RemoveField( + model_name='electrictariffoutputs', + name='lifecycle_export_benefit', + ), + migrations.RemoveField( + model_name='electrictariffoutputs', + name='lifecycle_export_benefit_bau', + ), + migrations.RemoveField( + model_name='electrictariffoutputs', + name='lifecycle_fixed_cost', + ), + migrations.RemoveField( + model_name='electrictariffoutputs', + name='lifecycle_fixed_cost_bau', + ), + migrations.RemoveField( + model_name='electrictariffoutputs', + name='lifecycle_min_charge_adder', + ), + migrations.RemoveField( + model_name='electrictariffoutputs', + name='lifecycle_min_charge_adder_bau', + ), + migrations.RemoveField( + model_name='electrictariffoutputs', + name='year_one_bill', + ), + migrations.RemoveField( + model_name='electrictariffoutputs', + name='year_one_bill_bau', + ), + migrations.RemoveField( + model_name='electrictariffoutputs', + name='year_one_chp_standby_cost', + ), + migrations.RemoveField( + model_name='electrictariffoutputs', + name='year_one_coincident_peak_cost', + ), + migrations.RemoveField( + model_name='electrictariffoutputs', + name='year_one_coincident_peak_cost_bau', + ), + migrations.RemoveField( + model_name='electrictariffoutputs', + name='year_one_demand_cost', + ), + migrations.RemoveField( + model_name='electrictariffoutputs', + name='year_one_demand_cost_bau', + ), + migrations.RemoveField( + model_name='electrictariffoutputs', + name='year_one_energy_cost', + ), + migrations.RemoveField( + model_name='electrictariffoutputs', + name='year_one_energy_cost_bau', + ), + migrations.RemoveField( + model_name='electrictariffoutputs', + name='year_one_export_benefit', + ), + migrations.RemoveField( + model_name='electrictariffoutputs', + name='year_one_export_benefit_bau', + ), + migrations.RemoveField( + model_name='electrictariffoutputs', + name='year_one_fixed_cost', + ), + migrations.RemoveField( + model_name='electrictariffoutputs', + name='year_one_fixed_cost_bau', + ), + migrations.RemoveField( + model_name='electrictariffoutputs', + name='year_one_min_charge_adder', + ), + migrations.RemoveField( + model_name='electrictariffoutputs', + name='year_one_min_charge_adder_bau', + ), + migrations.RemoveField( + model_name='financialoutputs', + name='lifecycle_capital_costs_plus_om', + ), + migrations.RemoveField( + model_name='financialoutputs', + name='lifecycle_om_costs_bau', + ), + migrations.RemoveField( + model_name='generatoroutputs', + name='fuel_used_gal', + ), + migrations.RemoveField( + model_name='generatoroutputs', + name='lifecycle_fixed_om_cost', + ), + migrations.RemoveField( + model_name='generatoroutputs', + name='lifecycle_fuel_cost', + ), + migrations.RemoveField( + model_name='generatoroutputs', + name='lifecycle_variable_om_cost', + ), + migrations.RemoveField( + model_name='generatoroutputs', + name='year_one_fixed_om_cost', + ), + migrations.RemoveField( + model_name='generatoroutputs', + name='year_one_fuel_cost', + ), + migrations.RemoveField( + model_name='generatoroutputs', + name='year_one_variable_om_cost', + ), + migrations.RemoveField( + model_name='pvoutputs', + name='lifecycle_om_cost', + ), + migrations.RemoveField( + model_name='windoutputs', + name='lifecycle_om_cost', + ), + migrations.RemoveField( + model_name='windoutputs', + name='year_one_om_cost', + ), + migrations.AddField( + model_name='electricloadinputs', + name='min_load_met_annual_pct', + field=models.FloatField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1)]), + ), + migrations.AddField( + model_name='electricloadinputs', + name='operating_reserve_required_pct', + field=models.FloatField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1)]), + ), + migrations.AddField( + model_name='electricloadoutputs', + name='offgrid_annual_oper_res_provided_series_kwh', + field=django.contrib.postgres.fields.ArrayField(base_field=models.FloatField(blank=True, null=True), default=list, help_text='Total operating reserves provided on an annual basis, for off-grid scenarios only', size=None), + ), + migrations.AddField( + model_name='electricloadoutputs', + name='offgrid_annual_oper_res_required_series_kwh', + field=django.contrib.postgres.fields.ArrayField(base_field=models.FloatField(blank=True, null=True), default=list, help_text='Total operating reserves required on an annual basis, for off-grid scenarios only', size=None), + ), + migrations.AddField( + model_name='electricloadoutputs', + name='offgrid_load_met_pct', + field=models.FloatField(blank=True, help_text='Percentage of total electric load met on an annual basis, for off-grid scenarios only', null=True), + ), + migrations.AddField( + model_name='electricloadoutputs', + name='offgrid_load_met_series_kw', + field=django.contrib.postgres.fields.ArrayField(base_field=models.FloatField(blank=True, null=True), default=list, help_text='Percentage of total electric load met on an annual basis, for off-grid scenarios only', size=None), + ), + migrations.AddField( + model_name='electrictariffinputs', + name='coincident_peak_load_active_time_steps', + field=django.contrib.postgres.fields.ArrayField(base_field=django.contrib.postgres.fields.ArrayField(base_field=models.IntegerField(blank=True), blank=True, default=list, size=None, validators=[django.core.validators.MinValueValidator(1)]), blank=True, default=list, help_text='The optional coincident_peak_load_charge_per_kw will apply to the max grid-purchased power during these time steps. Note time steps are indexed to a base of 1 not 0.', size=None), + ), + migrations.AddField( + model_name='electrictariffoutputs', + name='lifecycle_chp_standby_cost_after_tax', + field=models.FloatField(blank=True, help_text='Optimal life cycle standby charge cost incurred by CHP, after-tax', null=True), + ), + migrations.AddField( + model_name='electrictariffoutputs', + name='lifecycle_coincident_peak_cost_after_tax', + field=models.FloatField(blank=True, help_text='Optimal total coincident peak charges over the analysis period, after-tax', null=True), + ), + migrations.AddField( + model_name='electrictariffoutputs', + name='lifecycle_coincident_peak_cost_after_tax_bau', + field=models.FloatField(blank=True, help_text='Business as usual life cycle coincident peak charges, after-tax', null=True), + ), + migrations.AddField( + model_name='electrictariffoutputs', + name='lifecycle_demand_cost_after_tax', + field=models.FloatField(blank=True, help_text='Optimal life cycle utility demand cost, after-tax', null=True), + ), + migrations.AddField( + model_name='electrictariffoutputs', + name='lifecycle_demand_cost_after_tax_bau', + field=models.FloatField(blank=True, help_text='Business as usual life cycle lifecycle utility demand cost, after-tax', null=True), + ), + migrations.AddField( + model_name='electrictariffoutputs', + name='lifecycle_energy_cost_after_tax', + field=models.FloatField(blank=True, help_text='Optimal life cycle utility energy cost, after-tax', null=True), + ), + migrations.AddField( + model_name='electrictariffoutputs', + name='lifecycle_energy_cost_after_tax_bau', + field=models.FloatField(blank=True, help_text='Business as usual life cycle utility energy cost, after-tax', null=True), + ), + migrations.AddField( + model_name='electrictariffoutputs', + name='lifecycle_export_benefit_after_tax', + field=models.FloatField(blank=True, help_text='Optimal life cycle value of exported energy, after-tax', null=True), + ), + migrations.AddField( + model_name='electrictariffoutputs', + name='lifecycle_export_benefit_after_tax_bau', + field=models.FloatField(blank=True, help_text='Business as usual life cycle value of exported energy, after-tax', null=True), + ), + migrations.AddField( + model_name='electrictariffoutputs', + name='lifecycle_fixed_cost_after_tax', + field=models.FloatField(blank=True, help_text='Optimal life cycle utility fixed cost, after-tax', null=True), + ), + migrations.AddField( + model_name='electrictariffoutputs', + name='lifecycle_fixed_cost_after_tax_bau', + field=models.FloatField(blank=True, help_text='Business as usual life cycle utility fixed cost, after-tax', null=True), + ), + migrations.AddField( + model_name='electrictariffoutputs', + name='lifecycle_min_charge_adder_after_tax', + field=models.FloatField(blank=True, help_text='Optimal life cycle utility minimum charge adder, after-tax', null=True), + ), + migrations.AddField( + model_name='electrictariffoutputs', + name='lifecycle_min_charge_adder_after_tax_bau', + field=models.FloatField(blank=True, help_text='Business as usual life cycle utility minimum charge adder, after-tax', null=True), + ), + migrations.AddField( + model_name='electrictariffoutputs', + name='year_one_bill_before_tax', + field=models.FloatField(blank=True, help_text='Optimal year one utility bill', null=True), + ), + migrations.AddField( + model_name='electrictariffoutputs', + name='year_one_bill_before_tax_bau', + field=models.FloatField(blank=True, help_text='Business as usual year one utility bill', null=True), + ), + migrations.AddField( + model_name='electrictariffoutputs', + name='year_one_chp_standby_cost_before_tax', + field=models.FloatField(blank=True, help_text='Optimal year one standby charge cost incurred by CHP', null=True), + ), + migrations.AddField( + model_name='electrictariffoutputs', + name='year_one_coincident_peak_cost_before_tax', + field=models.FloatField(blank=True, help_text='Optimal year one coincident peak charges', null=True), + ), + migrations.AddField( + model_name='electrictariffoutputs', + name='year_one_coincident_peak_cost_before_tax_bau', + field=models.FloatField(blank=True, help_text='Business as usual year one coincident peak charges', null=True), + ), + migrations.AddField( + model_name='electrictariffoutputs', + name='year_one_demand_cost_before_tax', + field=models.FloatField(blank=True, help_text='Optimal year one utility demand cost', null=True), + ), + migrations.AddField( + model_name='electrictariffoutputs', + name='year_one_demand_cost_before_tax_bau', + field=models.FloatField(blank=True, help_text='Business as usual year one utility demand cost', null=True), + ), + migrations.AddField( + model_name='electrictariffoutputs', + name='year_one_energy_cost_before_tax', + field=models.FloatField(blank=True, help_text='Optimal year one utility energy cost', null=True), + ), + migrations.AddField( + model_name='electrictariffoutputs', + name='year_one_energy_cost_before_tax_bau', + field=models.FloatField(blank=True, help_text='Business as usual year one utility energy cost', null=True), + ), + migrations.AddField( + model_name='electrictariffoutputs', + name='year_one_export_benefit_before_tax', + field=models.FloatField(blank=True, help_text='Optimal year one value of exported energy', null=True), + ), + migrations.AddField( + model_name='electrictariffoutputs', + name='year_one_export_benefit_before_tax_bau', + field=models.FloatField(blank=True, help_text='Business as usual year one value of exported energy', null=True), + ), + migrations.AddField( + model_name='electrictariffoutputs', + name='year_one_fixed_cost_before_tax', + field=models.FloatField(blank=True, help_text='Optimal year one utility fixed cost', null=True), + ), + migrations.AddField( + model_name='electrictariffoutputs', + name='year_one_fixed_cost_before_tax_bau', + field=models.FloatField(blank=True, help_text='Business as usual year one utility fixed cost', null=True), + ), + migrations.AddField( + model_name='electrictariffoutputs', + name='year_one_min_charge_adder_before_tax', + field=models.FloatField(blank=True, help_text='Optimal year one utility minimum charge adder', null=True), + ), + migrations.AddField( + model_name='electrictariffoutputs', + name='year_one_min_charge_adder_before_tax_bau', + field=models.FloatField(blank=True, help_text='Business as usual year one utility minimum charge adder', null=True), + ), + migrations.AddField( + model_name='financialinputs', + name='offgrid_other_annual_costs', + field=models.FloatField(blank=True, default=0.0, help_text='Only applicable when off_grid_flag is true. Considered tax deductible for owner. Costs are per year.', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1000000.0)]), + ), + migrations.AddField( + model_name='financialinputs', + name='offgrid_other_capital_costs', + field=models.FloatField(blank=True, default=0.0, help_text='Only applicable when off_grid_flag is true, applies a straight-line depreciation to this capex cost, reducing taxable income.', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1000000.0)]), + ), + migrations.AddField( + model_name='financialoutputs', + name='lifecycle_MG_upgrade_and_fuel_cost', + field=models.FloatField(blank=True, help_text='Component of lifecycle costs, this is the cost to upgrade generation and storage technologies to be included in microgridplus present value of microgrid fuel costs.', null=True), + ), + migrations.AddField( + model_name='financialoutputs', + name='lifecycle_capital_costs_plus_om_after_tax', + field=models.FloatField(blank=True, help_text='Capital cost for all technologies plus present value of operations and maintenance over anlaysis period', null=True), + ), + migrations.AddField( + model_name='financialoutputs', + name='lifecycle_chp_standby_cost_after_tax', + field=models.FloatField(blank=True, help_text='Component of lifecycle costs, this value is the present value of all CHP standby charges, after tax.', null=True), + ), + migrations.AddField( + model_name='financialoutputs', + name='lifecycle_elecbill_after_tax', + field=models.FloatField(blank=True, help_text='Component of lifecycle costs, this value is the present value of all electric utility charges, after tax.', null=True), + ), + migrations.AddField( + model_name='financialoutputs', + name='lifecycle_fuel_costs_after_tax', + field=models.FloatField(blank=True, help_text='Component of lifecycle costs, this value is the present value of all fuel costs over the analysis period, after tax.', null=True), + ), + migrations.AddField( + model_name='financialoutputs', + name='lifecycle_generation_tech_capital_costs', + field=models.FloatField(blank=True, help_text='Component of lifecycle costs, this value is the net capital costs for all generation technologiesCosts are given in present value, including replacement costs and incentives.This value does not include offgrid_other_capital_costs.', null=True), + ), + migrations.AddField( + model_name='financialoutputs', + name='lifecycle_offgrid_other_annual_costs_after_tax', + field=models.FloatField(blank=True, help_text='Component of lifecycle costs, this value is the present value of offgrid_other_annual_costs over the analysis period, after tax.', null=True), + ), + migrations.AddField( + model_name='financialoutputs', + name='lifecycle_offgrid_other_capital_costs', + field=models.FloatField(blank=True, help_text="Component of lifecycle costs, this value is equal to offgrid_other_capital_costs with straight line depreciation applied over analysis period. The depreciation expense is assumed to reduce the owner's taxable income.", null=True), + ), + migrations.AddField( + model_name='financialoutputs', + name='lifecycle_om_costs_before_tax_bau', + field=models.FloatField(blank=True, help_text='Business-as-usual present value of operations and maintenance over analysis period', null=True), + ), + migrations.AddField( + model_name='financialoutputs', + name='lifecycle_outage_cost', + field=models.FloatField(blank=True, help_text='Component of lifecycle costs, expected outage cost.', null=True), + ), + migrations.AddField( + model_name='financialoutputs', + name='lifecycle_production_incentive_after_tax', + field=models.FloatField(blank=True, help_text='Component of lifecycle costs, this value is the present value of all production-based incentives, after tax.', null=True), + ), + migrations.AddField( + model_name='financialoutputs', + name='lifecycle_storage_capital_costs', + field=models.FloatField(blank=True, help_text='Component of lifecycle costs, this value is the Net capital costs for all storage technologiesValue is in present value, including replacement costs and incentives.This value does not include offgrid_other_capital_costs.', null=True), + ), + migrations.AddField( + model_name='financialoutputs', + name='offgrid_microgrid_lcoe_dollars_per_kwh', + field=models.FloatField(blank=True, help_text='Levelized cost of electricity for modeled off-grid system.', null=True), + ), + migrations.AddField( + model_name='financialoutputs', + name='replacements_future_cost_after_tax', + field=models.FloatField(blank=True, help_text='Future cost of replacing storage and/or generator systems, after tax.', null=True), + ), + migrations.AddField( + model_name='financialoutputs', + name='replacements_present_cost_after_tax', + field=models.FloatField(blank=True, help_text='Present value cost of replacing storage and/or generator systems, after tax.', null=True), + ), + migrations.AddField( + model_name='generatorinputs', + name='replace_cost_per_kw', + field=models.FloatField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1000000000.0)]), + ), + migrations.AddField( + model_name='generatorinputs', + name='replacement_year', + field=models.IntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(100)]), + ), + migrations.AddField( + model_name='generatoroutputs', + name='average_annual_fuel_used_gal', + field=models.FloatField(blank=True, null=True), + ), + migrations.AddField( + model_name='generatoroutputs', + name='lifecycle_fixed_om_cost_after_tax', + field=models.FloatField(blank=True, null=True), + ), + migrations.AddField( + model_name='generatoroutputs', + name='lifecycle_fuel_cost_after_tax', + field=models.FloatField(blank=True, null=True), + ), + migrations.AddField( + model_name='generatoroutputs', + name='lifecycle_variable_om_cost_after_tax', + field=models.FloatField(blank=True, null=True), + ), + migrations.AddField( + model_name='generatoroutputs', + name='year_one_fixed_om_cost_before_tax', + field=models.FloatField(blank=True, null=True), + ), + migrations.AddField( + model_name='generatoroutputs', + name='year_one_fuel_cost_before_tax', + field=models.FloatField(blank=True, null=True), + ), + migrations.AddField( + model_name='generatoroutputs', + name='year_one_variable_om_cost_before_tax', + field=models.FloatField(blank=True, null=True), + ), + migrations.AddField( + model_name='pvinputs', + name='operating_reserve_required_pct', + field=models.FloatField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1.0)]), + ), + migrations.AddField( + model_name='pvoutputs', + name='lifecycle_om_cost_after_tax', + field=models.FloatField(blank=True, null=True), + ), + migrations.AddField( + model_name='settings', + name='off_grid_flag', + field=models.BooleanField(blank=True, default=False, help_text='Set to true to enable off-grid analyses'), + ), + migrations.AddField( + model_name='windoutputs', + name='lifecycle_om_cost_after_tax', + field=models.FloatField(blank=True, null=True), + ), + migrations.AddField( + model_name='windoutputs', + name='year_one_om_cost_before_tax', + field=models.FloatField(blank=True, null=True), + ), + migrations.AlterField( + model_name='electricloadinputs', + name='critical_load_pct', + field=models.FloatField(blank=True, help_text='Critical load factor is multiplied by the typical load to determine the critical load that must be met during an outage. Value must be between zero and one, inclusive.', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(2)]), + ), + migrations.AlterField( + model_name='electricstorageinputs', + name='can_grid_charge', + field=models.BooleanField(blank=True, help_text='Flag to set whether the battery can be charged from the grid, or just onsite generation.'), + ), + migrations.AlterField( + model_name='electricstorageinputs', + name='soc_init_pct', + field=models.FloatField(blank=True, help_text='Battery state of charge at first hour of optimization as fraction of energy capacity.', validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1.0)]), + ), + migrations.AlterField( + model_name='electrictariffinputs', + name='coincident_peak_load_charge_per_kw', + field=django.contrib.postgres.fields.ArrayField(base_field=models.FloatField(blank=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(100000000.0)]), blank=True, default=list, help_text='Optional coincident peak demand charge that is applied to the max load during the time_steps specified in coincident_peak_load_active_time_steps', null=True, size=None), + ), + migrations.AlterField( + model_name='financialinputs', + name='microgrid_upgrade_cost_pct', + field=models.FloatField(blank=True, help_text='Additional cost, in percent of non-islandable capital costs, to make a distributed energy system islandable from the grid and able to serve critical loads. Includes all upgrade costs such as additional laber and critical load panels.', validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1)]), + ), + migrations.AlterField( + model_name='financialoutputs', + name='lifecycle_om_costs_after_tax', + field=models.FloatField(blank=True, help_text='Component of lifecycle costs, this value is the present value of all O&M costs, after tax.', null=True), + ), + migrations.AlterField( + model_name='generatorinputs', + name='fuel_avail_gal', + field=models.FloatField(blank=True, help_text='On-site generator fuel available in gallons.', validators=[django.core.validators.MinValueValidator(0.0), django.core.validators.MaxValueValidator(1000000000.0)]), + ), + migrations.AlterField( + model_name='generatorinputs', + name='min_turn_down_pct', + field=models.FloatField(blank=True, help_text='Minimum generator loading in percent of capacity (size_kw).', validators=[django.core.validators.MinValueValidator(0.0), django.core.validators.MaxValueValidator(1.0)]), + ), + migrations.AlterField( + model_name='pvinputs', + name='can_export_beyond_nem_limit', + field=models.BooleanField(blank=True, 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).'), + ), + migrations.AlterField( + model_name='pvinputs', + name='can_net_meter', + field=models.BooleanField(blank=True, 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).'), + ), + migrations.AlterField( + model_name='pvinputs', + name='can_wholesale', + field=models.BooleanField(blank=True, 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).'), + ), + migrations.AlterField( + model_name='settings', + name='run_bau', + field=models.BooleanField(blank=True, default=True, help_text='If True then the Business-As-Usual scenario is also solved to provide additional outputs such as the NPV and BAU costs.', null=True), + ), + ] diff --git a/job/models.py b/job/models.py index 4965ee4dd..23aa242e0 100644 --- a/job/models.py +++ b/job/models.py @@ -202,11 +202,13 @@ class TIME_STEP_CHOICES(models.IntegerChoices): ], help_text="The number of seconds allowed before the optimization times out." ) + time_steps_per_hour = models.IntegerField( default=TIME_STEP_CHOICES.ONE, choices=TIME_STEP_CHOICES.choices, help_text="The number of time steps per hour in the REopt model." ) + optimality_tolerance = models.FloatField( default=0.001, validators=[ @@ -216,19 +218,31 @@ class TIME_STEP_CHOICES(models.IntegerChoices): help_text=("The threshold for the difference between the solution's objective value and the best possible " "value at which the solver terminates") ) + add_soc_incentive = models.BooleanField( default=True, blank=True, help_text=("If True, then a small incentive to keep the battery's state of charge high is added to the " "objective of the optimization.") ) + run_bau = models.BooleanField( - default=True, blank=True, + null=True, + default=True, help_text=("If True then the Business-As-Usual scenario is also solved to provide additional outputs such as " - "the LCC and BAU costs.") + "the NPV and BAU costs.") + ) + + off_grid_flag = models.BooleanField( + default=False, + blank=True, + help_text=("Set to true to enable off-grid analyses") ) + def clean(self): + if self.off_grid_flag: + self.run_bau = False class SiteInputs(BaseModel, models.Model): key = "Site" @@ -400,7 +414,6 @@ class FinancialInputs(BaseModel, models.Model): "critical load.") ) microgrid_upgrade_cost_pct = models.FloatField( - default=0.3, validators=[ MinValueValidator(0), MaxValueValidator(1) @@ -410,6 +423,29 @@ class FinancialInputs(BaseModel, models.Model): "islandable from the grid and able to serve critical loads. Includes all upgrade costs such as " "additional laber and critical load panels.") ) + + offgrid_other_capital_costs = models.FloatField( + validators=[ + MinValueValidator(0), + MaxValueValidator(1e6) + ], + blank=True, + null=True, + default=0.0, + help_text=("Only applicable when off_grid_flag is true, applies a straight-line depreciation to this capex cost, reducing taxable income.") + ) + + offgrid_other_annual_costs = models.FloatField( + validators=[ + MinValueValidator(0), + MaxValueValidator(1e6) + ], + blank=True, + null=True, + default=0.0, + help_text=("Only applicable when off_grid_flag is true. Considered tax deductible for owner. Costs are per year.") + ) + # boiler_fuel_escalation_pct = models.FloatField( # default=0.034, # validators=[ @@ -457,11 +493,11 @@ class FinancialOutputs(BaseModel, models.Model): null=True, blank=True, help_text="Net present value of savings realized by the project" ) - lifecycle_capital_costs_plus_om = models.FloatField( + lifecycle_capital_costs_plus_om_after_tax = models.FloatField( null=True, blank=True, help_text="Capital cost for all technologies plus present value of operations and maintenance over anlaysis period" ) - lifecycle_om_costs_bau = models.FloatField( + lifecycle_om_costs_before_tax_bau = models.FloatField( null=True, blank=True, help_text="Business-as-usual present value of operations and maintenance over analysis period", ) @@ -577,7 +613,69 @@ class FinancialOutputs(BaseModel, models.Model): help_text=("Net O&M and replacement costs in present value, after-tax for the third-party developer." "Only calculated in the third-party case.") ) + lifecycle_generation_tech_capital_costs = models.FloatField( + null=True, blank=True, + help_text=("Component of lifecycle costs, this value is the net capital costs for all generation technologies" + "Costs are given in present value, including replacement costs and incentives." + "This value does not include offgrid_other_capital_costs.") + ) + lifecycle_storage_capital_costs = models.FloatField( + null=True, blank=True, + help_text=("Component of lifecycle costs, this value is the Net capital costs for all storage technologies" + "Value is in present value, including replacement costs and incentives." + "This value does not include offgrid_other_capital_costs.") + ) + lifecycle_om_costs_after_tax = models.FloatField( + null=True, blank=True, + help_text=("Component of lifecycle costs, this value is the present value of all O&M costs, after tax.") + ) + lifecycle_fuel_costs_after_tax = models.FloatField( + null=True, blank=True, + help_text=("Component of lifecycle costs, this value is the present value of all fuel costs over the analysis period, after tax.") + ) + lifecycle_chp_standby_cost_after_tax = models.FloatField( + null=True, blank=True, + help_text=("Component of lifecycle costs, this value is the present value of all CHP standby charges, after tax.") + ) + lifecycle_elecbill_after_tax = models.FloatField( + null=True, blank=True, + help_text=("Component of lifecycle costs, this value is the present value of all electric utility charges, after tax.") + ) + lifecycle_production_incentive_after_tax = models.FloatField( + null=True, blank=True, + help_text=("Component of lifecycle costs, this value is the present value of all production-based incentives, after tax.") + ) + lifecycle_offgrid_other_annual_costs_after_tax = models.FloatField( + null=True, blank=True, + help_text=("Component of lifecycle costs, this value is the present value of offgrid_other_annual_costs over the analysis period, after tax.") + ) + lifecycle_offgrid_other_capital_costs = models.FloatField( + null=True, blank=True, + help_text=("Component of lifecycle costs, this value is equal to offgrid_other_capital_costs with straight line depreciation applied" + " over analysis period. The depreciation expense is assumed to reduce the owner's taxable income.") + ) + lifecycle_outage_cost = models.FloatField( + null=True, blank=True, + help_text=("Component of lifecycle costs, expected outage cost.") + ) + lifecycle_MG_upgrade_and_fuel_cost = models.FloatField( + null=True, blank=True, + help_text=("Component of lifecycle costs, this is the cost to upgrade generation and storage technologies to be included in microgrid" + "plus present value of microgrid fuel costs.") + ) + replacements_future_cost_after_tax = models.FloatField( + null=True, blank=True, + help_text="Future cost of replacing storage and/or generator systems, after tax." + ) + replacements_present_cost_after_tax = models.FloatField( + null=True, blank=True, + help_text="Present value cost of replacing storage and/or generator systems, after tax." + ) + offgrid_microgrid_lcoe_dollars_per_kwh = models.FloatField( + null=True, blank=True, + help_text="Levelized cost of electricity for modeled off-grid system." + ) class ElectricLoadInputs(BaseModel, models.Model): key = "ElectricLoad" @@ -698,11 +796,34 @@ class ElectricLoadInputs(BaseModel, models.Model): MinValueValidator(0), MaxValueValidator(2) ], - default=0.5, + # default=0.5, help_text="Critical load factor is multiplied by the typical load to determine the critical load that must be " "met during an outage. Value must be between zero and one, inclusive." ) + + operating_reserve_required_pct = models.FloatField( + null=True, + blank=True, + validators=[ + MinValueValidator(0), + MaxValueValidator(1) + ], + help_text="" + + ) + + min_load_met_annual_pct = models.FloatField( + null=True, + blank=True, + validators=[ + MinValueValidator(0), + MaxValueValidator(1) + ], + help_text="" + + ) + blended_doe_reference_names = ArrayField( models.TextField( choices=DOE_REFERENCE_NAME.choices, @@ -799,7 +920,31 @@ class ElectricLoadOutputs(BaseModel, models.Model): null=True, blank=True, help_text="Number of time steps the existing system can sustain the critical load." ) - + offgrid_load_met_pct = models.FloatField( + null=True, blank=True, + help_text="Percentage of total electric load met on an annual basis, for off-grid scenarios only" + ) + offgrid_annual_oper_res_required_series_kwh = ArrayField( + models.FloatField( + null=True, blank=True + ), + default=list, + help_text="Total operating reserves required on an annual basis, for off-grid scenarios only" + ) + offgrid_annual_oper_res_provided_series_kwh = ArrayField( + models.FloatField( + null=True, blank=True + ), + default=list, + help_text="Total operating reserves provided on an annual basis, for off-grid scenarios only" + ) + offgrid_load_met_series_kw = ArrayField( + models.FloatField( + null=True, blank=True + ), + default=list, + help_text="Percentage of total electric load met on an annual basis, for off-grid scenarios only" + ) class ElectricTariffInputs(BaseModel, models.Model): key = "ElectricTariff" @@ -909,7 +1054,7 @@ class ElectricTariffInputs(BaseModel, models.Model): "will only be considered if a URDB rate is not provided.") ) - coincident_peak_load_active_timesteps = ArrayField( + coincident_peak_load_active_time_steps = ArrayField( ArrayField( models.IntegerField(blank=True), blank=True, @@ -933,8 +1078,8 @@ class ElectricTariffInputs(BaseModel, models.Model): ), null=True, blank=True, default=list, - help_text=("Optional coincident peak demand charge that is applied to the max load during the timesteps " - "specified in coincident_peak_load_active_timesteps") + help_text=("Optional coincident peak demand charge that is applied to the max load during the time_steps " + "specified in coincident_peak_load_active_time_steps") ) # chp_does_not_reduce_demand_charges = models.BooleanField( # default=False, @@ -955,7 +1100,7 @@ class ElectricTariffInputs(BaseModel, models.Model): # null=True, blank=True, # default=list, # help_text=("Carbon Dioxide emissions factor over all hours in one year. Can be provided as either a single " - # "constant fraction that will be applied across all timesteps, or an annual timeseries array at an " + # "constant fraction that will be applied across all time_steps, or an annual timeseries array at an " # "hourly (8,760 samples), 30 minute (17,520 samples), or 15 minute (35,040 samples) resolution.") # ) @@ -965,7 +1110,7 @@ def clean(self): # possible sets for defining tariff if not at_least_one_set(self.dict, self.possible_sets): error_messages["required inputs"] = \ - f"Must provide at least one set of valid inputs from {self.possible_sets}." + f"Must provide at least one set of valid inputs from {self.possible_sets}. If this is an off-grid analysis, ElectricTariff inputs will not be used in REopt, and can be removed from input JSON." for possible_set in self.possible_sets: if len(possible_set) == 2: # check dependencies @@ -976,10 +1121,10 @@ def clean(self): self.wholesale_rate = self.wholesale_rate * 8760 # upsampling handled in InputValidator.cross_clean if len(self.coincident_peak_load_charge_per_kw) > 0: - if len(self.coincident_peak_load_active_timesteps) != len(self.coincident_peak_load_charge_per_kw): + if len(self.coincident_peak_load_active_time_steps) != len(self.coincident_peak_load_charge_per_kw): error_messages["coincident peak"] = ( "The number of rates in coincident_peak_load_charge_per_kw must match the number of " - "timestep sets in coincident_peak_load_active_timesteps") + "timestep sets in coincident_peak_load_active_time_steps") if self.urdb_label is not None: label_checker = URDB_LabelValidator(self.urdb_label) @@ -998,15 +1143,15 @@ def clean(self): def save(self, *args, **kwargs): """ - Special case for coincident_peak_load_active_timesteps: back-end database requires that + Special case for coincident_peak_load_active_time_steps: back-end database requires that "multidimensional arrays must have array expressions with matching dimensions" so we fill the arrays that are shorter than the longest arrays with repeats of the last value. By repeating the last value we do not have to deal with a mix of data types in the arrays and it does not affect the constraints in REopt. """ - if len(self.coincident_peak_load_active_timesteps) > 0: - max_length = max(len(inner_array) for inner_array in self.coincident_peak_load_active_timesteps) - for inner_array in self.coincident_peak_load_active_timesteps: + if len(self.coincident_peak_load_active_time_steps) > 0: + max_length = max(len(inner_array) for inner_array in self.coincident_peak_load_active_time_steps) + for inner_array in self.coincident_peak_load_active_time_steps: if len(inner_array) != max_length: for _ in range(max_length - len(inner_array)): inner_array.append(inner_array[-1]) @@ -1020,10 +1165,10 @@ def dict(self): :return: dict """ d = copy.deepcopy(self.__dict__) - if "coincident_peak_load_active_timesteps" in d.keys(): + if "coincident_peak_load_active_time_steps" in d.keys(): # filter out repeated values created to make the inner arrays have equal length - d["coincident_peak_load_active_timesteps"] = \ - [list(set(l)) for l in d["coincident_peak_load_active_timesteps"]] + d["coincident_peak_load_active_time_steps"] = \ + [list(set(l)) for l in d["coincident_peak_load_active_time_steps"]] d.pop("_state", None) d.pop("id", None) d.pop("basemodel_ptr_id", None) @@ -1164,91 +1309,91 @@ class ElectricTariffOutputs(BaseModel, models.Model): null=True, blank=True ) - year_one_energy_cost = models.FloatField( + year_one_energy_cost_before_tax = models.FloatField( null=True, blank=True, help_text="Optimal year one utility energy cost" ) - year_one_demand_cost = models.FloatField( + year_one_demand_cost_before_tax = models.FloatField( null=True, blank=True, help_text="Optimal year one utility demand cost" ) - year_one_fixed_cost = models.FloatField( + year_one_fixed_cost_before_tax = models.FloatField( null=True, blank=True, help_text="Optimal year one utility fixed cost" ) - year_one_min_charge_adder = models.FloatField( + year_one_min_charge_adder_before_tax = models.FloatField( null=True, blank=True, help_text="Optimal year one utility minimum charge adder" ) - year_one_energy_cost_bau = models.FloatField( + year_one_energy_cost_before_tax_bau = models.FloatField( null=True, blank=True, help_text="Business as usual year one utility energy cost" ) - year_one_demand_cost_bau = models.FloatField( + year_one_demand_cost_before_tax_bau = models.FloatField( null=True, blank=True, help_text="Business as usual year one utility demand cost" ) - year_one_fixed_cost_bau = models.FloatField( + year_one_fixed_cost_before_tax_bau = models.FloatField( null=True, blank=True, help_text="Business as usual year one utility fixed cost" ) - year_one_min_charge_adder_bau = models.FloatField( + year_one_min_charge_adder_before_tax_bau = models.FloatField( null=True, blank=True, help_text="Business as usual year one utility minimum charge adder" ) - lifecycle_energy_cost = models.FloatField( + lifecycle_energy_cost_after_tax = models.FloatField( null=True, blank=True, help_text="Optimal life cycle utility energy cost, after-tax" ) - lifecycle_demand_cost = models.FloatField( + lifecycle_demand_cost_after_tax = models.FloatField( null=True, blank=True, help_text="Optimal life cycle utility demand cost, after-tax" ) - lifecycle_fixed_cost = models.FloatField( + lifecycle_fixed_cost_after_tax = models.FloatField( null=True, blank=True, help_text="Optimal life cycle utility fixed cost, after-tax" ) - lifecycle_min_charge_adder = models.FloatField( + lifecycle_min_charge_adder_after_tax = models.FloatField( null=True, blank=True, help_text="Optimal life cycle utility minimum charge adder, after-tax" ) - lifecycle_energy_cost_bau = models.FloatField( + lifecycle_energy_cost_after_tax_bau = models.FloatField( null=True, blank=True, help_text="Business as usual life cycle utility energy cost, after-tax" ) - lifecycle_demand_cost_bau = models.FloatField( + lifecycle_demand_cost_after_tax_bau = models.FloatField( null=True, blank=True, help_text="Business as usual life cycle lifecycle utility demand cost, after-tax" ) - lifecycle_fixed_cost_bau = models.FloatField( + lifecycle_fixed_cost_after_tax_bau = models.FloatField( null=True, blank=True, help_text="Business as usual life cycle utility fixed cost, after-tax" ) - lifecycle_min_charge_adder_bau = models.FloatField( + lifecycle_min_charge_adder_after_tax_bau = models.FloatField( null=True, blank=True, help_text="Business as usual life cycle utility minimum charge adder, after-tax" ) - lifecycle_export_benefit = models.FloatField( + lifecycle_export_benefit_after_tax = models.FloatField( null=True, blank=True, help_text="Optimal life cycle value of exported energy, after-tax" ) - lifecycle_export_benefit_bau = models.FloatField( + lifecycle_export_benefit_after_tax_bau = models.FloatField( null=True, blank=True, help_text="Business as usual life cycle value of exported energy, after-tax" ) - year_one_bill = models.FloatField( + year_one_bill_before_tax = models.FloatField( null=True, blank=True, help_text="Optimal year one utility bill" ) - year_one_bill_bau = models.FloatField( + year_one_bill_before_tax_bau = models.FloatField( null=True, blank=True, help_text="Business as usual year one utility bill" ) - year_one_export_benefit = models.FloatField( + year_one_export_benefit_before_tax = models.FloatField( null=True, blank=True, help_text="Optimal year one value of exported energy" ) - year_one_export_benefit_bau = models.FloatField( + year_one_export_benefit_before_tax_bau = models.FloatField( null=True, blank=True, help_text="Business as usual year one value of exported energy" ) @@ -1266,27 +1411,27 @@ class ElectricTariffOutputs(BaseModel, models.Model): default=list, blank=True, help_text="Optimal year one hourly demand costs" ) - year_one_coincident_peak_cost = models.FloatField( + year_one_coincident_peak_cost_before_tax = models.FloatField( null=True, blank=True, help_text="Optimal year one coincident peak charges" ) - year_one_coincident_peak_cost_bau = models.FloatField( + year_one_coincident_peak_cost_before_tax_bau = models.FloatField( null=True, blank=True, help_text="Business as usual year one coincident peak charges" ) - lifecycle_coincident_peak_cost = models.FloatField( + lifecycle_coincident_peak_cost_after_tax = models.FloatField( null=True, blank=True, help_text="Optimal total coincident peak charges over the analysis period, after-tax" ) - lifecycle_coincident_peak_cost_bau = models.FloatField( + lifecycle_coincident_peak_cost_after_tax_bau = models.FloatField( null=True, blank=True, help_text="Business as usual life cycle coincident peak charges, after-tax" ) - year_one_chp_standby_cost = models.FloatField( + year_one_chp_standby_cost_before_tax = models.FloatField( null=True, blank=True, help_text="Optimal year one standby charge cost incurred by CHP" ) - lifecycle_chp_standby_cost = models.FloatField( + lifecycle_chp_standby_cost_after_tax = models.FloatField( null=True, blank=True, help_text="Optimal life cycle standby charge cost incurred by CHP, after-tax" ) @@ -1620,19 +1765,16 @@ class PV_LOCATION_CHOICES(models.TextChoices): "generation data.") ) can_net_meter = models.BooleanField( - default=True, blank=True, 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).") ) can_wholesale = models.BooleanField( - default=True, blank=True, 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).") ) can_export_beyond_nem_limit = models.BooleanField( - default=True, blank=True, 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).") @@ -1643,6 +1785,16 @@ class PV_LOCATION_CHOICES(models.TextChoices): help_text="True/False for if technology has the ability to curtail energy production." ) + operating_reserve_required_pct = models.FloatField( + validators=[ + MinValueValidator(0), + MaxValueValidator(1.0) + ], + blank=True, + null=True, + help_text="" + ) + class PVOutputs(BaseModel, models.Model): key = "PVOutputs" @@ -1658,7 +1810,7 @@ class PVOutputs(BaseModel, models.Model): help_text="PV description for distinguishing between multiple PV models" ) size_kw = models.FloatField(null=True, blank=True) - lifecycle_om_cost = models.FloatField(null=True, blank=True) + lifecycle_om_cost_after_tax = models.FloatField(null=True, blank=True) lifecycle_om_cost_bau = models.FloatField(null=True, blank=True) # station_latitude = models.FloatField(null=True, blank=True) # station_longitude = models.FloatField(null=True, blank=True) @@ -1966,8 +2118,8 @@ class WindOutputs(BaseModel, models.Model): ) size_kw = models.FloatField(null=True, blank=True) - lifecycle_om_cost = models.FloatField(null=True, blank=True) - year_one_om_cost = models.FloatField(null=True, blank=True) + lifecycle_om_cost_after_tax = models.FloatField(null=True, blank=True) + year_one_om_cost_before_tax = models.FloatField(null=True, blank=True) average_annual_energy_produced_kwh = models.FloatField(null=True, blank=True) average_annual_energy_exported_kwh = models.FloatField(null=True, blank=True) year_one_energy_produced_kwh = models.FloatField(null=True, blank=True) @@ -2065,7 +2217,6 @@ class ElectricStorageInputs(BaseModel, models.Model): help_text="Minimum allowable battery state of charge as fraction of energy capacity." ) soc_init_pct = models.FloatField( - default=0.5, validators=[ MinValueValidator(0), MaxValueValidator(1.0) @@ -2074,7 +2225,6 @@ class ElectricStorageInputs(BaseModel, models.Model): help_text="Battery state of charge at first hour of optimization as fraction of energy capacity." ) can_grid_charge = models.BooleanField( - default=True, blank=True, help_text="Flag to set whether the battery can be charged from the grid, or just onsite generation." ) @@ -2303,7 +2453,6 @@ class GeneratorInputs(BaseModel, models.Model): help_text="Generator fuel consumption curve y-intercept in gallons per hour." ) fuel_avail_gal = models.FloatField( - default=660.0, validators=[ MinValueValidator(0.0), MaxValueValidator(1.0e9) @@ -2312,7 +2461,6 @@ class GeneratorInputs(BaseModel, models.Model): help_text="On-site generator fuel available in gallons." ) min_turn_down_pct = models.FloatField( - default=0.0, validators=[ MinValueValidator(0.0), MaxValueValidator(1.0) @@ -2503,7 +2651,25 @@ class GeneratorInputs(BaseModel, models.Model): blank=True, help_text="True/False for if technology has the ability to curtail energy production." ) - # emissions_factor_lb_CO2_per_gal = models.FloatField(null=True, blank=True) + replacement_year = models.IntegerField( + validators=[ + MinValueValidator(0), + MaxValueValidator(100) + ], + blank=True, + null=True, + help_text="" + ) + + replace_cost_per_kw = models.FloatField( + validators=[ + MinValueValidator(0), + MaxValueValidator(1.0e9) + ], + blank=True, + null=True, + help_text="" + ) def clean(self): if self.max_kw > 0 or self.existing_kw > 0: @@ -2544,7 +2710,7 @@ class GeneratorOutputs(BaseModel, models.Model): primary_key=True ) - fuel_used_gal = models.FloatField(null=True, blank=True) + average_annual_fuel_used_gal = models.FloatField(null=True, blank=True) fuel_used_gal_bau = models.FloatField(null=True, blank=True) size_kw = models.FloatField(null=True, blank=True) average_annual_energy_produced_kwh = models.FloatField(null=True, blank=True) @@ -2556,17 +2722,17 @@ class GeneratorOutputs(BaseModel, models.Model): models.FloatField(null=True, blank=True), null=True, blank=True, default=list) year_one_to_grid_series_kw = ArrayField( models.FloatField(null=True, blank=True), null=True, blank=True, default=list) - year_one_variable_om_cost = models.FloatField(null=True, blank=True) + year_one_variable_om_cost_before_tax = models.FloatField(null=True, blank=True) year_one_variable_om_cost_bau = models.FloatField(null=True, blank=True) - year_one_fuel_cost = models.FloatField(null=True, blank=True) + year_one_fuel_cost_before_tax = models.FloatField(null=True, blank=True) year_one_fuel_cost_bau = models.FloatField(null=True, blank=True) - year_one_fixed_om_cost = models.FloatField(null=True, blank=True) + year_one_fixed_om_cost_before_tax = models.FloatField(null=True, blank=True) year_one_fixed_om_cost_bau = models.FloatField(null=True, blank=True) - lifecycle_variable_om_cost = models.FloatField(null=True, blank=True) + lifecycle_variable_om_cost_after_tax = models.FloatField(null=True, blank=True) lifecycle_variable_om_cost_bau = models.FloatField(null=True, blank=True) - lifecycle_fuel_cost = models.FloatField(null=True, blank=True) + lifecycle_fuel_cost_after_tax = models.FloatField(null=True, blank=True) lifecycle_fuel_cost_bau = models.FloatField(null=True, blank=True) - lifecycle_fixed_om_cost = models.FloatField(null=True, blank=True) + lifecycle_fixed_om_cost_after_tax = models.FloatField(null=True, blank=True) lifecycle_fixed_om_cost_bau = models.FloatField(null=True, blank=True) year_one_emissions_lb_C02 = models.FloatField(null=True, blank=True) year_one_emissions_bau_lb_C02 = models.FloatField(null=True, blank=True) @@ -2603,7 +2769,6 @@ def get_input_dict_from_run_uuid(run_uuid:str): 'FinancialInputs', 'SiteInputs', 'ElectricLoadInputs', - 'ElectricTariffInputs', ).get(run_uuid=run_uuid) def filter_none_and_empty_array(d:dict): @@ -2615,7 +2780,6 @@ def filter_none_and_empty_array(d:dict): d["Financial"] = filter_none_and_empty_array(meta.FinancialInputs.dict) d["Site"] = filter_none_and_empty_array(meta.SiteInputs.dict) d["ElectricLoad"] = filter_none_and_empty_array(meta.ElectricLoadInputs.dict) - d["ElectricTariff"] = filter_none_and_empty_array(meta.ElectricTariffInputs.dict) # We have to try for the following objects because they may or may not be defined try: @@ -2628,6 +2792,10 @@ def filter_none_and_empty_array(d:dict): d["PV"].append(filter_none_and_empty_array(pv.dict)) except: pass + # Try to get electric tariff as it may be missing in off-grid scenarios + try: d["ElectricTariff"] = filter_none_and_empty_array(meta.ElectricTariffInputs.dict) + except: pass + try: d["ElectricUtility"] = filter_none_and_empty_array(meta.ElectricUtilityInputs.dict) except: pass diff --git a/job/src/run_jump_model.py b/job/src/run_jump_model.py index 8cc929638..837295d72 100644 --- a/job/src/run_jump_model.py +++ b/job/src/run_jump_model.py @@ -81,6 +81,8 @@ def run_jump_model(run_uuid): name = 'run_jump_model' data = get_input_dict_from_run_uuid(run_uuid) user_uuid = data.get('user_uuid') + + data.pop('user_uuid',None) # Remove user uuid from inputs dict to avoid downstream errors logger.info("Running JuMP model ...") try: diff --git a/job/test/test_job_endpoint.py b/job/test/test_job_endpoint.py index f96389c7a..04146b4f3 100644 --- a/job/test/test_job_endpoint.py +++ b/job/test/test_job_endpoint.py @@ -108,3 +108,41 @@ def test_pv_and_battery_scenario(self): self.assertAlmostEqual(results["ElectricStorage"]["size_kw"], 55.9, places=1) self.assertAlmostEqual(results["ElectricStorage"]["size_kwh"], 78.9, places=1) + def test_off_grid_defaults(self): + """ + Purpose of this test is to validate off-grid functionality and defaults in the API. + """ + scenario = { + "Settings":{ + "off_grid_flag": True, + "optimality_tolerance":0.05 + }, + "Site": { + "longitude": -118.1164613, + "latitude": 34.5794343 + }, + "PV": {}, + "ElectricStorage":{}, + "ElectricLoad": { + "doe_reference_name": "FlatLoad", + "annual_kwh": 8760.0, + "city": "LosAngeles", + "year": 2017 + } + } + + resp = self.api_client.post('/dev/job/', format='json', data=scenario) + self.assertHttpCreated(resp) + r = json.loads(resp.content) + run_uuid = r.get('run_uuid') + + resp = self.api_client.get(f'/dev/job/{run_uuid}/results') + r = json.loads(resp.content) + results = r["outputs"] + + # Validate that we got off-grid response fields + self.assertAlmostEqual(results["Financial"]["offgrid_microgrid_lcoe_dollars_per_kwh"], 0.337, places=-3) + self.assertAlmostEqual(results["ElectricTariff"]["year_one_bill_before_tax"], 0.0) + self.assertAlmostEqual(results["ElectricLoad"]["offgrid_load_met_pct"], 0.99999, places=-2) + self.assertAlmostEqual(sum(results["ElectricLoad"]["offgrid_load_met_series_kw"]), 8760.0, places=-1) + self.assertAlmostEqual(results["Financial"]["lifecycle_offgrid_other_annual_costs_after_tax"], 0.0, places=-2) \ No newline at end of file diff --git a/job/test/test_validator.py b/job/test/test_validator.py index 36d7c755c..e9111566d 100644 --- a/job/test/test_validator.py +++ b/job/test/test_validator.py @@ -115,3 +115,58 @@ def test_bad_blended_profile_inputs(self): validator.validation_errors['ElectricLoad']['blended_doe_reference_names'][0]) assert("Sum must = 1.0." in validator.validation_errors['ElectricLoad']['blended_doe_reference_percents'][0]) + + def test_off_grid_defaults_overrides(self): + post = { + "Settings":{ + "off_grid_flag": True + }, + "Site": { + "longitude": -118.1164613, + "latitude": 34.5794343 + }, + "PV": {}, + "Generator": { + "installed_cost_per_kw": 700, + "min_kw": 100, + "max_kw": 100 + }, + "ElectricLoad": { + "doe_reference_name": "RetailStore", + "annual_kwh": 10000000.0, + "city": "LosAngeles", + "year": 2017 + }, + "ElectricStorage": {}, + "Financial": {}, + "APIMeta": {} + } + + post["APIMeta"]["run_uuid"] = uuid.uuid4() + + validator = InputValidator(post) + validator.clean_fields() + validator.clean() + validator.cross_clean() + self.assertEquals(validator.is_valid, True) + + self.assertAlmostEqual(validator.models["ElectricLoad"].critical_load_pct, 1.0) + self.assertAlmostEqual(validator.models["Generator"].replacement_year, 10) + self.assertAlmostEqual(validator.models["Generator"].replace_cost_per_kw, validator.models["Generator"].installed_cost_per_kw) + + # Test default overrides below + + post["ElectricLoad"]["critical_load_pct"] = 0.95 + post["Generator"]["replacement_year"] = 7 + post["Generator"]["replace_cost_per_kw"] = 200 + post["APIMeta"]["run_uuid"] = uuid.uuid4() + + validator = InputValidator(post) + validator.clean_fields() + validator.clean() + validator.cross_clean() + self.assertEquals(validator.is_valid, True) + + self.assertAlmostEqual(validator.models["ElectricLoad"].critical_load_pct, 0.95) + self.assertAlmostEqual(validator.models["Generator"].replacement_year, 7) + self.assertAlmostEqual(validator.models["Generator"].replace_cost_per_kw, 200.0) \ No newline at end of file diff --git a/job/validators.py b/job/validators.py index 94fa165c6..8274b9c21 100644 --- a/job/validators.py +++ b/job/validators.py @@ -95,7 +95,7 @@ def __init__(self, raw_inputs: dict): ) self.pvnames = [] required_object_names = [ - "Site", "ElectricLoad", "ElectricTariff" + "Site", "ElectricLoad" ] filtered_user_post = dict() @@ -144,6 +144,11 @@ def messages(self): len(self.models["ElectricLoad"].blended_doe_reference_names) > 0: msg_dict["ignored inputs"] = ("Both doe_reference_name and blended_doe_reference_names were provided for " "ElectricLoad. This is redundant, so only doe_reference_name is being used.") + if self.models["Settings"].off_grid_flag==True: + if "ElectricTariff" in self.models.keys(): + msg_dict["ignored inputs"] = ("ElectricTariff inputs are not applicable when off_grid_flag is true, and will be ignored. " + "Provided ElectricTariff can be removed from inputs") + msg_dict["info"] = ("When off_grid_flag is true, only PV, ElectricStorage, Generator technologies can be modeled.") return msg_dict @property @@ -185,6 +190,7 @@ def clean_fields(self): def clean(self): """ Run all models' clean methods + Run ElectricTariff clean method in cross-clean :return: None """ for model in self.models.values(): @@ -215,13 +221,37 @@ def cross_clean_pv(pvmodel): if len(self.pvnames) > 0: # multiple PV for pvname in self.pvnames: cross_clean_pv(self.models[pvname]) + + if self.models["PV"].__getattribute__("can_net_meter") == None: + if self.models["Settings"].off_grid_flag==False: + self.models["PV"].can_net_meter = True + else: + self.models["PV"].can_net_meter = False + + if self.models["PV"].__getattribute__("can_wholesale") == None: + if self.models["Settings"].off_grid_flag==False: + self.models["PV"].can_wholesale = True + else: + self.models["PV"].can_wholesale = False + + if self.models["PV"].__getattribute__("can_export_beyond_nem_limit") == None: + if self.models["Settings"].off_grid_flag==False: + self.models["PV"].can_export_beyond_nem_limit = True + else: + self.models["PV"].can_export_beyond_nem_limit = False + + if self.models["PV"].__getattribute__("operating_reserve_required_pct") == None: + if self.models["Settings"].off_grid_flag==False: + self.models["PV"].operating_reserve_required_pct = 0.0 + else: + self.models["PV"].operating_reserve_required_pct = 0.25 """ Time series values are up or down sampled to align with Settings.time_steps_per_hour """ for key, time_series in zip( - ["ElectricLoad", "ElectricLoad", "ElectricTariff"], - ["loads_kw", "critical_loads_kw", "wholesale_rate"] + ["ElectricLoad", "ElectricLoad"], + ["loads_kw", "critical_loads_kw"] ): self.clean_time_series(key, time_series) @@ -250,28 +280,61 @@ def cross_clean_pv(pvmodel): """ ElectricTariff + Key can be absent when running off-grid scenarios """ - if len(self.models["ElectricTariff"].tou_energy_rates_per_kwh) > 0: - self.clean_time_series("ElectricTariff", "tou_energy_rates_per_kwh") - - cp_ts_arrays = self.models["ElectricTariff"].__getattribute__("coincident_peak_load_active_timesteps") - max_ts = 8760 * self.models["Settings"].time_steps_per_hour - if len(cp_ts_arrays) > 0: - if len(cp_ts_arrays[0]) > 0: - if any(ts > max_ts for a in cp_ts_arrays for ts in a): - self.add_validation_error("ElectricTariff", "coincident_peak_load_active_timesteps", - f"At least one time step is greater than the max allowable ({max_ts})") + if "ElectricTariff" in self.models.keys(): + + for key, time_series in zip( + ["ElectricTariff", "ElectricTariff"], + ["tou_energy_rates_per_kwh", "wholesale_rate"] + ): + self.clean_time_series(key, time_series) + + cp_ts_arrays = self.models["ElectricTariff"].__getattribute__("coincident_peak_load_active_time_steps") + max_ts = 8760 * self.models["Settings"].time_steps_per_hour + if len(cp_ts_arrays) > 0: + if len(cp_ts_arrays[0]) > 0: + if any(ts > max_ts for a in cp_ts_arrays for ts in a): + self.add_validation_error("ElectricTariff", "coincident_peak_load_active_timesteps", + f"At least one time step is greater than the max allowable ({max_ts})") + + if self.models["ElectricTariff"].urdb_response: + if "energyweekdayschedule" in self.models["ElectricTariff"].urdb_response.keys(): + urdb_rate_timesteps_per_hour = int(len(self.models["ElectricTariff"].urdb_response[ + "energyweekdayschedule"][1]) / 24) + if urdb_rate_timesteps_per_hour > self.models["Settings"].time_steps_per_hour: + # do not support down-sampling tariff + self.add_validation_error("ElectricTariff", "urdb_response", + ("The time steps per hour in the energyweekdayschedule must be no greater " + "than the Settings.time_steps_per_hour.")) - if self.models["ElectricTariff"].urdb_response: - if "energyweekdayschedule" in self.models["ElectricTariff"].urdb_response.keys(): - urdb_rate_timesteps_per_hour = int(len(self.models["ElectricTariff"].urdb_response[ - "energyweekdayschedule"][1]) / 24) - if urdb_rate_timesteps_per_hour > self.models["Settings"].time_steps_per_hour: - # do not support down-sampling tariff - self.add_validation_error("ElectricTariff", "urdb_response", - ("The time steps per hour in the energyweekdayschedule must be no greater " - "than the Settings.time_steps_per_hour.")) + """ + Financial + """ + if "Financial" in self.models.keys(): + if self.models["Financial"].__getattribute__("microgrid_upgrade_cost_pct") == None: + if self.models["Settings"].off_grid_flag==False: + self.models["Financial"].microgrid_upgrade_cost_pct = 0.3 + else: + self.models["Financial"].microgrid_upgrade_cost_pct = 0.0 + + """ + ElectricStorage + """ + if "ElectricStorage" in self.models.keys(): + if self.models["ElectricStorage"].__getattribute__("soc_init_pct") == None: + if self.models["Settings"].off_grid_flag==False: + self.models["ElectricStorage"].soc_init_pct = 0.5 + else: + self.models["ElectricStorage"].soc_init_pct = 1.0 + + if self.models["ElectricStorage"].__getattribute__("can_grid_charge") == None: + if self.models["Settings"].off_grid_flag==False: + self.models["ElectricStorage"].can_grid_charge = True + else: + self.models["ElectricStorage"].can_grid_charge = False + """ ElectricUtility """ @@ -285,6 +348,77 @@ def cross_clean_pv(pvmodel): self.add_validation_error("ElectricUtility", "outage_end_time_step", f"Value is greater than the max allowable ({max_ts})") + """ + ElectricLoad + If user does not provide values, set defaults conditional on off-grid flag + """ + + if self.models["ElectricLoad"].__getattribute__("critical_load_pct") == None: + if self.models["Settings"].off_grid_flag==False: + self.models["ElectricLoad"].critical_load_pct = 0.5 + else: + self.models["ElectricLoad"].critical_load_pct = 1.0 + + if self.models["ElectricLoad"].__getattribute__("operating_reserve_required_pct") == None: + if self.models["Settings"].off_grid_flag==False: + self.models["ElectricLoad"].operating_reserve_required_pct = 0.0 + else: + self.models["ElectricLoad"].operating_reserve_required_pct = 0.1 + + if self.models["ElectricLoad"].__getattribute__("min_load_met_annual_pct") == None: + if self.models["Settings"].off_grid_flag==False: + self.models["ElectricLoad"].min_load_met_annual_pct = 1.0 + else: + self.models["ElectricLoad"].min_load_met_annual_pct = 0.99999 + + """ + Generator + If user does not provide values, set defaults conditional on off-grid flag + """ + if "Generator" in self.models.keys(): + if self.models["Generator"].__getattribute__("fuel_avail_gal") == None: + if self.models["Settings"].off_grid_flag==False: + self.models["Generator"].fuel_avail_gal = 660.0 + else: + self.models["Generator"].fuel_avail_gal = 1.0e9 + + if self.models["Generator"].__getattribute__("min_turn_down_pct") == None: + if self.models["Settings"].off_grid_flag==False: + self.models["Generator"].min_turn_down_pct = 0.0 + else: + self.models["Generator"].min_turn_down_pct = 0.15 + + if self.models["Generator"].__getattribute__("replacement_year") == None: + if self.models["Settings"].off_grid_flag==False: + self.models["Generator"].replacement_year = 25 + else: + self.models["Generator"].replacement_year = 10 + + if self.models["Generator"].__getattribute__("replace_cost_per_kw") == None: + if self.models["Settings"].off_grid_flag==False: + self.models["Generator"].replace_cost_per_kw = 410.0 + else: + self.models["Generator"].replace_cost_per_kw = self.models["Generator"].installed_cost_per_kw + + """ + Off-grid input keys validation + """ + + def validate_offgrid_keys(self): + # From https://github.com/NREL/REopt.jl/blob/4b0fb7f6556b2b6e9a9a7e8fa65398096fb6610f/src/core/scenario.jl#L88 + valid_input_keys_offgrid = ["PV", "ElectricStorage", "Generator", "Settings", "Site", "Financial", "ElectricLoad", "ElectricTariff", "ElectricUtility"] + + invalid_input_keys_offgrid = list(set(list(self.models.keys()))-set(valid_input_keys_offgrid)) + if 'APIMeta' in invalid_input_keys_offgrid: + invalid_input_keys_offgrid.remove('APIMeta') + + if len(invalid_input_keys_offgrid) != 0: + self.add_validation_error("Settings", "off_grid_flag", + f"Currently, off-grid functionality doesn't allow modeling for following keys: ({invalid_input_keys_offgrid})") + + if self.models["Settings"].off_grid_flag==True: + validate_offgrid_keys(self) + def save(self): """ Save all values to database diff --git a/job/views.py b/job/views.py index 43d0f134f..873582014 100644 --- a/job/views.py +++ b/job/views.py @@ -122,7 +122,6 @@ def results(request, run_uuid): 'FinancialInputs', 'FinancialOutputs', 'SiteInputs', 'ElectricLoadInputs', - 'ElectricTariffInputs', 'ElectricTariffOutputs', 'ElectricUtilityOutputs' ).get(run_uuid=run_uuid) except Exception as e: @@ -146,7 +145,6 @@ def results(request, run_uuid): r["inputs"] = dict() r["inputs"]["Financial"] = meta.FinancialInputs.dict r["inputs"]["ElectricLoad"] = meta.ElectricLoadInputs.dict - r["inputs"]["ElectricTariff"] = meta.ElectricTariffInputs.dict r["inputs"]["Site"] = meta.SiteInputs.dict r["inputs"]["Settings"] = meta.Settings.dict @@ -164,6 +162,9 @@ def results(request, run_uuid): try: r["inputs"]["Meta"] = meta.UserProvidedMeta.dict except: pass + try: r["inputs"]["ElectricTariff"] = meta.ElectricTariffInputs.dict + except: pass + try: r["inputs"]["ElectricUtility"] = meta.ElectricUtilityInputs.dict except: pass diff --git a/julia_src/Manifest.toml b/julia_src/Manifest.toml index 8e35e71d9..6a4497678 100644 --- a/julia_src/Manifest.toml +++ b/julia_src/Manifest.toml @@ -1,7 +1,26 @@ # This file is machine-generated - editing it directly is not advised +julia_version = "1.7.0" manifest_format = "2.0" +[[deps.AbstractFFTs]] +deps = ["ChainRulesCore", "LinearAlgebra"] +git-tree-sha1 = "69f7020bd72f069c219b5e8c236c1fa90d2cb409" +uuid = "621f4979-c628-5d54-868e-fcf4e3e8185c" +version = "1.2.1" + +[[deps.Adapt]] +deps = ["LinearAlgebra"] +git-tree-sha1 = "af92965fb30777147966f58acb05da51c5616b5f" +uuid = "79e6a3ab-5dfb-504d-930d-738a2a938a0e" +version = "3.3.3" + +[[deps.ArchGDAL]] +deps = ["CEnum", "ColorTypes", "Dates", "DiskArrays", "Extents", "GDAL", "GeoFormatTypes", "GeoInterface", "GeoInterfaceRecipes", "ImageCore", "Tables"] +git-tree-sha1 = "65cdad9f49e0d2fec6b6abc80668ca49c034824b" +uuid = "c9ce4bd3-c3d5-55b8-8973-c0e20141b8c3" +version = "0.9.1" + [[deps.ArgTools]] uuid = "0dad84c5-d112-42e6-8d28-ef12dabb789f" @@ -29,6 +48,11 @@ git-tree-sha1 = "19a35467a82e236ff51bc17a3a44b69ef35185a2" uuid = "6e34b625-4abd-537c-b88f-471c36dfa7a0" version = "1.0.8+0" +[[deps.CEnum]] +git-tree-sha1 = "eb4cb44a499229b3b8426dcfb5dd85333951ff90" +uuid = "fa961155-64e5-5f13-b03f-caf6b980ea82" +version = "0.4.2" + [[deps.Calculus]] deps = ["LinearAlgebra"] git-tree-sha1 = "f641eb0a4f00c343bbc32346e1217b86f3ce9dad" @@ -37,15 +61,15 @@ version = "0.5.1" [[deps.ChainRulesCore]] deps = ["Compat", "LinearAlgebra", "SparseArrays"] -git-tree-sha1 = "9950387274246d08af38f6eef8cb5480862a435f" +git-tree-sha1 = "9489214b993cd42d17f44c36e359bf6a7c919abf" uuid = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4" -version = "1.14.0" +version = "1.15.0" [[deps.ChangesOfVariables]] deps = ["ChainRulesCore", "LinearAlgebra", "Test"] -git-tree-sha1 = "bf98fa45a0a4cee295de98d4c1462be26345b9a1" +git-tree-sha1 = "1e315e3f4b0b7ce40feded39c73049692126cf53" uuid = "9e997f8a-9a97-42d5-a9f1-ce6bfc15e2c0" -version = "0.1.2" +version = "0.1.3" [[deps.CodecBzip2]] deps = ["Bzip2_jll", "Libdl", "TranscodingStreams"] @@ -59,10 +83,28 @@ git-tree-sha1 = "ded953804d019afa9a3f98981d99b33e3db7b6da" uuid = "944b1d66-785c-5afd-91f1-9de20f533193" version = "0.7.0" +[[deps.ColorTypes]] +deps = ["FixedPointNumbers", "Random"] +git-tree-sha1 = "eb7f0f8307f71fac7c606984ea5fb2817275d6e4" +uuid = "3da002f7-5984-5a60-b8a6-cbb66c0b333f" +version = "0.11.4" + +[[deps.ColorVectorSpace]] +deps = ["ColorTypes", "FixedPointNumbers", "LinearAlgebra", "SpecialFunctions", "Statistics", "TensorCore"] +git-tree-sha1 = "d08c20eef1f2cbc6e60fd3612ac4340b89fea322" +uuid = "c3611d14-8923-5661-9e6a-0046d554d3a4" +version = "0.9.9" + +[[deps.Colors]] +deps = ["ColorTypes", "FixedPointNumbers", "Reexport"] +git-tree-sha1 = "417b0ed7b8b838aa6ca0a87aadf1bb9eb111ce40" +uuid = "5ae59095-9a9b-59fe-a467-6f913c188581" +version = "0.12.8" + [[deps.CommonSolve]] -git-tree-sha1 = "68a0743f578349ada8bc911a5cbd5a2ef6ed6d1f" +git-tree-sha1 = "332a332c97c7071600984b3c31d9067e1a4e6e25" uuid = "38540f10-b2f7-11e9-35d8-d573e4eb0ff2" -version = "0.2.0" +version = "0.2.1" [[deps.CommonSubexpressions]] deps = ["MacroTools", "Test"] @@ -71,10 +113,10 @@ uuid = "bbf7d656-a473-5ed7-a52c-81e309532950" version = "0.3.0" [[deps.Compat]] -deps = ["Base64", "Dates", "DelimitedFiles", "Distributed", "InteractiveUtils", "LibGit2", "Libdl", "LinearAlgebra", "Markdown", "Mmap", "Pkg", "Printf", "REPL", "Random", "SHA", "Serialization", "SharedArrays", "Sockets", "SparseArrays", "Statistics", "Test", "UUIDs", "Unicode"] -git-tree-sha1 = "b153278a25dd42c65abbf4e62344f9d22e59191b" +deps = ["Dates", "LinearAlgebra", "UUIDs"] +git-tree-sha1 = "924cdca592bc16f14d2f7006754a621735280b74" uuid = "34da2185-b29b-5c13-b0c7-acf172513d20" -version = "3.43.0" +version = "4.1.0" [[deps.CompilerSupportLibraries_jll]] deps = ["Artifacts", "Libdl"] @@ -82,9 +124,9 @@ uuid = "e66e0078-7015-5450-92f7-15fbd957f2ae" [[deps.ConstructionBase]] deps = ["LinearAlgebra"] -git-tree-sha1 = "f74e9d5388b8620b4cee35d4c5a618dd4dc547f4" +git-tree-sha1 = "59d00b3139a9de4eb961057eabb65ac6522be954" uuid = "187b0558-2788-49d3-abe0-74a17ed4e7c9" -version = "1.3.0" +version = "1.4.0" [[deps.DBFTables]] deps = ["Printf", "Tables", "WeakRefStrings"] @@ -99,9 +141,9 @@ version = "1.10.0" [[deps.DataStructures]] deps = ["Compat", "InteractiveUtils", "OrderedCollections"] -git-tree-sha1 = "3daef5523dd2e769dad2365274f760ff5f282c7d" +git-tree-sha1 = "d1fff3a548102f48987a52a2e0d114fa97d730f0" uuid = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8" -version = "0.18.11" +version = "0.18.13" [[deps.DataValueInterfaces]] git-tree-sha1 = "bfc1187b79289637fa0ef6d4436ebdfe6905cbd6" @@ -124,13 +166,15 @@ version = "1.0.3" [[deps.DiffRules]] deps = ["IrrationalConstants", "LogExpFunctions", "NaNMath", "Random", "SpecialFunctions"] -git-tree-sha1 = "dd933c4ef7b4c270aacd4eb88fa64c147492acf0" +git-tree-sha1 = "28d605d9a0ac17118fe2c5e9ce0fbb76c3ceb120" uuid = "b552c78f-8df3-52c6-915a-8e097449b14b" -version = "1.10.0" +version = "1.11.0" -[[deps.Distributed]] -deps = ["Random", "Serialization", "Sockets"] -uuid = "8ba89e20-285c-5b6f-9357-94700520ee1b" +[[deps.DiskArrays]] +deps = ["OffsetArrays"] +git-tree-sha1 = "230d999fc78652ea070312373ed1bfe2489e4fe5" +uuid = "3c3547ce-8d99-4f5e-a174-61eb10b00ae3" +version = "0.3.6" [[deps.DocStringExtensions]] deps = ["LibGit2"] @@ -142,21 +186,73 @@ version = "0.8.6" deps = ["ArgTools", "LibCURL", "NetworkOptions"] uuid = "f43a241f-c20a-4ad4-852c-f6b1247861c6" +[[deps.Expat_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg"] +git-tree-sha1 = "bad72f730e9e91c08d9427d5e8db95478a3c323d" +uuid = "2e619515-83b5-522b-bb60-26c02a35a201" +version = "2.4.8+0" + +[[deps.Extents]] +git-tree-sha1 = "5e1e4c53fa39afe63a7d356e30452249365fba99" +uuid = "411431e0-e8b7-467b-b5e0-f676ba4f2910" +version = "0.1.1" + +[[deps.FixedPointNumbers]] +deps = ["Statistics"] +git-tree-sha1 = "335bfdceacc84c5cdf16aadc768aa5ddfc5383cc" +uuid = "53c48c17-4a7d-5ca2-90c5-79b7896eea93" +version = "0.8.4" + [[deps.ForwardDiff]] deps = ["CommonSubexpressions", "DiffResults", "DiffRules", "LinearAlgebra", "LogExpFunctions", "NaNMath", "Preferences", "Printf", "Random", "SpecialFunctions", "StaticArrays"] -git-tree-sha1 = "40d1546a45abd63490569695a86a2d93c2021e54" +git-tree-sha1 = "2f18915445b248731ec5db4e4a17e451020bf21e" uuid = "f6369f11-7733-5829-9624-2563aa707210" -version = "0.10.26" +version = "0.10.30" [[deps.Future]] deps = ["Random"] uuid = "9fa8497b-333b-5362-9e8d-4d0656e87820" +[[deps.GDAL]] +deps = ["CEnum", "GDAL_jll", "NetworkOptions", "PROJ_jll"] +git-tree-sha1 = "9ce70502472a9f23f8889f0f9e2be8451413fe7b" +uuid = "add2ef01-049f-52c4-9ee2-e494f65e021a" +version = "1.4.0" + +[[deps.GDAL_jll]] +deps = ["Artifacts", "Expat_jll", "GEOS_jll", "JLLWrappers", "LibCURL_jll", "Libdl", "Libtiff_jll", "OpenJpeg_jll", "PROJ_jll", "Pkg", "SQLite_jll", "Zlib_jll", "Zstd_jll", "libgeotiff_jll"] +git-tree-sha1 = "756a15a73ded80cf194e7458abeb6f559d5070e2" +uuid = "a7073274-a066-55f0-b90d-d619367d196c" +version = "300.500.0+1" + +[[deps.GEOS_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg"] +git-tree-sha1 = "4ceb4cdae127931b852ced4d3782bb51ab5e2632" +uuid = "d604d12d-fa86-5845-992e-78dc15976526" +version = "3.10.2+0" + +[[deps.GeoFormatTypes]] +git-tree-sha1 = "434166198434a5c2fcc0a1a59d22c3b0ad460889" +uuid = "68eda718-8dee-11e9-39e7-89f7f65f511f" +version = "0.4.1" + [[deps.GeoInterface]] -deps = ["RecipesBase"] -git-tree-sha1 = "6b1a29c757f56e0ae01a35918a2c39260e2c4b98" +deps = ["Extents"] +git-tree-sha1 = "fb28b5dc239d0174d7297310ef7b84a11804dfab" uuid = "cf35fbd7-0cd7-5166-be24-54bfbe79505f" -version = "0.5.7" +version = "1.0.1" + +[[deps.GeoInterfaceRecipes]] +deps = ["GeoInterface", "RecipesBase"] +git-tree-sha1 = "29e1ec25cfb6762f503a19495aec347acf867a9e" +uuid = "0329782f-3d07-4b52-b9f6-d3137cf03c7a" +version = "1.0.0" + +[[deps.Graphics]] +deps = ["Colors", "LinearAlgebra", "NaNMath"] +git-tree-sha1 = "d61890399bc535850c4bf08e4e0d3a7ad0f21cbd" +uuid = "a2bd30eb-e257-5431-a919-1863eab51364" +version = "1.1.2" [[deps.HTTP]] deps = ["Base64", "Dates", "IniFile", "Logging", "MbedTLS", "NetworkOptions", "Sockets", "URIs"] @@ -164,6 +260,12 @@ git-tree-sha1 = "0fa77022fe4b511826b39c894c90daf5fce3334a" uuid = "cd3eb016-35fb-5094-929b-558a96fad6f3" version = "0.9.17" +[[deps.ImageCore]] +deps = ["AbstractFFTs", "ColorVectorSpace", "Colors", "FixedPointNumbers", "Graphics", "MappedArrays", "MosaicViews", "OffsetArrays", "PaddedViews", "Reexport"] +git-tree-sha1 = "acf614720ef026d38400b3817614c45882d75500" +uuid = "a09fc81d-aa75-5fe9-8630-4744c3626534" +version = "0.9.4" + [[deps.IniFile]] git-tree-sha1 = "f550e6e32074c939295eb5ea6de31849ac2c9625" uuid = "83e8ac13-25f8-5344-8a64-a9f2b223428f" @@ -171,9 +273,9 @@ version = "0.5.1" [[deps.InlineStrings]] deps = ["Parsers"] -git-tree-sha1 = "61feba885fac3a407465726d0c330b3055df897f" +git-tree-sha1 = "d19f9edd8c34760dca2de2b503f969d8700ed288" uuid = "842dd82b-1e85-43dc-bf29-5d0ee9dffc48" -version = "1.1.2" +version = "1.1.4" [[deps.InteractiveUtils]] deps = ["Markdown"] @@ -181,15 +283,15 @@ uuid = "b77e0a4c-d291-57a0-90e8-8db25a27a240" [[deps.IntervalSets]] deps = ["Dates", "Statistics"] -git-tree-sha1 = "eb381d885e30ef859068fce929371a8a5d06a914" +git-tree-sha1 = "ad841eddfb05f6d9be0bff1fa48dcae32f134a2d" uuid = "8197267c-284f-5f27-9208-e0e47529a953" -version = "0.6.1" +version = "0.6.2" [[deps.InverseFunctions]] deps = ["Test"] -git-tree-sha1 = "91b5dcf362c5add98049e6c29ee756910b03051d" +git-tree-sha1 = "b3364212fb5d870f724876ffcd34dd8ec6d98918" uuid = "3587e190-3f89-42d0-90ee-14403ec27112" -version = "0.1.3" +version = "0.1.7" [[deps.IrrationalConstants]] git-tree-sha1 = "7fd44fd4ff43fc60815f8e764c0f352b83c49151" @@ -224,12 +326,24 @@ git-tree-sha1 = "2f49f7f86762a0fbbeef84912265a1ae61c4ef80" uuid = "7d188eb4-7ad8-530c-ae41-71a32a6d4692" version = "0.3.4" +[[deps.JpegTurbo_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg"] +git-tree-sha1 = "b53380851c6e6664204efb2e62cd24fa5c47e4ba" +uuid = "aacddb02-875f-59d6-b918-886e6ef4fbf8" +version = "2.1.2+0" + [[deps.JuMP]] deps = ["Calculus", "DataStructures", "ForwardDiff", "JSON", "LinearAlgebra", "MathOptInterface", "MutableArithmetics", "NaNMath", "Printf", "Random", "SparseArrays", "SpecialFunctions", "Statistics"] git-tree-sha1 = "4358b7cbf2db36596bdbbe3becc6b9d87e4eb8f5" uuid = "4076af6c-e467-56ae-b986-b466b2749572" version = "0.21.10" +[[deps.LERC_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg"] +git-tree-sha1 = "bf36f528eec6634efc60d7ec062008f171071434" +uuid = "88015f11-f218-50d7-93a8-a6af411a945d" +version = "3.0.0+1" + [[deps.LibCURL]] deps = ["LibCURL_jll", "MozillaCACerts_jll"] uuid = "b27032c2-a3e7-50c8-80cd-2d36dbcbfd21" @@ -249,21 +363,33 @@ uuid = "29816b5a-b9ab-546f-933c-edad1886dfa8" [[deps.Libdl]] uuid = "8f399da3-3557-5675-b5ff-fb832c97cbdb" +[[deps.Libtiff_jll]] +deps = ["Artifacts", "JLLWrappers", "JpegTurbo_jll", "LERC_jll", "Libdl", "Pkg", "Zlib_jll", "Zstd_jll"] +git-tree-sha1 = "3eb79b0ca5764d4799c06699573fd8f533259713" +uuid = "89763e89-9b03-5906-acba-b20f662cd828" +version = "4.4.0+0" + [[deps.LinDistFlow]] deps = ["JuMP", "LinearAlgebra", "Logging", "SparseArrays"] -git-tree-sha1 = "45873540b9e225a7f130863bda5fa7e049239a49" +git-tree-sha1 = "ad15878e716a18b82325cacc02af4533bb9777e7" uuid = "bf674bac-ffe4-48d3-9f32-72124ffa9ede" -version = "0.1.3" +version = "0.1.4" [[deps.LinearAlgebra]] deps = ["Libdl", "libblastrampoline_jll"] uuid = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" +[[deps.LittleCMS_jll]] +deps = ["Artifacts", "JLLWrappers", "JpegTurbo_jll", "Libdl", "Libtiff_jll", "Pkg"] +git-tree-sha1 = "110897e7db2d6836be22c18bffd9422218ee6284" +uuid = "d3a379c0-f9a3-5b72-a4c0-6bf4d2e8af0f" +version = "2.12.0+0" + [[deps.LogExpFunctions]] deps = ["ChainRulesCore", "ChangesOfVariables", "DocStringExtensions", "InverseFunctions", "IrrationalConstants", "LinearAlgebra"] -git-tree-sha1 = "a970d55c2ad8084ca317a4658ba6ce99b7523571" +git-tree-sha1 = "09e4b894ce6a976c354a69041a04748180d43637" uuid = "2ab3a3ac-af41-5b50-aa03-7779005ae688" -version = "0.3.12" +version = "0.3.15" [[deps.Logging]] uuid = "56ddb016-857b-54e1-b83d-db4d58db5568" @@ -274,6 +400,11 @@ git-tree-sha1 = "3d3e902b31198a27340d0bf00d6ac452866021cf" uuid = "1914dd2f-81c6-5fcd-8719-6d5c9610ff09" version = "0.5.9" +[[deps.MappedArrays]] +git-tree-sha1 = "e8b359ef06ec72e8c030463fe02efe5527ee5142" +uuid = "dbb5928d-eab1-5f90-85c2-b9b0edb7c900" +version = "0.4.1" + [[deps.Markdown]] deps = ["Base64"] uuid = "d6f4376e-aef5-505a-96c1-9c027394607a" @@ -297,6 +428,12 @@ uuid = "c8ffd9c3-330d-5841-b78e-0817d7145fa1" [[deps.Mmap]] uuid = "a63ad114-7e13-5084-954f-fe012c677804" +[[deps.MosaicViews]] +deps = ["MappedArrays", "OffsetArrays", "PaddedViews", "StackViews"] +git-tree-sha1 = "b34e3bc3ca7c94914418637cb10cc4d1d80d877d" +uuid = "e94cdb99-869f-56ef-bcf0-1ae2bcbe0389" +version = "0.3.3" + [[deps.MozillaCACerts_jll]] uuid = "14a3606d-f60d-562e-9121-12d972cd8159" @@ -314,10 +451,22 @@ version = "0.3.7" [[deps.NetworkOptions]] uuid = "ca575930-c2e3-43a9-ace4-1e988b2c1908" +[[deps.OffsetArrays]] +deps = ["Adapt"] +git-tree-sha1 = "1ea784113a6aa054c5ebd95945fa5e52c2f378e7" +uuid = "6fe1bfb0-de20-5000-8ca7-80f57d26f881" +version = "1.12.7" + [[deps.OpenBLAS_jll]] deps = ["Artifacts", "CompilerSupportLibraries_jll", "Libdl"] uuid = "4536629a-c528-5b80-bd46-f80d51c5b363" +[[deps.OpenJpeg_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Libtiff_jll", "LittleCMS_jll", "Pkg", "libpng_jll"] +git-tree-sha1 = "76374b6e7f632c130e78100b166e5a48464256f8" +uuid = "643b3616-a352-519d-856d-80112ee9badc" +version = "2.4.0+0" + [[deps.OpenLibm_jll]] deps = ["Artifacts", "Libdl"] uuid = "05823500-19ac-5b8b-9628-191a04bc5112" @@ -333,21 +482,28 @@ git-tree-sha1 = "85f8e6578bf1f9ee0d11e7bb1b1456435479d47c" uuid = "bac558e1-5e72-5ebc-8fee-abe8a469f55d" version = "1.4.1" +[[deps.PROJ_jll]] +deps = ["Artifacts", "JLLWrappers", "LibCURL_jll", "Libdl", "Libtiff_jll", "Pkg", "SQLite_jll"] +git-tree-sha1 = "12bd68665a0c3cb4635c4359d3fa9e2769ed59e5" +uuid = "58948b4f-47e0-5654-a9ad-f609743f8632" +version = "900.0.0+0" + +[[deps.PaddedViews]] +deps = ["OffsetArrays"] +git-tree-sha1 = "03a7a85b76381a3d04c7a1656039197e70eda03d" +uuid = "5432bcbf-9aad-5242-b902-cca2824c8663" +version = "0.5.11" + [[deps.Parsers]] deps = ["Dates"] -git-tree-sha1 = "3b429f37de37f1fc603cc1de4a799dc7fbe4c0b6" +git-tree-sha1 = "0044b23da09b5608b4ecacb4e5e6c6332f833a7e" uuid = "69de0a69-1ddd-5017-9359-2bf0b02dc9f0" -version = "2.3.0" +version = "2.3.2" [[deps.Pkg]] deps = ["Artifacts", "Dates", "Downloads", "LibGit2", "Libdl", "Logging", "Markdown", "Printf", "REPL", "Random", "SHA", "Serialization", "TOML", "Tar", "UUIDs", "p7zip_jll"] uuid = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" -[[deps.PolygonInbounds]] -git-tree-sha1 = "8d50c96f4ba5e1e2fd524116b4ef97b29d5f77da" -uuid = "e4521ec6-8c1d-418e-9da2-b3bc4ae105d6" -version = "0.2.0" - [[deps.Preferences]] deps = ["TOML"] git-tree-sha1 = "47e5f437cc0e7ef2ce8406ce1e7e24d44915f88d" @@ -367,10 +523,10 @@ deps = ["InteractiveUtils", "Markdown", "Sockets", "Unicode"] uuid = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb" [[deps.REopt]] -deps = ["Dates", "DelimitedFiles", "HTTP", "JSON", "JuMP", "LinDistFlow", "Logging", "MathOptInterface", "PolygonInbounds", "Roots", "Shapefile", "TestEnv"] -git-tree-sha1 = "d450c849dd54cbd09c1d66bed2f77a81ab0b0295" +deps = ["ArchGDAL", "Dates", "DelimitedFiles", "HTTP", "JSON", "JuMP", "LinDistFlow", "Logging", "MathOptInterface", "Roots", "Shapefile", "TestEnv"] +git-tree-sha1 = "f281e867ed0b0ca7c18b8a0cdeb8d10a172422fd" uuid = "d36ad4e8-d74a-4f7a-ace1-eaea049febf6" -version = "0.15.0" +version = "0.16.2" [[deps.Random]] deps = ["SHA", "Serialization"] @@ -386,6 +542,11 @@ git-tree-sha1 = "6bf3f380ff52ce0832ddd3a2a7b9538ed1bcca7d" uuid = "3cdcf5f2-1ef4-517c-9805-6587b60abb01" version = "1.2.1" +[[deps.Reexport]] +git-tree-sha1 = "45e428421666073eab6f2da5c9d310d99bb12f9b" +uuid = "189a3867-3050-52da-a836-e630ba90ab69" +version = "1.2.2" + [[deps.Requires]] deps = ["UUIDs"] git-tree-sha1 = "838a3a4188e2ded87a4f9f184b4b0d78a1e91cb7" @@ -394,13 +555,19 @@ version = "1.3.0" [[deps.Roots]] deps = ["CommonSolve", "Printf", "Setfield"] -git-tree-sha1 = "e382260f6482c27b5062eba923e36fde2f5ab0b9" +git-tree-sha1 = "30e3981751855e2340e9b524ab58c1ec85c36f33" uuid = "f2b01f46-fcfa-551c-844a-d8ac1e96c665" -version = "2.0.0" +version = "2.0.1" [[deps.SHA]] uuid = "ea8e919c-243c-51af-8825-aaa63cd721ce" +[[deps.SQLite_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg", "Zlib_jll"] +git-tree-sha1 = "f2129f53b7c2ce4fe350e46d35b9772966a12ac0" +uuid = "76ed43ae-9a5d-5a62-8c75-30186b810ce8" +version = "3.39.0+0" + [[deps.Serialization]] uuid = "9e88b42a-f829-5b0c-bbe9-9e923198166b" @@ -411,14 +578,10 @@ uuid = "efcf1570-3423-57d1-acb7-fd33fddbac46" version = "0.8.2" [[deps.Shapefile]] -deps = ["DBFTables", "GeoInterface", "RecipesBase", "Tables"] -git-tree-sha1 = "213498e68fe72d9a62668d58d6be3bc423ebb81f" +deps = ["DBFTables", "Extents", "GeoFormatTypes", "GeoInterface", "GeoInterfaceRecipes", "RecipesBase", "Tables"] +git-tree-sha1 = "2f400236c85ba357dfdc2a56af80c939dc118f02" uuid = "8e980c4a-a4fe-5da2-b3a7-4b4b0353a2f4" -version = "0.7.4" - -[[deps.SharedArrays]] -deps = ["Distributed", "Mmap", "Random", "Serialization"] -uuid = "1a1011a3-84de-559e-8e89-a11a2f7dc383" +version = "0.8.0" [[deps.Sockets]] uuid = "6462fe0b-24de-5631-8697-dd941f90decc" @@ -429,15 +592,21 @@ uuid = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" [[deps.SpecialFunctions]] deps = ["ChainRulesCore", "IrrationalConstants", "LogExpFunctions", "OpenLibm_jll", "OpenSpecFun_jll"] -git-tree-sha1 = "cbf21db885f478e4bd73b286af6e67d1beeebe4c" +git-tree-sha1 = "69fa1bef454c483646e8a250f384e589fd76562b" uuid = "276daf66-3868-5448-9aa4-cd146d93841b" -version = "1.8.4" +version = "1.8.6" + +[[deps.StackViews]] +deps = ["OffsetArrays"] +git-tree-sha1 = "46e589465204cd0c08b4bd97385e4fa79a0c770c" +uuid = "cae243ae-269e-4f55-b966-ac2d0dc13c15" +version = "0.1.1" [[deps.StaticArrays]] deps = ["LinearAlgebra", "Random", "Statistics"] -git-tree-sha1 = "cd56bf18ed715e8b09f06ef8c6b781e6cdc49911" +git-tree-sha1 = "2bbd9f2e40afd197a1379aef05e0d85dba649951" uuid = "90137ffa-7385-5640-81b9-e52037218182" -version = "1.4.4" +version = "1.4.7" [[deps.Statistics]] deps = ["LinearAlgebra", "SparseArrays"] @@ -463,15 +632,21 @@ version = "1.7.0" deps = ["ArgTools", "SHA"] uuid = "a4e569a6-e804-4fa4-b0f3-eef7a1d5b13e" +[[deps.TensorCore]] +deps = ["LinearAlgebra"] +git-tree-sha1 = "1feb45f88d133a655e001435632f019a9a1bcdb6" +uuid = "62fd8b95-f654-4bbd-a8a5-9c27f68ccd50" +version = "0.1.1" + [[deps.Test]] deps = ["InteractiveUtils", "Logging", "Random", "Serialization"] uuid = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [[deps.TestEnv]] deps = ["Pkg"] -git-tree-sha1 = "ef092f327a954f1d56ac34c4276bce74d12c0ea8" +git-tree-sha1 = "43519ae06e007949a0e889f3234cf85875ac7e34" uuid = "1e6cf692-eddd-4d53-88a5-2d735e33781b" -version = "1.7.2" +version = "1.7.3" [[deps.TranscodingStreams]] deps = ["Random", "Test"] @@ -507,10 +682,28 @@ version = "0.13.2" deps = ["Libdl"] uuid = "83775a58-1f1d-513f-b197-d71354ab007a" +[[deps.Zstd_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg"] +git-tree-sha1 = "e45044cd873ded54b6a5bac0eb5c971392cf1927" +uuid = "3161d3a3-bdf6-5164-811a-617609db77b4" +version = "1.5.2+0" + [[deps.libblastrampoline_jll]] deps = ["Artifacts", "Libdl", "OpenBLAS_jll"] uuid = "8e850b90-86db-534c-a0d3-1478176c7d93" +[[deps.libgeotiff_jll]] +deps = ["Artifacts", "JLLWrappers", "LibCURL_jll", "Libdl", "Libtiff_jll", "PROJ_jll", "Pkg"] +git-tree-sha1 = "e51bca193c8a4774dc1d2e5d40d5c4491c1b4fd4" +uuid = "06c338fa-64ff-565b-ac2f-249532af990e" +version = "1.7.1+0" + +[[deps.libpng_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg", "Zlib_jll"] +git-tree-sha1 = "94d180a6d2b5e55e447e2d27a29ed04fe79eb30c" +uuid = "b53b4c65-9356-5827-b1ea-8c7a1a84506f" +version = "1.6.38+0" + [[deps.nghttp2_jll]] deps = ["Artifacts", "Libdl"] uuid = "8e850ede-7688-5339-a07c-302acd2aaf8d" diff --git a/julia_src/http.jl b/julia_src/http.jl index 41e933ed5..f77ba9adb 100644 --- a/julia_src/http.jl +++ b/julia_src/http.jl @@ -37,8 +37,9 @@ function reopt(req::HTTP.Request) settings = d["Settings"] timeout_seconds = -pop!(settings, "timeout_seconds") optimality_tolerance = pop!(settings, "optimality_tolerance") + run_bau = pop!(settings, "run_bau") ms = nothing - if get(settings, "run_bau", true) + if run_bau m1 = direct_model( Xpress.Optimizer( MAXTIME = timeout_seconds, diff --git a/reo/api.py b/reo/api.py index 300d02d1c..e3d42e0a9 100644 --- a/reo/api.py +++ b/reo/api.py @@ -50,7 +50,6 @@ from celery import group, chain log = logging.getLogger(__name__) -api_version = "version 1.0.0" saveToDb = True @@ -99,6 +98,7 @@ def obj_get_list(self, bundle, **kwargs): return self.get_object_list(bundle.request) def obj_create(self, bundle, **kwargs): + api_version = "version 1.0.0" # to use the Job API from within the REopt API (see futurecosts/api.py) if isinstance(bundle, dict): @@ -266,6 +266,7 @@ def obj_get_list(self, bundle, **kwargs): return self.get_object_list(bundle.request) def obj_create(self, bundle, **kwargs): + api_version = "version 2.0.0" # to use the Job API from within the REopt API (see futurecosts/api.py) if isinstance(bundle, dict): @@ -273,7 +274,7 @@ def obj_create(self, bundle, **kwargs): run_uuid = str(uuid.uuid4()) data = dict() - data["outputs"] = {"Scenario": {'run_uuid': run_uuid, 'api_version': "2", + data["outputs"] = {"Scenario": {'run_uuid': run_uuid, 'api_version': api_version, 'Profile': {'pre_setup_scenario_seconds': 0, 'setup_scenario_seconds': 0, 'reopt_seconds': 0, 'reopt_bau_seconds': 0, 'parse_run_outputs_seconds': 0}, @@ -362,7 +363,7 @@ def obj_create(self, bundle, **kwargs): content_type='application/json', status=500)) # internal server error setup = setup_scenario.s(run_uuid=run_uuid, data=data, api_version=2) - call_back = process_results.s(data=data, meta={'run_uuid': run_uuid, 'api_version': "2"}) + call_back = process_results.s(data=data, meta={'run_uuid': run_uuid, 'api_version': api_version}) # (use .si for immutable signature, if no outputs were passed from reopt_jobs) rjm = run_jump_model.s(data=data) rjm_bau = run_jump_model.s(data=data, bau=True) diff --git a/reo/src/load_profile.py b/reo/src/load_profile.py index d06227209..5cc68d6ec 100644 --- a/reo/src/load_profile.py +++ b/reo/src/load_profile.py @@ -763,8 +763,8 @@ def __init__(self, dfm=None, user_profile=None, pvs=[], critical_loads_kw=None, self.time_steps_per_hour) if bau_sustained_time_steps > 0: # include critical load in bau load for the time that it can be met - self.bau_load_list[outage_start_time_step:outage_start_time_step+bau_sustained_time_steps] = \ - critical_loads_kw[outage_start_time_step:outage_start_time_step+bau_sustained_time_steps] + self.bau_load_list[outage_start_time_step - 1:outage_start_time_step + bau_sustained_time_steps - 1] = \ + critical_loads_kw[outage_start_time_step - 1:outage_start_time_step + bau_sustained_time_steps - 1] # resilience_check_flag: True if existing diesel and/or PV can sustain critical load during outage self.outage_start_time_step = outage_start_time_step diff --git a/reo/tests/posts/critical_load_bau_can_sustain_part_outage.json b/reo/tests/posts/critical_load_bau_can_sustain_part_outage.json new file mode 100644 index 000000000..095a54b38 --- /dev/null +++ b/reo/tests/posts/critical_load_bau_can_sustain_part_outage.json @@ -0,0 +1,11 @@ +{ + "Scenario": {"add_soc_incentive": false, + "Site": {"latitude": 30.48157, "longitude": -86.50159, + "Financial": {"analysis_years": 20, "escalation_pct": 0.023, "offtaker_discount_pct": 0.083, "offtaker_tax_pct": 0.26, "om_cost_escalation_pct": 0.025}, + "ElectricTariff": {"blended_annual_rates_us_dollars_per_kwh": 0.1, "blended_annual_demand_charges_us_dollars_per_kw": 0}, + "LoadProfile": {"doe_reference_name": "FlatLoad", "annual_kwh": 8760000.0, "year": 2021, "loads_kw_is_net": false, "outage_is_major_event": true, "critical_load_pct": 0.8, "outage_start_time_step": 5196, "outage_end_time_step": 5244}, + "PV": {"installed_cost_us_dollars_per_kw": 1600.0, "can_export_beyond_site_load": true, "can_curtail": true, "existing_kw": 0.0}, + "Generator": {"installed_cost_us_dollars_per_kw": 1000.0, "generator_only_runs_during_grid_outage": true, "generator_fuel_escalation_pct": 0.027, "fuel_slope_gal_per_kwh": 0.076, "fuel_avail_gal": 1525.0, "min_turn_down_pct": 0.0, "existing_kw": 2000.0, "diesel_fuel_cost_us_dollars_per_gallon": 1.0, "om_cost_us_dollars_per_kw": 10.0, "om_cost_us_dollars_per_kwh": 0.01, "max_kw": 10000.0, "min_kw": 0} + } + } +} \ No newline at end of file diff --git a/reo/tests/test_critical_load_bau.py b/reo/tests/test_critical_load_bau.py index fa9410153..32a062df8 100644 --- a/reo/tests/test_critical_load_bau.py +++ b/reo/tests/test_critical_load_bau.py @@ -33,7 +33,7 @@ from reo.nested_to_flat_output import nested_to_flat from django.test import TestCase from reo.models import ModelManager -from reo.utilities import check_common_outputs +from reo.utilities import check_common_outputs, annuity class CriticalLoadBAUTests(ResourceTestCaseMixin, TestCase): @@ -46,6 +46,65 @@ def setUp(self): def get_response(self, data): return self.api_client.post(self.reopt_base, format='json', data=data) + def test_critical_load_bau_can_sustain_part_outage(self): + """ + Test scenario with + - outage_start_time_step: 5196 + - outage_end_time_step: 5244 + - existing diesel generator 2001 kW + - available fuel 1000000000 gallons + """ + test_post = os.path.join('reo', 'tests', 'posts', 'critical_load_bau_can_sustain_part_outage.json') + nested_data = json.load(open(test_post, 'rb')) + + resp = self.get_response(data=nested_data) + self.assertHttpCreated(resp) + r = json.loads(resp.content) + run_uuid = r.get('run_uuid') + d = ModelManager.make_response(run_uuid=run_uuid) + c = nested_to_flat(d['outputs']) + c['resilience_check_flag'] = d['outputs']['Scenario']['Site']['LoadProfile']['resilience_check_flag'] + c['bau_sustained_time_steps'] = d['outputs']['Scenario']['Site']['LoadProfile']['bau_sustained_time_steps'] + c["fuel_used_gal_bau"] = round(d['outputs']['Scenario']['Site']['Generator']['fuel_used_gal_bau']) + + d_expected = dict() + d_expected['status'] = 'optimal' + + # calculate expected year one energy cost + energy_rate = nested_data['Scenario']['Site']['ElectricTariff']["blended_annual_rates_us_dollars_per_kwh"] + flat_load = nested_data['Scenario']['Site']['LoadProfile']["annual_kwh"] / 8760 + outage_duration = (1 + nested_data['Scenario']['Site']['LoadProfile']["outage_end_time_step"] - nested_data['Scenario']['Site']['LoadProfile']["outage_start_time_step"]) + d_expected['year_one_energy_cost_bau'] = energy_rate * flat_load * (8760 - outage_duration) + + # calculate expected total energy cost + analysis_years = nested_data['Scenario']['Site']['Financial']["analysis_years"] + escalation_pct = nested_data['Scenario']['Site']['Financial']["escalation_pct"] + offtaker_discount_pct = nested_data['Scenario']['Site']['Financial']["offtaker_discount_pct"] + tax_fraction = 1 - nested_data['Scenario']['Site']['Financial']["offtaker_tax_pct"] + pwf_e = annuity(analysis_years, escalation_pct, offtaker_discount_pct) + d_expected["total_energy_cost_bau"] = round(pwf_e * flat_load * energy_rate * (8760 - outage_duration) * tax_fraction) + + # calculate expected BAU outage performance + d_expected['resilience_check_flag'] = False + d_expected['bau_sustained_time_steps'] = int(nested_data['Scenario']['Site']['Generator']["fuel_avail_gal"] / (nested_data['Scenario']['Site']['Generator']["fuel_slope_gal_per_kwh"] * flat_load * nested_data['Scenario']['Site']['LoadProfile']["critical_load_pct"])) + d_expected["fuel_used_gal_bau"] = d_expected['bau_sustained_time_steps'] * (nested_data['Scenario']['Site']['Generator']["fuel_slope_gal_per_kwh"] * flat_load * nested_data['Scenario']['Site']['LoadProfile']["critical_load_pct"]) + + # calculate lcc components and lcc + bau_energy_cost = d['outputs']['Scenario']['Site']['ElectricTariff']["total_energy_cost_bau_us_dollars"] + bau_fixed_om_cost = d['outputs']['Scenario']['Site']['Generator']["existing_gen_total_fixed_om_cost_us_dollars"] + bau_var_om_cost = d['outputs']['Scenario']['Site']['Generator']["existing_gen_total_variable_om_cost_us_dollars"] + bau_fuel_used = d['outputs']['Scenario']['Site']['Generator']["fuel_used_gal_bau"] + pwf_gen_fuel = annuity(analysis_years, nested_data['Scenario']['Site']['Generator']["generator_fuel_escalation_pct"], offtaker_discount_pct) + bau_fuel_cost = tax_fraction*bau_fuel_used*nested_data['Scenario']['Site']['Generator']["diesel_fuel_cost_us_dollars_per_gallon"]*pwf_gen_fuel + d_expected['lcc_bau'] = round(bau_energy_cost + bau_fixed_om_cost + bau_var_om_cost + bau_fuel_cost) + + try: + check_common_outputs(self, c, d_expected) + except: + print("Run {} expected outputs may have changed.".format(run_uuid)) + print("Error message: {}".format(d['messages']['errors'])) + raise + def test_critical_load_bau_can_sustain_outage(self): """ Test scenario with @@ -95,5 +154,5 @@ def test_critical_load_bau_can_sustain_outage(self): check_common_outputs(self, c, d_expected) except: print("Run {} expected outputs may have changed.".format(run_uuid)) - print("Error message: {}".format(d['messages'])) + print("Error message: {}".format(d['messages']['errors'])) raise diff --git a/reopt_api/urls.py b/reopt_api/urls.py index 4f24decf7..5c30fa335 100644 --- a/reopt_api/urls.py +++ b/reopt_api/urls.py @@ -48,7 +48,7 @@ v2_api.register(Job2()) stable_api = Api(api_name='stable') -stable_api.register(Job()) +stable_api.register(Job2()) stable_api.register(OutageSimJob()) stable_api.register(GHPGHXJob()) diff --git a/requirements.txt b/requirements.txt index 2180bdc69..1f2d8412f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -25,7 +25,7 @@ Cython==0.29.26 decorator==5.1.1 deepdish==0.3.7 Deprecated==1.2.13 -Django==4.0.4 +Django==4.0.6 django-celery-results==2.2.0 django-extensions==3.1.5 django-picklefield==3.0.1 @@ -54,7 +54,7 @@ ipython-genutils==0.2.0 isodate==0.6.1 jdcal==1.4.1 kombu==5.2.3 -lxml==4.7.1 +lxml==4.9.1 more-itertools==8.12.0 msrest==0.6.21 msrestazure==0.6.4 @@ -80,7 +80,7 @@ pyasn1-modules==0.2.8 pycodestyle==2.8.0 pycparser==2.21 Pygments==2.11.2 -PyJWT==2.3.0 +PyJWT==2.4.0 pyparsing==3.0.6 pyproj==3.3.0 python-dateutil==2.8.2