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
« 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
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.
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.
30 """
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()
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)
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
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.")
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
113 def add_generator_model(self, a_eq, b_eq, j, phase):
114 return a_eq, b_eq
116 def add_load_model(self, a_eq, b_eq, j, phase):
117 return a_eq, b_eq
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
130 def create_inequality_constraints(self):
131 return None, None
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
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
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
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