Skip to content

Commit b6bca6f

Browse files
committed
Refactor ramp rates a little more
Signed-off-by: Davey Elder <iandavidelder@gmail.com>
1 parent e55a676 commit b6bca6f

2 files changed

Lines changed: 115 additions & 120 deletions

File tree

temoa/components/operations.py

Lines changed: 46 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
from temoa.components import time
2121

2222
if TYPE_CHECKING:
23+
from pyomo.core.base import Expression
24+
2325
from temoa.core.model import TemoaModel
2426
from temoa.types import ExprLike
2527
from temoa.types.core_types import Period, Region, Season, Technology, TimeOfDay, Vintage
@@ -192,7 +194,7 @@ def create_operational_vintage_sets(model: TemoaModel) -> None:
192194
model.is_seasonal_storage[t] = t in model.tech_seasonal_storage
193195

194196

195-
def _ramp_constraint(
197+
def _ramp_activity_increase(
196198
model: TemoaModel,
197199
ramp_up: bool,
198200
r: Region,
@@ -203,25 +205,12 @@ def _ramp_constraint(
203205
d_next: TimeOfDay,
204206
t: Technology,
205207
v: Vintage,
206-
) -> ExprLike:
207-
"""
208-
Helper function to compute ramping constraints for both ramp-up and ramp-down.
209-
210-
Args:
211-
model (TemoaModel): The Temoa model instance.
212-
ramp_up (bool): True for ramp-up constraints, False for ramp-down constraints.
213-
r (Region): The region.
214-
p (Period): The period.
215-
s (Season): The season.
216-
d (TimeOfDay): The time of day.
217-
s_next (Season): The next season.
218-
d_next (TimeOfDay): The next time of day.
219-
t (Technology): The technology.
220-
v (Vintage): The vintage.
221-
222-
Returns:
223-
Expression | Constraint.Skip: A tuple containing the activity increase
224-
and rampable activity expressions or a constraint skip.
208+
) -> tuple[Expression, float] | None:
209+
"""Compute the hourly activity increase between adjacent time slices and the
210+
ramp fraction for the given direction.
211+
212+
Returns ``(activity_increase, ramp_fraction)`` or ``None`` when the ramp
213+
rate is so large that the constraint would never bind (skip).
225214
"""
226215
ramping_param = model.ramp_up_hourly if ramp_up else model.ramp_down_hourly
227216

@@ -246,19 +235,52 @@ def _ramp_constraint(
246235
f'Should be less than {1 / hours_elapsed:.4f}. Constraint skipped.'
247236
)
248237
logger.warning(msg.format(ramping_param.name, r, t, p, s, d, p, s_next, d_next))
249-
return Constraint.Skip
238+
return None
250239

251240
activity_increase = hourly_activity_sd_next - hourly_activity_sd
252-
rampable_activity = (
241+
return activity_increase, ramp_fraction
242+
243+
244+
def _rampable_activity(
245+
model: TemoaModel,
246+
r: Region,
247+
p: Period,
248+
t: Technology,
249+
v: Vintage,
250+
ramp_fraction: float,
251+
) -> ExprLike:
252+
"""Rampable activity for the core model: fraction of total installed capacity."""
253+
return (
253254
ramp_fraction
254255
* model.v_capacity[r, p, t, v]
255256
* value(model.capacity_to_activity[r, t])
256257
/ (24 * value(model.days_per_period))
257258
)
259+
260+
261+
def _ramp_constraint(
262+
model: TemoaModel,
263+
ramp_up: bool,
264+
r: Region,
265+
p: Period,
266+
s: Season,
267+
d: TimeOfDay,
268+
s_next: Season,
269+
d_next: TimeOfDay,
270+
t: Technology,
271+
v: Vintage,
272+
) -> ExprLike:
273+
"""Helper that composes :func:`_ramp_activity_increase` and
274+
:func:`_rampable_activity` into the core-model ramp constraint."""
275+
result = _ramp_activity_increase(model, ramp_up, r, p, s, d, s_next, d_next, t, v)
276+
if result is None:
277+
return Constraint.Skip
278+
activity_increase, ramp_fraction = result
279+
rampable = _rampable_activity(model, r, p, t, v, ramp_fraction)
258280
if ramp_up:
259-
return activity_increase <= rampable_activity
281+
return activity_increase <= rampable
260282
else:
261-
return -activity_increase <= rampable_activity
283+
return -activity_increase <= rampable
262284

263285

264286
def ramp_up_constraint(

temoa/extensions/unit_commitment/components/commitment.py

Lines changed: 69 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
from pyomo.environ import Constraint, NonNegativeIntegers, quicksum, value
1818

19-
from temoa.components import time
19+
from temoa.components.operations import _ramp_activity_increase
2020
from temoa.components.time import tod_elapsed_hours
2121
from temoa.components.utils import get_capacity_factor
2222

@@ -445,116 +445,51 @@ def uc_min_down_time_constraint(
445445
return offline >= stopped_sum
446446

447447

448-
def _ramp_constraint(
448+
def _rampable_activity(
449449
model: UnitCommitmentModel,
450-
ramp_up: bool,
451450
r: Region,
452451
p: Period,
453452
s: Season,
454453
d: TimeOfDay,
455-
s_next: Season,
456-
d_next: TimeOfDay,
457454
t: Technology,
458455
v: Vintage,
456+
ramp_up: bool,
457+
ramp_fraction: float,
459458
) -> ExprLike:
460-
r"""
461-
UC-aware ramp-up / ramp-down constraint.
462-
463-
The core model ramp constraint bounds the change in hourly activity between
464-
adjacent time slices against a fraction of **total installed capacity**:
465-
466-
.. math::
467-
468-
\Delta A \le RH_{r,t} \cdot \Delta H
469-
\cdot \frac{\textbf{CAP}_{r,p,t,v} \cdot C2A_{r,t}}{24 \cdot DPP}
470-
471-
The UC version replaces :math:`\textbf{CAP}` with a commitment-aware
472-
expression that accounts for *which* units are online and the operating
473-
point assumed for units that start up or shut down during the transition:
474-
475-
.. math::
476-
477-
\Delta A \;\le\; \left[
478-
RH_{r,t} \cdot \Delta H \cdot \textbf{UCN}_{r,p,s,d,t,v}
479-
+ f^{\min}_{\text{eff}} \cdot \textbf{UCST}_{r,p,s,d,t,v}
480-
- f^{\min}_{\text{eff}} \cdot \textbf{UCSP}_{r,p,s,d,t,v}
481-
\right]
482-
\cdot \frac{UC_{r,t} \cdot C2A_{r,t}}{24 \cdot DPP}
483-
484-
(signs on :math:`\textbf{UCST}` and :math:`\textbf{UCSP}` are reversed for
485-
the ramp-down direction.)
486-
487-
The differences from the core constraint are:
488-
489-
1. **Online units only.** The rampable capacity is proportional to
490-
:math:`\textbf{UCN}_{r,p,s,d,t,v} \cdot UC_{r,t}` rather than the full
491-
:math:`\textbf{CAP}_{r,p,t,v}`. Offline units contribute nothing to the
492-
ramp envelope.
493-
494-
2. **Startup headroom (ramp-up) / startup drag (ramp-down).** A unit that
495-
starts during slice :math:`(s,d)` is assumed to begin producing at the
496-
effective minimum output level :math:`f^{\min}_{\text{eff}}`. For
497-
ramp-up, this means the unit arrives part-way to its maximum output and
498-
therefore provides *additional* headroom equal to
499-
:math:`f^{\min}_{\text{eff}} \cdot UC_{r,t}`. For ramp-down, the same
500-
logic means a starting unit *reduces* the available ramp-down envelope
501-
(it is already contributing output that cannot be instantly removed).
502-
503-
3. **Shutdown contribution (ramp-down headroom / ramp-up drag).** A unit
504-
that stops during slice :math:`(s,d)` was assumed to be running at
505-
:math:`f^{\min}_{\text{eff}}` immediately before shutdown. For ramp-down
506-
this creates *additional* headroom (output falls by
507-
:math:`f^{\min}_{\text{eff}} \cdot UC_{r,t}`); for ramp-up it reduces
508-
headroom.
509-
510-
4. **Effective minimum fraction.** The assumed operating point for
511-
started/stopped units is
512-
:math:`f^{\min}_{\text{eff}} = \max(\underline{f}_{r,t},\, RH_{r,t} \cdot \Delta H)`,
513-
i.e. at least the configured minimum output fraction and at least the
514-
ramp fraction itself.
515-
"""
516-
ramping_param = model.ramp_up_hourly if ramp_up else model.ramp_down_hourly
517-
518-
hourly_activity_sd = _flow_out_sum(model, r, p, s, d, t, v) / time.hours_in_time_slice(
519-
model, s, d
520-
)
521-
hourly_activity_sd_next = _flow_out_sum(
522-
model, r, p, s_next, d_next, t, v
523-
) / time.hours_in_time_slice(model, s_next, d_next)
524-
525-
# Elapsed hours from middle of this time slice to middle of next time slice
526-
hours_elapsed = time.tod_elapsed_hours(model, d, d_next)
527-
ramp_fraction = hours_elapsed * value(ramping_param[r, t])
528-
if ramp_fraction >= 1:
529-
msg = (
530-
'Warning: Rate for {}[{}, {}] is too large to be constraining from '
531-
'({}, {}, {}) to ({}, {}, {}). '
532-
f'Should be less than {1 / hours_elapsed:.4f}. Constraint skipped.'
533-
)
534-
logger.warning(msg.format(ramping_param.name, r, t, p, s, d, p, s_next, d_next))
535-
return Constraint.Skip
536-
537-
activity_increase = hourly_activity_sd_next - hourly_activity_sd
538-
539459
unit_cap = value(model.uc_unit_capacity[r, t])
540460
min_fraction = max(value(model.uc_min_output_fraction[r, t]), ramp_fraction)
541461
online = ramp_fraction * model.v_uc_online[r, p, s, d, t, v] * unit_cap
542462
startup = min_fraction * model.v_uc_started[r, p, s, d, t, v] * unit_cap
543463
shutdown = min_fraction * model.v_uc_stopped[r, p, s, d, t, v] * unit_cap
544-
if ramp_up:
545-
capacity_ramp = online + startup - shutdown
546-
else:
547-
capacity_ramp = online + shutdown - startup
548-
549-
rampable_activity = (
464+
capacity_ramp = (online + startup - shutdown) if ramp_up else (online + shutdown - startup)
465+
return (
550466
capacity_ramp
551467
* value(model.capacity_to_activity[r, t])
552468
/ (24 * value(model.days_per_period))
553469
)
470+
471+
472+
def _ramp_constraint(
473+
model: UnitCommitmentModel,
474+
ramp_up: bool,
475+
r: Region,
476+
p: Period,
477+
s: Season,
478+
d: TimeOfDay,
479+
s_next: Season,
480+
d_next: TimeOfDay,
481+
t: Technology,
482+
v: Vintage,
483+
) -> ExprLike:
484+
result = _ramp_activity_increase(model, ramp_up, r, p, s, d, s_next, d_next, t, v)
485+
if result is None:
486+
return Constraint.Skip
487+
activity_increase, ramp_fraction = result
488+
rampable = _rampable_activity(model, r, p, s, d, t, v, ramp_up, ramp_fraction)
554489
if ramp_up:
555-
return activity_increase <= rampable_activity
490+
return activity_increase <= rampable
556491
else:
557-
return -activity_increase <= rampable_activity
492+
return -activity_increase <= rampable
558493

559494

560495
def uc_ramp_up_constraint(
@@ -571,8 +506,28 @@ def uc_ramp_up_constraint(
571506
:code:`ramp_up_constraint` for technologies that appear in both the
572507
:code:`unit_commitment` and :code:`ramp_up_hourly` tables.
573508
574-
See :func:`_ramp_constraint` for the full mathematical description of how
575-
the UC version differs from the core-model ramp constraint.
509+
The ramp envelope is driven by *online units* rather than total installed
510+
capacity. Units that start during slice :math:`(s,d)` arrive part-way to
511+
their maximum output, providing additional headroom; units that stop reduce
512+
it:
513+
514+
.. math::
515+
:label: uc_ramp_up
516+
517+
\frac{\sum_{I,O}\textbf{FO}_{r,p,s_{next},d_{next},i,t,v,o}}{H_{s_{next},d_{next}}}
518+
-
519+
\frac{\sum_{I,O}\textbf{FO}_{r,p,s,d,i,t,v,o}}{H_{s,d}}
520+
\;\le\;
521+
\left[
522+
RH_{r,t} \cdot \Delta H \cdot \textbf{UCN}_{r,p,s,d,t,v}
523+
+ f^{\min}_{\text{eff}} \cdot \textbf{UCST}_{r,p,s,d,t,v}
524+
- f^{\min}_{\text{eff}} \cdot \textbf{UCSP}_{r,p,s,d,t,v}
525+
\right]
526+
\cdot \frac{UC_{r,t} \cdot C2A_{r,t}}{24 \cdot DPP}
527+
528+
where :math:`f^{\min}_{\text{eff}} = \max(\underline{f}_{r,t},\, RH_{r,t} \cdot \Delta H)`
529+
is the effective minimum output fraction for started/stopped units, and
530+
:math:`\Delta H = (H_{s,d} + H_{s_{next},d_{next}}) / 2`.
576531
"""
577532

578533
s_next, d_next = model.time_next[s, d]
@@ -604,8 +559,26 @@ def uc_ramp_down_constraint(
604559
:code:`ramp_down_constraint` for technologies that appear in both the
605560
:code:`unit_commitment` and :code:`ramp_down_hourly` tables.
606561
607-
See :func:`_ramp_constraint` for the full mathematical description of how
608-
the UC version differs from the core-model ramp constraint.
562+
Symmetric to :func:`uc_ramp_up_constraint` with signs on
563+
:math:`\textbf{UCST}` and :math:`\textbf{UCSP}` reversed: a unit that stops
564+
during :math:`(s,d)` adds headroom (output falls), while a starting unit
565+
reduces it:
566+
567+
.. math::
568+
:label: uc_ramp_down
569+
570+
\frac{\sum_{I,O}\textbf{FO}_{r,p,s,d,i,t,v,o}}{H_{s,d}}
571+
-
572+
\frac{\sum_{I,O}\textbf{FO}_{r,p,s_{next},d_{next},i,t,v,o}}{H_{s_{next},d_{next}}}
573+
\;\le\;
574+
\left[
575+
RD_{r,t} \cdot \Delta H \cdot \textbf{UCN}_{r,p,s,d,t,v}
576+
+ f^{\min}_{\text{eff}} \cdot \textbf{UCSP}_{r,p,s,d,t,v}
577+
- f^{\min}_{\text{eff}} \cdot \textbf{UCST}_{r,p,s,d,t,v}
578+
\right]
579+
\cdot \frac{UC_{r,t} \cdot C2A_{r,t}}{24 \cdot DPP}
580+
581+
where :math:`f^{\min}_{\text{eff}} = \max(\underline{f}_{r,t},\, RD_{r,t} \cdot \Delta H)`.
609582
"""
610583

611584
s_next, d_next = model.time_next[s, d]

0 commit comments

Comments
 (0)