Skip to content

Commit 68a9601

Browse files
committed
add the possibility to initialize the contingency analysis from the results of the N powerflow
Signed-off-by: DONNOT Benjamin <[email protected]>
1 parent 1c73407 commit 68a9601

File tree

11 files changed

+207
-23
lines changed

11 files changed

+207
-23
lines changed

CHANGELOG.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,16 @@ TODO: integration test with pandapower (see `pandapower/contingency/contingency.
3737
- [FIXED] a performance issue for all "XXXSingleSlack" (*eg* KLUSingleSlack) algorithm (filling of the initial
3838
Jacobian matrix was extremly slow due to the massive 'insert' of data in the eigen sparse matrix instead
3939
of relying on the "setFromTriplets" method)
40+
- [FIXED] an normal "exception" was not catched in the `close()` method of LightSimBackend in case the
41+
backend was closed before any grid was loaded.
4042
- [ADDED] possibility to pickle independantly all part of the grid (*eg* gridmodel.get_lines()
4143
can be pickled independantly from anything else) **NB** pickling and un-pickling
4244
lightsim2grid objects can only be used for the same lightsim2grid version.
45+
- [ADDED] the `init_from_n_powerflow` property for ContingencyAnalysis (and
46+
ContingencyAnalysisCPP). It allows to chose if the computation of the contingencies
47+
are initialized with the complex voltages resulting of the powerflow in N
48+
(`init_from_n_powerflow=True`) or if they are initialized from the
49+
given input vector (`init_from_n_powerflow=False`). Defaults to `False`
4350

4451
[0.12.0] 2026-01-06
4552
--------------------

benchmarks/benchmark_grid_size.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
except ImportError:
2323
from lightsim2grid import SecurityAnalysis as ContingencyAnalysis
2424

25+
from lightsim2grid.solver import SolverType
26+
2527
from tqdm import tqdm
2628
import os
2729
from utils_benchmark import print_configuration, get_env_name_displayed
@@ -46,7 +48,7 @@
4648
"case300.json",
4749
"case1354pegase.json",
4850
"case1888rte.json",
49-
# "GBnetwork.json", # 2224 buses
51+
# "GBnetwork.json", # 2224 buses
5052
"case2848rte.json",
5153
"case2869pegase.json",
5254
"case3120sp.json",
@@ -235,6 +237,7 @@ def run_grid2op_env(env_lightsim, case, reset_solver,
235237

236238
if __name__ == "__main__":
237239
prng = np.random.default_rng(42)
240+
ls_solver_type = SolverType.KLU
238241
case_names_displayed = [get_env_name_displayed(el) for el in case_names]
239242
solver_preproc_solver_time = []
240243
g2op_speeds = []
@@ -324,6 +327,7 @@ def run_grid2op_env(env_lightsim, case, reset_solver,
324327
load_q,
325328
gen_p_g2op,
326329
sgen_p)
330+
env_lightsim.backend.set_solver_type(ls_solver_type)
327331
# Perform the computation using grid2op
328332
reset_solver = True # non default
329333
nb_step_reset = run_grid2op_env(env_lightsim, case, reset_solver,
@@ -360,7 +364,7 @@ def run_grid2op_env(env_lightsim, case, reset_solver,
360364
env_lightsim.backend.tol)
361365
time_serie._TimeSerie__computed = True
362366
a_or = time_serie.compute_A()
363-
assert status or computer.nb_solver() == nb_step_pp, f"some powerflow diverge for Time Series for {case_name}: {computer.nb_solved()} "
367+
assert status or computer.nb_solved() == nb_step_pp, f"some powerflow diverge for Time Series for {case_name}: {computer.nb_solved()} "
364368

365369
if VERBOSE:
366370
# print detailed results if needed
@@ -389,6 +393,7 @@ def run_grid2op_env(env_lightsim, case, reset_solver,
389393
sa.add_single_contingency(i)
390394
if i >= 1000:
391395
break
396+
sa.init_from_n_powerflow = True
392397
p_or, a_or, voltages = sa.get_flows()
393398
computer_sa = sa.computer
394399
sa_times.append(computer_sa.total_time() + computer_sa.amps_computation_time())

benchmarks/launch_all_benchmark.sh

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
echo "Solver"
2+
python benchmark_solvers.py --env_name l2rpn_case14_sandbox --no_test --number 1000
3+
sleep 1
4+
echo "\n"
5+
python benchmark_solvers.py --env_name l2rpn_neurips_2020_track2_small --no_test --number 1000
6+
sleep 5
7+
echo "\n\n DC solver"
8+
python benchmark_dc_solvers.py --env_name l2rpn_case14_sandbox --no_test --number 8000
9+
echo "\n"
10+
sleep 1
11+
python benchmark_dc_solvers.py --env_name l2rpn_neurips_2020_track2_small --no_test --number 8000
12+
echo "\n\n grid size"
13+
sleep 5
14+
python benchmark_grid_size.py
15+
echo "\n\n Vs pypowsybl"
16+
sleep 5
17+
python compare_lightsim2grid_pypowsybl.py --case ieee9
18+
python compare_lightsim2grid_pypowsybl.py --case ieee14
19+
python compare_lightsim2grid_pypowsybl.py --case ieee30
20+
python compare_lightsim2grid_pypowsybl.py --case ieee57
21+
python compare_lightsim2grid_pypowsybl.py --case ieee118
22+
python compare_lightsim2grid_pypowsybl.py --case ieee300

benchmarks/test_profile.py

Lines changed: 124 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,140 @@
1+
#!/home/donnotben/Documents/lightsim2grid/venv_ls/bin/python3
2+
13
#!/bin/python3
4+
import pickle
5+
import time
6+
7+
from kiwisolver import Solver
28
import grid2op
39
from lightsim2grid import LightSimBackend
10+
from lightsim2grid.solver import SolverType
411
import warnings
12+
import pandapower as pp
13+
import numpy as np
514

615
# usage:
716
# perf record ./test_profile.py
817
# perf report
9-
env_name = "l2rpn_neurips_2020_track2_small"
10-
with warnings.catch_warnings():
11-
warnings.filterwarnings("ignore")
12-
env_tmp = grid2op.make(env_name)
18+
# env_name = "l2rpn_neurips_2020_track2_small"
19+
20+
from benchmark_grid_size import (
21+
get_loads_gens,
22+
make_grid2op_env
23+
)
24+
prng = np.random.default_rng(42)
25+
26+
CASE_NAME = "case9241pegase.json"
27+
NB_TS = 10
1328

14-
param = env_tmp.parameters
15-
param.NO_OVERFLOW_DISCONNECTION = True
16-
env = grid2op.make(env_name, backend=LightSimBackend(), param=param)
1729

30+
def my_grid2op_env(case_name, nb_ts, prng):
31+
with warnings.catch_warnings():
32+
warnings.filterwarnings("ignore")
33+
# env_tmp = grid2op.make(env_name)
34+
35+
# load the case file
36+
case = pp.from_json(case_name)
37+
pp.runpp(case) # for slack
38+
load_p_init = 1.0 * case.load["p_mw"].values
39+
load_q_init = 1.0 * case.load["q_mvar"].values
40+
gen_p_init = 1.0 * case.gen["p_mw"].values
41+
sgen_p_init = 1.0 * case.sgen["p_mw"].values
42+
load_p, load_q, gen_p, sgen_p = get_loads_gens(load_p_init, load_q_init, gen_p_init, sgen_p_init, prng)
1843

19-
def make_steps(env, nb=1000):
44+
slack_gens = np.zeros((nb_ts, case.ext_grid.shape[0]))
45+
if "res_ext_grid" in case:
46+
slack_gens += np.tile(case.res_ext_grid["p_mw"].values.reshape(1,-1), (nb_ts, 1))
47+
gen_p_g2op = np.concatenate((gen_p, slack_gens), axis=1)
48+
env = make_grid2op_env(case,
49+
case_name,
50+
load_p,
51+
load_q,
52+
gen_p_g2op,
53+
sgen_p)
54+
55+
# param = env_tmp.parameters
56+
# param.NO_OVERFLOW_DISCONNECTION = True
57+
# env = grid2op.make(env_name, backend=LightSimBackend(), param=param)
58+
env.reset(seed=0, options={"time serie id": 0})
59+
env.backend._timer_preproc = 0
60+
env.backend._timer_solver = 0
61+
62+
with open(f"gridmodel_{case_name}.pickle", "wb") as f:
63+
pickle.dump(obj=env.backend._grid, file=f)
64+
return env
65+
66+
67+
def make_steps_glop(env, nb=NB_TS, reset_algo=False):
2068
for i in range(nb):
69+
if reset_algo:
70+
env.backend._grid.tell_solver_need_reset()
2171
_ = env.step(env.action_space())
72+
print(f"Total time: {env.backend._timer_preproc + env.backend._timer_solver}")
73+
2274

75+
def main_glop():
76+
env = my_grid2op_env(CASE_NAME, NB_TS, prng)
77+
make_steps_glop(env, NB_TS, reset_algo=True)
78+
2379

80+
def main_gridmodel(case_name=CASE_NAME, nb_ts=NB_TS, reset_algo=True, solver_used=SolverType.KLU):
81+
time_ls = 0.
82+
ls_timer_Fx = 0.
83+
ls_timer_solve = 0.
84+
ls_timer_initialize = 0.
85+
ls_timer_check = 0.
86+
ls_timer_dSbus = 0.
87+
ls_timer_fillJ = 0.
88+
ls_timer_Va_Vm = 0.
89+
ls_timer_pre_proc = 0.
90+
ls_timer_total_nr = 0.
91+
92+
with open(f"gridmodel_{case_name}.pickle", "rb") as f:
93+
ls_grid = pickle.load(f)
94+
ls_grid.change_solver(solver_used)
95+
v_init = ls_grid.dc_pf(np.ones(ls_grid.get_bus_vn_kv().shape[0], dtype=complex) * 1.04, 1, 0.1)
96+
for _ in range(nb_ts):
97+
if reset_algo:
98+
ls_grid.tell_solver_need_reset()
99+
beg_ls = time.perf_counter()
100+
V = ls_grid.ac_pf(v_init, 10, 1e-6)
101+
end_ls = time.perf_counter()
102+
time_ls += end_ls - beg_ls
103+
if V.shape[0] == 0:
104+
raise RuntimeError("Divergence")
105+
106+
(timer_Fx_, timer_solve_, timer_initialize_,
107+
timer_check_, timer_dSbus_, timer_fillJ_,
108+
timer_Va_Vm_, timer_pre_proc_, timer_total_nr_
109+
) = ls_grid.get_solver().get_timers_jacobian()
110+
ls_grid.unset_changes() # tell lightsim2grid that the state of the grid is consistent
111+
ls_timer_Fx += timer_Fx_
112+
ls_timer_solve += timer_solve_
113+
ls_timer_initialize += timer_initialize_
114+
ls_timer_check += timer_check_
115+
ls_timer_dSbus += timer_dSbus_
116+
ls_timer_fillJ += timer_fillJ_
117+
ls_timer_Va_Vm += timer_Va_Vm_
118+
ls_timer_pre_proc += timer_pre_proc_
119+
ls_timer_total_nr += timer_total_nr_
120+
print(f"Solver used: {solver_used}")
121+
print(f"Nb iter: {nb_ts}")
122+
print(f"Do reset each step: {reset_algo}")
123+
print("--------------------------------------")
124+
print("Detailed lightsim2grid timings: ")
125+
print(f"Total time {time_ls * 1000.:.2e}ms => {time_ls / nb_ts * 1e3:.2e} ms/pf | {nb_ts / time_ls:.0f} pf /s")
126+
print(f"Total time spent in the solver: {1e3 * ls_timer_total_nr:.2e} ms ({100. * ls_timer_total_nr / time_ls:.0f}% of total time spent in lightsim2grid)")
127+
print(f"\t Time to pre process Ybus, Sbus etc.: {1e3 * ls_timer_pre_proc:.2e} ms ({100. * ls_timer_pre_proc / ls_timer_total_nr:.0f} % of time in solver)")
128+
print(f"\t Time to initialize linear solver {1e3 * ls_timer_initialize:.2e} ms ({100. * ls_timer_initialize / ls_timer_total_nr:.0f} % of time in solver)")
129+
print(f"\t Time to compute dS/dV {1e3 * ls_timer_dSbus : .2e} ms ({100. * ls_timer_dSbus / ls_timer_total_nr:.0f} % of time in solver)")
130+
print(f"\t Time to fill the Jacobian {1e3 * ls_timer_fillJ:.2e} ms ({100. * ls_timer_fillJ / ls_timer_total_nr:.0f} % of time in solver)")
131+
print(f"\t Time to solve the Jacobian linear system: {1e3 * ls_timer_solve:.2e} ms ({100. * ls_timer_solve / ls_timer_total_nr:.0f} % of time in solver)")
132+
print(f"\t Time to update Va and Vm {1e3*ls_timer_Va_Vm:.2e} ms ({100. * ls_timer_Va_Vm / ls_timer_total_nr:.0f} % of time in solver)")
133+
print(f"\t Time to evaluate p,q mismmatch at each bus {1e3*ls_timer_Fx:.2e} ms ({100. * ls_timer_Fx / ls_timer_total_nr:.0f} % of time in solver)")
134+
print(f"\t Time to evaluate cvg criteria {1e3*ls_timer_check:.2e} ms ({100. * ls_timer_check / ls_timer_total_nr:.0f} % of time in solver)")
135+
print("--------------------------------------\n")
136+
137+
24138
if __name__ == "__main__":
25-
make_steps(env, nb=1000)
139+
# my_grid2op_env(case_name, nb_ts, prng)
140+
main_gridmodel(CASE_NAME, NB_TS, reset_algo=True, solver_used=SolverType.KLUSingleSlack)

env_compile_all.sh

100644100755
File mode changed.

lightsim2grid/contingencyAnalysis.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,16 @@ def all_contingencies(self, val):
122122
raise RuntimeError("Impossible to add new topologies like this. Please use `add_single_contingency` "
123123
"or `add_multiple_contingencies`.")
124124

125+
@property
126+
def init_from_n_powerflow(self):
127+
return self.computer.init_from_n_powerflow
128+
129+
@init_from_n_powerflow.setter
130+
def init_from_n_powerflow(self, val):
131+
if bool(val) != val:
132+
raise ValueError("The `init_from_n_powerflow` attribute must be a boolean.")
133+
self.computer.init_from_n_powerflow = bool(val)
134+
125135
# TODO implement that !
126136
def __update_grid(self, backend_act):
127137
raise NotImplementedError("TODO !")

lightsim2grid/lightSimBackend.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1458,7 +1458,12 @@ def close(self) -> None:
14581458
# self.init_pp_backend.close() # should not close it, the same init_pp_backend is used when copied
14591459
self._init_pp_backend = None
14601460
self._reset_res_pointers()
1461-
self._fill_nans()
1461+
try:
1462+
self._fill_nans()
1463+
except AttributeError:
1464+
# some attributes are not completely filled
1465+
1466+
pass
14621467
self._grid = None
14631468
self.__me_at_init = None
14641469

src/batch_algorithm/ContingencyAnalysis.cpp

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,9 @@ void ContingencyAnalysis::compute(const CplxVect & Vinit, int max_iter, real_typ
219219
bus_pq_,
220220
max_iter,
221221
tol);
222-
222+
// check if we init the n-1 cases with results from the n cases
223+
// or not
224+
if(_init_from_n_powerflow) Vinit_solver = _solver.get_V();
223225
// end of pre processing
224226
_timer_pre_proc = timer_preproc.duration();
225227
if(!conv) return;

src/batch_algorithm/ContingencyAnalysis.hpp

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ class ContingencyAnalysis final: public BaseBatchSolverSynch
2525
_li_coeffs(),
2626
_timer_total(0.),
2727
_timer_modif_Ybus(0.),
28-
_timer_pre_proc(0.)
28+
_timer_pre_proc(0.),
29+
_init_from_n_powerflow(false)
2930
{ }
3031

3132
~ContingencyAnalysis() noexcept = default;
@@ -34,6 +35,9 @@ class ContingencyAnalysis final: public BaseBatchSolverSynch
3435
ContingencyAnalysis & operator=(ContingencyAnalysis&&) = delete;
3536
ContingencyAnalysis & operator=(const ContingencyAnalysis&) = delete;
3637

38+
bool get_init_from_n_powerflow() const noexcept {return _init_from_n_powerflow;}
39+
void set_init_from_n_powerflow(bool do_it) noexcept {_init_from_n_powerflow = do_it;}
40+
3741
// utilities to add defaults to simulate
3842
void add_all_n1(){
3943
for(int l_id = 0; l_id < n_total_; ++l_id){
@@ -173,6 +177,7 @@ class ContingencyAnalysis final: public BaseBatchSolverSynch
173177
// sometimes, when i perform some disconnection, I make the graph non connexe
174178
// in this case, well, i don't use the results of the simulation
175179
bool check_invertible(const Eigen::SparseMatrix<cplx_type> & Ybus) const;
180+
176181
private:
177182
// li_default
178183
std::set<std::set<int> > _li_defaults; // do not use unordered_set here, we rely on the order for different functions !
@@ -182,5 +187,7 @@ class ContingencyAnalysis final: public BaseBatchSolverSynch
182187
double _timer_total; // total time spent in "compute"
183188
double _timer_modif_Ybus; // time to update the Ybus between the defaults simulation
184189
double _timer_pre_proc; // time to compute the coefficients of the Ybus
190+
191+
bool _init_from_n_powerflow;
185192
};
186193
#endif //COMPUTERS_H

src/main.cpp

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1421,6 +1421,15 @@ between 0 and `n_sub_ * max_nb_bus_per_sub_`
14211421

14221422
py::class_<ContingencyAnalysis>(m, "ContingencyAnalysisCPP", DocSecurityAnalysis::SecurityAnalysis.c_str())
14231423
.def(py::init<const GridModel &>())
1424+
.def_property("init_from_n_powerflow",
1425+
&ContingencyAnalysis::get_init_from_n_powerflow,
1426+
&ContingencyAnalysis::set_init_from_n_powerflow,
1427+
R"mydelim(Whether to initialize the complex voltages of "
1428+
"each contingencies with the results of a n-powerflow "
1429+
"(*ie* a powerflow without any line disconnection) or not. "
1430+
"Default: false, meaning each simulation is initialized "
1431+
"with the given input vector)mydelim")
1432+
14241433
// solver control
14251434
.def("change_solver", &ContingencyAnalysis::change_solver, DocGridModel::change_solver.c_str())
14261435
.def("available_solvers", &ContingencyAnalysis::available_solvers, DocGridModel::available_solvers.c_str())

0 commit comments

Comments
 (0)