1616
1717from pyomo .environ import Constraint , NonNegativeIntegers , quicksum , value
1818
19- from temoa .components import time
19+ from temoa .components . operations import _ramp_activity_increase
2020from temoa .components .time import tod_elapsed_hours
2121from 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
560495def 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