Skip to content

Conversation

@qian-harvard
Copy link

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.

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.
@hsunnrel hsunnrel requested a review from Copilot January 5, 2026 23:07
@codecov-commenter
Copy link

codecov-commenter commented Jan 5, 2026

Codecov Report

❌ Patch coverage is 78.53881% with 47 lines in your changes missing coverage. Please review.
✅ Project coverage is 82.49%. Comparing base (fbde94e) to head (18e08f9).

Files with missing lines Patch % Lines
PRASCapacityCredits.jl/src/ELCC_Secant.jl 68.08% 30 Missing ⚠️
PRASCapacityCredits.jl/src/EFC_Secant.jl 79.51% 17 Missing ⚠️
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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link

Copilot AI left a 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.

Copy link

Copilot AI left a 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.

Comment on lines 147 to 156
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))
```

Copy link

Copilot AI Jan 5, 2026

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.

Copilot uses AI. Check for mistakes.
Comment on lines 30 to 123
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
Copy link

Copilot AI Jan 5, 2026

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.

Copilot uses AI. Check for mistakes.
Comment on lines +17 to +19
include("EFC_Secant.jl")
include("ELCC.jl")
include("ELCC_Secant.jl")
Copy link

Copilot AI Jan 5, 2026

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.

Copilot uses AI. Check for mistakes.
Comment on lines +5 to +18
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)
Copy link

Copilot AI Jan 5, 2026

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.

Copilot uses AI. Check for mistakes.
Comment on lines 90 to 102
# 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
Copy link

Copilot AI Jan 5, 2026

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.

Copilot uses AI. Check for mistakes.
@sriharisundar
Copy link
Member

Hi @qian-harvard, thanks for your contribution! Can you please add relevant tests to your updates? Also please address any relevant copilot comments.
Can you also please update the PR description to detail why this root finding method might be better in terms of speed, accuracy etc.?

@scdhulipala scdhulipala self-requested a review January 6, 2026 17:01
qian-harvard and others added 12 commits January 6, 2026 18:27
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.
@qian-harvard
Copy link
Author

Hi @qian-harvard, thanks for your contribution! Can you please add relevant tests to your updates? Also please address any relevant copilot comments. Can you also please update the PR description to detail why this root finding method might be better in terms of speed, accuracy etc.?

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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants