-
Notifications
You must be signed in to change notification settings - Fork 28
add Secant-based methods for ELCC and EFC calculations #99
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
Introduces Secant-based root-finding as an alternative to the default bisection method for calculating Effective Load Carrying Capability (ELCC) and Equivalent Firm Capacity (EFC). This update also includes a significant refactor of shared system modification logic to improve long-term maintainability.
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #99 +/- ##
==========================================
- Coverage 83.13% 82.49% -0.65%
==========================================
Files 45 47 +2
Lines 2325 2502 +177
==========================================
+ Hits 1933 2064 +131
- Misses 392 438 +46 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
Copilot reviewed 7 out of 7 changed files in this pull request and generated 23 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| You can also choose the solution method for ELCC and EFC calculation. The default is a bisection method (EFC, ELCC), but a secant method (EFC_Secant, ELCC_Secant) is also available. | ||
|
|
||
| ```julia | ||
| # Bisection method (default) | ||
| elcc_bisection = assess(rts_gmlc_sys, rts_gmlc_sys_growth, ELCC{EUE}(200, "1"), SequentialMonteCarlo(samples=10,seed=1)) | ||
| # Secant method | ||
| elcc_secant = assess(rts_gmlc_sys, rts_gmlc_sys_growth, ELCC_Secant{EUE}(200, "1"), SequentialMonteCarlo(samples=10,seed=1)) | ||
| ``` | ||
|
|
Copilot
AI
Jan 5, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The new documentation section breaks the existing ELCC example. It appears mid-example without properly closing the previous code block. Lines 140-146 start a code block defining base_system and augmented_system, but this block is never closed before the new content begins at line 147. The new method comparison section (lines 147-155) should be moved to appear after the complete ELCC example ends (after line 162), maintaining proper markdown code block structure.
| function assess(sys_baseline::S, sys_augmented::S, | ||
| params::EFC_Secant{M}, simulationspec::SequentialMonteCarlo | ||
| ) where {N, L, T, P, S <: SystemModel{N,L,T,P}, M <: ReliabilityMetric} | ||
|
|
||
| _, powerunit, _ = unitsymbol(sys_baseline) | ||
|
|
||
| regionnames = sys_baseline.regions.names | ||
| regionnames != sys_augmented.regions.names && | ||
| error("Systems provided do not have matching regions") | ||
|
|
||
| # Add firm capacity generators to the relevant regions | ||
| efc_gens, sys_variable, sys_target = | ||
| add_firmcapacity(sys_baseline, sys_augmented, params.regions) | ||
|
|
||
| target_metric = M(first(assess(sys_target, simulationspec, Shortfall()))) | ||
|
|
||
| capacities = Int[] | ||
| metrics = typeof(target_metric)[] | ||
|
|
||
| # Initial points for Secant method | ||
| # Point 0: 0 MW | ||
| c_prev = 0 | ||
| update_firmcapacity!(sys_variable, efc_gens, c_prev) | ||
| m_prev = M(first(assess(sys_variable, simulationspec, Shortfall()))) | ||
| push!(capacities, c_prev) | ||
| push!(metrics, m_prev) | ||
|
|
||
| # Point 1: Max Capacity | ||
| c_curr = params.capacity_max | ||
| update_firmcapacity!(sys_variable, efc_gens, c_curr) | ||
| m_curr = M(first(assess(sys_variable, simulationspec, Shortfall()))) | ||
| push!(capacities, c_curr) | ||
| push!(metrics, m_curr) | ||
|
|
||
| iter = 0 | ||
| max_iter = 100 | ||
|
|
||
| final_val = c_curr | ||
|
|
||
| while iter < max_iter | ||
| iter += 1 | ||
|
|
||
| params.verbose && println( | ||
| "Iteration $iter: c_prev=$c_prev, c_curr=$c_curr, m_prev=$(val(m_prev)), m_curr=$(val(m_curr)), target=$(val(target_metric))" | ||
| ) | ||
|
|
||
| # g(c) = Metric(c) - Target | ||
| g_prev = val(m_prev) - val(target_metric) | ||
| g_curr = val(m_curr) - val(target_metric) | ||
|
|
||
| if abs(g_curr - g_prev) < 1e-9 | ||
| params.verbose && @info "Denominator too small in Secant method, stopping." | ||
| final_val = c_curr | ||
| break | ||
| end | ||
|
|
||
| # Secant update | ||
| c_next_float = c_curr - g_curr * (c_curr - c_prev) / (g_curr - g_prev) | ||
| c_next = round(Int, c_next_float) | ||
|
|
||
| # Check stopping criteria: Capacity gap | ||
| if abs(c_next - c_curr) <= params.capacity_gap | ||
| params.verbose && @info "Capacity change within tolerance ($(params.capacity_gap)), stopping." | ||
| final_val = c_next | ||
|
|
||
| # Evaluate final point if different | ||
| if c_next != c_curr | ||
| update_firmcapacity!(sys_variable, efc_gens, c_next) | ||
| m_next = M(first(assess(sys_variable, simulationspec, Shortfall()))) | ||
| push!(capacities, c_next) | ||
| push!(metrics, m_next) | ||
| end | ||
| break | ||
| end | ||
|
|
||
| # Evaluate new point | ||
| update_firmcapacity!(sys_variable, efc_gens, c_next) | ||
| m_next = M(first(assess(sys_variable, simulationspec, Shortfall()))) | ||
| push!(capacities, c_next) | ||
| push!(metrics, m_next) | ||
|
|
||
| # Setup for next iteration | ||
| c_prev = c_curr | ||
| m_prev = m_curr | ||
| c_curr = c_next | ||
| m_curr = m_next | ||
| final_val = c_curr | ||
|
|
||
| end | ||
|
|
||
| return CapacityCreditResult{typeof(params), typeof(target_metric), P}( | ||
| target_metric, final_val, final_val, capacities, metrics) | ||
|
|
||
| end |
Copilot
AI
Jan 5, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The EFC_Secant and ELCC_Secant implementations are nearly identical, differing only in which utility functions they call (update_firmcapacity! vs update_load! and add_firmcapacity vs copy_load). This code duplication increases maintenance burden and the risk of inconsistent fixes. Consider refactoring to extract the common secant algorithm into a shared helper function that accepts the system modification functions as parameters.
| include("EFC_Secant.jl") | ||
| include("ELCC.jl") | ||
| include("ELCC_Secant.jl") |
Copilot
AI
Jan 5, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The new EFC_Secant and ELCC_Secant methods lack test coverage. The test file only includes tests for the original bisection-based EFC and ELCC methods. Since these are new alternative solution methods with different convergence properties, they should have dedicated test cases to verify correctness and convergence behavior.
| p_value::Float64 | ||
| regions::Vector{Tuple{String,Float64}} | ||
| verbose::Bool | ||
|
|
||
| function ELCC_Secant{M}( | ||
| capacity_max::Int, regions::Vector{Pair{String,Float64}}; | ||
| capacity_gap::Int=1, p_value::Float64=0.05, verbose::Bool=false) where M | ||
|
|
||
| @assert capacity_max > 0 | ||
| @assert capacity_gap > 0 | ||
| @assert 0 < p_value < 1 | ||
| @assert sum(x.second for x in regions) ≈ 1.0 | ||
|
|
||
| return new{M}(capacity_max, capacity_gap, p_value, Tuple.(regions), verbose) |
Copilot
AI
Jan 5, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The p_value parameter is defined in the struct and passed to the constructor, but it is never used in the assess function. Unlike the bisection-based methods (EFC and ELCC) which use p_value for statistical significance testing, the Secant implementation ignores this parameter entirely. This creates an inconsistent API where users might expect p_value to control stopping criteria. Either remove the unused parameter or implement statistical significance checks similar to the bisection methods.
| # Check stopping criteria: Capacity gap | ||
| if abs(c_next - c_curr) <= params.capacity_gap | ||
| params.verbose && @info "Capacity change within tolerance ($(params.capacity_gap)), stopping." | ||
| final_val = c_next | ||
|
|
||
| # Evaluate final point if different | ||
| if c_next != c_curr | ||
| update_firmcapacity!(sys_variable, efc_gens, c_next) | ||
| m_next = M(first(assess(sys_variable, simulationspec, Shortfall()))) | ||
| push!(capacities, c_next) | ||
| push!(metrics, m_next) | ||
| end | ||
| break |
Copilot
AI
Jan 5, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The Secant method lacks a convergence check based on the function value. If the metric converges to the target (abs(g_curr) is small), the algorithm should terminate even if the capacity change is large. Without this check, the algorithm may continue iterating unnecessarily or fail to recognize that an acceptable solution has been found.
|
Hi @qian-harvard, thanks for your contribution! Can you please add relevant tests to your updates? Also please address any relevant copilot comments. |
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Introduces Secant-based root-finding implementations for Equivalent Firm Capacity (EFC) and Effective Load Carrying Capability (ELCC), providing improved speed and robustness over bisection. Updates both EFC_Secant.jl and ELCC_Secant.jl to use metric values for root-finding, and adds corresponding test cases to validate their correctness. Benchmarks on the RTS-GMLC system show these methods are generally 2-3x faster than bisection and more robust to different perturbation step sizes.
Thanks for the suggestion. Just updated the run test and PR description. |
Simplifies and improves the bracketing and iteration logic for the secant method in both EFC_Secant.jl and ELCC_Secant.jl. The new approach uses clearer variable names, robust bracket searching, and a more consistent interpolation loop, improving reliability and maintainability.
Introduces Secant-based root-finding as an alternative to the default bisection method for calculating Effective Load Carrying Capability (ELCC) and Equivalent Firm Capacity (EFC). This update also includes a significant refactor of shared system modification logic to improve long-term maintainability.