Coverage for src/distopf/distOPF.py: 10%

336 statements  

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

1""" 

2This module contains high-level helper functions for creating and running provided models, solvers, and objectives. 

3""" 

4 

5from typing import Optional 

6from collections.abc import Callable 

7from pathlib import Path 

8import json 

9import pandas as pd 

10import numpy as np 

11 

12from distopf.matrix_models.base import LinDistBase 

13from distopf import ( 

14 DSSToCSVConverter, 

15 CASES_DIR, 

16 LinDistModelCapMI, 

17 LinDistModelCapacitorRegulatorMI, 

18) 

19from distopf.matrix_models.lindist_p_gen import LinDistModelPGen 

20from distopf.matrix_models.lindist_q_gen import LinDistModelQGen 

21from distopf.matrix_models.lindist import LinDistModel 

22from distopf.matrix_models.solvers import ( 

23 lp_solve, 

24 cvxpy_solve, 

25) 

26from distopf.matrix_models.objectives import ( 

27 cp_obj_loss, 

28 cp_obj_curtail, 

29 cp_obj_target_p_3ph, 

30 cp_obj_target_p_total, 

31 cp_obj_target_q_3ph, 

32 cp_obj_target_q_total, 

33 gradient_load_min, 

34 gradient_curtail, 

35) 

36from distopf.plot import plot_network, plot_voltages, plot_power_flows, plot_gens 

37from distopf.utils import ( 

38 handle_branch_input, 

39 handle_bus_input, 

40 handle_gen_input, 

41 handle_cap_input, 

42 handle_reg_input, 

43) 

44 

45 

46def create_model( 

47 control_variable: str = "", 

48 control_regulators: bool = False, 

49 control_capacitors: bool = False, 

50 **kwargs, 

51) -> LinDistBase: 

52 """ 

53 Create the correct LinDistModel object based on the control variable. 

54 Parameters 

55 ---------- 

56 control_variable : str, optional : No Control Variables-None, Active Power Control-'p', Reactive Power Control-'q' 

57 control_regulators : bool, optional : Default False, if true use mixed integer control of regulators 

58 control_capacitors : bool, optional : Default False, if true use mixed integer control of capacitors 

59 kwargs : 

60 branch_data : pd.DataFrame 

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

62 bus_data : pd.DataFrame 

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

64 gen_data : pd.DataFrame 

65 DataFrame containing generator/DER data 

66 cap_data : pd.DataFrame 

67 DataFrame containing capacitor data 

68 reg_data : pd.DataFrame 

69 DataFrame containing regulator data 

70 Returns 

71 ------- 

72 model: LinDistModel, or LinDistModelP, or LinDistModelQ object appropriate for the control variable 

73 """ 

74 

75 if control_capacitors and not control_regulators: 

76 return LinDistModelCapMI(**kwargs) 

77 if control_regulators: 

78 return LinDistModelCapacitorRegulatorMI(**kwargs) 

79 if control_variable is None or control_variable == "": 

80 return LinDistModel(**kwargs) 

81 if control_variable.upper() == "P": 

82 return LinDistModelPGen(**kwargs) 

83 if control_variable.upper() == "Q": 

84 return LinDistModelQGen(**kwargs) 

85 if control_variable.upper() == "PQ": 

86 return LinDistModel(**kwargs) 

87 raise ValueError( 

88 f"Unknown control variable '{control_variable}'. Valid options are 'P', 'Q' or None" 

89 ) 

90 

91 

92def auto_solve(model: LinDistBase, objective_function=None, **kwargs): 

93 """ 

94 Solve with selected objective function and model. Automatically chooses the appropriate function. 

95 

96 Parameters 

97 ---------- 

98 model : LinDistBase 

99 objective_function : str or Callable 

100 kwargs : kwargs to pass to objective function and solver function. 

101 solver: str 

102 Solver to use for solving with CVXPY. Default is CLARABEL. OSQP is also recommended. 

103 target: 

104 Used with target objectives. Target to track. 

105 Scalar for target_p_total and target_q_total and size-3 array for target_p_3ph, and target_q_3ph. 

106 error_percent: 

107 Used with target objectives. Percent error expected in total system load compared exact solution. 

108 

109 Returns 

110 ------- 

111 result: scipy.optimize.OptimizeResult 

112 

113 """ 

114 if objective_function is None: 

115 objective_function = np.zeros(model.n_x) 

116 if not isinstance(objective_function, (str, Callable, np.ndarray, list)): # type: ignore 

117 raise TypeError( 

118 "objective_function must be a function handle, array, or string" 

119 ) 

120 objective_function_map_gradient: dict[str, Callable] = { 

121 "gen_max": gradient_curtail, 

122 "load_min": gradient_load_min, 

123 } 

124 objective_function_map: dict[str, Callable] = { 

125 "loss_min": cp_obj_loss, 

126 "curtail_min": cp_obj_curtail, 

127 "target_p_3ph": cp_obj_target_p_3ph, 

128 "target_q_3ph": cp_obj_target_q_3ph, 

129 "target_p_total": cp_obj_target_p_total, 

130 "target_q_total": cp_obj_target_q_total, 

131 } 

132 if isinstance(objective_function, str): 

133 objective_function = objective_function.lower() 

134 if objective_function in objective_function_map.keys(): 

135 objective_function = objective_function_map[objective_function] 

136 if objective_function in objective_function_map_gradient.keys(): 

137 objective_function = objective_function_map[objective_function](model) 

138 if isinstance(objective_function, Callable): # type: ignore 

139 if hasattr(model, "solve"): 

140 return model.solve(objective_function, **kwargs) 

141 return cvxpy_solve(model, objective_function, **kwargs) 

142 if isinstance(objective_function, (np.ndarray, list)): 

143 return lp_solve(model, objective_function) # type: ignore 

144 

145 

146def _handle_path_input(data_path: Path) -> Path: 

147 cwd = Path.cwd() 

148 if data_path.is_absolute(): 

149 return data_path 

150 if (cwd / data_path).exists(): 

151 return cwd / data_path 

152 if (CASES_DIR / "csv" / data_path).exists(): 

153 return CASES_DIR / "csv" / data_path 

154 if (CASES_DIR / "dss" / data_path).exists(): 

155 return CASES_DIR / "dss" / data_path 

156 return data_path 

157 

158 

159def _get_data_from_path(data_path: Path) -> dict: 

160 branch_data = None 

161 bus_data = None 

162 gen_data = None 

163 cap_data = None 

164 reg_data = None 

165 if not data_path.exists(): 

166 raise FileNotFoundError() 

167 if data_path.is_file() and data_path.suffix.lower() != ".dss": 

168 raise ValueError( 

169 "The variable, data_path, must point to a directory containing model CSVs or an OpenDSS model file." 

170 ) 

171 if data_path.is_dir(): 

172 branch_path = data_path / "branch_data.csv" 

173 if branch_path.exists(): 

174 branch_data = pd.read_csv(branch_path, header=0) 

175 bus_path = data_path / "bus_data.csv" 

176 if bus_path.exists(): 

177 bus_data = pd.read_csv(bus_path, header=0) 

178 gen_path = data_path / "gen_data.csv" 

179 if gen_path.exists(): 

180 gen_data = pd.read_csv(gen_path, header=0) 

181 cap_path = data_path / "cap_data.csv" 

182 if cap_path.exists(): 

183 cap_data = pd.read_csv(cap_path, header=0) 

184 reg_path = data_path / "reg_data.csv" 

185 if reg_path.exists(): 

186 reg_data = pd.read_csv(data_path / "reg_data.csv", header=0) 

187 if data_path.suffix.lower() == ".dss": 

188 dss_parser = DSSToCSVConverter(data_path) 

189 branch_data = dss_parser.branch_data 

190 bus_data = dss_parser.bus_data 

191 gen_data = dss_parser.gen_data 

192 cap_data = dss_parser.cap_data 

193 reg_data = dss_parser.reg_data 

194 

195 branch_data = handle_branch_input(branch_data) 

196 bus_data = handle_bus_input(bus_data) 

197 gen_data = handle_gen_input(gen_data) 

198 cap_data = handle_cap_input(cap_data) 

199 reg_data = handle_reg_input(reg_data) 

200 return { 

201 "branch_data": branch_data, 

202 "bus_data": bus_data, 

203 "gen_data": gen_data, 

204 "cap_data": cap_data, 

205 "reg_data": reg_data, 

206 } 

207 

208 

209class DistOPFCase(object): 

210 """ 

211 Use this class to create a distOPF case, run it, and save and plot results. 

212 Parameters 

213 ---------- 

214 config: str or dict 

215 Path to JSON config or dictionary with parameters to create case. Alternative to using **config. 

216 data_path: str or pathlib.Path 

217 Path to the directory containing the data CSVs or path to OpenDSS model. Will also accept names of 

218 cases include in package e.g. "ieee13", "ieee34", "ieee123". 

219 output_dir: str or pathlib.Path 

220 (default: "output") Directory to save results. 

221 branch_data : pd.DataFrame or None 

222 DataFrame containing branch data (r and x values, limits). Overrides data found from data_path. 

223 bus_data : pd.DataFrame or None 

224 DataFrame containing bus data (loads, voltages, limits). Overrides data found from data_path. 

225 gen_data : pd.DataFrame or None 

226 DataFrame containing generator/DER data. Overrides data found from data_path. 

227 cap_data : pd.DataFrame or None 

228 DataFrame containing capacitor data. Overrides data found from data_path. 

229 reg_data : pd.DataFrame or None 

230 DataFrame containing regulator data. Overrides data found from data_path. 

231 v_swing: Number or size-3 array 

232 Override substation voltage. Scalar or 3-phase array. Per Unit. 

233 v_min: Number 

234 Override all voltage minimum limits. Per Unit. 

235 v_max: Number 

236 Override all voltage maximum limits. Per Unit. 

237 gen_mult: Number 

238 Scale all generator outputs and ratings. Per Unit. 

239 load_mult: 

240 Scale all loads. 

241 cvr_p: 

242 CVR factor for voltage dependent loads. Active power component. cvr_p = (dP/P)/(dV/V) 

243 To convert from ZIP parameters, kz, ki, kp: cvr_p = 2kz + 1ki 

244 cvr_q: 

245 CVR factor for voltage dependent loads. Reactive power component.cvr_q = (dQ/Q)/(dV/V) 

246 To convert from ZIP parameters, kz, ki, kp: cvr_q = 2kz + 1ki 

247 control_variable: str 

248 Control variable for optimization. Options (case-insensitive): 

249 None: Power flow only with no optimization. `objective_function` options will be ignored. 

250 "P": Active power injections from generators. Active power outputs set in gen_data.csv will be ignored 

251 and reactive power outputs set in gen_data static. 

252 "Q": Reactive power injections from generators. 

253 Active power outputs set in gen_data.csv are constant and reactive power outputs set in 

254 gen_data.csv will be ignored. 

255 objective_function: str or Callable 

256 Objective function for optimization. Options (case-insensitive): 

257 "gen_max": Maximize output of generators. Uses scipy.optimize.linprog. 

258 "load_min": Minimize total substation active power load. Uses scipy.optimize.linprog. 

259 "loss_min": Minimize total line active power losses. Quadratic. Uses CVXPY. 

260 "curtail_min": Minimize DER/Generator curtailment. Quadratic. Uses CVXPY. 

261 "target_p_3ph": Substation load tracks active power target on each phase. Quadratic. Uses CVXPY. 

262 "target_q_3ph": Substation load tracks reactive power target on each phase. Quadratic. Uses CVXPY. 

263 "target_p_total": Substation load tracks total active power. Quadratic. Uses CVXPY. 

264 "target_q_total": Substation load tracks total reactive power. Quadratic. Uses CVXPY. 

265 show_plots: bool 

266 (default False) If true, renders plots in browser 

267 save_results: bool 

268 (default False) If true, saves result data to CSVs in output_dir 

269 save_plots: bool 

270 (default False) If true, saves interactive plots as html to output folder 

271 save_inputs: bool 

272 (default False) If true, saves model CSV and other input parameters. 

273 NOTE CSVs include any modifications made by other parameters such as gen_mult, load_mult, v_max, v_min, or 

274 v_swing. 

275 """ 

276 

277 def __init__(self, **kwargs): 

278 config = kwargs.get("config") 

279 if config is not None: 

280 if len(kwargs) != 1: 

281 raise ValueError( 

282 "If config is provided, other parameters are not allowed." 

283 ) 

284 if isinstance(config, (str, Path)): 

285 config = _handle_path_input(Path(config)) 

286 if not config.suffix.lower() == ".json": 

287 raise ValueError("config file must be a JSON formatted file.") 

288 with open(config) as f: 

289 config = json.load(f) 

290 if not isinstance(config, dict): 

291 raise ValueError( 

292 "config must be a dictionary or a path to a JSON formatted file." 

293 ) 

294 kwargs = config 

295 

296 self.data_path = kwargs.get("data_path") 

297 self.v_swing = kwargs.get("v_swing") 

298 self.v_max = kwargs.get("v_max") 

299 self.v_min = kwargs.get("v_min") 

300 self.gen_mult = kwargs.get("gen_mult") 

301 self.load_mult = kwargs.get("load_mult") 

302 self.cvr_p = kwargs.get("cvr_p") 

303 self.cvr_q = kwargs.get("cvr_q") 

304 

305 self.control_variable = kwargs.get("control_variable") 

306 self.control_regulators = kwargs.get("control_regulators", False) 

307 self.control_capacitors = kwargs.get("control_capacitors", False) 

308 self.objective_function = kwargs.get("objective_function") 

309 self.target = kwargs.get("target") 

310 self.error_percent = kwargs.get("error_percent") 

311 self.solver = kwargs.get("solver", "CLARABEL") 

312 

313 self.output_dir = Path(kwargs.get("output_dir", "output")) 

314 self.save_inputs = kwargs.get("save_inputs", False) 

315 self.save_results = kwargs.get("save_results", False) 

316 self.save_plots = kwargs.get("save_plots", False) 

317 self.show_plots = kwargs.get("show_plots", False) 

318 

319 # Import case 

320 self.branch_data: Optional[pd.DataFrame] = None 

321 self.bus_data: Optional[pd.DataFrame] = None 

322 self.gen_data: Optional[pd.DataFrame] = None 

323 self.cap_data: Optional[pd.DataFrame] = None 

324 self.reg_data: Optional[pd.DataFrame] = None 

325 if self.data_path is not None: 

326 self.data_path = _handle_path_input(Path(self.data_path)) 

327 case_data = _get_data_from_path(self.data_path) 

328 self.branch_data: pd.DataFrame = case_data["branch_data"] 

329 self.bus_data: pd.DataFrame = case_data["bus_data"] 

330 self.gen_data: pd.DataFrame = case_data["gen_data"] 

331 self.cap_data: pd.DataFrame = case_data["cap_data"] 

332 self.reg_data: pd.DataFrame = case_data["reg_data"] 

333 if kwargs.get("branch_data") is not None: 

334 self.branch_data = handle_branch_input(kwargs.get("branch_data")) 

335 if kwargs.get("bus_data") is not None: 

336 self.bus_data = handle_bus_input(kwargs.get("bus_data")) 

337 if kwargs.get("gen_data") is not None: 

338 self.gen_data = handle_gen_input(kwargs.get("gen_data")) 

339 if kwargs.get("cap_data") is not None: 

340 self.cap_data = handle_cap_input(kwargs.get("cap_data")) 

341 if kwargs.get("reg_data") is not None: 

342 self.reg_data = handle_reg_input(kwargs.get("reg_data")) 

343 if self.branch_data is None or self.bus_data is None: 

344 raise ValueError( 

345 "At least one of branch_data or bus_data was not found. " 

346 "Either provide them as CSV files found at the location " 

347 "specified in data_path or pass them in directly." 

348 ) 

349 

350 # Modify case 

351 if self.gen_mult is not None and self.gen_data is not None: 

352 self.gen_data.loc[:, ["pa", "pb", "pc"]] *= self.gen_mult 

353 self.gen_data.loc[:, ["qa", "qb", "qc"]] *= self.gen_mult 

354 self.gen_data.loc[:, ["sa_max", "sb_max", "sc_max"]] *= self.gen_mult 

355 if self.control_variable is not None and self.gen_data is not None: 

356 if self.control_variable == "": 

357 self.gen_data.control_variable = "P" 

358 if self.control_variable.upper() == "P": 

359 self.gen_data.control_variable = "P" 

360 if self.control_variable.upper() == "Q": 

361 self.gen_data.control_variable = "Q" 

362 if self.control_variable.upper() == "PQ": 

363 self.gen_data.control_variable = "PQ" 

364 if self.load_mult is not None: 

365 self.bus_data.loc[:, ["pl_a", "ql_a", "pl_b", "ql_b", "pl_c", "ql_c"]] *= ( 

366 self.load_mult 

367 ) 

368 if self.v_swing is not None: 

369 self.bus_data.loc[ 

370 self.bus_data.bus_type == "SWING", ["v_a", "v_b", "v_c"] 

371 ] = self.v_swing 

372 if self.v_min is not None: 

373 self.bus_data.loc[:, "v_min"] = self.v_min 

374 if self.v_max is not None: 

375 self.bus_data.loc[:, "v_max"] = self.v_max 

376 if self.cvr_p is not None: 

377 self.bus_data.loc[:, "cvr_p"] = self.cvr_p 

378 if self.cvr_q is not None: 

379 self.bus_data.loc[:, "cvr_q"] = self.cvr_q 

380 self.model = None 

381 self.results = None 

382 self.voltages_df = None 

383 self.power_flows_df = None 

384 self.decision_variables_df = None 

385 self.p_gens = None 

386 self.q_gens = None 

387 

388 def run_pf(self, raw_result=False): 

389 """ 

390 Run the unconstrained power flow, save and plot the results. 

391 Returns 

392 ------- 

393 voltages_df: pd.DataFrame 

394 power_flows_df: pd.DataFrame 

395 """ 

396 bus_data = self.bus_data.copy() 

397 bus_data.loc[:, "v_min"] = 0.0 

398 bus_data.loc[:, "v_max"] = 2.0 

399 if self.gen_data is not None: 

400 gen_data = self.gen_data.copy() 

401 gen_data.control_variable = "" 

402 else: 

403 gen_data = None 

404 # Create model 

405 self.model = create_model( 

406 "", 

407 branch_data=self.branch_data, 

408 bus_data=bus_data, 

409 gen_data=gen_data, 

410 cap_data=self.cap_data, 

411 reg_data=self.reg_data, 

412 ) 

413 # Solve 

414 result = auto_solve(self.model) 

415 if raw_result: 

416 return result 

417 

418 self.voltages_df = self.model.get_voltages(result.x) 

419 self.power_flows_df = self.model.get_apparent_power_flows(result.x) 

420 self.p_gens = self.model.get_p_gens(result.x) 

421 self.q_gens = self.model.get_q_gens(result.x) 

422 

423 if self.save_inputs: 

424 self.save_input_data() 

425 if self.save_results: 

426 self.save_result_data() 

427 if self.save_plots or self.show_plots: 

428 self.make_plots() 

429 return self.voltages_df, self.power_flows_df 

430 

431 def run( 

432 self, 

433 objective_function=None, 

434 control_regulators=False, 

435 control_capacitors=False, 

436 raw_result=False, 

437 **kwargs, 

438 ): 

439 """ 

440 Run the optimization, save and plot the results. 

441 Returns 

442 ------- 

443 voltages_df: pd.DataFrame 

444 power_flows_df: pd.DataFrame 

445 decision_variables_df: pd.DataFrame 

446 """ 

447 

448 # Create model 

449 self.model = create_model( 

450 control_variable=self.control_variable, 

451 control_regulators=control_regulators, 

452 control_capacitors=control_capacitors, 

453 branch_data=self.branch_data, 

454 bus_data=self.bus_data, 

455 gen_data=self.gen_data, 

456 cap_data=self.cap_data, 

457 reg_data=self.reg_data, 

458 ) 

459 if objective_function is not None: 

460 self.objective_function = objective_function 

461 # Solve 

462 result = auto_solve(self.model, self.objective_function, **kwargs) 

463 self.results = result 

464 self.voltages_df = self.model.get_voltages(result.x) 

465 self.power_flows_df = self.model.get_apparent_power_flows(result.x) 

466 self.p_gens = self.model.get_p_gens(result.x) 

467 self.q_gens = self.model.get_q_gens(result.x) 

468 

469 if raw_result: 

470 return result 

471 

472 if self.save_inputs: 

473 self.save_input_data() 

474 if self.save_results: 

475 self.save_result_data() 

476 if self.save_plots or self.show_plots: 

477 self.make_plots() 

478 return self.voltages_df, self.power_flows_df, self.p_gens, self.q_gens 

479 

480 def save_result_data(self): 

481 if not self.output_dir.exists(): 

482 self.output_dir.mkdir() 

483 self.voltages_df.to_csv( 

484 Path(self.output_dir) / "node_voltages.csv", index=False 

485 ) 

486 self.power_flows_df.to_csv( 

487 Path(self.output_dir) / "power_flows.csv", index=False 

488 ) 

489 self.p_gens.to_csv(Path(self.output_dir) / "p_gens.csv", index=False) 

490 self.q_gens.to_csv(Path(self.output_dir) / "q_gens.csv", index=False) 

491 

492 def save_input_data(self): 

493 config_parameters = { 

494 "model_type": type(self.model), 

495 "objective_function": str(self.objective_function), 

496 "control_variable": self.control_variable, 

497 } 

498 if not self.output_dir.exists(): 

499 self.output_dir.mkdir() 

500 case_data_dir = Path(self.output_dir) / "case_data" 

501 if not case_data_dir.exists(): 

502 case_data_dir.mkdir() 

503 with open(Path(self.output_dir) / "case_data" / "config.json", "w") as f: 

504 json.dump(config_parameters, f, ensure_ascii=False, indent=4) 

505 self.branch_data.to_csv( 

506 Path(self.output_dir) / "case_data" / "branch_data.csv", index=False 

507 ) 

508 self.bus_data.to_csv( 

509 Path(self.output_dir) / "case_data" / "bus_data.csv", index=False 

510 ) 

511 if self.gen_data is not None: 

512 self.gen_data.to_csv( 

513 Path(self.output_dir) / "case_data" / "gen_data.csv", index=False 

514 ) 

515 if self.cap_data is not None: 

516 self.cap_data.to_csv( 

517 Path(self.output_dir) / "case_data" / "cap_data.csv", index=False 

518 ) 

519 if self.reg_data is not None: 

520 self.reg_data.to_csv( 

521 Path(self.output_dir) / "case_data" / "reg_data.csv", index=False 

522 ) 

523 

524 def make_plots(self): 

525 fig1 = plot_network( 

526 self.model, self.voltages_df, self.power_flows_df, show_reactive_power=False 

527 ) 

528 fig2 = plot_power_flows(self.power_flows_df) 

529 fig3 = plot_voltages(self.voltages_df) 

530 fig4 = plot_gens(self.p_gens, self.q_gens) 

531 fig1.show() 

532 fig2.show() 

533 fig3.show() 

534 fig4.show() 

535 if self.save_plots: 

536 fig1.write_html(self.output_dir / "network_plot.html") 

537 fig2.write_html(self.output_dir / "power_flow_plot.html") 

538 fig3.write_html(self.output_dir / "voltage_plot.html") 

539 fig4.write_html(self.output_dir / "gens.html") 

540 

541 def plot_network( 

542 self, 

543 v_min: float = 0.95, 

544 v_max: float = 1.05, 

545 show_phases: str = "abc", 

546 show_reactive_power: bool = False, 

547 ): 

548 """ 

549 Plot the distribution network showing voltage and power results. 

550 Parameters 

551 ---------- 

552 v_min : (default=0.95) Used for scaling node colors. 

553 v_max : (default=1.05) Used for scaling node colors. 

554 show_phases : (default="abc") valid options: "a", "b", "c", or "abc" 

555 show_reactive_power : (default=False) If True, show reactive power flows instead of active power flows. 

556 

557 Returns 

558 ------- 

559 fig: plotly.graph_objects.Figure 

560 """ 

561 return plot_network( 

562 self.model, 

563 v=self.voltages_df, 

564 s=self.power_flows_df, 

565 p_gen=self.p_gens, 

566 q_gen=self.q_gens, 

567 v_min=v_min, 

568 v_max=v_max, 

569 show_phases=show_phases, 

570 show_reactive_power=show_reactive_power, 

571 ) 

572 

573 def plot_power_flows(self): 

574 """ 

575 Plot the power flows 

576 Returns 

577 ------- 

578 fig: plotly.graph_objects.Figure 

579 """ 

580 return plot_power_flows(self.power_flows_df) 

581 

582 def plot_voltages(self): 

583 """ 

584 Plot the bus voltages 

585 Returns 

586 ------- 

587 fig: plotly.graph_objects.Figure 

588 """ 

589 return plot_voltages(self.voltages_df) 

590 

591 def plot_decision_variables(self): 

592 """ 

593 Plot the decision variables 

594 Returns 

595 ------- 

596 fig: plotly.graph_objects.Figure 

597 """ 

598 return plot_gens(self.p_gens, self.q_gens) 

599 

600 # def delete_generator(self, node_name: str) -> None: 

601 # gen = self.gen_data.copy() 

602 def add_generator( 

603 self, 

604 name: str, 

605 phases: Optional[str] = None, 

606 p=0, 

607 q=0, 

608 s_rated=None, 

609 q_max=None, 

610 q_min=None, 

611 ): 

612 gen = self.gen_data.copy() 

613 i = gen.shape[0] 

614 _ids = self.bus_data.loc[self.bus_data.name == name, "id"].to_numpy() 

615 if len(_ids) == 0: 

616 raise ValueError(f"Bus {name} (type: {type(name)}) not found in bus_data.") 

617 _id = _ids[0] 

618 if _id in gen.loc[:, "id"].to_numpy(): 

619 i = self.gen_data.loc[self.gen_data.id == _id, "id"].index[0] 

620 gen.at[i, "name"] = name 

621 gen.at[i, "id"] = _id 

622 bus_phases = self.bus_data.loc[self.bus_data.name == "13", "phases"].to_numpy()[ 

623 0 

624 ] 

625 if phases is None: 

626 phases = bus_phases 

627 if s_rated is None: 

628 s_rated = (p**2 + q**2) ** (1 / 2) * 1.2 

629 n_phases = len(phases) 

630 p_phase = round(p / n_phases, 9) 

631 q_phase = round(q / n_phases, 9) 

632 s_rated_phase = round(s_rated / n_phases, 9) 

633 gen.loc[i, "phases"] = phases 

634 gen.loc[i, [f"s{ph}_max" for ph in phases]] = s_rated_phase # unlimited 

635 gen.loc[i, [f"p{ph}" for ph in phases]] = p_phase # unlimited 

636 gen.loc[i, [f"q{ph}" for ph in phases]] = q_phase # unlimited 

637 if q_max is None: 

638 q_max = s_rated 

639 if q_min is None: 

640 q_min = -s_rated 

641 gen.loc[i, ["qa_max", "qb_max", "qc_max"]] = q_max # unlimited 

642 gen.loc[i, ["qa_min", "qb_min", "qc_min"]] = q_min # unlimited 

643 

644 gen.loc[:, ["pa", "pb", "pc", "qa", "qb", "qc"]] = ( 

645 gen.loc[:, ["pa", "pb", "pc", "qa", "qb", "qc"]].astype(float).fillna(0.0) 

646 ) 

647 gen.loc[:, [f"s{a}_max" for a in "abc"]] = ( 

648 gen.loc[:, [f"s{a}_max" for a in "abc"]].astype(float).fillna(0.0) 

649 ) 

650 self.gen_data = gen 

651 

652 def add_capacitor( 

653 self, 

654 name: any, 

655 phases: Optional[str] = None, 

656 q=0, 

657 ): 

658 cap = self.cap_data.copy() 

659 i = cap.shape[0] 

660 _ids = self.bus_data.loc[self.bus_data.name == name, "id"].to_numpy() 

661 if len(_ids) == 0: 

662 raise ValueError(f"Bus {name} (type: {type(name)}) not found in bus_data.") 

663 _id = _ids[0] 

664 if _id in cap.loc[:, "id"].to_numpy(): 

665 i = self.cap_data.loc[self.cap_data.id == _id, "id"].index[0] 

666 print(cap.name.dtype) 

667 cap.at[i, "name"] = name 

668 cap.at[i, "id"] = _id 

669 bus_phases = self.bus_data.loc[self.bus_data.name == "13", "phases"].to_numpy()[ 

670 0 

671 ] 

672 if phases is None: 

673 phases = bus_phases 

674 n_phases = len(phases) 

675 q_phase = round(q / n_phases, 9) 

676 cap.loc[i, "phases"] = phases 

677 cap.loc[i, [f"q{ph}" for ph in phases]] = q_phase # unlimited 

678 cap.loc[i, [f"q{ph}" for ph in phases]] = ( 

679 cap.loc[i, [f"q{ph}" for ph in phases]].astype(float).fillna(0.0) 

680 ) 

681 self.cap_data = cap 

682 

683 

684if __name__ == "__main__": 

685 test_config = { 

686 "data_path": "ieee123_dss/Run_IEEE123Bus.DSS", 

687 "output_dir": "output", 

688 "control_variable": "Q", 

689 "v_max": 1.05, 

690 "v_min": 0.95, 

691 # etc... 

692 "objective_function": "loss_min", 

693 "solver": "CLARABEL", 

694 "show_plots": True, 

695 } 

696 # run_from_dict(config) 

697 case = DistOPFCase( 

698 config=test_config, 

699 ) 

700 case.run()