diff --git a/CMakeLists.txt b/CMakeLists.txt index 1467388432..22953f3bd7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -79,7 +79,8 @@ set(SAM_SRC src/shadingfactors.cpp src/materials.cpp src/codegenerator.cpp - src/pythonhandler.cpp src/pythonhandler.h) + src/pythonhandler.cpp src/pythonhandler.h + src/combinecases.cpp src/combinecases.h) ##################################################################################################################### diff --git a/deploy/runtime/macros/Combine Cases.lk b/deploy/runtime/macros/Combine Cases.lk deleted file mode 100644 index 228326d1d4..0000000000 --- a/deploy/runtime/macros/Combine Cases.lk +++ /dev/null @@ -1,564 +0,0 @@ -/*@ -

This macro adds the hourly power generation profiles of two or more cases and uses the resulting profile as input to a Generic System case that represents the combined project. You can use it to model a project's cash flow when the generation profile is from different sources.

-

To learn about cases, see the "Manage Cases" topic in Help under Reference in the table of contents.

-

Some examples of analysis scenarios this macro can model include:

- -

The following diagram shows an example of how the macro works to combine two cases. It works the same way for more than two cases:

-

-
Instructions:
-
    -
  1. Create a case for each system to combine. - -
  2. -
  3. For each case, design the system and assign costs as appropriate. You may want to run a simulation for each case to make sure the results are as you expect.
  4. -
  5. Create a special Generic System case with the financial model for the combined project.
  6. -
  7. Specify inputs for the Generic System case's Financial Parameters, Incentives and other finanical model input pages. - -
  8. Use the "Choose an option" list to the right to choose the cases to include in the combined project.
  9. - -
  10. Click Run macro above and follow the prompts.
  11. -
-

When the macro finishes, it displays the case for the combined project. Review the information in the page notes for the Power Plant and System Costs page, and then click Simulate to generate results for the combined project.

-

If you make changes to the combined project financial model inputs, you can run a simulation to generate new results. You only need to rerun this macro if you change the system design for any of the system cases.

-

This macro only uses power generation data for Year 1. It prompts you for a degradation rate to apply to the combined system output.

- -@*/ -//Macro user interface widgets -//@ name=mode;type=combo;label=Choose an option;value=1) Combine all cases,2) Combine only the cases listed below;sel=0 -//@ name=cases;type=text;label=List cases for Option 2 (comma-separated);value= - -//Check that this macro was run from within a case -if ( typeof(macro) == 'unknown' ) { - msgbox('This macro must be run from within a case.'); - exit; - /*macro = {}; - macro.mode = '1) Combine all cases'; - macro.cases = '';*/ -} - -function num_to_str( x , d ) -{ - // only format x if it is a number - if ( typeof(x) == 'number' ) - { - // The sprintf() function in this step converts a number (for d = 0) like 409.857 to 41 instead of 410. - // This if-else condition is a workaround that bug. - if( ( d == 0) && (mod( round(x) , 10) <= 0) ) - { - x = round(x); - str = sprintf('%,',x); - } - else - { str = sprintf('%.'+to_string(d)+',',x); } - - arr = split(str,'.'); - if ( #arr > 1 ) - { num_decs = strlen(to_string(arr[1])); } - else - { num_decs = 0; } - // if number has one or more decimal places less than the desired number - if ( #arr > 1 && num_decs < d) - { - for (i=0; i 0 ) - { - str += '.'; - for (i=0; i 1) //more than one generic case -{ - financial = choose_from_list(generic, 'Choose which generic system to use as the financial configuration for the combined system.', 'Select financial case', 0); -} -else //exactly one generic case -{ - financial = generic[0]; -} -outln('Case for combined project: ' + financial); - -//Verify the configuration before running it -msg = 'Review Configuration\nThis macro will combine these cases:\n'; -for (i=0; i 8760 * analysis_period_this) - { - outln('The ' + config[0] + ' model runs sub-hourly simulations, which this script does not support. Exiting script.'); - msgbox('Subhourly simulations not supported!\nThis macro is not currently enabled to work with sub-hourly simulations. Change the ' + cases[i] + ' case to hourly simulations modify the macro code to run sub-hourly.'); - exit; - } - - for (j=0; j<8760; j++) - { - if ( constant_generation ) - { - hourly_energy[j] += annual_energy_this / 8760; - } - else - { - hourly_energy[j] += hourly_energy_this[j]; - } - } - - //Add the financial parameters of this case, if applicable - if (config[1] == 'LCOE Calculator') //o&m is different for this one - { - total_installed_cost_this = get('capital_cost'); // to display on System Costs page note - total_installed_cost += get('capital_cost'); - om_fixed_this = get('fixed_operating_cost'); - om_variable = get('variable_operating_cost') * annual_energy; - for(j=0; j value('first_year_output_peak' ) ) { diff --git a/src/casewin.cpp b/src/casewin.cpp index 9f5eb99242..5ab6af30d9 100644 --- a/src/casewin.cpp +++ b/src/casewin.cpp @@ -656,6 +656,21 @@ void CaseWindow::OnCommand( wxCommandEvent &evt ) } } +wxUIObject* CaseWindow::FindObject(const wxString& name, ActiveInputPage** ipage) +{ + auto aPage = GetInputPages(); + for (auto &page : aPage) + { + SwitchToInputPage(page); + if (wxUIObject* obj = FindActiveObject(name, ipage)) + return obj; + } + + if (ipage) *ipage = 0; + return 0; +} + + wxUIObject *CaseWindow::FindActiveObject( const wxString &name, ActiveInputPage **ipage ) { for( size_t i=0;iGetStringSelection(); + // do checks + return input_page; +} + wxArrayString CaseWindow::GetInputPages() { wxArrayString list; diff --git a/src/casewin.h b/src/casewin.h index d15972b705..aa8b361e15 100644 --- a/src/casewin.h +++ b/src/casewin.h @@ -64,8 +64,10 @@ class CaseWindow : public wxSplitterWindow, CaseEventListener void UpdateConfiguration(); bool SwitchToInputPage( const wxString &name ); + wxString GetInputPage(); wxArrayString GetInputPages(); - wxUIObject *FindActiveObject( const wxString &name, ActiveInputPage **page = 0 ); + wxUIObject* FindActiveObject(const wxString& name, ActiveInputPage** page = 0); + wxUIObject* FindObject(const wxString& name, ActiveInputPage** page = 0); bool SwitchToPage( const wxString &name ); // can navigate to results, parametrics, as well as input pages diff --git a/src/combinecases.cpp b/src/combinecases.cpp new file mode 100644 index 0000000000..452cf40b40 --- /dev/null +++ b/src/combinecases.cpp @@ -0,0 +1,536 @@ +/** +BSD-3-Clause +Copyright 2019 Alliance for Sustainable Energy, LLC +Redistribution and use in source and binary forms, with or without modification, are permitted provided +that the following conditions are met : +1. Redistributions of source code must retain the above copyright notice, this list of conditions +and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions +and the following disclaimer in the documentation and/or other materials provided with the distribution. +3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse +or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED.IN NO EVENT SHALL THE COPYRIGHT HOLDER, CONTRIBUTORS, UNITED STATES GOVERNMENT OR UNITED STATES +DEPARTMENT OF ENERGY, NOR ANY OF THEIR EMPLOYEES, BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, +OR CONSEQUENTIAL DAMAGES(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT +OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include +#include +#include +#include + +#include "casewin.h" +#include "combinecases.h" +#include "main.h" +#include "script.h" + +enum { + ID_chlCases, + ID_chkOverwriteCapital, + ID_spndDegradation +}; + +BEGIN_EVENT_TABLE( CombineCasesDialog, wxDialog ) + EVT_CHECKLISTBOX(ID_chlCases, CombineCasesDialog::OnEvt) + EVT_CHECKBOX(ID_chkOverwriteCapital, CombineCasesDialog::OnEvt) + //EVT_SPINCTRLDOUBLE(ID_spndDegradation, CombineCasesDialog::OnEvt) + EVT_BUTTON(wxID_OK, CombineCasesDialog::OnEvt) + EVT_BUTTON(wxID_HELP, CombineCasesDialog::OnEvt) +END_EVENT_TABLE() + +CombineCasesDialog::CombineCasesDialog(wxWindow* parent, const wxString& title, lk::invoke_t& cxt) + : wxDialog(parent, wxID_ANY, title, wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER) +{ + // Initializations + m_result_code = -1; + m_generic_case = SamApp::Window()->GetCurrentCase(); + m_generic_case_name = SamApp::Window()->Project().GetCaseName(m_generic_case); + m_generic_case_window = SamApp::Window()->GetCaseWindow(m_generic_case); + if (m_generic_case->Values().Get("system_use_lifetime_output")->Boolean()) { + m_generic_degradation = m_generic_case->Values().Get("generic_degradation")->Array(); + } + else { + m_generic_degradation = m_generic_case->Values().Get("degradation")->Array(); + } + + // Text at top of window + wxString msg = "Select open cases, simulate those cases and combine their generation\n"; + msg += "profiles into a single profile to be used with this generic case.\n\n"; + msg += "SAM will switch to each case in the project and run a simulation.\n"; + msg += "Depending on the configuration, SAM may be temporarily unresponsive."; + + // Case selection list + m_chlCases = new wxCheckListBox(this, ID_chlCases, wxDefaultPosition, wxSize(400, 200)); // populate with active cases + this->GetOpenCases(); + this->RefreshList(0.); + wxBoxSizer* szcases = new wxBoxSizer(wxVERTICAL); + szcases->Add(new wxStaticText(this, wxID_ANY, "1. Select cases:"), 0, wxALL, 2); + szcases->Add(m_chlCases, 10, wxALL | wxEXPAND, 1); + + // Overwrite capital checkbox + m_chkOverwriteCapital = new wxCheckBox(this, ID_chkOverwriteCapital, "Overwrite Installation and Operating Costs with combined cases costs"); + + // Annual AC degradation + // Due to complexity of AC and DC degradation and lifetime and single year simulations, require user to provide an + // AC degradation rate for the combined project and ignore degradation rate inputs of individual system cases. + m_schnDegradation = new AFSchedNumeric(this, ID_spndDegradation, wxDefaultPosition, wxSize(64, 22)); + if (m_generic_degradation.size() == 1) { + m_schnDegradation->UseSchedule(false); + m_schnDegradation->SetValue(m_generic_degradation[0]); + } + else { + m_schnDegradation->UseSchedule(true); + m_schnDegradation->SetSchedule(m_generic_degradation); + } + wxString degradation_label = "%/year Annual AC degradation rate for all cases"; + wxBoxSizer* szdegradation = new wxBoxSizer(wxHORIZONTAL); + szdegradation->Add(m_schnDegradation, 0, wxLEFT, 2); + szdegradation->Add(new wxStaticText(this, wxID_ANY, degradation_label), 0, wxALL | wxALIGN_CENTER, 1); + + // Combine all into main vertical sizer + wxBoxSizer* szmain = new wxBoxSizer(wxVERTICAL); + szmain->Add(new wxStaticText(this, wxID_ANY, msg), 0, wxALL | wxEXPAND, 10); + szmain->Add(szcases, 10, wxALL | wxEXPAND, 1); + szmain->Add(m_chkOverwriteCapital, 0, wxALL, 5); + szmain->Add(szdegradation, 0, wxALL, 1); + szmain->Add(CreateButtonSizer(wxHELP | wxOK | wxCANCEL), 0, wxALL | wxEXPAND, 10); + + SetSizer(szmain); + Fit(); + m_chlCases->SetFocus(); +} + +void CombineCasesDialog::OnEvt(wxCommandEvent& e) +{ + switch (e.GetId()) + { + case wxID_HELP: + SamApp::ShowHelp("combine_cases"); + break; + case ID_chlCases: + { + for (size_t i = 0; i < m_cases.size(); i++) { + if (m_cases[i].display_name == m_chlCases->GetString(e.GetInt())) { + m_cases[i].is_selected = m_chlCases->IsChecked(e.GetInt()); + } + } + } + break; + case wxID_OK: + { + // See which cases are selected + wxArrayInt arychecked; + for (size_t i = 0; i < m_cases.size(); i++) { + if (m_cases[i].is_selected) { + arychecked.push_back((int)i); + } + } + + if (arychecked.Count() >= 2) { + + // Get analysis period and inflation from generic case + // TODO: Move some of this to constructor? + wxString technology_name = m_generic_case->GetTechnology(); + wxString financial_name = m_generic_case->GetFinancing(); + double analysis_period = std::numeric_limits::quiet_NaN(); + double inflation = std::numeric_limits::quiet_NaN(); + if (financial_name == "LCOE Calculator" || financial_name == "LCOH Calculator") { + analysis_period = m_generic_case->Values().Get("c_lifetime")->Value(); + inflation = m_generic_case->Values().Get("c_inflation")->Value(); + } + else if (financial_name != "None") { + analysis_period = m_generic_case->Values().Get("analysis_period")->Value(); + inflation = m_generic_case->Values().Get("inflation_rate")->Value(); + } + + // Allocate and initialize variables to run through the cases to combine + double nameplate = 0.; + matrix_t hourly_energy(1, 8760, 0.); + double annual_energy = 0.; + double total_installed_cost = 0.; + std::vector om_fixed(analysis_period, 0.); + bool is_notices = false; + bool has_a_contingency = false; + + // Run each simulation + for (size_t i = 0; i < arychecked.Count(); i++) { + // Switch to case + SamApp::Window()->SwitchToCaseWindow(m_cases[arychecked[i]].name); + Case* current_case = SamApp::Window()->GetCurrentCase(); + CaseWindow* case_window = SamApp::Window()->GetCaseWindow(current_case); + wxString case_page_orig = case_window->GetInputPage(); + Simulation& bcsim = current_case->BaseCase(); + wxString technology_name = current_case->GetTechnology(); + wxString financial_name = current_case->GetFinancing(); + + // Set degradation, saving original value + VarValue* degradation_vv = nullptr; + std::vector degradation_orig = { std::numeric_limits::quiet_NaN() }; + if (financial_name != "None" && financial_name != "LCOE Calculator" && financial_name != "LCOH Calculator") { + degradation_vv = current_case->Values().Get("degradation"); + degradation_orig = degradation_vv->Array(); + if (m_schnDegradation->UseSchedule()) { + degradation_vv->Set(m_schnDegradation->GetSchedule()); + } + else { + degradation_vv->Set(new double[1]{ m_schnDegradation->GetValue() }, 1); + } + } + + // Deal with inflation + VarValue* inflation_vv = nullptr; + double inflation_orig = std::numeric_limits::quiet_NaN(); + if (financial_name != "None") { + if (financial_name == "LCOE Calculator" || financial_name == "LCOH Calculator") { + inflation_vv = current_case->Values().Get("c_inflation"); + } + else { + inflation_vv = current_case->Values().Get("inflation_rate"); + } + inflation_orig = inflation_vv->Value(); + inflation_vv->Set(inflation); + } + + // Simulate + bcsim.Clear(); + bool ok = bcsim.Invoke(); + + // check that the case ran + if (!ok) { + m_result_code = 1; + m_generic_case_window->UpdateResults(); + case_window->SwitchToPage("results:notices"); + wxArrayString messages = current_case->BaseCase().GetAllMessages(); + wxMessageBox("Error in " + technology_name + "\n\n" + + technology_name + " returned the following error:\n\n + " + + messages.Last(), + "Combine Cases Message", wxOK, this); + EndModal(wxID_OK); + return; + } + + // optionally display messages returned from the simulation + wxArrayString messages = current_case->BaseCase().GetAllMessages(); + if (!messages.IsEmpty()) { + is_notices = true; + } + + // Add the performance parameters of this case + double nameplate_this = current_case->Values().Get("system_capacity")->Value(); + nameplate += nameplate_this; + matrix_t hourly_energy_this(1, 1, std::numeric_limits::quiet_NaN()); + if (bcsim.GetOutput("gen")) { + hourly_energy_this = bcsim.GetOutput("gen")->Matrix(); + } + + // need annual energy to calculate variable O&M cost for LCOE calculator + // and for constant generation profile for Marine Energy + // Note that Wind with Weibull distribution as input reports gen calculated as annual_energy / 8760 + double annual_energy_this = std::numeric_limits::quiet_NaN(); + if (technology_name == "Geothermal Power") { + annual_energy_this = bcsim.GetOutput("first_year_output")->Value(); + } + else { + annual_energy_this = bcsim.GetOutput("annual_energy")->Value(); + } + annual_energy += annual_energy_this; + + // for lifetime simulation, truncate results to first 8760 values + double analysis_period_this = std::numeric_limits::quiet_NaN(); + if (financial_name == "LCOE Calculator" || financial_name == "LCOH Calculator") { + analysis_period_this = current_case->Values().Get("c_lifetime")->Value(); + } + else if (financial_name != "None") { + analysis_period_this = current_case->Values().Get("analysis_period")->Value(); + } + + // determine hourly generation profile of current case + bool constant_generation = false; + bool lifetime = false; + if (hourly_energy_this.ncells() <= 1) { + constant_generation = true; // TODO: Report this somewhere?: "Model does not generate hourly generation data. Calculating constant generation profile from annual energy." + } + else if (hourly_energy_this.ncells() == 8760 * analysis_period_this) { + lifetime = true; // TODO: Report this somewhere?: "Model runs the simulation over the analysis period. Only Year 1 data will be combined with other cases." + } + else if (hourly_energy_this > 8760 * analysis_period_this) { + m_result_code = 1; + m_generic_case_window->UpdateResults(); + SamApp::Window()->SwitchToCaseWindow(m_generic_case_name); + m_generic_case_window->SwitchToInputPage("Power Plant"); + wxMessageBox("Subhourly simulations unsupported\n\n" + "The subhourly simulation for case " + technology_name + " is not supported.", + "Combine Cases Message", wxOK, this); + return; + } + + for (int i = 0; i < 8760; i++) { + if (constant_generation) { + hourly_energy[i] += annual_energy_this / 8760; + } + else { + hourly_energy[i] += hourly_energy_this[i]; + } + } + + // Add the financial parameters of this case, if applicable + double total_installed_cost_this = std::numeric_limits::quiet_NaN(); + std::vector om_total_this(analysis_period, std::numeric_limits::quiet_NaN()); + if (financial_name == "LCOE Calculator" || financial_name == "LCOH Calculator") { + total_installed_cost_this = current_case->Values().Get("capital_cost")->Value(); + total_installed_cost += total_installed_cost_this; + double om_fixed_this = current_case->Values().Get("fixed_operating_cost")->Value(); + double om_variable = current_case->Values().Get("variable_operating_cost")->Value() * annual_energy; + std::fill(om_total_this.begin(), om_total_this.end(), om_fixed_this + om_variable); + for (int i = 0; i < analysis_period; i++) { + om_fixed[i] += om_total_this[i]; + } + } + else if (financial_name == "None" || financial_name == "Third Party") { + total_installed_cost_this = 0.; + std::fill(om_total_this.begin(), om_total_this.end(), 0.); + } + else if (financial_name != "None" && analysis_period > std::numeric_limits::epsilon()) { + total_installed_cost_this = current_case->Values().Get("total_installed_cost")->Value(); + total_installed_cost += total_installed_cost_this; + has_a_contingency = has_a_contingency || HasContingency(bcsim); + // O&M costs are taken from the cash flows of each system. All types of O&M costs + // are entered in as fixed O&M costs in the financial case. This accounts for several things: + // (a) the escalation of O&M costs + // (b) the O&M costs by capacity and by generation, appropriately weighted by the system size/generation, + // (c) allows for inclusion of fuel costs that are found in some technologies but not others (fuel costs) + matrix_t om_fixed_this = bcsim.GetOutput("cf_om_fixed_expense")->Matrix(); // O&M fixed + matrix_t om_capacity = bcsim.GetOutput("cf_om_capacity_expense")->Matrix(); // O&M capacity based + matrix_t om_production = bcsim.GetOutput("cf_om_production_expense")->Matrix(); // O&M production based + matrix_t cf_om_fuel = bcsim.GetOutput("cf_om_fuel_expense")->Matrix(); // O&M fuel + matrix_t cf_opt_fuel_1 = bcsim.GetOutput("cf_om_opt_fuel_1_expense")->Matrix(); // O&M biomass + matrix_t cf_opt_fuel_2 = bcsim.GetOutput("cf_om_opt_fuel_2_expense")->Matrix(); // O&M coal + + // Battery costs + matrix_t om_fixed_this_1(1, analysis_period + 1, 0.); + matrix_t om_capacity_1(1, analysis_period + 1, 0.); + matrix_t om_production_1(1, analysis_period + 1, 0.); + matrix_t cf_batt_repl(1, analysis_period + 1, 0.); + if (bcsim.GetOutput("cf_om_fixed1_expense")) { + om_fixed_this_1 = bcsim.GetOutput("cf_om_fixed1_expense")->Matrix(); // battery fixed + om_capacity_1 = bcsim.GetOutput("cf_om_capacity1_expense")->Matrix(); // battery capacity based + om_production_1 = bcsim.GetOutput("cf_om_production1_expense")->Matrix(); // battery production based + } + if (bcsim.GetOutput("cf_battery_replacement_cost")) { + cf_batt_repl = bcsim.GetOutput("cf_battery_replacement_cost")->Matrix(); // battery replacement + } + + // Fuel cell costs + matrix_t om_fixed_this_2(1, analysis_period + 1, 0.); + matrix_t om_capacity_2(1, analysis_period + 1, 0.); + matrix_t om_production_2(1, analysis_period + 1, 0.); + matrix_t cf_fuelcell_repl(1, analysis_period + 1, 0.); + if (bcsim.GetOutput("cf_om_fixed2_expense")) { + om_fixed_this_2 = bcsim.GetOutput("cf_om_fixed2_expense")->Matrix(); // fuel cell fixed + om_capacity_2 = bcsim.GetOutput("cf_om_capacity2_expense")->Matrix(); // fuel cell capacity based + om_production_2 = bcsim.GetOutput("cf_om_production2_expense")->Matrix(); // fuel cell production based + cf_fuelcell_repl = bcsim.GetOutput("cf_fuelcell_replacement_cost")->Matrix(); // fuel cell replacement + } + + //in cash flows, the first entry in the array is "Year 0", so must call j+1 in loop + for (int i = 0; i < analysis_period; i++) { + om_total_this[i] = om_fixed_this[i + 1] + om_capacity[i + 1] + om_production[i + 1] + + om_fixed_this_1[i + 1] + om_capacity_1[i + 1] + om_production_1[i + 1] + + om_fixed_this_2[i + 1] + om_capacity_2[i + 1] + om_production_2[i + 1] + + cf_om_fuel[i + 1] + cf_opt_fuel_1[i + 1] + cf_opt_fuel_2[i + 1] + + cf_batt_repl[i + 1] + cf_fuelcell_repl[i + 1]; + + om_fixed[i] += om_total_this[i]; + } + } + + // put degradation and inflation rate back + if (financial_name != "None" && financial_name != "LCOE Calculator" && financial_name != "LCOH Calculator") { + degradation_vv->Set(degradation_orig); + } + if (financial_name != "None") { + inflation_vv->Set(inflation_orig); + } + + // Update UI with results + case_window->UpdateResults(); + case_window->SwitchToInputPage(case_page_orig); + case_window->SwitchToPage("results"); + } + + //For user benefit, change the fixed O&M schedule back to a single value if all entries are the same + bool constant1 = true; + if (financial_name != "None") { + for (int i = 1; i < analysis_period; i++) { //don't start at zero because comparing j-1 + if (om_fixed[i] != om_fixed[i - 1]) { + constant1 = false; + } + } + if (constant1) { + om_fixed.resize(1); + } + } + + // Set the generic system performance parameters + m_generic_case->Values().Get("system_capacity")->Set(nameplate); + m_generic_case->Values().Get("spec_mode")->Set(2); // specify the third radio button + m_generic_case->Values().Get("derate")->Set(0); // no additional losses- losses were computed in the individual models + m_generic_case->Values().Get("heat_rate")->Set(0); // no fuel costs- accounted for in O&M fuel costs from subsystem cash flows + m_generic_case->Values().Get("energy_output_array")->Set(hourly_energy.data(), hourly_energy.ncells()); + m_generic_case->VariableChanged("energy_output_array"); // triggers UI update + + bool overwrite_capital = m_chkOverwriteCapital->IsChecked(); + if (financial_name == "LCOE Calculator" || financial_name == "LCOH Calculator") { + if (!constant1) { + m_result_code = 1; + m_generic_case_window->UpdateResults(); + SamApp::Window()->SwitchToCaseWindow(m_generic_case_name); + m_generic_case_window->SwitchToInputPage("Power Plant"); + wxMessageBox("LCOE calculator error\n\n" + "Single annualized fixed operating costs must be used.\n\n" + "Check O&M inputs in case " + technology_name, + "Combine Cases Message", wxOK, this); + return; + } + else if (overwrite_capital) { + m_generic_case->Values().Get("fixed_operating_cost")->Set(om_fixed); + } + } + + // Set installation and operating costs + if (financial_name != "None" && financial_name != "Third Party" && overwrite_capital) { + // Installation Costs + m_generic_case->Values().Get("fixed_plant_input")->Set(total_installed_cost); + m_generic_case->Values().Get("genericsys.cost.per_watt")->Set(0.); + m_generic_case->Values().Get("genericsys.cost.contingency_percent")->Set(0.); + m_generic_case->Values().Get("genericsys.cost.epc.percent")->Set(0.); + m_generic_case->Values().Get("genericsys.cost.epc.fixed")->Set(0.); + m_generic_case->Values().Get("genericsys.cost.plm.percent")->Set(0.); + m_generic_case->Values().Get("genericsys.cost.plm.fixed")->Set(0.); + m_generic_case->Values().Get("genericsys.cost.sales_tax.percent")->Set(0.); + + // Operating Costs - all zero except fixed (see explanation above) + m_generic_case->Values().Get("om_fixed")->Set(om_fixed); + m_generic_case->Values().Get("om_capacity")->Set(new double[1]{0.}, 1); + m_generic_case->Values().Get("om_production")->Set(new double[1]{0.}, 1); + //O&M escalation rates are also zeroed because they are accounted for in the fixed O&M costs + m_generic_case->Values().Get("om_fixed_escal")->Set(0.); + m_generic_case->Values().Get("om_capacity_escal")->Set(0.); + m_generic_case->Values().Get("om_production_escal")->Set(0.); + + if (m_generic_case->Values().Get("om_fuel_cost")) { + m_generic_case->Values().Get("om_fuel_cost")->Set(new double[1]{0.}, 1); + m_generic_case->Values().Get("om_fuel_cost_escal")->Set(0.); + } + + if (m_generic_case->Values().Get("om_replacement_cost1")) { + m_generic_case->Values().Get("om_replacement_cost1")->Set(new double[1]{0.}, 1); + m_generic_case->Values().Get("om_replacement_cost_escal")->Set(0.); + } + } + + // Update UI with results + m_result_code = 0; // 0=success + SamApp::Window()->SwitchToCaseWindow(m_generic_case_name); + int result = m_generic_case->RecalculateAll(); + m_generic_case_window->UpdateResults(); + m_generic_case_window->SwitchToInputPage("Power Plant"); + if (is_notices) { + wxMessageBox("Notices\n\n" + "At least one of the models generated notices.\n\n" + "View these messages or warnings on the Notices pane of the Results page.", + "Combine Cases Message", wxOK | wxSTAY_ON_TOP, this); + } + if (has_a_contingency && technology_name == "Generic Battery" && financial_name != "Third Party") { + wxMessageBox("Notices\n\n" + "At least one of the models has a contingency specified.\n\n" + "Verify contingency is not double-counted on this generic-battery system's Installation Costs page.", + "Combine Cases Message", wxOK | wxSTAY_ON_TOP, this); + } + EndModal(wxID_OK); + + // 'Press' Edit array... button to show energy output array + ActiveInputPage* aip = 0; + wxUIObject* energy_output_array = m_generic_case_window->FindObject("energy_output_array", &aip); + if (AFDataArrayButton* btn_energy_output_array = energy_output_array->GetNative()) { + btn_energy_output_array->OnPressed(e); + } + } + else if (arychecked.Count() == 1) { + wxMessageBox("Not enough cases selected.\n\n" + "Choose at least two cases to combine.", + "Combine Cases Message", wxOK, this); + } + else { + wxMessageBox("No cases selected.\n\n" + "Choose at least two cases to combine.", + "Combine Cases Message", wxOK, this); + } + } + break; + } +} + +// Remake checklist widget with info in cases vector +void CombineCasesDialog::RefreshList(size_t first_item) +{ + m_chlCases->Freeze(); + m_chlCases->Clear(); + for (size_t i = 0; i < m_cases.size(); i++) + { + // Exclude generic case from displaying in case list + if (m_cases[i].display_name != m_generic_case_name) { + int ndx = m_chlCases->Append(m_cases[i].display_name); + if (m_cases[i].is_selected) { + m_chlCases->Check(ndx, true); + } + else { + m_chlCases->Check(ndx, false); + } + } + } + m_chlCases->Thaw(); + if (m_chlCases->GetCount() > 0) { + m_chlCases->SetFirstItem(first_item); + } +} + +void CombineCasesDialog::GetOpenCases() +{ + m_cases.clear(); + wxArrayString names = SamApp::Window()->Project().GetCaseNames(); + for (size_t i = 0; i < names.size(); i++) { + m_cases.push_back(CaseInfo(names[i], names[i])); + } +} + +bool CombineCasesDialog::HasContingency(Simulation& bcsim) +{ + std::vector contingency_names{ + "contingency_rate", + "contingency_percent", + "csp.dtr.cost.contingency_percent", + "csp.pt.cost.contingency_percent", + "csp.mslf.cost.contingency_percent", + "csp.lf.cost.contingency_percent", + "csp.gss.cost.contingency_percent", + "csp.tr.cost.contingency_percent", + "biopwr.cost.contingency_percent", + "geotherm.cost.contingency_percent" + }; + + for (auto& contingency_name : contingency_names) { + VarValue* contingency_vv = bcsim.GetOutput(contingency_name); + if (contingency_vv && contingency_vv->Value() > std::numeric_limits::epsilon()) { + return true; + } + } + + return false; +} diff --git a/src/combinecases.h b/src/combinecases.h new file mode 100644 index 0000000000..b01ccbfb42 --- /dev/null +++ b/src/combinecases.h @@ -0,0 +1,80 @@ +/** +BSD-3-Clause +Copyright 2019 Alliance for Sustainable Energy, LLC +Redistribution and use in source and binary forms, with or without modification, are permitted provided +that the following conditions are met : +1. Redistributions of source code must retain the above copyright notice, this list of conditions +and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions +and the following disclaimer in the documentation and/or other materials provided with the distribution. +3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse +or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED.IN NO EVENT SHALL THE COPYRIGHT HOLDER, CONTRIBUTORS, UNITED STATES GOVERNMENT OR UNITED STATES +DEPARTMENT OF ENERGY, NOR ANY OF THEIR EMPLOYEES, BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, +OR CONSEQUENTIAL DAMAGES(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT +OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#ifndef __CombineCasesDialog_h +#define __CombineCasesDialog_h + +#include + + +class wxCheckListBox; +class wxCheckbox; +class wxSpinCtrlDouble; + +class CombineCasesDialog : public wxDialog +{ + +public: + CombineCasesDialog(wxWindow* parent, const wxString& title, lk::invoke_t& cxt); + int GetResultCode() { + return m_result_code; + }; + + struct CaseInfo + { + wxString name; // dataset - e.g. psm + wxString display_name; + bool is_visible; + bool is_selected; + CaseInfo(wxString& _n, wxString& _dn) + : name(_n), display_name(_dn) + { + is_visible = true; + is_selected = false; + } + }; + +private: + void OnEvt(wxCommandEvent &); + + void RefreshList(size_t first_item); + + void GetOpenCases(); + + bool HasContingency(Simulation& bcsim); + + int m_result_code; + Case* m_generic_case; + wxString m_generic_case_name; + CaseWindow* m_generic_case_window; + std::vector m_generic_degradation; + std::vector m_cases; + wxCheckListBox* m_chlCases; + wxCheckBox* m_chkOverwriteCapital; + wxSpinCtrlDouble* m_spndDegradation; + AFSchedNumeric* m_schnDegradation; + + DECLARE_EVENT_TABLE() +}; + + +#endif diff --git a/src/invoke.cpp b/src/invoke.cpp index 17a5a57d3c..6329e4d63b 100644 --- a/src/invoke.cpp +++ b/src/invoke.cpp @@ -73,6 +73,7 @@ OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include "stochastic.h" #include "codegencallback.h" #include "nsrdb.h" +#include "combinecases.h" #include "wavetoolkit.h" #include "graph.h" @@ -2206,6 +2207,28 @@ void fcall_nsrdbquery(lk::invoke_t &cxt) cxt.result().hash_item("addfolder").assign(addfolder); } +void fcall_combinecasesquery(lk::invoke_t& cxt) +{ + LK_DOC("combinecasesquery", "Creates the Combine Cases dialog box, lists all open cases, simulates selected cases and returns a combined generation profile", "(none) : string"); + CombineCasesDialog dlgCombineCases(SamApp::Window(), "Combine Cases", cxt); + dlgCombineCases.CenterOnParent(); + int code = dlgCombineCases.ShowModal(); //shows the dialog and makes it so you can't interact with other parts until window is closed + + //Return an empty string if the window was dismissed + if (code == wxID_CANCEL) + { + cxt.result().assign(wxEmptyString); + return; + } + + int result = dlgCombineCases.GetResultCode(); // 0 = success, 1 = error + + cxt.result().empty_hash(); + + // meta data + cxt.result().hash_item("result_code").assign(result); +} + void fcall_wavetoolkit(lk::invoke_t& cxt) { LK_DOC("wavetoolkit", "Creates the Wave data download dialog box, lists all avaialble resource files, downloads multiple solar resource files, and returns local file name for weather file", "(none) : string"); @@ -5601,6 +5624,7 @@ lk::fcall_t* invoke_uicallback_funcs() fcall_current_at_voltage_sandia, fcall_windtoolkit, fcall_nsrdbquery, + fcall_combinecasesquery, fcall_wavetoolkit, fcall_openeiutilityrateform, fcall_group_read, diff --git a/src/uiobjects.cpp b/src/uiobjects.cpp index dab5127849..462c5e39ba 100644 --- a/src/uiobjects.cpp +++ b/src/uiobjects.cpp @@ -114,7 +114,8 @@ class wxUISchedNumericObject : public wxUIObject virtual void OnNativeEvent() { - /* nothing to do here ... */ + if (AFSchedNumeric* sn = GetNative()) + Property("UseSchedule").Set(sn->UseSchedule()); } }; diff --git a/src/widgets.cpp b/src/widgets.cpp index bb3785858a..e8c6a26b0d 100644 --- a/src/widgets.cpp +++ b/src/widgets.cpp @@ -263,7 +263,7 @@ class SchedNumericDialog : public wxDialog m_numVals = 0; if ( with_resize_options ) { - tools->Add( new wxStaticText( this, wxID_ANY, "Number of values:"), 0, wxALL|wxALIGN_CENTER_VERTICAL, 5 ); + tools->Add( new wxStaticText( this, wxID_ANY, "Number of values:"), 0, wxALL, 5 ); tools->Add( m_numVals = new wxNumericCtrl( this, ID_numValueCount, 50, wxNUMERIC_INTEGER ), 0, wxALL, 3 ); }