Coverage for src/distopf/matrix_models/lindist_p_gen.py: 17%

127 statements  

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

1from typing import Optional 

2from functools import cache 

3import numpy as np 

4import pandas as pd 

5import distopf as opf 

6from distopf.matrix_models.base import LinDistBase 

7from distopf.utils import get 

8 

9 

10class LinDistModelPGen(LinDistBase): 

11 """ 

12 LinDistFlow Model with active power control of generators. 

13 No variables for reactive power generation exist. This can make the 

14 model smaller and run faster if there are many generators and only 

15 active power control is required. 

16 

17 Parameters 

18 ---------- 

19 branch_data : pd.DataFrame 

20 DataFrame containing branch data including resistance and reactance values and limits. 

21 bus_data : pd.DataFrame 

22 DataFrame containing bus data such as loads, voltages, and limits. 

23 gen_data : pd.DataFrame 

24 DataFrame containing generator data. 

25 cap_data : pd.DataFrame 

26 DataFrame containing capacitor data. 

27 reg_data : pd.DataFrame 

28 DataFrame containing regulator data. 

29 

30 """ 

31 

32 def __init__( 

33 self, 

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

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

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

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

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

39 ): 

40 super().__init__(branch_data, bus_data, gen_data, cap_data, reg_data) 

41 self.build() 

42 

43 def initialize_variable_index_pointers(self): 

44 self.x_maps, self.n_x = self._variable_tables(self.branch) 

45 self.v_map, self.n_x = self._add_device_variables(self.n_x, self.all_buses) 

46 self.pg_map, self.n_x = self._add_device_variables(self.n_x, self.gen_buses) 

47 self.qc_map, self.n_x = self._add_device_variables(self.n_x, self.cap_buses) 

48 self.vx_map, self.n_x = self._add_device_variables(self.n_x, self.reg_buses) 

49 

50 def add_generator_limits(self, x_lim_lower, x_lim_upper): 

51 for a in "abc": 

52 if not self.phase_exists(a): 

53 continue 

54 p_out = self.gen[f"p{a}"] 

55 for j in self.gen_buses[a]: 

56 pg = self.idx("pg", j, a) 

57 x_lim_lower[pg] = 0 

58 x_lim_upper[pg] = p_out[j] 

59 return x_lim_lower, x_lim_upper 

60 

61 @cache 

62 def idx(self, var, node_j, phase): 

63 if var in self.x_maps[phase].columns: 

64 return self.branch_into_j(var, node_j, phase) 

65 if var in ["pjk"]: # indexes of all branch active power out of node j 

66 return self.branches_out_of_j("pij", node_j, phase) 

67 if var in ["qjk"]: # indexes of all branch reactive power out of node j 

68 return self.branches_out_of_j("qij", node_j, phase) 

69 if var in ["v"]: # active power generation at node 

70 return get(self.v_map[phase], node_j, []) 

71 if var in ["pg", "p_gen"]: # active power generation at node 

72 return get(self.pg_map[phase], node_j, []) 

73 if var in ["qc", "q_cap"]: # reactive power injection by capacitor 

74 return get(self.qc_map[phase], node_j, []) 

75 if var in ["vx"]: 

76 return self.vx_map[phase].get(node_j, []) 

77 ix = self.additional_variable_idx(var, node_j, phase) 

78 if ix is not None: 

79 return ix 

80 raise ValueError(f"Variable name, '{var}', not found.") 

81 

82 def add_power_flow_model(self, a_eq, b_eq, j, phase): 

83 pij = self.idx("pij", j, phase) 

84 qij = self.idx("qij", j, phase) 

85 pjk = self.idx("pjk", j, phase) 

86 qjk = self.idx("qjk", j, phase) 

87 pg = self.idx("pg", j, phase) 

88 qc = self.idx("q_cap", j, phase) 

89 vj = self.idx("v", j, phase) 

90 q_gen_nom = 0 

91 if self.gen is not None: 

92 q_gen_nom = get(self.gen[f"q{phase}"], j, 0) 

93 p_load_nom, q_load_nom = 0, 0 

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

95 p_load_nom = self.bus[f"pl_{phase}"][j] 

96 q_load_nom = self.bus[f"ql_{phase}"][j] 

97 # Set P equation variable coefficients in a_eq 

98 a_eq[pij, pij] = 1 

99 a_eq[pij, pjk] = -1 

100 a_eq[pij, pg] = 1 

101 # Set Q equation variable coefficients in a_eq 

102 a_eq[qij, qij] = 1 

103 a_eq[qij, qjk] = -1 

104 a_eq[qij, qc] = 1 

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

106 # Set Load equation variable coefficients in a_eq 

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

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

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

110 b_eq[qij] = (1 - (self.bus.cvr_q[j] / 2)) * q_load_nom - q_gen_nom 

111 return a_eq, b_eq 

112 

113 def add_generator_model(self, a_eq, b_eq, j, phase): 

114 return a_eq, b_eq 

115 

116 def add_load_model(self, a_eq, b_eq, j, phase): 

117 return a_eq, b_eq 

118 

119 def add_capacitor_model(self, a_eq, b_eq, j, phase): 

120 q_cap_nom = 0 

121 if self.cap is not None: 

122 q_cap_nom = get(self.cap[f"q{phase}"], j, 0) 

123 # equation indexes 

124 vj = self.idx("v", j, phase) 

125 qc = self.idx("q_cap", j, phase) 

126 a_eq[qc, qc] = 1 

127 a_eq[qc, vj] = -q_cap_nom 

128 return a_eq, b_eq 

129 

130 def create_inequality_constraints(self): 

131 return None, None 

132 

133 def get_device_variables(self, x, variable_map): 

134 if len(variable_map.keys()) == 0: 

135 return pd.DataFrame(columns=["id", "name", "t", "a", "b", "c"]) 

136 index = np.unique( 

137 np.r_[ 

138 variable_map["a"].index, 

139 variable_map["b"].index, 

140 variable_map["c"].index, 

141 ] 

142 ) 

143 bus_id = index + 1 

144 df = pd.DataFrame(columns=["id", "name", "a", "b", "c"], index=bus_id) 

145 df.id = bus_id 

146 df.loc[bus_id, "name"] = self.bus.loc[index, "name"].to_numpy() 

147 for a in "abc": 

148 df.loc[variable_map[a].index + 1, a] = x[variable_map[a]] 

149 df.loc[:, ["a", "b", "c"]] = df.loc[:, ["a", "b", "c"]].astype(float) 

150 return df 

151 

152 def get_voltages(self, x): 

153 v_df = self.get_device_variables(x, self.v_map) 

154 v_df.loc[:, ["a", "b", "c"]] = v_df.loc[:, ["a", "b", "c"]] ** 0.5 

155 return v_df 

156 

157 def get_q_gens(self, x): 

158 df = self.get_device_variables(x, self.pg_map) 

159 df.a = self.gen_data.qa.to_numpy() 

160 df.b = self.gen_data.qb.to_numpy() 

161 df.c = self.gen_data.qc.to_numpy() 

162 return df 

163 

164 def get_apparent_power_flows(self, x): 

165 s_df = pd.DataFrame( 

166 columns=["fb", "tb", "from_name", "to_name", "a", "b", "c"], 

167 index=range(2, self.nb + 1), 

168 ) 

169 s_df["a"] = s_df["a"].astype(complex) 

170 s_df["b"] = s_df["b"].astype(complex) 

171 s_df["c"] = s_df["c"].astype(complex) 

172 for ph in "abc": 

173 fb_idxs = self.x_maps[ph].bi.to_numpy() 

174 fb_names = self.bus.name[fb_idxs].to_numpy() 

175 tb_idxs = self.x_maps[ph].bj.to_numpy() 

176 tb_names = self.bus.name[tb_idxs].to_numpy() 

177 s_df.loc[self.x_maps[ph].bj.to_numpy() + 1, "fb"] = fb_idxs + 1 

178 s_df.loc[self.x_maps[ph].bj.to_numpy() + 1, "tb"] = tb_idxs + 1 

179 s_df.loc[self.x_maps[ph].bj.to_numpy() + 1, "from_name"] = fb_names 

180 s_df.loc[self.x_maps[ph].bj.to_numpy() + 1, "to_name"] = tb_names 

181 s_df.loc[self.x_maps[ph].bj.to_numpy() + 1, ph] = ( 

182 x[self.x_maps[ph].pij] + 1j * x[self.x_maps[ph].qij] 

183 ) 

184 return s_df