@@ -38,6 +38,19 @@ class SystemBuilder(ComponentBuilder):
3838 ---------------------------
3939 - `spin_shortfall`: Spinning reserve shortfall (system-wide). Unit: MW.
4040
41+ Curtailment variables:
42+ ---------------------------
43+ - `pthermal_curtail`: Thermal unit curtailment. Unit: MW.
44+ - `phydro_curtail`: Hydro unit curtailment. Unit: MW.
45+ - `psolar_curtail`: Solar unit curtailment. Unit: MW.
46+ - `pwind_curtail`: Wind unit curtailment. Unit: MW.
47+ - `pimp_curtail`: Import unit curtailment. Unit: MW.
48+
49+ Trade variables:
50+ ---------------------------
51+ - `trade_fwd`: Forward trade on interties. Unit: MW.
52+ - `trade_bwd`: Backward trade on interties. Unit: MW.
53+
4154 Fixed objective terms
4255 ===========================
4356 - load shortfall penalty
@@ -77,6 +90,10 @@ def __init__(self, model: gp.Model, inputs: SystemInput):
7790 self .pwind_curtail = gp .tupledict ()
7891 self .pimp_curtail = gp .tupledict ()
7992
93+ # Trade variables
94+ self .trade_fwd = gp .tupledict ()
95+ self .trade_bwd = gp .tupledict ()
96+
8097 # --- Objective terms ---
8198 # Fixed objective terms
8299 self .load_shortfall_penalty_expr = gp .LinExpr ()
@@ -110,6 +127,15 @@ def __init__(self, model: gp.Model, inputs: SystemInput):
110127 self .c_daily_hydro_curtail_ess = gp .tupledict ()
111128 self .c_weekly_hydro_curtail_ess = gp .tupledict ()
112129
130+ # Trade constraints
131+ # Note: Set the RHS of the opposite flow path to zero for mutual exclusivity
132+ self .c_trade_fwd_ub = gp .tupledict ()
133+ self .c_trade_bwd_ub = gp .tupledict ()
134+
135+ # Trade LMPs
136+ self .trade_lmps : dict [tuple [str , str ], float ] = {}
137+
138+
113139 def add_variables (self , step_k : int ) -> None :
114140
115141 ##############################################
@@ -201,6 +227,23 @@ def add_variables(self, step_k: int) -> None:
201227 name = "theta" ,
202228 )
203229
230+ # --- Trade variables ---
231+ # UB set by constraints.
232+ self .trade_fwd = self .model .addVars (
233+ self .inputs .intertie_points ,
234+ self .timesteps ,
235+ lb = 0 ,
236+ vtype = gp .GRB .CONTINUOUS ,
237+ name = "trade_fwd" ,
238+ )
239+ self .trade_bwd = self .model .addVars (
240+ self .inputs .intertie_points ,
241+ self .timesteps ,
242+ lb = 0 ,
243+ vtype = gp .GRB .CONTINUOUS ,
244+ name = "trade_bwd" ,
245+ )
246+
204247 #############################################
205248 # Variables with time-dependent upper bounds
206249 #############################################
@@ -221,7 +264,7 @@ def add_variables(self, step_k: int) -> None:
221264 ]
222265 for t in self .timesteps
223266 for source , sink in self .inputs .edges
224- },
267+ },
225268 vtype = gp .GRB .CONTINUOUS ,
226269 name = "flow_fwd" ,
227270 )
@@ -471,12 +514,14 @@ def _add_nondispatchable_unit_curtailment(
471514 )
472515
473516 def add_constraints (self , step_k : int , init_conds : dict , ** kwargs ) -> None :
474-
475517 # Thermal-unit specific variables
476518 spin_vars = kwargs .get ("spin_vars" , None )
477519 vpowerbar_vars = kwargs .get ("vpowerbar_vars" , None )
478520 thermal_status_vars = kwargs .get ("thermal_status_vars" , None )
479521
522+ # Power trading
523+ trade_limits = kwargs .get ("trade_limits" , {})
524+
480525 # Power variables
481526 pthermal = kwargs .get ("pthermal" , None )
482527 phydro = kwargs .get ("phydro" , None )
@@ -531,6 +576,8 @@ def add_constraints(self, step_k: int, init_conds: dict, **kwargs) -> None:
531576 neg_pmismatch = self .neg_pmismatch ,
532577 flow_fwd = self .flow_fwd ,
533578 flow_bwd = self .flow_bwd ,
579+ trade_fwd = self .trade_fwd ,
580+ trade_bwd = self .trade_bwd ,
534581 timesteps = self .timesteps ,
535582 step_k = step_k ,
536583 thermal_units = self .inputs .thermal_units ,
@@ -540,6 +587,7 @@ def add_constraints(self, step_k: int, init_conds: dict, **kwargs) -> None:
540587 import_units = self .inputs .import_units ,
541588 nodes = self .inputs .nodes ,
542589 node_edge = self .inputs .node_edge ,
590+ intertie_points = self .inputs .intertie_points ,
543591 node_generator = self .inputs .node_generator ,
544592 ess_charge_units = self .inputs .ess_substation_units ,
545593 ess_discharge_units = self .inputs .ess_attach_unit ,
@@ -619,6 +667,20 @@ def add_constraints(self, step_k: int, init_conds: dict, **kwargs) -> None:
619667 ess_attached = self .inputs .ess_daily_hydro_units ,
620668 )
621669
670+ # --- Trade flow constraints ---
671+ self .c_trade_fwd_ub = system_constr .add_c_trade_fwd_ub (
672+ model = self .model ,
673+ trade_fwd = self .trade_fwd ,
674+ intertie_points = self .inputs .intertie_points ,
675+ timesteps = self .timesteps ,
676+ )
677+ self .c_trade_bwd_ub = system_constr .add_c_trade_bwd_ub (
678+ model = self .model ,
679+ trade_bwd = self .trade_bwd ,
680+ intertie_points = self .inputs .intertie_points ,
681+ timesteps = self .timesteps ,
682+ )
683+
622684 def update_variables (self , step_k : int ) -> None :
623685 """Update the time-dependent upper bounds of the flow variables.
624686
@@ -666,6 +728,9 @@ def update_constraints(self, step_k: int, init_conds: dict, **kwargs) -> None:
666728 pdischarge = kwargs .get ("pdischarge" , None )
667729 charge_state = kwargs .get ("charge_state" , None )
668730
731+ # Power trade limits
732+ trade_limits = kwargs .get ("trade_limits" , {})
733+
669734 # --- Spinning reserve constraints ---
670735 self .model .remove (self .c_reserve_req )
671736 if self .inputs .use_spin_var :
@@ -711,6 +776,8 @@ def update_constraints(self, step_k: int, init_conds: dict, **kwargs) -> None:
711776 neg_pmismatch = self .neg_pmismatch ,
712777 flow_fwd = self .flow_fwd ,
713778 flow_bwd = self .flow_bwd ,
779+ trade_fwd = self .trade_fwd ,
780+ trade_bwd = self .trade_bwd ,
714781 timesteps = self .timesteps ,
715782 step_k = step_k ,
716783 thermal_units = self .inputs .thermal_units ,
@@ -720,6 +787,7 @@ def update_constraints(self, step_k: int, init_conds: dict, **kwargs) -> None:
720787 import_units = self .inputs .import_units ,
721788 nodes = self .inputs .nodes ,
722789 node_edge = self .inputs .node_edge ,
790+ intertie_points = self .inputs .intertie_points ,
723791 node_generator = self .inputs .node_generator ,
724792 ess_charge_units = self .inputs .ess_substation_units ,
725793 ess_discharge_units = self .inputs .ess_attach_unit ,
@@ -804,6 +872,39 @@ def update_constraints(self, step_k: int, init_conds: dict, **kwargs) -> None:
804872 ess_attached = self .inputs .ess_daily_hydro_units ,
805873 )
806874
875+ # --- Trade flow constraints ---
876+ # Reset the trade upper bounds to zero for the next step_k.
877+ self .model .remove (self .c_trade_fwd_ub )
878+ self .c_trade_fwd_ub = system_constr .add_c_trade_fwd_ub (
879+ model = self .model ,
880+ trade_fwd = self .trade_fwd ,
881+ intertie_points = self .inputs .intertie_points ,
882+ timesteps = self .timesteps ,
883+ )
884+ self .model .remove (self .c_trade_bwd_ub )
885+ self .c_trade_bwd_ub = system_constr .add_c_trade_bwd_ub (
886+ model = self .model ,
887+ trade_bwd = self .trade_bwd ,
888+ intertie_points = self .inputs .intertie_points ,
889+ timesteps = self .timesteps ,
890+ )
891+
892+ def update_c_trade_ub_rhs (self , trade_limits : dict [tuple [str , str ], float ]) -> None :
893+ # Forward trade upper bounds
894+ for constr_name , constr in self .c_trade_fwd_ub .items ():
895+ intertie_point = constr_name [0 ]
896+ if intertie_point in trade_limits :
897+ constr .RHS = trade_limits [intertie_point ]
898+ else :
899+ constr .RHS = 0.0
900+
901+ for constr_name , constr in self .c_trade_bwd_ub .items ():
902+ intertie_point = constr_name [0 ]
903+ if intertie_point in trade_limits :
904+ constr .RHS = trade_limits [intertie_point ]
905+ else :
906+ constr .RHS = 0.0
907+
807908 def get_variables (self ) -> dict [str , gp .tupledict ]:
808909 """Get the variables of the system builder.
809910
@@ -816,5 +917,7 @@ def get_variables(self) -> dict[str, gp.tupledict]:
816917 "spin_shortfall" : self .spin_shortfall ,
817918 "flow_fwd" : self .flow_fwd ,
818919 "flow_bwd" : self .flow_bwd ,
920+ "trade_fwd" : self .trade_fwd ,
921+ "trade_bwd" : self .trade_bwd ,
819922 "theta" : self .theta ,
820923 }
0 commit comments