Coverage for src/distopf/matrix_models/multiperiod/lindist_loads_mp.py: 91%

78 statements  

« prev     ^ index     » next       coverage.py v7.10.6, created at 2025-11-13 17:34 -0800

1from typing import Optional, override 

2import pandas as pd 

3import distopf as opf 

4from distopf.importer import Case 

5from distopf.matrix_models.multiperiod.base_mp import LinDistBaseMP 

6 

7 

8class LinDistMPL(LinDistBaseMP): 

9 """ 

10 LinDistFlow Model for multiperiod linear power flow modeling which includes active and 

11 reactive load powers as variables. This may be useful starting point if 

12 custom load models need to be added. The disadvantage is that significantly 

13 more variables are included which will increase computation time. 

14 

15 This class represents a linearized distribution model used for calculating 

16 power flows, voltages, and other system properties in a distribution network 

17 using the linearized branch-flow formulation from [1]. The model is composed of several power system components 

18 such as buses, branches, generators, capacitors, and regulators. 

19 

20 Parameters 

21 ---------- 

22 branch_data : pd.DataFrame 

23 DataFrame containing branch data (r and x values, limits) 

24 bus_data : pd.DataFrame 

25 DataFrame containing bus data (loads, voltages, limits) 

26 gen_data : pd.DataFrame 

27 DataFrame containing generator/DER data 

28 cap_data : pd.DataFrame 

29 DataFrame containing capacitor data 

30 reg_data : pd.DataFrame 

31 DataFrame containing regulator data 

32 bat_data : pd DataFrame 

33 DataFrame containing battery data 

34 loadshape_data : pd.DataFrame 

35 DataFrame containing loadshape multipliers for P values 

36 pv_loadshape_data : pd.DataFrame 

37 DataFrame containing PV profile of 1h interval for 24h 

38 n_steps : int, 

39 Number of time intervals for multi period optimization. Default is 24. 

40 case : Case, 

41 Case object containing all of the parameters. Alternative to listing seperately. 

42 

43 References 

44 ---------- 

45 [1] R. R. Jha, A. Dubey, C.-C. Liu, and K. P. Schneider, 

46 “Bi-Level Volt-VAR Optimization to Coordinate Smart Inverters 

47 With Voltage Control Devices,” 

48 IEEE Trans. Power Syst., vol. 34, no. 3, pp. 1801–1813, 

49 May 2019, doi: 10.1109/TPWRS.2018.2890613. 

50 """ 

51 

52 @override 

53 def __init__( 

54 self, 

55 branch_data: Optional[pd.DataFrame] = None, 

56 bus_data: Optional[pd.DataFrame] = None, 

57 gen_data: Optional[pd.DataFrame] = None, 

58 cap_data: Optional[pd.DataFrame] = None, 

59 reg_data: Optional[pd.DataFrame] = None, 

60 bat_data: Optional[pd.DataFrame] = None, 

61 schedules: Optional[pd.DataFrame] = None, 

62 start_step: int = 0, 

63 n_steps: int = 24, 

64 delta_t: float = 1, # hours per step 

65 case: Optional[Case] = None, 

66 ): 

67 super().__init__( 

68 branch_data=branch_data, 

69 bus_data=bus_data, 

70 gen_data=gen_data, 

71 cap_data=cap_data, 

72 reg_data=reg_data, 

73 bat_data=bat_data, 

74 schedules=schedules, 

75 start_step=start_step, 

76 n_steps=n_steps, 

77 delta_t=delta_t, 

78 case=case, 

79 ) 

80 self.pl_map: dict[int, dict[str, pd.Series]] = {} 

81 self.ql_map: dict[int, dict[str, pd.Series]] = {} 

82 self.build() 

83 

84 @override 

85 def initialize_variable_index_pointers(self): 

86 # ~~ initialize index pointers ~~ 

87 self.x_maps = {} 

88 self.v_map = {} 

89 self.pg_map = {} 

90 self.qg_map = {} 

91 self.qc_map = {} 

92 self.charge_map = {} 

93 self.discharge_map = {} 

94 self.soc_map = {} 

95 self.vx_map = {} 

96 self.pl_map = {} 

97 self.ql_map = {} 

98 self.n_x = 0 

99 for t in range(self.start_step, self.start_step + self.n_steps): 

100 self.x_maps[t], self.n_x = self._variable_tables(self.branch, n_x=self.n_x) 

101 self.v_map[t], self.n_x = self._add_device_variables( 

102 self.n_x, self.all_buses 

103 ) 

104 self.pg_map[t], self.n_x = self._add_device_variables( 

105 self.n_x, self.gen_buses 

106 ) 

107 self.qg_map[t], self.n_x = self._add_device_variables( 

108 self.n_x, self.gen_buses 

109 ) 

110 self.qc_map[t], self.n_x = self._add_device_variables( 

111 self.n_x, self.cap_buses 

112 ) 

113 self.charge_map[t], self.n_x = self._add_device_variables( 

114 self.n_x, self.bat_buses 

115 ) 

116 self.discharge_map[t], self.n_x = self._add_device_variables( 

117 self.n_x, self.bat_buses 

118 ) 

119 self.soc_map[t], self.n_x = self._add_device_variables( 

120 self.n_x, self.bat_buses 

121 ) 

122 self.vx_map[t], self.n_x = self._add_device_variables( 

123 self.n_x, self.reg_buses 

124 ) 

125 self.pl_map[t], self.n_x = self._add_device_variables( 

126 self.n_x, self.load_buses 

127 ) 

128 self.ql_map[t], self.n_x = self._add_device_variables( 

129 self.n_x, self.load_buses 

130 ) 

131 

132 @override 

133 def additional_variable_idx(self, var, node_j, phase, t=0): 

134 """ 

135 User added index function. Override this function to add custom variables. Return None if `var` is not found. 

136 Parameters 

137 ---------- 

138 var : name of variable 

139 node_j : node index (0 based; bus.id - 1) 

140 phase : "a", "b", or "c" 

141 

142 Returns 

143 ------- 

144 ix : index or list of indices of variable within x-vector or None if `var` is not found. 

145 """ 

146 if t < self.start_step: 

147 t = self.start_step 

148 if var in ["pl", "p_load"]: # reactive power load at node 

149 return self.pl_map[t][phase].get(node_j, []) 

150 if var in ["ql", "q_load"]: # reactive power injection by capacitor 

151 return self.ql_map[t][phase].get(node_j, []) 

152 return None 

153 

154 def add_load_model(self, a_eq, b_eq, j, a, t=0): 

155 pij = self.idx("pij", j, a, t=t) 

156 qij = self.idx("qij", j, a, t=t) 

157 pl = self.idx("pl", j, a, t=t) 

158 ql = self.idx("ql", j, a, t=t) 

159 vj = self.idx("v", j, a, t=t) 

160 

161 a_eq[pij, pl] = -1 # add load variable to power flow equation 

162 a_eq[qij, ql] = -1 # add load variable to power flow equation 

163 p_load_nom, q_load_nom = 0, 0 

164 load_mult_p = load_mult_q = 1 

165 load_shape = self.bus.load_shape[j] 

166 if load_shape in self.schedules.columns: 

167 load_mult_p = load_mult_q = self.schedules.at[t, load_shape] 

168 elif f"{load_shape}.{a}.p" in self.schedules.columns: 

169 load_mult_p = self.schedules.at[t, f"{load_shape}.{a}.p"] 

170 load_mult_q = self.schedules.at[t, f"{load_shape}.{a}.q"] 

171 if self.bus.bus_type[j] == opf.PQ_BUS: 

172 p_load_nom = self.bus[f"pl_{a}"][j] * load_mult_p 

173 q_load_nom = self.bus[f"ql_{a}"][j] * load_mult_q 

174 if self.bus.bus_type[j] != opf.PQ_FREE: 

175 # Set Load equation variable coefficients in a_eq 

176 a_eq[pl, pl] = 1 

177 a_eq[pl, vj] = -(self.bus.cvr_p[j] / 2) * p_load_nom 

178 b_eq[pl] = (1 - (self.bus.cvr_p[j] / 2)) * p_load_nom 

179 

180 a_eq[ql, ql] = 1 

181 a_eq[ql, vj] = -(self.bus.cvr_q[j] / 2) * q_load_nom 

182 b_eq[ql] = (1 - (self.bus.cvr_q[j] / 2)) * q_load_nom 

183 return a_eq, b_eq 

184 

185 def get_p_loads(self, x): 

186 return self.get_device_variables(x, self.pl_map) 

187 

188 def get_q_loads(self, x): 

189 return self.get_device_variables(x, self.ql_map)