Skip to content

Commit

Permalink
adding support for more than 2 busbars per substation
Browse files Browse the repository at this point in the history
  • Loading branch information
BDonnot committed Mar 7, 2024
1 parent b5646dd commit 3ed4bad
Show file tree
Hide file tree
Showing 8 changed files with 463 additions and 66 deletions.
5 changes: 4 additions & 1 deletion CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ Change Log
[0.7.6] 2023-xx-yy
--------------------
- [BREAKING] now able to retrieve `dcSbus` with a dedicated method (and not with the old `get_Sbus`).
If you previously used `gridmodel.get_Subus()` to retrieve the Sbus used for DC powerflow, please use
If you previously used `gridmodel.get_Sbus()` to retrieve the Sbus used for DC powerflow, please use
`gridmodel.get_dcSbus()` instead.
- [DEPRECATED] in the cpp class: the old `SecurityAnalysisCPP` has been renamed `ContingencyAnalysisCPP`
(you should not import it, but it you do you can `from lightsim2grid.securityAnalysis import ContingencyAnalysisCPP` now)
Expand All @@ -45,6 +45,9 @@ Change Log
- [ADDED] embed in the generator models the "non pv" behaviour. (TODO need to be able to change Q from python side)
- [ADDED] computation of PTPF (Power Transfer Distribution Factor) is now possible
- [ADDED] (not tested) support for more than 2 busbars per substation
- [ADDED] a timer to get the time spent in the gridmodel for the powerflow (env.backend.timer_gridmodel_xx_pf)
which also include the time
- [ADDED] support for more than 2 busbars per substation (requires grid2op >= 1.10.0)
- [IMPROVED] now performing the new grid2op `create_test_suite`
- [IMPROVED] now lightsim2grid properly throw `BackendError`
- [IMPROVED] clean ce cpp side by refactoring: making clearer the difference (linear) solver
Expand Down
51 changes: 45 additions & 6 deletions benchmarks/benchmark_grid_size.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,12 @@
import matplotlib.pyplot as plt
from grid2op import make, Parameters
from grid2op.Chronics import FromNPY
from lightsim2grid import LightSimBackend, TimeSerie, SecurityAnalysis
from lightsim2grid import LightSimBackend, TimeSerie
try:
from lightsim2grid import ContingencyAnalysis
except ImportError:
from lightsim2grid import SecurityAnalysis as ContingencyAnalysis

from tqdm import tqdm
import os
from utils_benchmark import print_configuration, get_env_name_displayed
Expand Down Expand Up @@ -157,6 +162,8 @@ def get_loads_gens(load_p_init, load_q_init, gen_p_init, sgen_p_init):
g2op_speeds = []
g2op_sizes = []
g2op_step_time = []
ls_solver_time = []
ls_gridmodel_time = []

ts_times = []
ts_speeds = []
Expand Down Expand Up @@ -227,13 +234,18 @@ def get_loads_gens(load_p_init, load_q_init, gen_p_init, sgen_p_init):
g2op_times.append(None)
g2op_speeds.append(None)
g2op_step_time.append(None)
ls_solver_time.append(None)
ls_gridmodel_time.append(None)
g2op_sizes.append(env_lightsim.n_sub)
else:
total_time = env_lightsim.backend._timer_preproc + env_lightsim.backend._timer_solver # + env_lightsim.backend._timer_postproc
total_time = env_lightsim._time_step
g2op_times.append(total_time)
g2op_speeds.append(1.0 * nb_step / total_time)
g2op_step_time.append(1.0 * env_lightsim._time_step / nb_step)
g2op_sizes.append(env_lightsim.n_sub)
ls_solver_time.append(env_lightsim.backend.comp_time)
ls_gridmodel_time.append(env_lightsim.backend.timer_gridmodel_xx_pf)
g2op_sizes.append(env_lightsim.n_sub)

# Perform the computation using TimeSerie
env_lightsim.reset()
Expand Down Expand Up @@ -274,7 +286,7 @@ def get_loads_gens(load_p_init, load_q_init, gen_p_init, sgen_p_init):

# Perform a securtiy analysis (up to 1000 contingencies)
env_lightsim.reset()
sa = SecurityAnalysis(env_lightsim)
sa = ContingencyAnalysis(env_lightsim)
for i in range(env_lightsim.n_line):
sa.add_single_contingency(i)
if i >= 1000:
Expand All @@ -297,11 +309,24 @@ def get_loads_gens(load_p_init, load_q_init, gen_p_init, sgen_p_init):
print("Results using grid2op.steps (288 consecutive steps, only measuring 'dc pf [init] + ac pf')")
tab_g2op = []
for i, nm_ in enumerate(case_names_displayed):
tab_g2op.append((nm_, ts_sizes[i], 1000. / g2op_speeds[i] if g2op_speeds[i] else None, g2op_speeds[i],
1000. * g2op_step_time[i] if g2op_step_time[i] else None))
tab_g2op.append((nm_,
ts_sizes[i],
1000. / g2op_speeds[i] if g2op_speeds[i] else None,
g2op_speeds[i],
1000. * g2op_step_time[i] if g2op_step_time[i] else None,
1000. * ls_gridmodel_time[i] / nb_step if ls_gridmodel_time[i] else None,
1000. * ls_solver_time[i] / nb_step if ls_solver_time[i] else None,
))
if TABULATE_AVAIL:
res_use_with_grid2op_2 = tabulate(tab_g2op,
headers=["grid", "size (nb bus)", "time (ms / pf)", "speed (pf / s)", "avg step duration (ms)"],
headers=["grid",
"size (nb bus)",
"step time (ms / pf)",
"speed (pf / s)",
"avg step duration (ms)",
"time in 'gridmodel' (ms / pf)",
"time in 'pf algo' (ms / pf)",
],
tablefmt="rst")
print(res_use_with_grid2op_2)
else:
Expand Down Expand Up @@ -349,6 +374,20 @@ def get_loads_gens(load_p_init, load_q_init, gen_p_init, sgen_p_init):
plt.title(f"Computation speed using Grid2Op.step (dc pf [init] + ac pf)")
plt.yscale("log")
plt.show()

plt.plot(g2op_sizes, ls_solver_time, linestyle='solid', marker='+', markersize=8)
plt.xlabel("Size (number of substation)")
plt.ylabel("Speed (solver time)")
plt.title(f"Computation speed for solving the powerflow only")
plt.yscale("log")
plt.show()

plt.plot(g2op_sizes, ls_gridmodel_time, linestyle='solid', marker='+', markersize=8)
plt.xlabel("Size (number of substation)")
plt.ylabel("Speed (solver time)")
plt.title(f"Computation speed for solving the powerflow only")
plt.yscale("log")
plt.show()

# make the plot summarizing all results
plt.plot(ts_sizes, ts_times, linestyle='solid', marker='+', markersize=8)
Expand Down
134 changes: 82 additions & 52 deletions lightsim2grid/lightSimBackend.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,17 @@ def __init__(self,

# available solver in lightsim
self.available_solvers = []
self.comp_time = 0. # computation time of just the powerflow

# computation time of just the powerflow (when the grid is formatted
# by the gridmodel already)
# it takes only into account the time spend in the powerflow algorithm
self.comp_time = 0.

# computation time of the powerflow
# it takes into account everything in the gridmodel, including the mapping
# to the solver, building of Ybus and Sbus AND the time to solve the powerflow
self.timer_gridmodel_xx_pf = 0.

self._timer_postproc = 0.
self._timer_preproc = 0.
self._timer_solver = 0.
Expand All @@ -195,22 +205,6 @@ def __init__(self,

# add the static gen to the list of controlable gen in grid2Op
self._use_static_gen = use_static_gen # TODO implement it

# storage data for this object (otherwise it's in the class)
# self.n_storage = None
# self.storage_to_subid = None
# self.storage_pu_to_kv = None
# self.name_storage = None
# self.storage_to_sub_pos = None
# self.storage_type = None
# self.storage_Emin = None
# self.storage_Emax = None
# self.storage_max_p_prod = None
# self.storage_max_p_absorb = None
# self.storage_marginal_cost = None
# self.storage_loss = None
# self.storage_discharging_efficiency = None
# self.storage_charging_efficiency = None

def turnedoff_no_pv(self):
self._turned_off_pv = False
Expand Down Expand Up @@ -411,13 +405,53 @@ def _assign_right_solver(self):
self._grid.change_solver(SolverType.SparseLU)

def load_grid(self, path=None, filename=None):
if hasattr(type(self), "can_handle_more_than_2_busbar"):
# grid2op version >= 1.10.0 then we use this
self.can_handle_more_than_2_busbar()

if self._loader_method == "pandapower":
self._load_grid_pandapower(path, filename)
elif self._loader_method == "pypowsybl":
self._load_grid_pypowsybl(path, filename)
else:
raise BackendError(f"Impossible to initialize the backend with '{self._loader_method}'")

def _should_not_have_to_do_this(self, path=None, filename=None):
# included in grid2op now !
# but before `make_complete_path` was introduced we need to still
# be able to use lightsim2grid
import os
from grid2op.Exceptions import Grid2OpException
if path is None and filename is None:
raise Grid2OpException(
"You must provide at least one of path or file to load a powergrid."
)
if path is None:
full_path = filename
elif filename is None:
full_path = path
else:
full_path = os.path.join(path, filename)
if not os.path.exists(full_path):
raise Grid2OpException('There is no powergrid at "{}"'.format(full_path))
return full_path

def _aux_pypowsybl_init_substations(self, loader_kwargs):
# now handle the legacy "make as if there are 2 busbars per substation"
# as it is done with grid2Op simulated environment
if (("double_bus_per_sub" in loader_kwargs and loader_kwargs["double_bus_per_sub"]) or
("n_busbar_per_sub" in loader_kwargs and loader_kwargs["n_busbar_per_sub"])):
bus_init = self._grid.get_bus_vn_kv()
orig_to_ls = np.array(self._grid._orig_to_ls)
bus_doubled = np.concatenate([bus_init for _ in range(self.n_busbar_per_sub)])
self._grid.init_bus(bus_doubled, 0, 0)
for i in range(self.__nb_bus_before):
self._grid.deactivate_bus(i + self.__nb_bus_before)
new_orig_to_ls = np.concatenate([orig_to_ls + i * self.__nb_bus_before
for i in range(self.n_busbar_per_sub)]
)
self._grid._orig_to_ls = new_orig_to_ls

def _load_grid_pypowsybl(self, path=None, filename=None):
from lightsim2grid.gridmodel.from_pypowsybl import init as init_pypow
import pypowsybl.network as pypow_net
Expand All @@ -429,28 +463,15 @@ def _load_grid_pypowsybl(self, path=None, filename=None):
full_path = self.make_complete_path(path, filename)
except AttributeError as exc_:
warnings.warn("Please upgrade your grid2op version")
import os
from grid2op.Exceptions import Grid2OpException
def make_complete_path(path, filename):
if path is None and filename is None:
raise Grid2OpException(
"You must provide at least one of path or file to load a powergrid."
)
if path is None:
full_path = filename
elif filename is None:
full_path = path
else:
full_path = os.path.join(path, filename)
if not os.path.exists(full_path):
raise Grid2OpException('There is no powergrid at "{}"'.format(full_path))
return full_path
full_path = make_complete_path(path, filename)
full_path = self._should_not_have_to_do_this(path, filename)

grid_tmp = pypow_net.load(full_path)
gen_slack_id = None
if "gen_slack_id" in loader_kwargs:
gen_slack_id = int(loader_kwargs["gen_slack_id"])
self._grid = init_pypow(grid_tmp, gen_slack_id=gen_slack_id, sort_index=True)
self.__nb_bus_before = len(self._grid.get_bus_vn_kv())
self._aux_pypowsybl_init_substations(loader_kwargs)
self._aux_setup_right_after_grid_init()

# mandatory for the backend
Expand Down Expand Up @@ -482,7 +503,11 @@ def make_complete_path(path, filename):
self.shunt_to_subid = np.array([el.bus_id for el in self._grid.get_shunts()], dtype=dt_int)
else:
# TODO get back the sub id from the grid_tmp.get_substations()
raise NotImplementedError("TODO")
raise NotImplementedError("Today the only supported behaviour is to consider the 'buses' of the powsybl grid "
"are the 'substation' of this backend. "
"This will change in the future, but in the meantime please add "
"'use_buses_for_sub' = True in the `loader_kwargs` when loading "
"a lightsim2grid grid.")

# the names
self.name_load = np.array([f"load_{el.bus_id}_{id_obj}" for id_obj, el in enumerate(self._grid.get_loads())])
Expand All @@ -496,25 +521,10 @@ def make_complete_path(path, filename):
self._compute_pos_big_topo()

self.__nb_powerline = len(self._grid.get_lines())
self.__nb_bus_before = len(self._grid.get_bus_vn_kv())

# init this
self.prod_p = np.array([el.target_p_mw for el in self._grid.get_generators()], dtype=dt_float)
self.next_prod_p = np.array([el.target_p_mw for el in self._grid.get_generators()], dtype=dt_float)

# now handle the legacy "make as if there are 2 busbars per substation"
# as it is done with grid2Op simulated environment
if "double_bus_per_sub" in loader_kwargs and loader_kwargs["double_bus_per_sub"]:
bus_init = self._grid.get_bus_vn_kv()
orig_to_ls = np.array(self._grid._orig_to_ls)
bus_doubled = np.concatenate((bus_init, bus_init))
self._grid.init_bus(bus_doubled, 0, 0)
for i in range(self.__nb_bus_before):
self._grid.deactivate_bus(i + self.__nb_bus_before)
new_orig_to_ls = np.concatenate((orig_to_ls,
orig_to_ls + self.__nb_bus_before)
)
self._grid._orig_to_ls = new_orig_to_ls
self.nb_bus_total = len(self._grid.get_bus_vn_kv())

# and now things needed by the backend (legacy)
Expand All @@ -531,6 +541,7 @@ def make_complete_path(path, filename):
self._aux_finish_setup_after_reading()

def _aux_setup_right_after_grid_init(self):
self._grid.set_n_sub(self.__nb_bus_before)
self._handle_turnedoff_pv()

self.available_solvers = self._grid.available_solvers()
Expand All @@ -549,12 +560,19 @@ def _aux_setup_right_after_grid_init(self):
# check that the solver type provided is installed with lightsim2grid
self._check_suitable_solver_type(self.__current_solver_type)
self._grid.change_solver(self.__current_solver_type)

# handle multiple busbar per substations
if hasattr(type(self), "can_handle_more_than_2_busbar"):
# grid2op version >= 1.10.0 then we use this
self._grid._max_nb_bus_per_sub = self.n_busbar_per_sub

def _load_grid_pandapower(self, path=None, filename=None):
type(self.init_pp_backend).n_busbar_per_sub = self.n_busbar_per_sub
self.init_pp_backend.load_grid(path, filename)
self.can_output_theta = True # i can compute the "theta" and output it to grid2op

self._grid = init(self.init_pp_backend._grid)
self._grid = init(self.init_pp_backend._grid)
self.__nb_bus_before = self.init_pp_backend.get_nb_active_bus()
self._aux_setup_right_after_grid_init()

self.n_line = self.init_pp_backend.n_line
Expand Down Expand Up @@ -653,7 +671,6 @@ def _aux_finish_setup_after_reading(self):
# set up the "lightsim grid" accordingly
cls = type(self)

self._grid.set_n_sub(self.__nb_bus_before)
self._grid.set_load_pos_topo_vect(cls.load_pos_topo_vect)
self._grid.set_gen_pos_topo_vect(cls.gen_pos_topo_vect)
self._grid.set_line_or_pos_topo_vect(cls.line_or_pos_topo_vect[:self.__nb_powerline])
Expand Down Expand Up @@ -913,8 +930,18 @@ def runpf(self, is_dc=False):
beg_postroc = time.perf_counter()
if is_dc:
self.comp_time += self._grid.get_dc_computation_time()
self.timer_gridmodel_xx_pf += self._grid.timer_last_dc_pf
else:
self.comp_time += self._grid.get_computation_time()
# NB get_computation_time returns "time_total_nr", which is
# defined in the powerflow algorithm and not on the linear solver.
# it takes into account everything needed to solve the powerflow
# once everything is passed to the solver.
# It does not take into account the time to format the data in the
# from the GridModel

self.timer_gridmodel_xx_pf += self._grid.timer_last_ac_pf
# timer_gridmodel_xx_pf takes all the time within the gridmodel "ac_pf"

self.V[:] = V
(self.p_or[:self.__nb_powerline],
Expand Down Expand Up @@ -1046,6 +1073,8 @@ def copy(self):
####################
# res = copy.deepcopy(self) # super slow
res = type(self).__new__(type(self))
res.comp_time = self.comp_time
res.timer_gridmodel_xx_pf = self.timer_gridmodel_xx_pf

# copy the regular attribute
res.__has_storage = self.__has_storage
Expand Down Expand Up @@ -1197,6 +1226,7 @@ def reset(self, grid_path, grid_filename=None):
self._handle_turnedoff_pv()
self.topo_vect[:] = self.__init_topo_vect
self.comp_time = 0.
self.timer_gridmodel_xx_pf = 0.
self._timer_postproc = 0.
self._timer_preproc = 0.
self._timer_solver = 0.
Loading

0 comments on commit 3ed4bad

Please sign in to comment.