Coverage for src/distopf/matrix_models/lindist_loads.py: 94%

47 statements  

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

1from typing import Optional 

2 

3import pandas as pd 

4import distopf as opf 

5from distopf.matrix_models.base import LinDistBase 

6 

7 

8class LinDistModelL(LinDistBase): 

9 """ 

10 LinDistFlow Model for 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 including resistance and reactance values and limits. 

24 bus_data : pd.DataFrame 

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

26 gen_data : pd.DataFrame 

27 DataFrame containing generator data. 

28 cap_data : pd.DataFrame 

29 DataFrame containing capacitor data. 

30 reg_data : pd.DataFrame 

31 DataFrame containing regulator data. 

32 

33 References 

34 ---------- 

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

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

37 With Voltage Control Devices,” 

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

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

40 

41 Examples 

42 -------- 

43 This example demonstrates how to set up and solve a linear distribution flow model 

44 using a provided case, and visualize the results. 

45 

46 >>> import distopf as opf 

47 >>> # Prepare the case data 

48 >>> case = opf.DistOPFCase(data_path="ieee123_30der") 

49 >>> # Initialize the LinDistModel 

50 >>> model = LinDistModelL( 

51 ... branch_data=case.branch_data, 

52 ... bus_data=case.bus_data, 

53 ... gen_data=case.gen_data, 

54 ... cap_data=case.cap_data, 

55 ... reg_data=case.reg_data, 

56 ... ) 

57 >>> # Solve the model using the specified objective function 

58 >>> result = opf.lp_solve(model, opf.gradient_load_min(model)) 

59 >>> # Extract and plot results 

60 >>> v = model.get_voltages(result.x) 

61 >>> s = model.get_apparent_power_flows(result.x) 

62 >>> p_gens = model.get_p_gens(result.x) 

63 >>> q_gens = model.get_q_gens(result.x) 

64 >>> # Visualize network and power flows 

65 >>> opf.plot_network(model, v=v, s=s, p_gen=p_gens, q_gen=q_gens).show() 

66 >>> opf.plot_voltages(v).show() 

67 >>> opf.plot_power_flows(s).show() 

68 >>> opf.plot_gens(p_gens, q_gens).show() 

69 """ 

70 

71 def __init__( 

72 self, 

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

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

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

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

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

78 ): 

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

80 self.build() 

81 

82 def initialize_variable_index_pointers(self): 

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

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

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

86 self.qg_map, self.n_x = self._add_device_variables(self.n_x, self.gen_buses) 

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

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

89 self.pl_map, self.n_x = self._add_device_variables(self.n_x, self.load_buses) 

90 self.ql_map, self.n_x = self._add_device_variables(self.n_x, self.load_buses) 

91 

92 def additional_variable_idx(self, var, node_j, phase): 

93 """ 

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

95 Parameters 

96 ---------- 

97 var : name of variable 

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

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

100 

101 Returns 

102 ------- 

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

104 """ 

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

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

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

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

109 return None 

110 

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

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

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

114 pl = self.idx("pl", j, phase) 

115 ql = self.idx("ql", j, phase) 

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

117 p_load_nom, q_load_nom = 0, 0 

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

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

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

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

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

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

124 # Set Load equation variable coefficients in a_eq 

125 a_eq[pl, pl] = 1 

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

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

128 

129 a_eq[ql, ql] = 1 

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

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

132 return a_eq, b_eq 

133 

134 def get_p_loads(self, x): 

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

136 

137 def get_q_loads(self, x): 

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