From efd4d872f7eca021e9b259514cb9f83f441710af Mon Sep 17 00:00:00 2001 From: Brian Mirletz Date: Wed, 6 Nov 2024 09:34:02 -0700 Subject: [PATCH] Implement adjust losses within the capacity model. Not yet fully hooked up to cmod battery --- shared/lib_battery.cpp | 34 +++-- shared/lib_battery.h | 18 ++- shared/lib_battery_capacity.cpp | 48 ++++++- shared/lib_battery_capacity.h | 8 ++ shared/lib_battery_dispatch.cpp | 2 +- shared/lib_battery_dispatch_automatic_btm.cpp | 12 +- shared/lib_battery_dispatch_automatic_fom.cpp | 10 +- .../lib_battery_dispatch_pvsmoothing_fom.cpp | 6 +- shared/logger.cpp | 2 +- ssc/cmod_battery.cpp | 6 +- ssc/cmod_battery_stateful.cpp | 4 +- .../lib_battery_dispatch_automatic_btm_test.h | 3 +- .../lib_battery_dispatch_automatic_fom_test.h | 3 +- .../lib_battery_dispatch_manual_test.h | 3 +- test/shared_test/lib_battery_test.cpp | 132 +++++++++++++++--- test/shared_test/lib_battery_test.h | 9 +- test/shared_test/lib_resilience_test.cpp | 2 +- 17 files changed, 232 insertions(+), 70 deletions(-) diff --git a/shared/lib_battery.cpp b/shared/lib_battery.cpp index 8caab948f..8fed84407 100644 --- a/shared/lib_battery.cpp +++ b/shared/lib_battery.cpp @@ -179,7 +179,7 @@ Define Losses */ void losses_t::initialize() { state = std::make_shared(); - state->loss_kw = 0; + state->ancillary_loss_kw = 0; if (params->loss_choice == losses_params::MONTHLY) { if (params->monthly_charge_loss.size() == 1) { params->monthly_charge_loss = std::vector(12, params->monthly_charge_loss[0]); @@ -210,19 +210,21 @@ void losses_t::initialize() { } } -losses_t::losses_t(const std::vector& monthly_charge, const std::vector& monthly_discharge, const std::vector& monthly_idle) { +losses_t::losses_t(const std::vector& monthly_charge, const std::vector& monthly_discharge, const std::vector& monthly_idle, const std::vector& adjust_losses) { params = std::make_shared(); params->loss_choice = losses_params::MONTHLY; params->monthly_charge_loss = monthly_charge; params->monthly_discharge_loss = monthly_discharge; params->monthly_idle_loss = monthly_idle; + params->adjust_loss = adjust_losses; initialize(); } -losses_t::losses_t(const std::vector& schedule_loss) { +losses_t::losses_t(const std::vector& schedule_loss, const std::vector& adjust_losses) { params = std::make_shared(); params->loss_choice = losses_params::SCHEDULE; params->schedule_loss = schedule_loss; + params->adjust_loss = adjust_losses; initialize(); } @@ -252,18 +254,24 @@ void losses_t::run_losses(size_t lifetimeIndex, double dtHour, double charge_ope // update system losses depending on user input if (params->loss_choice == losses_params::MONTHLY) { if (charge_operation == capacity_state::CHARGE) - state->loss_kw = params->monthly_charge_loss[monthIndex]; + state->ancillary_loss_kw = params->monthly_charge_loss[monthIndex]; if (charge_operation == capacity_state::DISCHARGE) - state->loss_kw = params->monthly_discharge_loss[monthIndex]; + state->ancillary_loss_kw = params->monthly_discharge_loss[monthIndex]; if (charge_operation == capacity_state::NO_CHARGE) - state->loss_kw = params->monthly_idle_loss[monthIndex]; + state->ancillary_loss_kw = params->monthly_idle_loss[monthIndex]; } else if (params->loss_choice == losses_params::SCHEDULE) { - state->loss_kw = params->schedule_loss[lifetimeIndex % params->schedule_loss.size()]; + state->ancillary_loss_kw = params->schedule_loss[lifetimeIndex % params->schedule_loss.size()]; } + + state->adjust_loss_percent = getAvailabilityLoss(lifetimeIndex); } -double losses_t::getLoss() { return state->loss_kw; } +double losses_t::getAncillaryLoss() { return state->ancillary_loss_kw; } + +double losses_t::getAvailabilityLoss(size_t lifetimeIndex) { + return params->adjust_loss[lifetimeIndex % params->adjust_loss.size()]; +} losses_state losses_t::get_state() { return *state; } @@ -584,7 +592,7 @@ double battery_t::run(size_t lifetimeIndex, double &I) { while (iterate_count < 5) { runThermalModel(I, lifetimeIndex); - runCapacityModel(I); + runCapacityModel(I, lifetimeIndex); double numerator = std::abs(I - I_initial); if ((numerator > 0.0) && (numerator / std::abs(I_initial) > tolerance)) { @@ -622,12 +630,14 @@ double battery_t::estimateCycleDamage() { return lifetime->estimateCycleDamage(); } -void battery_t::runCapacityModel(double &I) { +void battery_t::runCapacityModel(double &I, size_t lifetimeIndex) { // Don't update max capacity if the battery is idle if (std::abs(I) > tolerance) { // Need to first update capacity model to ensure temperature accounted for capacity->updateCapacityForThermal(thermal->capacity_percent()); } + double availability_loss = losses->getAvailabilityLoss(lifetimeIndex); + capacity->updateCapacityForAvailability(availability_loss); capacity->updateCapacity(I, params->dt_hr); } @@ -772,8 +782,8 @@ double battery_t::calculate_loss(double power, size_t lifetimeIndex) { } } -double battery_t::getLoss() { - return losses->getLoss(); +double battery_t::getAncillaryLoss() { + return losses->getAncillaryLoss(); } battery_state battery_t::get_state() { return *state; } diff --git a/shared/lib_battery.h b/shared/lib_battery.h index eeb80b45e..2ed411702 100644 --- a/shared/lib_battery.h +++ b/shared/lib_battery.h @@ -151,7 +151,8 @@ class thermal_t { */ struct losses_state { - double loss_kw; + double ancillary_loss_kw; + double adjust_loss_percent; friend std::ostream &operator<<(std::ostream &os, const losses_state &p); }; @@ -166,6 +167,7 @@ struct losses_params { std::vector monthly_discharge_loss; std::vector monthly_idle_loss; std::vector schedule_loss; + std::vector adjust_loss; friend std::ostream &operator<<(std::ostream &os, const losses_params &p); }; @@ -181,8 +183,9 @@ class losses_t { * \param[in] monthly_charge vector (size 1 for annual or 12 for monthly) containing battery system losses when charging (kW) (applied to PV or grid) * \param[in] monthly_discharge vector (size 1 for annual or 12 for monthly) containing battery system losses when discharge (kW) (applied to battery power) * \param[in] monthly_idle vector (size 1 for annual or 12 for monthly) containing battery system losses when idle (kW) (applied to PV or grid) + * \param[in] adjust_losses vector (size 0 for constant or per timestep) containing battery system availability losses (%) (applies to both power and energy capacity - if a system has 4 packs, a 25% loss means one pack is offline) */ - losses_t(const std::vector& monthly_charge, const std::vector& monthly_discharge, const std::vector& monthly_idle); + losses_t(const std::vector& monthly_charge, const std::vector& monthly_discharge, const std::vector& monthly_idle, const std::vector& adjust_losses); /** * \function losses_t @@ -190,8 +193,9 @@ class losses_t { * Construct the losses object for schedule of timeseries losses * * \param[in] schedule_loss vector (size 0 for constant or per timestep) containing battery system losses + * \param[in] adjust_losses vector (size 0 for constant or per timestep) containing battery system availability losses (%) (applies to both power and energy capacity - if a system has 4 packs, a 25% loss means one pack is offline) */ - explicit losses_t(const std::vector& schedule_loss = std::vector(1, 0)); + explicit losses_t(const std::vector& schedule_loss = std::vector(1, 0), const std::vector& adjust_losses = std::vector(1,0)); explicit losses_t(std::shared_ptr p); @@ -203,7 +207,9 @@ class losses_t { void run_losses(size_t lifetimeIndex, double dt_hour, double charge_operation); /// Get the loss at the specified simulation index (year 1) - double getLoss(); + double getAncillaryLoss(); + + double getAvailabilityLoss(size_t lifetimeIndex); losses_state get_state(); @@ -362,7 +368,7 @@ class battery_t { double estimateCycleDamage(); // Run a component level model - void runCapacityModel(double &I); + void runCapacityModel(double &I, size_t lifetimeIndex); void runVoltageModel(); @@ -409,7 +415,7 @@ class battery_t { double calculate_loss(double power, size_t lifetimeIndex); // Get the losses at the current step - double getLoss(); + double getAncillaryLoss(); battery_state get_state(); diff --git a/shared/lib_battery_capacity.cpp b/shared/lib_battery_capacity.cpp index 1be184caf..0fc94bbe8 100644 --- a/shared/lib_battery_capacity.cpp +++ b/shared/lib_battery_capacity.cpp @@ -140,16 +140,19 @@ capacity_params capacity_t::get_params() { return *params; } capacity_state capacity_t::get_state() { return *state; } void capacity_t::check_SOC() { - double q_upper = state->qmax_lifetime * params->maximum_SOC * 0.01; - double q_lower = state->qmax_lifetime * params->minimum_SOC * 0.01; + double max_SOC_available = params->maximum_SOC * (1 - state->percent_unavailable); + double min_SOC_available = params->minimum_SOC * (1 - state->percent_unavailable); + + double q_upper = state->qmax_lifetime * max_SOC_available * 0.01; + double q_lower = state->qmax_lifetime * min_SOC_available * 0.01; // set capacity to upper thermal limit - if (q_upper > state->qmax_thermal * params->maximum_SOC * 0.01) { - q_upper = state->qmax_thermal * params->maximum_SOC * 0.01; + if (q_upper > state->qmax_thermal * max_SOC_available * 0.01) { + q_upper = state->qmax_thermal * max_SOC_available * 0.01; } // do this so battery can cycle full depth and we calculate correct SOC min - if (q_lower > state->qmax_thermal * params->minimum_SOC * 0.01) { - q_lower = state->qmax_thermal * params->minimum_SOC * 0.01; + if (q_lower > state->qmax_thermal * min_SOC_available * 0.01) { + q_lower = state->qmax_thermal * min_SOC_available * 0.01; } if (state->q0 > q_upper + tolerance) { @@ -175,6 +178,7 @@ void capacity_t::check_SOC() { } void capacity_t::update_SOC() { + // Want to continue to define SOC as nameplate minus degradation (availability losses lower SOC, not nameplate) double max = fmin(state->qmax_lifetime, state->qmax_thermal); if (max == 0) { state->q0 = 0; @@ -282,6 +286,8 @@ void capacity_kibam_t::replace_battery(double replacement_percent) { state->leadacid.q2_0 = state->q0 - state->leadacid.q1_0; state->SOC = params->initial_SOC; state->SOC_prev = 50; + state->percent_unavailable = 0.0; + state->percent_unavailable_prev = 0.0; update_SOC(); } @@ -438,6 +444,22 @@ void capacity_kibam_t::updateCapacityForLifetime(double capacity_percent) { update_SOC(); } +void capacity_kibam_t::updateCapacityForAvailability(double availability_percent) { + state->percent_unavailable_prev = state->percent_unavailable; + state->percent_unavailable = availability_percent; + + double timestep_loss = state->percent_unavailable_prev - state->percent_unavailable; + if (timestep_loss > 1e-7) { + double q0_orig = state->q0; + state->q0 *= (1 - timestep_loss); + state->leadacid.q1 *= (1 - timestep_loss); + state->leadacid.q2 *= (1 - timestep_loss); + state->I_loss += (q0_orig - state->q0) / params->dt_hr; + } + + update_SOC(); +} + double capacity_kibam_t::q1() { return state->leadacid.q1_0; } double capacity_kibam_t::q2() { return state->leadacid.q2_0; } @@ -533,6 +555,20 @@ void capacity_lithium_ion_t::updateCapacityForLifetime(double capacity_percent) update_SOC(); } +void capacity_lithium_ion_t::updateCapacityForAvailability(double availability_percent) { + state->percent_unavailable_prev = state->percent_unavailable; + state->percent_unavailable = availability_percent; + + double timestep_loss = state->percent_unavailable - state->percent_unavailable_prev; + if (timestep_loss > 1e-7) { + double q0_orig = state->q0; + state->q0 *= (1 - timestep_loss); + state->I_loss += (q0_orig - state->q0) / params->dt_hr; + } + + update_SOC(); +} + double capacity_lithium_ion_t::q1() { return state->q0; } double capacity_lithium_ion_t::q10() { return state->qmax_lifetime; } diff --git a/shared/lib_battery_capacity.h b/shared/lib_battery_capacity.h index 41e8d7a58..3c67cf0df 100644 --- a/shared/lib_battery_capacity.h +++ b/shared/lib_battery_capacity.h @@ -47,6 +47,8 @@ struct capacity_state { double I_loss; // [A] - Lifetime and thermal losses double SOC; // [%] - State of Charge double SOC_prev; // [%] - previous step + double percent_unavailable; // [%] - Percent of system that is down + double percent_unavailable_prev; // [%] - Percent of system that was down last step enum { CHARGE, NO_CHARGE, DISCHARGE @@ -120,6 +122,8 @@ class capacity_t { virtual void replace_battery(double replacement_percent) = 0; + virtual void updateCapacityForAvailability(double availability_percent) = 0; + void change_SOC_limits(double min, double max) { params->minimum_SOC = min; params->maximum_SOC = max; @@ -199,6 +203,8 @@ class capacity_kibam_t : public capacity_t { void replace_battery(double replacement_percent) override; + void updateCapacityForAvailability(double availability_percent) override; + double q1() override; // Available charge double q2(); // Bound charge double q10() override; // Capacity at 10 hour discharge rate @@ -254,6 +260,8 @@ class capacity_lithium_ion_t : public capacity_t { void replace_battery(double replacement_percent) override; + void updateCapacityForAvailability(double availability_percent) override; + double q1() override; // Available charge double q10() override; // Capacity at 10 hour discharge rate }; diff --git a/shared/lib_battery_dispatch.cpp b/shared/lib_battery_dispatch.cpp index a98e1c428..552d1607d 100644 --- a/shared/lib_battery_dispatch.cpp +++ b/shared/lib_battery_dispatch.cpp @@ -436,7 +436,7 @@ void dispatch_t::runDispatch(size_t lifetimeIndex) // Run Battery Model to update charge based on charge/discharge m_batteryPower->powerBatteryDC = _Battery->run(lifetimeIndex, I); - m_batteryPower->powerSystemLoss = _Battery->getLoss(); + m_batteryPower->powerSystemLoss = _Battery->getAncillaryLoss(); // Update power flow calculations, calculate AC power, and check the constraints m_batteryPowerFlow->calculate(); diff --git a/shared/lib_battery_dispatch_automatic_btm.cpp b/shared/lib_battery_dispatch_automatic_btm.cpp index 9ef100e67..c9053fd29 100644 --- a/shared/lib_battery_dispatch_automatic_btm.cpp +++ b/shared/lib_battery_dispatch_automatic_btm.cpp @@ -244,13 +244,13 @@ void dispatch_automatic_behind_the_meter_t::update_dispatch(size_t year, size_t { // extract input power by modifying lifetime index to year 1 m_batteryPower->powerBatteryTarget = _P_battery_use[idx % (8760 * _steps_per_hour)]; - double loss_kw = _Battery->calculate_loss(m_batteryPower->powerBatteryTarget, idx); // Battery is responsible for covering discharge losses + double ancillary_loss_kw = _Battery->calculate_loss(m_batteryPower->powerBatteryTarget, idx); // Battery is responsible for covering discharge losses if (m_batteryPower->connectionMode == AC_CONNECTED) { - m_batteryPower->powerBatteryTarget = m_batteryPower->adjustForACEfficiencies(m_batteryPower->powerBatteryTarget, loss_kw); + m_batteryPower->powerBatteryTarget = m_batteryPower->adjustForACEfficiencies(m_batteryPower->powerBatteryTarget, ancillary_loss_kw); } else if (m_batteryPower->powerBatteryTarget > 0) { // Adjust for DC discharge losses - m_batteryPower->powerBatteryTarget += loss_kw; + m_batteryPower->powerBatteryTarget += ancillary_loss_kw; } } @@ -882,14 +882,14 @@ void dispatch_automatic_behind_the_meter_t::check_power_restrictions(double& pow void dispatch_automatic_behind_the_meter_t::set_battery_power(size_t idx, size_t day_index, FILE *p, const bool debug) { - double loss_kw = _Battery->calculate_loss(_P_battery_use[day_index], idx); // Units are kWac for AC connected batteries, and kWdc for DC connected + double ancillary_loss_kw = _Battery->calculate_loss(_P_battery_use[day_index], idx); // Units are kWac for AC connected batteries, and kWdc for DC connected // At this point the target power is expressed in AC, must convert to DC for battery if (m_batteryPower->connectionMode == m_batteryPower->AC_CONNECTED) { - _P_battery_use[day_index] = m_batteryPower->adjustForACEfficiencies(_P_battery_use[day_index], loss_kw); + _P_battery_use[day_index] = m_batteryPower->adjustForACEfficiencies(_P_battery_use[day_index], ancillary_loss_kw); } else { - _P_battery_use[day_index] = m_batteryPower->adjustForDCEfficiencies(_P_battery_use[day_index], loss_kw); + _P_battery_use[day_index] = m_batteryPower->adjustForDCEfficiencies(_P_battery_use[day_index], ancillary_loss_kw); } if (debug) diff --git a/shared/lib_battery_dispatch_automatic_fom.cpp b/shared/lib_battery_dispatch_automatic_fom.cpp index 9e828030b..9e021193e 100644 --- a/shared/lib_battery_dispatch_automatic_fom.cpp +++ b/shared/lib_battery_dispatch_automatic_fom.cpp @@ -348,9 +348,9 @@ void dispatch_automatic_front_of_meter_t::update_dispatch(size_t year, size_t ho // Discharge if we are in a high-price period and have battery and inverter capacity if (highDischargeValuePeriod && revenueToDischarge > 0 && excessAcCapacity && batteryHasDischargeCapacity && interconnectionHasCapacity) { - double loss_kw = _Battery->calculate_loss(m_batteryPower->powerBatteryTarget, lifetimeIndex); // Battery is responsible for covering discharge losses + double ancillary_loss_kw = _Battery->calculate_loss(m_batteryPower->powerBatteryTarget, lifetimeIndex); // Battery is responsible for covering discharge losses if (m_batteryPower->connectionMode == BatteryPower::DC_CONNECTED) { - powerBattery = _inverter_paco + loss_kw - m_batteryPower->powerSystem; + powerBattery = _inverter_paco + ancillary_loss_kw - m_batteryPower->powerSystem; } else { powerBattery = _inverter_paco; // AC connected battery is already maxed out by AC power limit, cannot increase dispatch to ccover losses @@ -365,13 +365,13 @@ void dispatch_automatic_front_of_meter_t::update_dispatch(size_t year, size_t ho { // extract input power by modifying lifetime index to year 1 m_batteryPower->powerBatteryTarget = _P_battery_use[lifetimeIndex % (8760 * _steps_per_hour)]; - double loss_kw = _Battery->calculate_loss(m_batteryPower->powerBatteryTarget, lifetimeIndex); // Battery is responsible for covering discharge losses + double ancillary_loss_kw = _Battery->calculate_loss(m_batteryPower->powerBatteryTarget, lifetimeIndex); // Battery is responsible for covering discharge losses if (m_batteryPower->connectionMode == AC_CONNECTED){ - m_batteryPower->powerBatteryTarget = m_batteryPower->adjustForACEfficiencies(m_batteryPower->powerBatteryTarget, loss_kw); + m_batteryPower->powerBatteryTarget = m_batteryPower->adjustForACEfficiencies(m_batteryPower->powerBatteryTarget, ancillary_loss_kw); } else if (m_batteryPower->powerBatteryTarget > 0) { // Adjust for DC discharge losses - m_batteryPower->powerBatteryTarget += loss_kw; + m_batteryPower->powerBatteryTarget += ancillary_loss_kw; } } diff --git a/shared/lib_battery_dispatch_pvsmoothing_fom.cpp b/shared/lib_battery_dispatch_pvsmoothing_fom.cpp index ca1d22ef2..85ab764c4 100644 --- a/shared/lib_battery_dispatch_pvsmoothing_fom.cpp +++ b/shared/lib_battery_dispatch_pvsmoothing_fom.cpp @@ -361,13 +361,13 @@ void dispatch_pvsmoothing_front_of_meter_t::update_dispatch(size_t year, size_t // adjust for loss and efficiencies m_batteryPower->powerBatteryTarget = (m_batt_dispatch_pvs_nameplate_ac > 0 ? m_batt_dispatch_pvs_nameplate_ac * m_batt_dispatch_pvs_battpower : m_batt_dispatch_pvs_battpower); - double loss_kw = _Battery->calculate_loss(m_batteryPower->powerBatteryTarget, lifetimeIndex); // Battery is responsible for covering discharge losses + double ancillary_loss_kw = _Battery->calculate_loss(m_batteryPower->powerBatteryTarget, lifetimeIndex); // Battery is responsible for covering discharge losses if (m_batteryPower->connectionMode == AC_CONNECTED) { - m_batteryPower->powerBatteryTarget = m_batteryPower->adjustForACEfficiencies(m_batteryPower->powerBatteryTarget, loss_kw); + m_batteryPower->powerBatteryTarget = m_batteryPower->adjustForACEfficiencies(m_batteryPower->powerBatteryTarget, ancillary_loss_kw); } else if (m_batteryPower->powerBatteryTarget > 0) { // Adjust for DC discharge losses - m_batteryPower->powerBatteryTarget += loss_kw; + m_batteryPower->powerBatteryTarget += ancillary_loss_kw; } m_batteryPower->powerBatteryDC = m_batteryPower->powerBatteryTarget; diff --git a/shared/logger.cpp b/shared/logger.cpp index a4f6ddc9e..b2196641b 100644 --- a/shared/logger.cpp +++ b/shared/logger.cpp @@ -243,7 +243,7 @@ std::ostream &operator<<(std::ostream &os, const thermal_params &p) { std::ostream &operator<<(std::ostream& os, const losses_state &p) { char buf[256]; - sprintf(buf, R"("losses_state": { "loss_percent": %.3f })", p.loss_kw); + sprintf(buf, R"("losses_state": { "loss_percent": %.3f })", p.ancillary_loss_kw); os << buf; return os; } diff --git a/ssc/cmod_battery.cpp b/ssc/cmod_battery.cpp index 5a9c4634a..a8b808015 100644 --- a/ssc/cmod_battery.cpp +++ b/ssc/cmod_battery.cpp @@ -1172,13 +1172,15 @@ battstor::battstor(var_table& vt, bool setup_model, size_t nrec, double dt_hr, c batt_vars->batt_Qfull_flow, batt_vars->batt_initial_SOC, batt_vars->batt_maximum_SOC, batt_vars->batt_minimum_SOC, dt_hr); } + // TODO: fix this! + std::vector adj_losses; if (batt_vars->batt_loss_choice == losses_params::MONTHLY) { if (*std::min_element(batt_vars->batt_losses_charging.begin(), batt_vars->batt_losses_charging.end()) < 0 || *std::min_element(batt_vars->batt_losses_discharging.begin(), batt_vars->batt_losses_discharging.end()) < 0 || *std::min_element(batt_vars->batt_losses_idle.begin(), batt_vars->batt_losses_idle.end()) < 0) { throw exec_error("battery", "Battery loss inputs batt_losses_charging, batt_losses_discharging, and batt_losses_idle cannot include negative numbers."); } - losses_model = new losses_t(batt_vars->batt_losses_charging, batt_vars->batt_losses_discharging, batt_vars->batt_losses_idle); + losses_model = new losses_t(batt_vars->batt_losses_charging, batt_vars->batt_losses_discharging, batt_vars->batt_losses_idle, adj_losses); } else if (batt_vars->batt_loss_choice == losses_params::SCHEDULE) { if (!(batt_vars->batt_losses.size() == 1 || batt_vars->batt_losses.size() == nrec)) { @@ -1187,7 +1189,7 @@ battstor::battstor(var_table& vt, bool setup_model, size_t nrec, double dt_hr, c if (*std::min_element(batt_vars->batt_losses.begin(), batt_vars->batt_losses.end()) < 0) { throw exec_error("battery", "Battery loss input batt_losses cannot include negative numbers."); } - losses_model = new losses_t(batt_vars->batt_losses); + losses_model = new losses_t(batt_vars->batt_losses, adj_losses); } else { losses_model = new losses_t(); diff --git a/ssc/cmod_battery_stateful.cpp b/ssc/cmod_battery_stateful.cpp index 03d80973c..917d3f1c6 100644 --- a/ssc/cmod_battery_stateful.cpp +++ b/ssc/cmod_battery_stateful.cpp @@ -296,7 +296,7 @@ void write_battery_state(const battery_state& state, var_table* vt) { } } - vt->assign_match_case("loss_kw", state.losses->loss_kw); + vt->assign_match_case("loss_kw", state.losses->ancillary_loss_kw); vt->assign_match_case("n_replacements", state.replacement->n_replacements); vt->assign_match_case("indices_replaced", state.replacement->indices_replaced); @@ -410,7 +410,7 @@ void read_battery_state(battery_state& state, var_table* vt) { } } - vt_get_number(vt, "loss_kw", &state.losses->loss_kw); + vt_get_number(vt, "loss_kw", &state.losses->ancillary_loss_kw); vt_get_int(vt, "n_replacements", &state.replacement->n_replacements); vt_get_array_vec(vt, "indices_replaced", state.replacement->indices_replaced); diff --git a/test/shared_test/lib_battery_dispatch_automatic_btm_test.h b/test/shared_test/lib_battery_dispatch_automatic_btm_test.h index df3d61c70..2e30078a3 100644 --- a/test/shared_test/lib_battery_dispatch_automatic_btm_test.h +++ b/test/shared_test/lib_battery_dispatch_automatic_btm_test.h @@ -128,7 +128,8 @@ class AutoBTMTest_lib_battery_dispatch : public BatteryProperties , public Dispa std::vector charging_losses(12, 1); // Monthly losses std::vector discharging_losses(12, 2); std::vector idle_losses(12, 0.5); - lossModel = new losses_t(charging_losses, discharging_losses, idle_losses); + std::vector adjust_losses(8760, 0); + lossModel = new losses_t(charging_losses, discharging_losses, idle_losses, adjust_losses); batteryModel = new battery_t(dtHour, chemistry, capacityModel, voltageModel, lifetimeModel, thermalModel, lossModel); int numberOfInverters = 40; diff --git a/test/shared_test/lib_battery_dispatch_automatic_fom_test.h b/test/shared_test/lib_battery_dispatch_automatic_fom_test.h index 96cf981e8..e75902c64 100644 --- a/test/shared_test/lib_battery_dispatch_automatic_fom_test.h +++ b/test/shared_test/lib_battery_dispatch_automatic_fom_test.h @@ -104,7 +104,8 @@ class AutoFOM_lib_battery_dispatch : public BatteryProperties , public DispatchP std::vector charging_losses(12, 10); // Monthly losses std::vector discharging_losses(12, 20); std::vector idle_losses(12, 5); - lossModel = new losses_t(charging_losses, discharging_losses, idle_losses); + std::vector adjust_losses(8760, 0); + lossModel = new losses_t(charging_losses, discharging_losses, idle_losses, adjust_losses); batteryModel = new battery_t(dtHour, chemistry, capacityModel, voltageModel, lifetimeModel, thermalModel, lossModel); int numberOfInverters = 40; diff --git a/test/shared_test/lib_battery_dispatch_manual_test.h b/test/shared_test/lib_battery_dispatch_manual_test.h index b5b0dfdee..50e8fd2ce 100644 --- a/test/shared_test/lib_battery_dispatch_manual_test.h +++ b/test/shared_test/lib_battery_dispatch_manual_test.h @@ -124,8 +124,9 @@ class ManualTest_lib_battery_dispatch_losses : public ManualTest_lib_battery_dis std::vector charging_losses(12, 1); // Monthly losses std::vector discharging_losses(12, 2); std::vector idle_losses(12, 0.5); + std::vector adjust_losses(8760, 0); - lossModel = new losses_t(charging_losses, discharging_losses, idle_losses); + lossModel = new losses_t(charging_losses, discharging_losses, idle_losses, adjust_losses); batteryModel = new battery_t(dtHour, chemistry, capacityModel, voltageModel, lifetimeModel, thermalModel, lossModel); int numberOfInverters = 1; diff --git a/test/shared_test/lib_battery_test.cpp b/test/shared_test/lib_battery_test.cpp index 7af0f9a6c..23049eaa0 100644 --- a/test/shared_test/lib_battery_test.cpp +++ b/test/shared_test/lib_battery_test.cpp @@ -179,7 +179,7 @@ TEST_F(lib_battery_thermal_test, updateTemperatureTestSubMinute) { } TEST_F(lib_battery_losses_test, MonthlyLossesTest){ - model = std::unique_ptr(new losses_t(chargingLosses, dischargingLosses, chargingLosses)); + model = std::unique_ptr(new losses_t(chargingLosses, dischargingLosses, chargingLosses, adjustLosses)); // losses for charging and idling are the same int charge_mode = capacity_state::CHARGE; @@ -187,30 +187,30 @@ TEST_F(lib_battery_losses_test, MonthlyLossesTest){ size_t idx = 0; double dt_hr = 1; model->run_losses(idx, dt_hour, charge_mode); - EXPECT_NEAR(model->getLoss(), 0, tol) << "MonthlyLossesTest: 1"; + EXPECT_NEAR(model->getAncillaryLoss(), 0, tol) << "MonthlyLossesTest: 1"; idx = 40 * 24; model->run_losses(idx, dt_hour, charge_mode); - EXPECT_NEAR(model->getLoss(), 1, tol) << "MonthlyLossesTest: 2"; + EXPECT_NEAR(model->getAncillaryLoss(), 1, tol) << "MonthlyLossesTest: 2"; idx = 70 * 24; model->run_losses(idx, dt_hour, charge_mode); - EXPECT_NEAR(model->getLoss(), 2, tol) << "MonthlyLossesTest: 3"; + EXPECT_NEAR(model->getAncillaryLoss(), 2, tol) << "MonthlyLossesTest: 3"; // discharging charge_mode = capacity_state::DISCHARGE; idx = 0; model->run_losses(idx, dt_hour, charge_mode); - EXPECT_NEAR(model->getLoss(), 1, tol) << "MonthlyLossesTest: 4"; + EXPECT_NEAR(model->getAncillaryLoss(), 1, tol) << "MonthlyLossesTest: 4"; idx = 40 * 24; model->run_losses(idx, dt_hour, charge_mode); - EXPECT_NEAR(model->getLoss(), 2, tol) << "MonthlyLossesTest: 5"; + EXPECT_NEAR(model->getAncillaryLoss(), 2, tol) << "MonthlyLossesTest: 5"; idx = 70 * 24; model->run_losses(idx, dt_hour, charge_mode); - EXPECT_NEAR(model->getLoss(), 3, tol) << "MonthlyLossesTest: 6"; + EXPECT_NEAR(model->getAncillaryLoss(), 3, tol) << "MonthlyLossesTest: 6"; } @@ -222,15 +222,15 @@ TEST_F(lib_battery_losses_test, TimeSeriesLossesTest){ size_t idx = 0; model->run_losses(idx, dt_hour, charge_mode); - EXPECT_NEAR(model->getLoss(), 0, tol) << "TimeSeriesLossesTest: 1"; + EXPECT_NEAR(model->getAncillaryLoss(), 0, tol) << "TimeSeriesLossesTest: 1"; idx = 40; model->run_losses(idx, dt_hour, charge_mode); - EXPECT_NEAR(model->getLoss(), 40./8760, tol) << "TimeSeriesLossesTest: 2"; + EXPECT_NEAR(model->getAncillaryLoss(), 40./8760, tol) << "TimeSeriesLossesTest: 2"; idx = 70; model->run_losses(idx, dt_hour, charge_mode); - EXPECT_NEAR(model->getLoss(), 70./8760, tol) << "TimeSeriesLossesTest: 3"; + EXPECT_NEAR(model->getAncillaryLoss(), 70./8760, tol) << "TimeSeriesLossesTest: 3"; } @@ -247,7 +247,7 @@ TEST_F(lib_battery_test, runTestCycleAt1C){ // std::cerr << "\n" << idx << ": " << capacity_passed << "\n"; auto s = battery_state_test(lifetime_params::CALCYC); - s.capacity = {479.75, 1000, 960.01, 20.25, 0, 49.97, 52.09, 2}; + s.capacity = {479.75, 1000, 960.01, 20.25, 0, 49.97, 52.09, 0.0, 0.0, 2}; s.batt_voltage = 552.03; s.lifetime.calendar->q_relative_calendar = 102; s.lifetime.cycle->q_relative_cycle = 100; @@ -262,7 +262,7 @@ TEST_F(lib_battery_test, runTestCycleAt1C){ } // std::cerr << idx << ": soc " << batteryModel->SOC() << ", cap " << capacity_passed << "\n"; // the SOC isn't at 5 so it means the controller is not able to calculate a current/voltage at which to discharge to 5 - s.capacity = {54.5, 1000, 960.01, 20.25, 0, 5.67, 7.79, 2}; + s.capacity = {54.5, 1000, 960.01, 20.25, 0, 5.67, 7.79, 0.0, 0.0, 2}; s.batt_voltage = 470.17; s.lifetime.day_age_of_battery = 0.875; s.lifetime.q_relative = 100; @@ -288,7 +288,7 @@ TEST_F(lib_battery_test, runTestCycleAt1C){ } // std::cerr << idx << ": soc " << batteryModel->SOC() << ", cap " << capacity_passed << "\n"; // the SOC isn't at 5 so it means the controller is not able to calculate a current/voltage at which to discharge to 5 - s.capacity = { 45.62, 920.29, 883.48, 8.995, 0, 5.164, 6.182, 2}; + s.capacity = { 45.62, 920.29, 883.48, 8.995, 0, 5.164, 6.182, 0.0, 0.0, 2}; s.batt_voltage = 465.54; s.lifetime.q_relative = 93.08; s.lifetime.cycle->q_relative_cycle = 92.03; @@ -320,7 +320,7 @@ TEST_F(lib_battery_test, runTestCycleAt3C){ // std::cerr << "\n" << idx << ": " << capacity_passed << "\n"; auto s = battery_state_test(lifetime_params::CALCYC); - s.capacity = {439.25, 1000, 960.02, 60.75, 0, 45.75, 52.08, 2}; + s.capacity = {439.25, 1000, 960.02, 60.75, 0, 45.75, 52.08, 0.0, 0.0, 2}; s.batt_voltage = 550.10; s.lifetime.q_relative = 100; s.lifetime.cycle->q_relative_cycle = 100; @@ -336,7 +336,7 @@ TEST_F(lib_battery_test, runTestCycleAt3C){ } // std::cerr << idx << ": soc " << batteryModel->SOC() << ", cap " << capacity_passed << "\n"; // the SOC isn't at 5 so it means the controller is not able to calculate a current/voltage at which to discharge to 5 - s.capacity = {48.01, 1000, 960.11, 26.74, 0, 5.00, 7.78, 2}; + s.capacity = {48.01, 1000, 960.11, 26.74, 0, 5.00, 7.78, 0.0, 0.0, 2}; s.batt_voltage = 463.93; s.lifetime.day_age_of_battery = 0.29; s.lifetime.q_relative = 101.98; @@ -360,7 +360,7 @@ TEST_F(lib_battery_test, runTestCycleAt3C){ } // std::cerr << idx << ": soc " << batteryModel->SOC() << ", cap " << capacity_passed << "\n"; // the SOC isn't at 5 so it means the controller is not able to calculate a current/voltage at which to discharge to 5 - s.capacity = { 47.106, 920.30, 883.49, 9.08, 0, 5.33, 6.36, 2}; + s.capacity = { 47.106, 920.30, 883.49, 9.08, 0, 5.33, 6.36, 0.0, 0.0, 2}; s.batt_voltage = 467.105; s.lifetime.q_relative = 93.08; s.lifetime.day_age_of_battery = 2591.17; @@ -800,7 +800,7 @@ TEST_F(lib_battery_test, NMCLifeModel) { auto capacityModelNMC = new capacity_lithium_ion_t(q, SOC_init, SOC_max, SOC_min, dtHour); auto voltageModelNMC = new voltage_dynamic_t(n_series, n_strings, Vnom_default, Vfull, Vexp, Vnom, Qfull, Qexp, Qnom, Vcut, C_rate, resistance, dtHour); - auto lossModelNMC = new losses_t(monthlyLosses, monthlyLosses, monthlyLosses); + auto lossModelNMC = new losses_t(monthlyLosses, monthlyLosses, monthlyLosses, adjustLosses); auto batteryNMC = std::unique_ptr(new battery_t(dtHour, chemistry, capacityModelNMC, voltageModelNMC, lifetimeModelNMC, thermalModelNMC, lossModelNMC)); double I = Qfull * n_strings * 2; @@ -836,7 +836,7 @@ TEST_F(lib_battery_test, AdaptiveTimestepNMC) { auto capacityModelNMC = new capacity_lithium_ion_t(q, SOC_init, SOC_max, SOC_min, dtHour); auto voltageModelNMC = new voltage_dynamic_t(n_series, n_strings, Vnom_default, Vfull, Vexp, Vnom, Qfull, Qexp, Qnom, Vcut, C_rate, resistance, dtHour); - auto lossModelNMC = new losses_t(monthlyLosses, monthlyLosses, monthlyLosses); + auto lossModelNMC = new losses_t(monthlyLosses, monthlyLosses, monthlyLosses, adjustLosses); batteryModel = std::unique_ptr(new battery_t(dtHour, chemistry, capacityModelNMC, voltageModelNMC, lifetimeModelNMC, thermalModelNMC, lossModelNMC)); @@ -937,7 +937,7 @@ TEST_F(lib_battery_test, AdaptiveTimestepNonIntegerStep) { auto capacityModelNMC = new capacity_lithium_ion_t(q, SOC_init, SOC_max, SOC_min, dtHour); auto voltageModelNMC = new voltage_dynamic_t(n_series, n_strings, Vnom_default, Vfull, Vexp, Vnom, Qfull, Qexp, Qnom, 0, C_rate, resistance, dtHour); - auto lossModelNMC = new losses_t(monthlyLosses, monthlyLosses, monthlyLosses); + auto lossModelNMC = new losses_t(monthlyLosses, monthlyLosses, monthlyLosses, adjustLosses); auto batt_adaptive = std::unique_ptr(new battery_t(dtHour, chemistry, capacityModelNMC, voltageModelNMC, lifetimeModelNMC, thermalModelNMC, lossModelNMC)); @@ -956,7 +956,7 @@ TEST_F(lib_battery_test, LMOLTOLifeModel) { auto capacityModelNMC = new capacity_lithium_ion_t(q, SOC_init, SOC_max, SOC_min, dtHour); auto voltageModelNMC = new voltage_dynamic_t(n_series, n_strings, Vnom_default, Vfull, Vexp, Vnom, Qfull, Qexp, Qnom, Vcut, C_rate, resistance, dtHour); - auto lossModelNMC = new losses_t(monthlyLosses, monthlyLosses, monthlyLosses); + auto lossModelNMC = new losses_t(monthlyLosses, monthlyLosses, monthlyLosses, adjustLosses); auto batteryNMC = std::unique_ptr(new battery_t(dtHour, chemistry, capacityModelNMC, voltageModelNMC, lifetimeModelNMC, thermalModelNMC, lossModelNMC)); double I = Qfull * n_strings * 2; @@ -986,7 +986,7 @@ TEST_F(lib_battery_test, AdaptiveTimestepLMOLTO) { auto capacityModel = new capacity_lithium_ion_t(q, SOC_init, SOC_max, SOC_min, dtHour); auto voltageModel = new voltage_dynamic_t(n_series, n_strings, Vnom_default, Vfull, Vexp, Vnom, Qfull, Qexp, Qnom, Vcut, C_rate, resistance, dtHour); - auto lossModel = new losses_t(monthlyLosses, monthlyLosses, monthlyLosses); + auto lossModel = new losses_t(monthlyLosses, monthlyLosses, monthlyLosses, adjustLosses); batteryModel = std::unique_ptr(new battery_t(dtHour, chemistry, capacityModel, voltageModel, lifetimeModel, thermalModel, lossModel)); @@ -1078,3 +1078,93 @@ TEST_F(lib_battery_test, AdaptiveTimestepLMOLTO) { delete batt_adaptive; delete batt_subhourly; } + +TEST_F(lib_battery_test, testCyclesWithAvailabilityLosses) { + auto lifetimeModel = new lifetime_calendar_cycle_t(cycleLifeMatrix, dtHour, 1.02, 2.66e-3, -7280, 930); + auto thermalModel = new thermal_t(dtHour, mass, surface_area, resistance, Cp, h, capacityVsTemperature, T_room); + auto capacityModel = new capacity_lithium_ion_t(q, SOC_init, SOC_max, SOC_min, dtHour); + auto voltageModel = new voltage_dynamic_t(n_series, n_strings, Vnom_default, Vfull, Vexp, Vnom, Qfull, Qexp, Qnom, + Vcut, C_rate, resistance, dtHour); + + adjustLosses.clear(); + for (size_t i = 0; i < 8760; i++) { + adjustLosses.push_back(0.5); + } + + auto lossModel = new losses_t(monthlyLosses, monthlyLosses, monthlyLosses, adjustLosses); + + batteryModel = std::unique_ptr(new battery_t(dtHour, chemistry, capacityModel, voltageModel, lifetimeModel, thermalModel, lossModel)); + + + size_t idx = 0; + double capacity_passed = 0.; + double I = Qfull * n_strings; + batteryModel->run(idx++, I); + capacity_passed += batteryModel->I() * batteryModel->V() / 1000.; + // std::cerr << "\n" << idx << ": " << capacity_passed << "\n"; + + auto s = battery_state_test(lifetime_params::CALCYC); + // 50% availabilty loss means half of SOC lost out of the gate + s.capacity = { 229.75, 1000, 960.0, 20.25, 0, 23.93, 26.04, 0.5, 0.0, 2 }; + s.batt_voltage = 532.94; + s.lifetime.calendar->q_relative_calendar = 102; + s.lifetime.cycle->q_relative_cycle = 100; + s.lifetime.cycle->rainflow_jlt = 1; + s.lifetime.q_relative = 100; + s.thermal = { 96.0, 20.00, 20 }; + compareState(batteryModel, s, "testCyclesWithAvailabilityLosses: 1"); + + while (batteryModel->SOC() > SOC_min + 1) { + batteryModel->run(idx++, I); + capacity_passed += batteryModel->I() * batteryModel->V() / 1000.; + } + // std::cerr << idx << ": soc " << batteryModel->SOC() << ", cap " << capacity_passed << "\n"; + // + s.capacity = { 47.5, 1000, 960.015, 20.25, 0, 4.94, 7.05, 0.5, 0.5, 2 }; + s.batt_voltage = 463.43; + s.lifetime.day_age_of_battery = 0.375; + s.lifetime.q_relative = 100; + s.lifetime.cycle->q_relative_cycle = 100; + s.lifetime.calendar->q_relative_calendar = 101.99; + s.lifetime.calendar->dq_relative_calendar_old = 0.0002; + s.thermal = { 96.01, 20.01, 20 }; + compareState(batteryModel, s, "testCyclesWithAvailabilityLosses: 2"); + + size_t n_cycles = 400; + + while (n_cycles-- > 0) { + I *= -1; + while (batteryModel->SOC() < SOC_max * 0.5 - 1) { + batteryModel->run(idx++, I); + capacity_passed += -batteryModel->I() * batteryModel->V() / 1000.; + } + I *= -1; + while (batteryModel->SOC() > SOC_min + 1) { + batteryModel->run(idx++, I); + capacity_passed += batteryModel->I() * batteryModel->V() / 1000.; + } + } + // std::cerr << idx << ": soc " << batteryModel->SOC() << ", cap " << capacity_passed << "\n"; + // Lower DOD cycles relative to runTestCycleAt1C means lower degradation after 400 cycles + s.capacity = { 50.44, 960.60, 922.18, 9.31, 0, 5.47, 6.48, 0.5, 0.5, 2 }; + s.batt_voltage = 468.37; + s.lifetime.q_relative = 93.08; + s.lifetime.cycle->q_relative_cycle = 96.06; + s.lifetime.n_cycles = 399; + s.lifetime.cycle_range = 41.39; + s.lifetime.average_range = 42.02; + s.lifetime.cycle->rainflow_Xlt = 41.40; + s.lifetime.cycle->rainflow_Ylt = 42.03; + s.lifetime.cycle->rainflow_jlt = 3; + s.lifetime.day_age_of_battery = 972.20; + s.lifetime.calendar->q_relative_calendar = 101.25; + s.lifetime.calendar->dq_relative_calendar_old = 0.007; + s.thermal = { 96.0, 20.00, 20 }; + s.last_idx = 32991; + + compareState(batteryModel, s, "testCyclesWithAvailabilityLosses: 3"); + + EXPECT_NEAR(capacity_passed, 167601, 1000) << "Current passing through cell"; + double qmax = fmax(s.capacity.qmax_lifetime, s.capacity.qmax_thermal); + EXPECT_NEAR(qmax / q, 0.9606, 0.01) << "capacity relative to max capacity"; +} diff --git a/test/shared_test/lib_battery_test.h b/test/shared_test/lib_battery_test.h index 2ed8e2325..2b9d389ad 100644 --- a/test/shared_test/lib_battery_test.h +++ b/test/shared_test/lib_battery_test.h @@ -100,6 +100,7 @@ class lib_battery_losses_test : public ::testing::Test std::vector dischargingLosses; std::vector fullLosses; + std::vector adjustLosses; double dt_hour = 1; int nyears = 1; @@ -114,6 +115,7 @@ class lib_battery_losses_test : public ::testing::Test } for (size_t i = 0; i < 8760; i++) { fullLosses.push_back((double)i/8760); + adjustLosses.push_back(0.0); } } }; @@ -207,6 +209,7 @@ class lib_battery_test : public ::testing::Test std::vector fullLosses; std::vector fullLossesMinute; int lossChoice; + std::vector adjustLosses; // battery int chemistry; @@ -273,6 +276,10 @@ class lib_battery_test : public ::testing::Test fullLossesMinute.push_back(0); } lossChoice = 0; + for (size_t i = 0; i < 8760; i++) { + adjustLosses.push_back(0); + } + // battery chemistry = 1; @@ -283,7 +290,7 @@ class lib_battery_test : public ::testing::Test C_rate, resistance, dtHour ); lifetimeModel = new lifetime_calendar_cycle_t(cycleLifeMatrix, dtHour, 1.02, 2.66e-3, -7280, 930); thermalModel = new thermal_t(dtHour, mass, surface_area, resistance, Cp, h, capacityVsTemperature, T_room); - lossModel = new losses_t(monthlyLosses, monthlyLosses, monthlyLosses); + lossModel = new losses_t(monthlyLosses, monthlyLosses, monthlyLosses, adjustLosses); batteryModel = std::unique_ptr(new battery_t(dtHour, chemistry, capacityModel, voltageModel, lifetimeModel, thermalModel, lossModel)); } diff --git a/test/shared_test/lib_resilience_test.cpp b/test/shared_test/lib_resilience_test.cpp index 8531e2220..70cb33ccf 100644 --- a/test/shared_test/lib_resilience_test.cpp +++ b/test/shared_test/lib_resilience_test.cpp @@ -294,7 +294,7 @@ TEST_F(ResilienceTest_lib_resilience, PVWattsSetUp) batt->advance(vartab, ac[count], 500); // printf("%f\t current, %f\t voltage, %f\t losses, %f\t power\n", -// cap->I(), vol->V(), batt->battery_model->losses_model()->getLoss(count), power_model->powerBatteryDC); +// cap->I(), vol->V(), batt->battery_model->losses_model()->getAncillaryLoss(count), power_model->powerBatteryDC); count ++; }