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
« 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"""
5from typing import Optional
6from collections.abc import Callable
7from pathlib import Path
8import json
9import pandas as pd
10import numpy as np
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)
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 """
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 )
92def auto_solve(model: LinDistBase, objective_function=None, **kwargs):
93 """
94 Solve with selected objective function and model. Automatically chooses the appropriate function.
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.
109 Returns
110 -------
111 result: scipy.optimize.OptimizeResult
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
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
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
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 }
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 """
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
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")
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")
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)
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 )
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
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
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)
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
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 """
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)
469 if raw_result:
470 return result
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
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)
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 )
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")
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.
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 )
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)
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)
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)
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
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
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
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()