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
« 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
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.
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.
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.
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 """
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()
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 )
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"
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
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)
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
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
185 def get_p_loads(self, x):
186 return self.get_device_variables(x, self.pl_map)
188 def get_q_loads(self, x):
189 return self.get_device_variables(x, self.ql_map)