diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 001d8c1..a259c23 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -50,7 +50,7 @@ jobs: steps: - name: Checkout sources - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: submodules: true @@ -117,13 +117,13 @@ jobs: python3 -v -c "from lightsim2grid import LightSimBackend; import grid2op; env = grid2op.make('l2rpn_case14_sandbox', test=True, backend=LightSimBackend())" - name: Upload wheel - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: lightsim2grid-wheel-linux-${{ matrix.python.name }} path: wheelhouse/*.whl - name: Upload source archive - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 if: matrix.python.name == 'cp311' with: name: lightsim2grid-sources @@ -167,12 +167,12 @@ jobs: steps: - name: Checkout sources - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: submodules: true - name: Setup Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python.version }} @@ -220,7 +220,7 @@ jobs: python -c "from lightsim2grid import LightSimBackend; import grid2op; env = grid2op.make('l2rpn_case14_sandbox', test=True, backend=LightSimBackend())" - name: Upload wheel - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: lightsim2grid-wheel-${{ matrix.config.name }}-${{ matrix.python.name }} path: dist/*.whl @@ -238,12 +238,12 @@ jobs: } steps: - name: Checkout sources - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: submodules: true - name: Setup Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python.version }} @@ -282,7 +282,7 @@ jobs: python -c "from lightsim2grid import LightSimBackend; import grid2op; env = grid2op.make('l2rpn_case14_sandbox', test=True, backend=LightSimBackend())" - name: Upload wheel - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: lightsim2grid-wheel-darwin-${{ matrix.python.name }} path: dist/*.whl @@ -321,12 +321,12 @@ jobs: steps: - name: Checkout sources - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: submodules: true - name: Setup Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python.version }} @@ -356,7 +356,7 @@ jobs: python -c "from lightsim2grid import LightSimBackend; import grid2op; env = grid2op.make('l2rpn_case14_sandbox', test=True, backend=LightSimBackend())" - name: Upload wheel - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: wheels-darwin-${{ matrix.python.name }} path: ./wheelhouse/*.whl @@ -373,7 +373,7 @@ jobs: path: download - name: Upload wheels - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: lightsim2grid-wheels path: | diff --git a/CHANGELOG.rst b/CHANGELOG.rst index e20196c..4d3c64e 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -39,6 +39,8 @@ Change Log change the topology of the grid) - [ADDED] a "reward" module in lightsim2grid with custom reward based on lightsim2grid. +- [ADDED] a class `N1ContingencyReward` that can leverage lightsim2grid to + assess the number of safe / unsafe N-1. - [IMPROVED] time measurments in python and c++ - [IMPROVED] now test lightsim2grid with oldest grid2op version - [IMPROVED] speed, by accelerating the reading back of the data (now read only once and then @@ -46,6 +48,7 @@ Change Log - [IMPROVED] c++ side avoid allocating memory (which allow to gain speed python side too) - [IMPROVED] type hinting in `LightSimBackend` for all 'public' methods (most notably the one used by grid2op) +- [IMPROVED] now the benchmarks are more verbose (detailing some compilation options) [0.8.0] 2024-03-18 -------------------- diff --git a/lightsim2grid/lightSimBackend.py b/lightsim2grid/lightSimBackend.py index 648f0b6..7119b80 100644 --- a/lightsim2grid/lightSimBackend.py +++ b/lightsim2grid/lightSimBackend.py @@ -690,11 +690,45 @@ def _aux_setup_right_after_grid_init(self): self._grid._max_nb_bus_per_sub = self.n_busbar_per_sub self._grid.tell_solver_need_reset() - + + def init_from_loaded_pandapower(self, pp_net): + if hasattr(type(self), "can_handle_more_than_2_busbar"): + type(self.init_pp_backend).n_busbar_per_sub = self.n_busbar_per_sub + self.init_pp_backend = pp_net.copy() + self._aux_init_pandapower() + + # handles redispatching + if type(pp_net).redispatching_unit_commitment_availble: + self.redispatching_unit_commitment_availble = True + for attr_nm in ["gen_type", "gen_pmin", "gen_pmax", + "gen_redispatchable", "gen_max_ramp_up", + "gen_max_ramp_down", "gen_min_uptime", + "gen_min_downtime", "gen_cost_per_MW", + "gen_startup_cost", "gen_shutdown_cost", + "gen_renewable" + ]: + setattr(self, attr_nm, copy.deepcopy(getattr( type(pp_net), attr_nm))) + + # handles storages + for attr_nm in ["storage_type", + "storage_Emax", + "storage_Emin", + "storage_max_p_prod" , + "storage_max_p_absorb", + "storage_marginal_cost", + "storage_loss", + "storage_charging_efficiency", + "storage_discharging_efficiency", + ]: + setattr(self, attr_nm, copy.deepcopy(getattr( type(pp_net), attr_nm))) + def _load_grid_pandapower(self, path=None, filename=None): if hasattr(type(self), "can_handle_more_than_2_busbar"): type(self.init_pp_backend).n_busbar_per_sub = self.n_busbar_per_sub self.init_pp_backend.load_grid(path, filename) + self._aux_init_pandapower() + + def _aux_init_pandapower(self): self.can_output_theta = True # i can compute the "theta" and output it to grid2op self._grid = init(self.init_pp_backend._grid) diff --git a/lightsim2grid/rewards/n1ContingencyReward.py b/lightsim2grid/rewards/n1ContingencyReward.py index cab4049..0a0de40 100644 --- a/lightsim2grid/rewards/n1ContingencyReward.py +++ b/lightsim2grid/rewards/n1ContingencyReward.py @@ -9,9 +9,8 @@ import time import numpy as np +import grid2op from grid2op.Reward import BaseReward -from grid2op.Environment import Environment -from grid2op.Backend import PandaPowerBackend from grid2op.Action._backendAction import _BackendAction from lightsim2grid import LightSimBackend, ContingencyAnalysis @@ -52,14 +51,14 @@ def __init__(self, tol=1e-8, nb_iter=10): BaseReward.__init__(self, logger=logger) - self._backend = None + self._backend : LightSimBackend = None self._backend_action = None self._l_ids = None - self._dc = dc - self._normalize = normalize + self._dc : bool = dc + self._normalize : bool = normalize if l_ids is not None: self._l_ids = [int(el) for el in l_ids] - self._threshold_margin = float(threshold_margin) + self._threshold_margin :float = float(threshold_margin) if klu_solver_available: if self._dc: self._solver_type = SolverType.KLUDC @@ -78,7 +77,13 @@ def __init__(self, self._timer_compute = 0. self._timer_post_proc = 0. - def initialize(self, env: Environment): + def initialize(self, env: "grid2op.Environment.Environment"): + from grid2op.Environment import Environment + from grid2op.Backend import PandaPowerBackend # lazy import because grid2op -> pandapower-> lightsim2grid -> grid2op + if not isinstance(env, Environment): + raise RuntimeError("You can only initialize this reward with a " + "proper grid2op environment") + if not isinstance(env.backend, (PandaPowerBackend, LightSimBackend)): raise RuntimeError("Impossible to use the `N1ContingencyReward` with " "a environment with a backend that is not " @@ -88,10 +93,9 @@ def initialize(self, env: Environment): self._backend : LightSimBackend = env.backend.copy() self._backend_ls :bool = True elif isinstance(env.backend, PandaPowerBackend): - from lightsim2grid.gridmodel import init - gridmodel = init(env.backend._grid) self._backend = LightSimBackend.init_grid(type(env.backend))() - self._backend._grid = gridmodel + self._backend.init_from_loaded_pandapower(env.backend) + self._backend.is_loaded = True else: raise NotImplementedError() @@ -143,22 +147,21 @@ def __call__(self, action, env, has_error, is_done, is_illegal, is_ambiguous): self._timer_compute += now_2 - now_ if self._dc: # In DC is study p, but take into account q in the limits - res = np.abs(tmp[0]) # this is Por + tmp_res = np.abs(tmp[0]) # this is Por # now transform the limits in A in MW por, qor, vor, aor = env.backend.lines_or_info() p_sq = (1e-3*th_lim_a)**2 * 3. * vor**2 - qor**2 p_sq[p_sq <= 0.] = 0. limits = np.sqrt(p_sq) else: - res = tmp[1] + tmp_res = tmp[1] limits = th_lim_a # print("Reward:") - # print(res) + # print(tmp_res) # print(self._threshold_margin * limits) - res = ((res > self._threshold_margin * limits) | (~np.isfinite(res))).any(axis=1) # whether one powerline is above its limit, per cont + res = ((tmp_res > self._threshold_margin * limits) | (~np.isfinite(tmp_res))).any(axis=1) # whether one powerline is above its limit, per cont + res |= (np.abs(tmp_res) <= self._tol).all(axis=1) # other type of divergence: all 0. # print(res.nonzero()) - # import pdb - # pdb.set_trace() res = res.sum() # count total of n-1 unsafe res = len(self._l_ids) - res # reward = things to maximise if self._normalize: diff --git a/lightsim2grid/tests/test_n1contingencyrewards.py b/lightsim2grid/tests/test_n1contingencyrewards.py index 0652960..11b4479 100644 --- a/lightsim2grid/tests/test_n1contingencyrewards.py +++ b/lightsim2grid/tests/test_n1contingencyrewards.py @@ -11,12 +11,14 @@ import numpy as np import grid2op +from grid2op.Backend import PandaPowerBackend from grid2op.Action import CompleteAction from grid2op.Reward import EpisodeDurationReward from lightsim2grid import LightSimBackend from lightsim2grid.rewards import N1ContingencyReward + TH_LIM_A_REF = np.array([ 541.0, 450.0, @@ -55,7 +57,6 @@ def threshold_margin(self): return 1. def l_ids(self): - return [0] return None def setUp(self) -> None: @@ -107,20 +108,18 @@ def _aux_test_reward(self, obs, reward): # MW**2 = kA**2 * 3. * kV**2 - MVAr**2 p_square = 3. * (1e-3*th_lim)**2 * (obs.v_or)**2 - (obs.q_or)**2 p_square[p_square <= 0.] = 0. - th_lim_p = np.sqrt(p_square) + th_lim_p = np.sqrt(p_square) * self.threshold_margin() # print("test:") for l_id in self.my_ids: sim_obs, sim_r, sim_d, sim_i = obs.simulate(self.env.action_space({"set_line_status": [(l_id, -1)]}), time_step=0) if not self.is_dc(): - if np.any(sim_obs.a_or > obs._thermal_limit) or sim_d: + if np.any(sim_obs.a_or > obs._thermal_limit * self.threshold_margin()) or sim_d: unsafe_cont += 1 else: - # print(sim_obs.p_or) - # print(th_lim_p) if np.any(np.abs(sim_obs.p_or) > th_lim_p) or sim_d: - unsafe_cont += 1 + unsafe_cont += 1 assert reward == (len(self.my_ids) - unsafe_cont), f"{reward} vs {(len(self.my_ids) - unsafe_cont)}" @@ -184,8 +183,20 @@ def test_copy(self): class TestN1ContingencyReward_DC(TestN1ContingencyReward_Base): def is_dc(self): return True + # def l_ids(self): + # return [18] + + +class TestN1ContingencyReward_LIDS(TestN1ContingencyReward_Base): + def l_ids(self): + return [0, 1, 2, 18, 16] -# TODO test with only a subset of powerlines -# TODO test with the "margin" -# TODO test with pandapower and lightsim as base backend -# TODO test AC and DC \ No newline at end of file + +class TestN1ContingencyReward_Margins(TestN1ContingencyReward_Base): + def threshold_margin(self): + return 0.9 + + +class TestN1ContingencyReward_PP(TestN1ContingencyReward_Base): + def init_backend(self): + return PandaPowerBackend(with_numba=False, lightsim2grid=False) diff --git a/lightsim2grid/timeSerie.py b/lightsim2grid/timeSerie.py index dc38190..72b6445 100644 --- a/lightsim2grid/timeSerie.py +++ b/lightsim2grid/timeSerie.py @@ -84,7 +84,7 @@ class ___TimeSerie: """ def __init__(self, grid2op_env): - if not GRID2OP_INSTALL: + if not GRID2OP_INSTALLED: raise RuntimeError("Impossible to use the python wrapper `TimeSerie` " "when grid2op is not installed. Please fall back to the " "c++ version (available in python) with:\n"