Use curtailment in renewable cost - #1614
Conversation
Renewable variable cost previously got a -1.0 objective multiplier so that RenewableGenerationCost.variable doubled as both a VOM cost and a dispatch incentive. Now that RenewableGenerationCost has a separate curtailment_cost field, each field can carry its true sign: variable contributes +VOM*p (real cost), and curtailment_cost contributes -curtailment_cost*p (a reward for dispatching). CurtailmentCostExpression keeps reporting the positive dollar value of curtailed energy. Fixes #1154. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Updates renewable generation cost handling so dispatch is no longer incentivized via a negative variable cost; instead, the incentive is applied via the curtailment_cost field, aligning the implementation with Issue #1154.
Changes:
- Switch renewable dispatch objective multiplier for
ActivePowerVariablefrom negative to positive (sovariablecost contributes with+1.0). - Add an explicit negative objective contribution derived from
curtailment_costwhile continuing to recordCurtailmentCostExpressionfor reporting. - Update RenewableGen formulation docs to describe the new
(C_var - C_curt) * pnet objective effect and clarify curtailment reporting.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
src/devices_models/devices/renewable_generation.jl |
Flips renewable dispatch objective multiplier sign to make variable cost positive. |
src/devices_models/devices/common/objective_function/linear_curve.jl |
Adds the curtailment incentive term to the objective for linear curtailment costs. |
docs/src/formulation_library/RenewableGen.md |
Updates formulation documentation to reflect the new objective terms and reporting behavior. |
Comments suppressed due to low confidence (2)
src/devices_models/devices/common/objective_function/linear_curve.jl:273
- Same as above: prefer using a named sign constant (e.g.,
OBJECTIVE_FUNCTION_NEGATIVE) instead of hard-coding the negation for the curtailment objective term, to keep sign semantics consistent across objective-function implementations.
_add_proportional_term!(
container,
T(),
component,
-proportional_term_per_unit * dt,
t,
)
docs/src/formulation_library/RenewableGen.md:138
- Same wording issue here: “otherwise it curtails” is too absolute because dispatch can still occur due to feasibility requirements or relative costs. Consider describing the marginal incentive/disincentive of
(C^{re,var} - C^{re,curt})without implying it alone determines dispatch.
The net contribution is ``(C^\text{re,var} - C^\text{re,curt}) \cdot p_t^\text{re}``: when ``C^\text{re,curt} > C^\text{re,var}`` the optimizer dispatches the unit, otherwise it curtails. `CurtailmentCostExpression` reports the per-device dollar value of curtailed energy ``C^\text{re,curt} \cdot (p^\text{re,max}_t - p_t^\text{re})`` and is not propagated to `ProductionCostExpression`.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| _add_proportional_term!( | ||
| container, | ||
| T(), | ||
| component, | ||
| -proportional_term_per_unit * dt, | ||
| t, | ||
| ) |
There was a problem hiding this comment.
@acostarelli This is correct. We should add a test that verifies that this works. I would make the suggested test for copilot
|
Performance Results
|
rodrigomha
left a comment
There was a problem hiding this comment.
@acostarelli Minor changes here, but this is almost ready for review.
Here is suggestion from copilot to handle the test:
Proposed test (to add to test/test_device_renewable_generation_constructors.jl)
Add a testset after the nonnegativity test that:
- Builds a
RenewableFullDispatchmodel using a system with bothvariableandcurtailment_costset onRenewableGenerationCost - Solves the model
- Asserts dispatch changes (or objective value changes) relative to a baseline with
curtailment_cost = 0 - Optionally asserts the sign of the objective coefficient on
ActivePowerVariableis negative whencurtailment_cost > variable_cost
- Use OBJECTIVE_FUNCTION_NEGATIVE constant for the curtailment objective coefficient instead of an inline unary `-`. - Rephrase RenewableFullDispatch and RenewableConstantPowerFactor docstrings to describe the marginal incentive and note that system constraints (load balance, reserves) may still require dispatch. - Add a regression test that sets `curtailment_cost > variable` and asserts both that renewable dispatch increases vs. a baseline with `curtailment_cost = 0` and that the JuMP coefficient on ActivePowerVariable is negative. - Fix `_add_curtailment_cost!` to derive `offer_max` from the ActivePowerTimeSeriesParameter (falling back to `get_max_active_power` for renewables without a time series). The previous `_get_pwl_data` call required offer curves that RenewableGenerationCost does not provide, so the curtailment-cost incentive crashed at build time for any non-zero `curtailment_cost`. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
| param_array = | ||
| get_parameter_array( | ||
| container, | ||
| ActivePowerTimeSeriesParameter(), | ||
| PSY.RenewableDispatch, | ||
| ) | ||
| param_multiplier = get_parameter_multiplier_array( | ||
| container, | ||
| ActivePowerTimeSeriesParameter(), | ||
| PSY.RenewableDispatch, | ||
| ) |
There was a problem hiding this comment.
This is fine I believe. We want to fail if the user does not give timeseries to renewables anyways
| param_array = | ||
| get_parameter_array(container, ActivePowerTimeSeriesParameter(), PSY.RenewableGen) | ||
| param_multiplier = get_parameter_multiplier_array( | ||
| container, | ||
| ActivePowerTimeSeriesParameter(), | ||
| PSY.RenewableGen, | ||
| ) | ||
| has_ts_param = name in axes(param_array)[1] |
There was a problem hiding this comment.
I'm doubting myself here. @acostarelli can you check in a test what happen if only 1 renewable (of two or more) has timeseries and what happen if no renewable has timeseries. This could be a bug.
Closes #1154